mirror of https://github.com/mitsuhiko/flask.git
David Lord
7 years ago
103 changed files with 3340 additions and 2237 deletions
Before Width: | Height: | Size: 65 KiB |
@ -0,0 +1,336 @@ |
|||||||
|
.. currentmodule:: flask |
||||||
|
|
||||||
|
Blog Blueprint |
||||||
|
============== |
||||||
|
|
||||||
|
You'll use the same techniques you learned about when writing the |
||||||
|
authentication blueprint to write the blog blueprint. The blog should |
||||||
|
list all posts, allow logged in users to create posts, and allow the |
||||||
|
author of a post to edit or delete it. |
||||||
|
|
||||||
|
As you implement each view, keep the development server running. As you |
||||||
|
save your changes, try going to the URL in your browser and testing them |
||||||
|
out. |
||||||
|
|
||||||
|
The Blueprint |
||||||
|
------------- |
||||||
|
|
||||||
|
Define the blueprint and register it in the application factory. |
||||||
|
|
||||||
|
.. code-block:: python |
||||||
|
:caption: ``flaskr/blog.py`` |
||||||
|
|
||||||
|
from flask import ( |
||||||
|
Blueprint, flash, g, redirect, render_template, request, url_for |
||||||
|
) |
||||||
|
from werkzeug.exceptions import abort |
||||||
|
|
||||||
|
from flaskr.auth import login_required |
||||||
|
from flaskr.db import get_db |
||||||
|
|
||||||
|
bp = Blueprint('blog', __name__) |
||||||
|
|
||||||
|
Import and register the blueprint from the factory using |
||||||
|
:meth:`app.register_blueprint() <Flask.register_blueprint>`. Place the |
||||||
|
new code at the end of the factory function before returning the app. |
||||||
|
|
||||||
|
.. code-block:: python |
||||||
|
:caption: ``flaskr/__init__.py`` |
||||||
|
|
||||||
|
def create_app(): |
||||||
|
app = ... |
||||||
|
# existing code omitted |
||||||
|
|
||||||
|
from . import blog |
||||||
|
app.register_blueprint(blog.bp) |
||||||
|
app.add_url_rule('/', endpoint='index') |
||||||
|
|
||||||
|
return app |
||||||
|
|
||||||
|
|
||||||
|
Unlike the auth blueprint, the blog blueprint does not have a |
||||||
|
``url_prefix``. So the ``index`` view will be at ``/``, the ``create`` |
||||||
|
view at ``/create``, and so on. The blog is the main feature of Flaskr, |
||||||
|
so it makes sense that the blog index will be the main index. |
||||||
|
|
||||||
|
However, the endpoint for the ``index`` view defined below will be |
||||||
|
``blog.index``. Some of the authentication views referred to a plain |
||||||
|
``index`` endpoint. :meth:`app.add_url_rule() <Flask.add_url_rule>` |
||||||
|
associates the endpoint name ``'index'`` with the ``/`` url so that |
||||||
|
``url_for('index')`` or ``url_for('blog.index')`` will both work, |
||||||
|
generating the same ``/`` URL either way. |
||||||
|
|
||||||
|
In another application you might give the blog blueprint a |
||||||
|
``url_prefix`` and define a separate ``index`` view in the application |
||||||
|
factory, similar to the ``hello`` view. Then the ``index`` and |
||||||
|
``blog.index`` endpoints and URLs would be different. |
||||||
|
|
||||||
|
|
||||||
|
Index |
||||||
|
----- |
||||||
|
|
||||||
|
The index will show all of the posts, most recent first. A ``JOIN`` is |
||||||
|
used so that the author information from the ``user`` table is |
||||||
|
available in the result. |
||||||
|
|
||||||
|
.. code-block:: python |
||||||
|
:caption: ``flaskr/blog.py`` |
||||||
|
|
||||||
|
@bp.route('/') |
||||||
|
def index(): |
||||||
|
db = get_db() |
||||||
|
posts = db.execute( |
||||||
|
'SELECT p.id, title, body, created, author_id, username' |
||||||
|
' FROM post p JOIN user u ON p.author_id = u.id' |
||||||
|
' ORDER BY created DESC' |
||||||
|
).fetchall() |
||||||
|
return render_template('blog/index.html', posts=posts) |
||||||
|
|
||||||
|
.. code-block:: html+jinja |
||||||
|
:caption: ``flaskr/templates/blog/index.html`` |
||||||
|
|
||||||
|
{% extends 'base.html' %} |
||||||
|
|
||||||
|
{% block header %} |
||||||
|
<h1>{% block title %}Posts{% endblock %}</h1> |
||||||
|
{% if g.user %} |
||||||
|
<a class="action" href="{{ url_for('blog.create') }}">New</a> |
||||||
|
{% endif %} |
||||||
|
{% endblock %} |
||||||
|
|
||||||
|
{% block content %} |
||||||
|
{% for post in posts %} |
||||||
|
<article class="post"> |
||||||
|
<header> |
||||||
|
<div> |
||||||
|
<h1>{{ post['title'] }}</h1> |
||||||
|
<div class="about">by {{ post['username'] }} on {{ post['created'].strftime('%Y-%m-%d') }}</div> |
||||||
|
</div> |
||||||
|
{% if g.user['id'] == post['author_id'] %} |
||||||
|
<a class="action" href="{{ url_for('blog.update', id=post['id']) }}">Edit</a> |
||||||
|
{% endif %} |
||||||
|
</header> |
||||||
|
<p class="body">{{ post['body'] }}</p> |
||||||
|
</article> |
||||||
|
{% if not loop.last %} |
||||||
|
<hr> |
||||||
|
{% endif %} |
||||||
|
{% endfor %} |
||||||
|
{% endblock %} |
||||||
|
|
||||||
|
When a user is logged in, the ``header`` block adds a link to the |
||||||
|
``create`` view. When the user is the author of a post, they'll see an |
||||||
|
"Edit" link to the ``update`` view for that post. ``loop.last`` is a |
||||||
|
special variable available inside `Jinja for loops`_. It's used to |
||||||
|
display a line after each post except the last one, to visually separate |
||||||
|
them. |
||||||
|
|
||||||
|
.. _Jinja for loops: http://jinja.pocoo.org/docs/templates/#for |
||||||
|
|
||||||
|
|
||||||
|
Create |
||||||
|
------ |
||||||
|
|
||||||
|
The ``create`` view works the same as the auth ``register`` view. Either |
||||||
|
the form is displayed, or the posted data is validated and the post is |
||||||
|
added to the database or an error is shown. |
||||||
|
|
||||||
|
The ``login_required`` decorator you wrote earlier is used on the blog |
||||||
|
views. A user must be logged in to visit these views, otherwise they |
||||||
|
will be redirected to the login page. |
||||||
|
|
||||||
|
.. code-block:: python |
||||||
|
:caption: ``flaskr/blog.py`` |
||||||
|
|
||||||
|
@bp.route('/create', methods=('GET', 'POST')) |
||||||
|
@login_required |
||||||
|
def create(): |
||||||
|
if request.method == 'POST': |
||||||
|
title = request.form['title'] |
||||||
|
body = request.form['body'] |
||||||
|
error = None |
||||||
|
|
||||||
|
if not title: |
||||||
|
error = 'Title is required.' |
||||||
|
|
||||||
|
if error is not None: |
||||||
|
flash(error) |
||||||
|
else: |
||||||
|
db = get_db() |
||||||
|
db.execute( |
||||||
|
'INSERT INTO post (title, body, author_id)' |
||||||
|
' VALUES (?, ?, ?)', |
||||||
|
(title, body, g.user['id']) |
||||||
|
) |
||||||
|
db.commit() |
||||||
|
return redirect(url_for('blog.index')) |
||||||
|
|
||||||
|
return render_template('blog/create.html') |
||||||
|
|
||||||
|
.. code-block:: html+jinja |
||||||
|
:caption: ``flaskr/templates/blog/create.html`` |
||||||
|
|
||||||
|
{% extends 'base.html' %} |
||||||
|
|
||||||
|
{% block header %} |
||||||
|
<h1>{% block title %}New Post{% endblock %}</h1> |
||||||
|
{% endblock %} |
||||||
|
|
||||||
|
{% block content %} |
||||||
|
<form method="post"> |
||||||
|
<label for="title">Title</label> |
||||||
|
<input name="title" id="title" value="{{ request.form['title'] }}" required> |
||||||
|
<label for="body">Body</label> |
||||||
|
<textarea name="body" id="body">{{ request.form['body'] }}</textarea> |
||||||
|
<input type="submit" value="Save"> |
||||||
|
</form> |
||||||
|
{% endblock %} |
||||||
|
|
||||||
|
|
||||||
|
Update |
||||||
|
------ |
||||||
|
|
||||||
|
Both the ``update`` and ``delete`` views will need to fetch a ``post`` |
||||||
|
by ``id`` and check if the author matches the logged in user. To avoid |
||||||
|
duplicating code, you can write a function to get the ``post`` and call |
||||||
|
it from each view. |
||||||
|
|
||||||
|
.. code-block:: python |
||||||
|
:caption: ``flaskr/blog.py`` |
||||||
|
|
||||||
|
def get_post(id, check_author=True): |
||||||
|
post = get_db().execute( |
||||||
|
'SELECT p.id, title, body, created, author_id, username' |
||||||
|
' FROM post p JOIN user u ON p.author_id = u.id' |
||||||
|
' WHERE p.id = ?', |
||||||
|
(id,) |
||||||
|
).fetchone() |
||||||
|
|
||||||
|
if post is None: |
||||||
|
abort(404, "Post id {0} doesn't exist.".format(id)) |
||||||
|
|
||||||
|
if check_author and post['author_id'] != g.user['id']: |
||||||
|
abort(403) |
||||||
|
|
||||||
|
return post |
||||||
|
|
||||||
|
:func:`abort` will raise a special exception that returns an HTTP status |
||||||
|
code. It takes an optional message to show with the error, otherwise a |
||||||
|
default message is used. ``404`` means "Not Found", and ``403`` means |
||||||
|
"Forbidden". (``401`` means "Unauthorized", but you redirect to the |
||||||
|
login page instead of returning that status.) |
||||||
|
|
||||||
|
The ``check_author`` argument is defined so that the function can be |
||||||
|
used to get a ``post`` without checking the author. This would be useful |
||||||
|
if you wrote a view to show an individual post on a page, where the user |
||||||
|
doesn't matter because they're not modifying the post. |
||||||
|
|
||||||
|
.. code-block:: python |
||||||
|
:caption: ``flaskr/blog.py`` |
||||||
|
|
||||||
|
@bp.route('/<int:id>/update', methods=('GET', 'POST')) |
||||||
|
@login_required |
||||||
|
def update(id): |
||||||
|
post = get_post(id) |
||||||
|
|
||||||
|
if request.method == 'POST': |
||||||
|
title = request.form['title'] |
||||||
|
body = request.form['body'] |
||||||
|
error = None |
||||||
|
|
||||||
|
if not title: |
||||||
|
error = 'Title is required.' |
||||||
|
|
||||||
|
if error is not None: |
||||||
|
flash(error) |
||||||
|
else: |
||||||
|
db = get_db() |
||||||
|
db.execute( |
||||||
|
'UPDATE post SET title = ?, body = ?' |
||||||
|
' WHERE id = ?', |
||||||
|
(title, body, id) |
||||||
|
) |
||||||
|
db.commit() |
||||||
|
return redirect(url_for('blog.index')) |
||||||
|
|
||||||
|
return render_template('blog/update.html', post=post) |
||||||
|
|
||||||
|
Unlike the views you've written so far, the ``update`` function takes |
||||||
|
an argument, ``id``. That corresponds to the ``<int:id>`` in the route. |
||||||
|
A real URL will look like ``/1/update``. Flask will capture the ``1``, |
||||||
|
ensure it's an :class:`int`, and pass it as the ``id`` argument. If you |
||||||
|
don't specify ``int:`` and instead do ``<id>``, it will be a string. |
||||||
|
To generate a URL to the update page, :func:`url_for` needs to be passed |
||||||
|
the ``id`` so it knows what to fill in: |
||||||
|
``url_for('blog.update', id=post['id'])``. This is also in the |
||||||
|
``index.html`` file above. |
||||||
|
|
||||||
|
The ``create`` and ``update`` views look very similar. The main |
||||||
|
difference is that the ``update`` view uses a ``post`` object and an |
||||||
|
``UPDATE`` query instead of an ``INSERT``. With some clever refactoring, |
||||||
|
you could use one view and template for both actions, but for the |
||||||
|
tutorial it's clearer to keep them separate. |
||||||
|
|
||||||
|
.. code-block:: html+jinja |
||||||
|
:caption: ``flaskr/templates/blog/update.html`` |
||||||
|
|
||||||
|
{% extends 'base.html' %} |
||||||
|
|
||||||
|
{% block header %} |
||||||
|
<h1>{% block title %}Edit "{{ post['title'] }}"{% endblock %}</h1> |
||||||
|
{% endblock %} |
||||||
|
|
||||||
|
{% block content %} |
||||||
|
<form method="post"> |
||||||
|
<label for="title">Title</label> |
||||||
|
<input name="title" id="title" |
||||||
|
value="{{ request.form['title'] or post['title'] }}" required> |
||||||
|
<label for="body">Body</label> |
||||||
|
<textarea name="body" id="body">{{ request.form['body'] or post['body'] }}</textarea> |
||||||
|
<input type="submit" value="Save"> |
||||||
|
</form> |
||||||
|
<hr> |
||||||
|
<form action="{{ url_for('blog.delete', id=post['id']) }}" method="post"> |
||||||
|
<input class="danger" type="submit" value="Delete" onclick="return confirm('Are you sure?');"> |
||||||
|
</form> |
||||||
|
{% endblock %} |
||||||
|
|
||||||
|
This template has two forms. The first posts the edited data to the |
||||||
|
current page (``/<id>/update``). The other form contains only a button |
||||||
|
and specifies an ``action`` attribute that posts to the delete view |
||||||
|
instead. The button uses some JavaScript to show a confirmation dialog |
||||||
|
before submitting. |
||||||
|
|
||||||
|
The pattern ``{{ request.form['title'] or post['title'] }}`` is used to |
||||||
|
choose what data appears in the form. When the form hasn't been |
||||||
|
submitted, the original ``post`` data appears, but if invalid form data |
||||||
|
was posted you want to display that so the user can fix the error, so |
||||||
|
``request.form`` is used instead. :data:`request` is another variable |
||||||
|
that's automatically available in templates. |
||||||
|
|
||||||
|
|
||||||
|
Delete |
||||||
|
------ |
||||||
|
|
||||||
|
The delete view doesn't have its own template, the delete button is part |
||||||
|
of ``update.html`` and posts to the ``/<id>/delete`` URL. Since there |
||||||
|
is no template, it will only handle the ``POST`` method then redirect |
||||||
|
to the ``index`` view. |
||||||
|
|
||||||
|
.. code-block:: python |
||||||
|
:caption: ``flaskr/blog.py`` |
||||||
|
|
||||||
|
@bp.route('/<int:id>/delete', methods=('POST',)) |
||||||
|
@login_required |
||||||
|
def delete(id): |
||||||
|
get_post(id) |
||||||
|
db = get_db() |
||||||
|
db.execute('DELETE FROM post WHERE id = ?', (id,)) |
||||||
|
db.commit() |
||||||
|
return redirect(url_for('blog.index')) |
||||||
|
|
||||||
|
Congratulations, you've now finished writing your application! Take some |
||||||
|
time to try out everything in the browser. However, there's still more |
||||||
|
to do before the project is complete. |
||||||
|
|
||||||
|
Continue to :doc:`install`. |
@ -1,31 +0,0 @@ |
|||||||
.. _tutorial-css: |
|
||||||
|
|
||||||
Step 8: Adding Style |
|
||||||
==================== |
|
||||||
|
|
||||||
Now that everything else works, it's time to add some style to the |
|
||||||
application. Just create a stylesheet called :file:`style.css` in the |
|
||||||
:file:`static` folder: |
|
||||||
|
|
||||||
.. 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; } |
|
||||||
|
|
||||||
Continue with :ref:`tutorial-testing`. |
|
@ -0,0 +1,213 @@ |
|||||||
|
.. currentmodule:: flask |
||||||
|
|
||||||
|
Define and Access the Database |
||||||
|
============================== |
||||||
|
|
||||||
|
The application will use a `SQLite`_ database to store users and posts. |
||||||
|
Python comes with built-in support for SQLite in the :mod:`sqlite3` |
||||||
|
module. |
||||||
|
|
||||||
|
SQLite is convenient because it doesn't require setting up a separate |
||||||
|
database server and is built-in to Python. However, if concurrent |
||||||
|
requests try to write to the database at the same time, they will slow |
||||||
|
down as each write happens sequentially. Small applications won't notice |
||||||
|
this. Once you become big, you may want to switch to a different |
||||||
|
database. |
||||||
|
|
||||||
|
The tutorial doesn't go into detail about SQL. If you are not familiar |
||||||
|
with it, the SQLite docs describe the `language`_. |
||||||
|
|
||||||
|
.. _SQLite: https://sqlite.org/about.html |
||||||
|
.. _language: https://sqlite.org/lang.html |
||||||
|
|
||||||
|
|
||||||
|
Connect to the Database |
||||||
|
----------------------- |
||||||
|
|
||||||
|
The first thing to do when working with a SQLite database (and most |
||||||
|
other Python database libraries) is to create a connection to it. Any |
||||||
|
queries and operations are performed using the connection, which is |
||||||
|
closed after the work is finished. |
||||||
|
|
||||||
|
In web applications this connection is typically tied to the request. It |
||||||
|
is created at some point when handling a request, and closed before the |
||||||
|
response is sent. |
||||||
|
|
||||||
|
.. code-block:: python |
||||||
|
:caption: ``flaskr/db.py`` |
||||||
|
|
||||||
|
import sqlite3 |
||||||
|
|
||||||
|
import click |
||||||
|
from flask import current_app, g |
||||||
|
from flask.cli import with_appcontext |
||||||
|
|
||||||
|
|
||||||
|
def get_db(): |
||||||
|
if 'db' not in g: |
||||||
|
g.db = sqlite3.connect( |
||||||
|
current_app.config['DATABASE'], |
||||||
|
detect_types=sqlite3.PARSE_DECLTYPES |
||||||
|
) |
||||||
|
g.db.row_factory = sqlite3.Row |
||||||
|
|
||||||
|
return g.db |
||||||
|
|
||||||
|
|
||||||
|
def close_db(e=None): |
||||||
|
db = g.pop('db', None) |
||||||
|
|
||||||
|
if db is not None: |
||||||
|
db.close() |
||||||
|
|
||||||
|
:data:`g` is a special object that is unique for each request. It is |
||||||
|
used to store data that might be accessed by multiple functions during |
||||||
|
the request. The connection is stored and reused instead of creating a |
||||||
|
new connection if ``get_db`` is called a second time in the same |
||||||
|
request. |
||||||
|
|
||||||
|
:data:`current_app` is another special object that points to the Flask |
||||||
|
application handling the request. Since you used an application factory, |
||||||
|
there is no application object when writing the rest of your code. |
||||||
|
``get_db`` will be called when the application has been created and is |
||||||
|
handling a request, so :data:`current_app` can be used. |
||||||
|
|
||||||
|
:func:`sqlite3.connect` establishes a connection to the file pointed at |
||||||
|
by the ``DATABASE`` configuration key. This file doesn't have to exist |
||||||
|
yet, and won't until you initialize the database later. |
||||||
|
|
||||||
|
:class:`sqlite3.Row` tells the connection to return rows that behave |
||||||
|
like dicts. This allows accessing the columns by name. |
||||||
|
|
||||||
|
``close_db`` checks if a connection was created by checking if ``g.db`` |
||||||
|
was set. If the connection exists, it is closed. Further down you will |
||||||
|
tell your application about the ``close_db`` function in the application |
||||||
|
factory so that it is called after each request. |
||||||
|
|
||||||
|
|
||||||
|
Create the Tables |
||||||
|
----------------- |
||||||
|
|
||||||
|
In SQLite, data is stored in *tables* and *columns*. These need to be |
||||||
|
created before you can store and retrieve data. Flaskr will store users |
||||||
|
in the ``user`` table, and posts in the ``post`` table. Create a file |
||||||
|
with the SQL commands needed to create empty tables: |
||||||
|
|
||||||
|
.. code-block:: sql |
||||||
|
:caption: ``flaskr/schema.sql`` |
||||||
|
|
||||||
|
DROP TABLE IF EXISTS user; |
||||||
|
DROP TABLE IF EXISTS post; |
||||||
|
|
||||||
|
CREATE TABLE user ( |
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT, |
||||||
|
username TEXT UNIQUE NOT NULL, |
||||||
|
password TEXT NOT NULL |
||||||
|
); |
||||||
|
|
||||||
|
CREATE TABLE post ( |
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT, |
||||||
|
author_id INTEGER NOT NULL, |
||||||
|
created TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, |
||||||
|
title TEXT NOT NULL, |
||||||
|
body TEXT NOT NULL, |
||||||
|
FOREIGN KEY (author_id) REFERENCES user (id) |
||||||
|
); |
||||||
|
|
||||||
|
Add the Python functions that will run these SQL commands to the |
||||||
|
``db.py`` file: |
||||||
|
|
||||||
|
.. code-block:: python |
||||||
|
:caption: ``flaskr/db.py`` |
||||||
|
|
||||||
|
def init_db(): |
||||||
|
db = get_db() |
||||||
|
|
||||||
|
with current_app.open_resource('schema.sql') as f: |
||||||
|
db.executescript(f.read().decode('utf8')) |
||||||
|
|
||||||
|
|
||||||
|
@click.command('init-db') |
||||||
|
@with_appcontext |
||||||
|
def init_db_command(): |
||||||
|
"""Clear the existing data and create new tables.""" |
||||||
|
init_db() |
||||||
|
click.echo('Initialized the database.') |
||||||
|
|
||||||
|
:meth:`open_resource() <Flask.open_resource>` opens a file relative to |
||||||
|
the ``flaskr`` package, which is useful since you won't necessarily know |
||||||
|
where that location is when deploying the application later. ``get_db`` |
||||||
|
returns a database connection, which is used to execute the commands |
||||||
|
read from the file. |
||||||
|
|
||||||
|
:func:`click.command` defines a command line command called ``init-db`` |
||||||
|
that calls the ``init_db`` function and shows a success message to the |
||||||
|
user. You can read :ref:`cli` to learn more about writing commands. |
||||||
|
|
||||||
|
|
||||||
|
Register with the Application |
||||||
|
----------------------------- |
||||||
|
|
||||||
|
The ``close_db`` and ``init_db_command`` functions need to be registered |
||||||
|
with the application instance, otherwise they won't be used by the |
||||||
|
application. However, since you're using a factory function, that |
||||||
|
instance isn't available when writing the functions. Instead, write a |
||||||
|
function that takes an application and does the registration. |
||||||
|
|
||||||
|
.. code-block:: python |
||||||
|
:caption: ``flaskr/db.py`` |
||||||
|
|
||||||
|
def init_app(app): |
||||||
|
app.teardown_appcontext(close_db) |
||||||
|
app.cli.add_command(init_db_command) |
||||||
|
|
||||||
|
:meth:`app.teardown_appcontext() <Flask.teardown_appcontext>` tells |
||||||
|
Flask to call that function when cleaning up after returning the |
||||||
|
response. |
||||||
|
|
||||||
|
:meth:`app.cli.add_command() <click.Group.add_command>` adds a new |
||||||
|
command that can be called with the ``flask`` command. |
||||||
|
|
||||||
|
Import and call this function from the factory. Place the new code at |
||||||
|
the end of the factory function before returning the app. |
||||||
|
|
||||||
|
.. code-block:: python |
||||||
|
:caption: ``flaskr/__init__.py`` |
||||||
|
|
||||||
|
def create_app(): |
||||||
|
app = ... |
||||||
|
# existing code omitted |
||||||
|
|
||||||
|
from . import db |
||||||
|
db.init_app(app) |
||||||
|
|
||||||
|
return app |
||||||
|
|
||||||
|
|
||||||
|
Initialize the Database File |
||||||
|
---------------------------- |
||||||
|
|
||||||
|
Now that ``init-db`` has been registered with the app, it can be called |
||||||
|
using the ``flask`` command, similar to the ``run`` command from the |
||||||
|
previous page. |
||||||
|
|
||||||
|
.. note:: |
||||||
|
|
||||||
|
If you're still running the server from the previous page, you can |
||||||
|
either stop the server, or run this command in a new terminal. If |
||||||
|
you use a new terminal, remember to change to your project directory |
||||||
|
and activate the env as described in :ref:`install-activate-env`. |
||||||
|
You'll also need to set ``FLASK_APP`` and ``FLASK_ENV`` as shown on |
||||||
|
the previous page. |
||||||
|
|
||||||
|
Run the ``init-db`` command: |
||||||
|
|
||||||
|
.. code-block:: none |
||||||
|
|
||||||
|
flask init-db |
||||||
|
Initialized the database. |
||||||
|
|
||||||
|
There will now be a ``flaskr.sqlite`` file in the ``instance`` folder in |
||||||
|
your project. |
||||||
|
|
||||||
|
Continue to :doc:`views`. |
@ -1,78 +0,0 @@ |
|||||||
.. _tutorial-dbcon: |
|
||||||
|
|
||||||
Step 4: Database Connections |
|
||||||
---------------------------- |
|
||||||
|
|
||||||
Let's continue building our code in the ``flaskr.py`` file. |
|
||||||
(Scroll to the end of the page for more about project layout.) |
|
||||||
|
|
||||||
You currently have a function for establishing a database connection with |
|
||||||
`connect_db`, but by itself, it is not particularly useful. Creating and |
|
||||||
closing database connections all the time is very inefficient, so you will |
|
||||||
need to keep it around for longer. Because database connections |
|
||||||
encapsulate a transaction, you will need to make sure that only one |
|
||||||
request at a time uses the connection. An elegant way to do this is by |
|
||||||
utilizing the *application context*. |
|
||||||
|
|
||||||
Flask provides two contexts: the *application context* and the |
|
||||||
*request context*. For the time being, all you have to know is that there |
|
||||||
are special variables that use these. For instance, the |
|
||||||
:data:`~flask.request` variable is the request object associated with |
|
||||||
the current request, whereas :data:`~flask.g` is a general purpose |
|
||||||
variable associated with the current application context. The tutorial |
|
||||||
will cover some more details of this later on. |
|
||||||
|
|
||||||
For the time being, all you have to know is that you can store information |
|
||||||
safely on the :data:`~flask.g` object. |
|
||||||
|
|
||||||
So when do you put it on there? To do that you can make a helper |
|
||||||
function. The first time the function is called, it will create a database |
|
||||||
connection for the current context, and successive calls will return the |
|
||||||
already established connection:: |
|
||||||
|
|
||||||
def get_db(): |
|
||||||
"""Opens a new database connection if there is none yet for the |
|
||||||
current application context. |
|
||||||
""" |
|
||||||
if not hasattr(g, 'sqlite_db'): |
|
||||||
g.sqlite_db = connect_db() |
|
||||||
return g.sqlite_db |
|
||||||
|
|
||||||
Now you know how to connect, but how can you properly disconnect? For |
|
||||||
that, Flask provides us with the :meth:`~flask.Flask.teardown_appcontext` |
|
||||||
decorator. It's executed every time the application context tears down:: |
|
||||||
|
|
||||||
@app.teardown_appcontext |
|
||||||
def close_db(error): |
|
||||||
"""Closes the database again at the end of the request.""" |
|
||||||
if hasattr(g, 'sqlite_db'): |
|
||||||
g.sqlite_db.close() |
|
||||||
|
|
||||||
Functions marked with :meth:`~flask.Flask.teardown_appcontext` are called |
|
||||||
every time the app context tears down. What does this mean? |
|
||||||
Essentially, the app context is created before the request comes in and is |
|
||||||
destroyed (torn down) whenever the request finishes. A teardown can |
|
||||||
happen because of two reasons: either everything went well (the error |
|
||||||
parameter will be ``None``) or an exception happened, in which case the error |
|
||||||
is passed to the teardown function. |
|
||||||
|
|
||||||
Curious about what these contexts mean? Have a look at the |
|
||||||
:ref:`app-context` documentation to learn more. |
|
||||||
|
|
||||||
Continue to :ref:`tutorial-dbinit`. |
|
||||||
|
|
||||||
.. hint:: Where do I put this code? |
|
||||||
|
|
||||||
If you've been following along in this tutorial, you might be wondering |
|
||||||
where to put the code from this step and the next. A logical place is to |
|
||||||
group these module-level functions together, and put your new |
|
||||||
``get_db`` and ``close_db`` functions below your existing |
|
||||||
``connect_db`` function (following the tutorial line-by-line). |
|
||||||
|
|
||||||
If you need a moment to find your bearings, take a look at how the `example |
|
||||||
source`_ is organized. In Flask, you can put all of your application code |
|
||||||
into a single Python module. You don't have to, and if your app :ref:`grows |
|
||||||
larger <larger-applications>`, it's a good idea not to. |
|
||||||
|
|
||||||
.. _example source: |
|
||||||
https://github.com/pallets/flask/tree/master/examples/flaskr/ |
|
@ -1,80 +0,0 @@ |
|||||||
.. _tutorial-dbinit: |
|
||||||
|
|
||||||
Step 5: Creating The Database |
|
||||||
============================= |
|
||||||
|
|
||||||
As outlined earlier, Flaskr is a database powered application, and more |
|
||||||
precisely, it is an application powered by a relational database system. Such |
|
||||||
systems need a schema that tells them how to store that information. |
|
||||||
Before starting the server for the first time, it's important to create |
|
||||||
that schema. |
|
||||||
|
|
||||||
Such a schema could be created by piping the ``schema.sql`` file into the |
|
||||||
``sqlite3`` command as follows:: |
|
||||||
|
|
||||||
sqlite3 /tmp/flaskr.db < schema.sql |
|
||||||
|
|
||||||
However, the downside of this is that it requires the ``sqlite3`` command |
|
||||||
to be installed, which is not necessarily the case on every system. This |
|
||||||
also requires that you provide the path to the database, which can introduce |
|
||||||
errors. |
|
||||||
|
|
||||||
Instead of the ``sqlite3`` command above, it's a good idea to add a function |
|
||||||
to our application that initializes the database for you. To do this, you |
|
||||||
can create a function and hook it into a :command:`flask` command that |
|
||||||
initializes the database. |
|
||||||
|
|
||||||
Take a look at the code segment below. A good place to add this function, |
|
||||||
and command, is just below the ``connect_db`` function in :file:`flaskr.py`:: |
|
||||||
|
|
||||||
def init_db(): |
|
||||||
db = get_db() |
|
||||||
|
|
||||||
with app.open_resource('schema.sql', mode='r') as f: |
|
||||||
db.cursor().executescript(f.read()) |
|
||||||
|
|
||||||
db.commit() |
|
||||||
|
|
||||||
|
|
||||||
@app.cli.command('initdb') |
|
||||||
def initdb_command(): |
|
||||||
"""Initializes the database.""" |
|
||||||
|
|
||||||
init_db() |
|
||||||
print('Initialized the database.') |
|
||||||
|
|
||||||
The ``app.cli.command()`` decorator registers a new command with the |
|
||||||
:command:`flask` script. When the command executes, Flask will automatically |
|
||||||
create an application context which is bound to the right application. |
|
||||||
Within the function, you can then access :attr:`flask.g` and other things as |
|
||||||
you might expect. When the script ends, the application context tears down |
|
||||||
and the database connection is released. |
|
||||||
|
|
||||||
You will want to keep an actual function around that initializes the database, |
|
||||||
though, so that we can easily create databases in unit tests later on. (For |
|
||||||
more information see :ref:`testing`.) |
|
||||||
|
|
||||||
The :func:`~flask.Flask.open_resource` method of the application object |
|
||||||
is a convenient helper function that will open a resource that the |
|
||||||
application provides. This function opens a file from the resource |
|
||||||
location (the :file:`flaskr/flaskr` folder) and allows you to read from it. |
|
||||||
It is used in this example to execute a script on the database connection. |
|
||||||
|
|
||||||
The connection object provided by SQLite can give you a cursor object. |
|
||||||
On that cursor, there is a method to execute a complete script. Finally, you |
|
||||||
only have to commit the changes. SQLite3 and other transactional |
|
||||||
databases will not commit unless you explicitly tell it to. |
|
||||||
|
|
||||||
Now, in a terminal, from the application root directory :file:`flaskr/` it is |
|
||||||
possible to create a database with the :command:`flask` script:: |
|
||||||
|
|
||||||
flask initdb |
|
||||||
Initialized the database. |
|
||||||
|
|
||||||
.. admonition:: Troubleshooting |
|
||||||
|
|
||||||
If you get an exception later on stating that a table cannot be found, check |
|
||||||
that you did execute the ``initdb`` command and that your table names are |
|
||||||
correct (singular vs. plural, for example). |
|
||||||
|
|
||||||
Continue with :ref:`tutorial-views` |
|
@ -0,0 +1,121 @@ |
|||||||
|
Deploy to Production |
||||||
|
==================== |
||||||
|
|
||||||
|
This part of the tutorial assumes you have a server that you want to |
||||||
|
deploy your application to. It gives an overview of how to create the |
||||||
|
distribution file and install it, but won't go into specifics about |
||||||
|
what server or software to use. You can set up a new environment on your |
||||||
|
development computer to try out the instructions below, but probably |
||||||
|
shouldn't use it for hosting a real public application. See |
||||||
|
:doc:`/deploying/index` for a list of many different ways to host your |
||||||
|
application. |
||||||
|
|
||||||
|
|
||||||
|
Build and Install |
||||||
|
----------------- |
||||||
|
|
||||||
|
When you want to deploy your application elsewhere, you build a |
||||||
|
distribution file. The current standard for Python distribution is the |
||||||
|
*wheel* format, with the ``.whl`` extension. Make sure the wheel library |
||||||
|
is installed first: |
||||||
|
|
||||||
|
.. code-block:: none |
||||||
|
|
||||||
|
pip install wheel |
||||||
|
|
||||||
|
Running ``setup.py`` with Python gives you a command line tool to issue |
||||||
|
build-related commands. The ``bdist_wheel`` command will build a wheel |
||||||
|
distribution file. |
||||||
|
|
||||||
|
.. code-block:: none |
||||||
|
|
||||||
|
python setup.py bdist_wheel |
||||||
|
|
||||||
|
You can find the file in ``dist/flaskr-1.0.0-py3-none-any.whl``. The |
||||||
|
file name is the name of the project, the version, and some tags about |
||||||
|
the file can install. |
||||||
|
|
||||||
|
Copy this file to another machine, |
||||||
|
:ref:`set up a new virtualenv <install-create-env>`, then install the |
||||||
|
file with ``pip``. |
||||||
|
|
||||||
|
.. code-block:: none |
||||||
|
|
||||||
|
pip install flaskr-1.0.0-py3-none-any.whl |
||||||
|
|
||||||
|
Pip will install your project along with its dependencies. |
||||||
|
|
||||||
|
Since this is a different machine, you need to run ``init-db`` again to |
||||||
|
create the database in the instance folder. |
||||||
|
|
||||||
|
.. code-block:: none |
||||||
|
|
||||||
|
export FLASK_APP=flaskr |
||||||
|
flask init-db |
||||||
|
|
||||||
|
When Flask detects that it's installed (not in editable mode), it uses |
||||||
|
a different directory for the instance folder. You can find it at |
||||||
|
``venv/var/flaskr-instance`` instead. |
||||||
|
|
||||||
|
|
||||||
|
Configure the Secret Key |
||||||
|
------------------------ |
||||||
|
|
||||||
|
In the beginning of the tutorial that you gave a default value for |
||||||
|
:data:`SECRET_KEY`. This should be changed to some random bytes in |
||||||
|
production. Otherwise, attackers could use the public ``'dev'`` key to |
||||||
|
modify the session cookie, or anything else that uses the secret key. |
||||||
|
|
||||||
|
You can use the following command to output a random secret key: |
||||||
|
|
||||||
|
.. code-block:: none |
||||||
|
|
||||||
|
python -c 'import os; print(os.urandom(16))' |
||||||
|
|
||||||
|
b'_5#y2L"F4Q8z\n\xec]/' |
||||||
|
|
||||||
|
Create the ``config.py`` file in the instance folder, which the factory |
||||||
|
will read from if it exists. Copy the generated value into it. |
||||||
|
|
||||||
|
.. code-block:: python |
||||||
|
:caption: ``venv/var/flaskr-instance/config.py`` |
||||||
|
|
||||||
|
SECRET_KEY = b'_5#y2L"F4Q8z\n\xec]/' |
||||||
|
|
||||||
|
You can also set any other necessary configuration here, although |
||||||
|
``SECRET_KEY`` is the only one needed for Flaskr. |
||||||
|
|
||||||
|
|
||||||
|
Run with a Production Server |
||||||
|
---------------------------- |
||||||
|
|
||||||
|
When running publicly rather than in development, you should not use the |
||||||
|
built-in development server (``flask run``). The development server is |
||||||
|
provided by Werkzeug for convenience, but is not designed to be |
||||||
|
particularly efficient, stable, or secure. |
||||||
|
|
||||||
|
Instead, use a production WSGI server. For example, to use `Waitress`_, |
||||||
|
first install it in the virtual environment: |
||||||
|
|
||||||
|
.. code-block:: none |
||||||
|
|
||||||
|
pip install waitress |
||||||
|
|
||||||
|
You need to tell Waitress about your application, but it doesn't use |
||||||
|
``FLASK_APP`` like ``flask run`` does. You need to tell it to import and |
||||||
|
call the application factory to get an application object. |
||||||
|
|
||||||
|
.. code-block:: none |
||||||
|
|
||||||
|
waitress-serve --call 'flaskr:create_app' |
||||||
|
|
||||||
|
Serving on http://0.0.0.0:8080 |
||||||
|
|
||||||
|
See :doc:`/deploying/index` for a list of many different ways to host |
||||||
|
your application. Waitress is just an example, chosen for the tutorial |
||||||
|
because it supports both Windows and Linux. There are many more WSGI |
||||||
|
servers and deployment options that you may choose for your project. |
||||||
|
|
||||||
|
.. _Waitress: https://docs.pylonsproject.org/projects/waitress/ |
||||||
|
|
||||||
|
Continue to :doc:`next`. |
@ -0,0 +1,177 @@ |
|||||||
|
.. currentmodule:: flask |
||||||
|
|
||||||
|
Application Setup |
||||||
|
================= |
||||||
|
|
||||||
|
A Flask application is an instance of the :class:`Flask` class. |
||||||
|
Everything about the application, such as configuration and URLs, will |
||||||
|
be registered with this class. |
||||||
|
|
||||||
|
The most straightforward way to create a Flask application is to create |
||||||
|
a global :class:`Flask` instance directly at the top of your code, like |
||||||
|
how the "Hello, World!" example did on the previous page. While this is |
||||||
|
simple and useful in some cases, it can cause some tricky issues as the |
||||||
|
project grows. |
||||||
|
|
||||||
|
Instead of creating a :class:`Flask` instance globally, you will create |
||||||
|
it inside a function. This function is known as the *application |
||||||
|
factory*. Any configuration, registration, and other setup the |
||||||
|
application needs will happen inside the function, then the application |
||||||
|
will be returned. |
||||||
|
|
||||||
|
|
||||||
|
The Application Factory |
||||||
|
----------------------- |
||||||
|
|
||||||
|
It's time to start coding! Create the ``flaskr`` directory and add the |
||||||
|
``__init__.py`` file. The ``__init__.py`` serves double duty: it will |
||||||
|
contain the application factory, and it tells Python that the ``flaskr`` |
||||||
|
directory should be treated as a package. |
||||||
|
|
||||||
|
.. code-block:: none |
||||||
|
|
||||||
|
mkdir flaskr |
||||||
|
|
||||||
|
.. code-block:: python |
||||||
|
:caption: ``flaskr/__init__.py`` |
||||||
|
|
||||||
|
import os |
||||||
|
|
||||||
|
from flask import Flask |
||||||
|
|
||||||
|
|
||||||
|
def create_app(test_config=None): |
||||||
|
# create and configure the app |
||||||
|
app = Flask(__name__, instance_relative_config=True) |
||||||
|
app.config.from_mapping( |
||||||
|
SECRET_KEY='dev', |
||||||
|
DATABASE=os.path.join(app.instance_path, 'flaskr.sqlite'), |
||||||
|
) |
||||||
|
|
||||||
|
if test_config is None: |
||||||
|
# load the instance config, if it exists, when not testing |
||||||
|
app.config.from_pyfile('config.py', silent=True) |
||||||
|
else: |
||||||
|
# load the test config if passed in |
||||||
|
app.config.from_mapping(test_config) |
||||||
|
|
||||||
|
# ensure the instance folder exists |
||||||
|
try: |
||||||
|
os.makedirs(app.instance_path) |
||||||
|
except OSError: |
||||||
|
pass |
||||||
|
|
||||||
|
# a simple page that says hello |
||||||
|
@app.route('/hello') |
||||||
|
def hello(): |
||||||
|
return 'Hello, World!' |
||||||
|
|
||||||
|
return app |
||||||
|
|
||||||
|
``create_app`` is the application factory function. You'll add to it |
||||||
|
later in the tutorial, but it already does a lot. |
||||||
|
|
||||||
|
#. ``app = Flask(__name__, instance_relative_config=True)`` creates the |
||||||
|
:class:`Flask` instance. |
||||||
|
|
||||||
|
* ``__name__`` is the name of the current Python module. The app |
||||||
|
needs to know where it's located to set up some paths, and |
||||||
|
``__name__`` is a convenient way to tell it that. |
||||||
|
|
||||||
|
* ``instance_relative_config=True`` tells the app that |
||||||
|
configuration files are relative to the |
||||||
|
:ref:`instance folder <instance-folders>`. The instance folder |
||||||
|
is located outside the ``flaskr`` package and can hold local |
||||||
|
data that shouldn't be committed to version control, such as |
||||||
|
configuration secrets and the database file. |
||||||
|
|
||||||
|
#. :meth:`app.config.from_mapping() <Config.from_mapping>` sets |
||||||
|
some default configuration that the app will use: |
||||||
|
|
||||||
|
* :data:`SECRET_KEY` is used by Flask and extensions to keep data |
||||||
|
safe. It's set to ``'dev'`` to provide a convenient value |
||||||
|
during development, but it should be overridden with a random |
||||||
|
value when deploying. |
||||||
|
|
||||||
|
* ``DATABASE`` is the path where the SQLite database file will be |
||||||
|
saved. It's under |
||||||
|
:attr:`app.instance_path <Flask.instance_path>`, which is the |
||||||
|
path that Flask has chosen for the instance folder. You'll learn |
||||||
|
more about the database in the next section. |
||||||
|
|
||||||
|
#. :meth:`app.config.from_pyfile() <Config.from_pyfile>` overrides |
||||||
|
the default configuration with values taken from the ``config.py`` |
||||||
|
file in the instance folder if it exists. For example, when |
||||||
|
deploying, this can be used to set a real ``SECRET_KEY``. |
||||||
|
|
||||||
|
* ``test_config`` can also be passed to the factory, and will be |
||||||
|
used instead of the instance configuration. This is so the tests |
||||||
|
you'll write later in the tutorial can be configured |
||||||
|
independently of any development values you have configured. |
||||||
|
|
||||||
|
#. :func:`os.makedirs` ensures that |
||||||
|
:attr:`app.instance_path <Flask.instance_path>` exists. Flask |
||||||
|
doesn't create the instance folder automatically, but it needs to be |
||||||
|
created because your project will create the SQLite database file |
||||||
|
there. |
||||||
|
|
||||||
|
#. :meth:`@app.route() <Flask.route>` creates a simple route so you can |
||||||
|
see the application working before getting into the rest of the |
||||||
|
tutorial. It creates a connection between the URL ``/hello`` and a |
||||||
|
function that returns a response, the string ``'Hello, World!'`` in |
||||||
|
this case. |
||||||
|
|
||||||
|
|
||||||
|
Run The Application |
||||||
|
------------------- |
||||||
|
|
||||||
|
Now you can run your application using the ``flask`` command. From the |
||||||
|
terminal, tell Flask where to find your application, then run it in |
||||||
|
development mode. |
||||||
|
|
||||||
|
Development mode shows an interactive debugger whenever a page raises an |
||||||
|
exception, and restarts the server whenever you make changes to the |
||||||
|
code. You can leave it running and just reload the browser page as you |
||||||
|
follow the tutorial. |
||||||
|
|
||||||
|
For Linux and Mac: |
||||||
|
|
||||||
|
.. code-block:: none |
||||||
|
|
||||||
|
export FLASK_APP=flaskr |
||||||
|
export FLASK_ENV=development |
||||||
|
flask run |
||||||
|
|
||||||
|
For Windows cmd, use ``set`` instead of ``export``: |
||||||
|
|
||||||
|
.. code-block:: none |
||||||
|
|
||||||
|
set FLASK_APP=flaskr |
||||||
|
set FLASK_ENV=development |
||||||
|
flask run |
||||||
|
|
||||||
|
For Windows PowerShell, use ``$env:`` instead of ``export``: |
||||||
|
|
||||||
|
.. code-block:: none |
||||||
|
|
||||||
|
$env:FLASK_APP = "flaskr" |
||||||
|
$env:FLASK_ENV = "development" |
||||||
|
flask run |
||||||
|
|
||||||
|
You'll see output similar to this: |
||||||
|
|
||||||
|
.. code-block:: none |
||||||
|
|
||||||
|
* Serving Flask app "flaskr" |
||||||
|
* Environment: development |
||||||
|
* Debug mode: on |
||||||
|
* Running on http://127.0.0.1:5000/ (Press CTRL+C to quit) |
||||||
|
* Restarting with stat |
||||||
|
* Debugger is active! |
||||||
|
* Debugger PIN: 855-212-761 |
||||||
|
|
||||||
|
Visit http://127.0.0.1:5000/hello in a browser and you should see the |
||||||
|
"Hello, World!" message. Congratulations, you're now running your Flask |
||||||
|
web application! |
||||||
|
|
||||||
|
Continue to :doc:`database`. |
After Width: | Height: | Size: 13 KiB |
After Width: | Height: | Size: 11 KiB |
After Width: | Height: | Size: 7.3 KiB |
@ -1,31 +0,0 @@ |
|||||||
.. _tutorial-folders: |
|
||||||
|
|
||||||
Step 0: Creating The Folders |
|
||||||
============================ |
|
||||||
|
|
||||||
It is recommended to install your Flask application within a virtualenv. Please |
|
||||||
read the :ref:`installation` section to set up your environment. |
|
||||||
|
|
||||||
Now that you have installed Flask, you will need to create the folders required |
|
||||||
for this tutorial. Your directory structure will look like this:: |
|
||||||
|
|
||||||
/flaskr |
|
||||||
/flaskr |
|
||||||
/static |
|
||||||
/templates |
|
||||||
|
|
||||||
The application will be installed and run as Python package. This is the |
|
||||||
recommended way to install and run Flask applications. You will see exactly |
|
||||||
how to run ``flaskr`` later on in this tutorial. |
|
||||||
|
|
||||||
For now go ahead and create the applications directory structure. In the next |
|
||||||
few steps you will be creating the database schema as well as the main module. |
|
||||||
|
|
||||||
As a quick side note, the files inside of the :file:`static` folder are |
|
||||||
available to users of the application via HTTP. This is the place where CSS and |
|
||||||
JavaScript files go. Inside the :file:`templates` folder, Flask will look for |
|
||||||
`Jinja2`_ templates. You will see examples of this later on. |
|
||||||
|
|
||||||
For now you should continue with :ref:`tutorial-schema`. |
|
||||||
|
|
||||||
.. _Jinja2: http://jinja.pocoo.org/ |
|
@ -0,0 +1,113 @@ |
|||||||
|
Make the Project Installable |
||||||
|
============================ |
||||||
|
|
||||||
|
Making your project installable means that you can build a |
||||||
|
*distribution* file and install that in another environment, just like |
||||||
|
you installed Flask in your project's environment. This makes deploying |
||||||
|
your project the same as installing any other library, so you're using |
||||||
|
all the standard Python tools to manage everything. |
||||||
|
|
||||||
|
Installing also comes with other benefits that might not be obvious from |
||||||
|
the tutorial or as a new Python user, including: |
||||||
|
|
||||||
|
* Currently, Python and Flask understand how to use the ``flaskr`` |
||||||
|
package only because you're running from your project's directory. |
||||||
|
Installing means you can import it no matter where you run from. |
||||||
|
|
||||||
|
* You can manage your project's dependencies just like other packages |
||||||
|
do, so ``pip install yourproject.whl`` installs them. |
||||||
|
|
||||||
|
* Test tools can isolate your test environment from your development |
||||||
|
environment. |
||||||
|
|
||||||
|
.. note:: |
||||||
|
This is being introduced late in the tutorial, but in your future |
||||||
|
projects you should always start with this. |
||||||
|
|
||||||
|
|
||||||
|
Describe the Project |
||||||
|
-------------------- |
||||||
|
|
||||||
|
The ``setup.py`` file describes your project and the files that belong |
||||||
|
to it. |
||||||
|
|
||||||
|
.. code-block:: python |
||||||
|
:caption: ``setup.py`` |
||||||
|
|
||||||
|
from setuptools import find_packages, setup |
||||||
|
|
||||||
|
setup( |
||||||
|
name='flaskr', |
||||||
|
version='1.0.0', |
||||||
|
packages=find_packages(), |
||||||
|
include_package_data=True, |
||||||
|
zip_safe=False, |
||||||
|
install_requires=[ |
||||||
|
'flask', |
||||||
|
], |
||||||
|
) |
||||||
|
|
||||||
|
|
||||||
|
``packages`` tells Python what package directories (and the Python files |
||||||
|
they contain) to include. ``find_packages()`` finds these directories |
||||||
|
automatically so you don't have to type them out. To include other |
||||||
|
files, such as the static and templates directories, |
||||||
|
``include_package_data`` is set. Python needs another file named |
||||||
|
``MANIFEST.in`` to tell what this other data is. |
||||||
|
|
||||||
|
.. code-block:: none |
||||||
|
:caption: ``MANIFEST.in`` |
||||||
|
|
||||||
|
include flaskr/schema.sql |
||||||
|
graft flaskr/static |
||||||
|
graft flaskr/templates |
||||||
|
global-exclude *.pyc |
||||||
|
|
||||||
|
This tells Python to copy everything in the ``static`` and ``templates`` |
||||||
|
directories, and the ``schema.sql`` file, but to exclude all bytecode |
||||||
|
files. |
||||||
|
|
||||||
|
See the `official packaging guide`_ for another explanation of the files |
||||||
|
and options used. |
||||||
|
|
||||||
|
.. _official packaging guide: https://packaging.python.org/tutorials/distributing-packages/ |
||||||
|
|
||||||
|
|
||||||
|
Install the Project |
||||||
|
------------------- |
||||||
|
|
||||||
|
Use ``pip`` to install your project in the virtual environment. |
||||||
|
|
||||||
|
.. code-block:: none |
||||||
|
|
||||||
|
pip install -e . |
||||||
|
|
||||||
|
This tells pip to find ``setup.py`` in the current directory and install |
||||||
|
it in *editable* or *development* mode. Editable mode means that as you |
||||||
|
make changes to your local code, you'll only need to re-install if you |
||||||
|
change the metadata about the project, such as its dependencies. |
||||||
|
|
||||||
|
You can observe that the project is now installed with ``pip list``. |
||||||
|
|
||||||
|
.. code-block:: none |
||||||
|
|
||||||
|
pip list |
||||||
|
|
||||||
|
Package Version Location |
||||||
|
-------------- --------- ---------------------------------- |
||||||
|
click 6.7 |
||||||
|
Flask 1.0 |
||||||
|
flaskr 1.0.0 /home/user/Projects/flask-tutorial |
||||||
|
itsdangerous 0.24 |
||||||
|
Jinja2 2.10 |
||||||
|
MarkupSafe 1.0 |
||||||
|
pip 9.0.3 |
||||||
|
setuptools 39.0.1 |
||||||
|
Werkzeug 0.14.1 |
||||||
|
wheel 0.30.0 |
||||||
|
|
||||||
|
Nothing changes from how you've been running your project so far. |
||||||
|
``FLASK_APP`` is still set to ``flaskr`` and ``flask run`` still runs |
||||||
|
the application. |
||||||
|
|
||||||
|
Continue to :doc:`tests`. |
@ -1,39 +0,0 @@ |
|||||||
.. _tutorial-introduction: |
|
||||||
|
|
||||||
Introducing Flaskr |
|
||||||
================== |
|
||||||
|
|
||||||
This tutorial will demonstrate a blogging application named Flaskr, but feel |
|
||||||
free to choose your own less Web-2.0-ish name ;) Essentially, it will 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, they 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 index page shows all entries so far in reverse chronological order |
|
||||||
(newest on top) and the user can add new ones from there if logged in. |
|
||||||
|
|
||||||
SQLite3 will be used directly for this application because it's good enough |
|
||||||
for an application of this size. For larger applications, however, |
|
||||||
it makes a lot of sense to use `SQLAlchemy`_, as it handles database |
|
||||||
connections in a more intelligent way, allowing 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. |
|
||||||
|
|
||||||
.. warning:: |
|
||||||
If you're following the tutorial from a specific version of the docs, be |
|
||||||
sure to check out the same tag in the repository, otherwise the tutorial |
|
||||||
may be different than the example. |
|
||||||
|
|
||||||
Here is a screenshot of the final application: |
|
||||||
|
|
||||||
.. image:: ../_static/flaskr.png |
|
||||||
:align: center |
|
||||||
:class: screenshot |
|
||||||
:alt: screenshot of the final application |
|
||||||
|
|
||||||
Continue with :ref:`tutorial-folders`. |
|
||||||
|
|
||||||
.. _SQLAlchemy: https://www.sqlalchemy.org/ |
|
@ -0,0 +1,38 @@ |
|||||||
|
Keep Developing! |
||||||
|
================ |
||||||
|
|
||||||
|
You've learned about quite a few Flask and Python concepts throughout |
||||||
|
the tutorial. Go back and review the tutorial and compare your code with |
||||||
|
the steps you took to get there. Compare your project to the |
||||||
|
:gh:`example project <examples/tutorial>`, which might look a bit |
||||||
|
different due to the step-by-step nature of the tutorial. |
||||||
|
|
||||||
|
There's a lot more to Flask than what you've seen so far. Even so, |
||||||
|
you're now equipped to start developing your own web applications. Check |
||||||
|
out the :ref:`quickstart` for an overview of what Flask can do, then |
||||||
|
dive into the docs to keep learning. Flask uses `Jinja`_, `Click`_, |
||||||
|
`Werkzeug`_, and `ItsDangerous`_ behind the scenes, and they all have |
||||||
|
their own documentation too. You'll also be interested in |
||||||
|
:ref:`extensions` which make tasks like working with the database or |
||||||
|
validating form data easier and more powerful. |
||||||
|
|
||||||
|
If you want to keep developing your Flaskr project, here are some ideas |
||||||
|
for what to try next: |
||||||
|
|
||||||
|
* A detail view to show a single post. Click a post's title to go to |
||||||
|
its page. |
||||||
|
* Like / unlike a post. |
||||||
|
* Comments. |
||||||
|
* Tags. Clicking a tag shows all the posts with that tag. |
||||||
|
* A search box that filters the index page by name. |
||||||
|
* Paged display. Only show 5 posts per page. |
||||||
|
* Upload an image to go along with a post. |
||||||
|
* Format posts using Markdown. |
||||||
|
* An RSS feed of new posts. |
||||||
|
|
||||||
|
Have fun and make awesome applications! |
||||||
|
|
||||||
|
.. _Jinja: https://palletsprojects.com/p/jinja/ |
||||||
|
.. _Click: https://palletsprojects.com/p/click/ |
||||||
|
.. _Werkzeug: https://palletsprojects.com/p/werkzeug/ |
||||||
|
.. _ItsDangerous: https://palletsprojects.com/p/itsdangerous/ |
@ -1,108 +0,0 @@ |
|||||||
.. _tutorial-packaging: |
|
||||||
|
|
||||||
Step 3: Installing flaskr as a Package |
|
||||||
====================================== |
|
||||||
|
|
||||||
Flask is now shipped with built-in support for `Click`_. Click provides |
|
||||||
Flask with enhanced and extensible command line utilities. Later in this |
|
||||||
tutorial you will see exactly how to extend the ``flask`` command line |
|
||||||
interface (CLI). |
|
||||||
|
|
||||||
A useful pattern to manage a Flask application is to install your app |
|
||||||
following the `Python Packaging Guide`_. Presently this involves |
|
||||||
creating two new files; :file:`setup.py` and :file:`MANIFEST.in` in the |
|
||||||
projects root directory. You also need to add an :file:`__init__.py` |
|
||||||
file to make the :file:`flaskr/flaskr` directory a package. After these |
|
||||||
changes, your code structure should be:: |
|
||||||
|
|
||||||
/flaskr |
|
||||||
/flaskr |
|
||||||
__init__.py |
|
||||||
/static |
|
||||||
/templates |
|
||||||
flaskr.py |
|
||||||
schema.sql |
|
||||||
setup.py |
|
||||||
MANIFEST.in |
|
||||||
|
|
||||||
Create the ``setup.py`` file for ``flaskr`` with the following content:: |
|
||||||
|
|
||||||
from setuptools import setup |
|
||||||
|
|
||||||
setup( |
|
||||||
name='flaskr', |
|
||||||
packages=['flaskr'], |
|
||||||
include_package_data=True, |
|
||||||
install_requires=[ |
|
||||||
'flask', |
|
||||||
], |
|
||||||
) |
|
||||||
|
|
||||||
When using setuptools, it is also necessary to specify any special files |
|
||||||
that should be included in your package (in the :file:`MANIFEST.in`). |
|
||||||
In this case, the static and templates directories need to be included, |
|
||||||
as well as the schema. |
|
||||||
|
|
||||||
Create the :file:`MANIFEST.in` and add the following lines:: |
|
||||||
|
|
||||||
graft flaskr/templates |
|
||||||
graft flaskr/static |
|
||||||
include flaskr/schema.sql |
|
||||||
|
|
||||||
Next, to simplify locating the application, create the file, |
|
||||||
:file:`flaskr/__init__.py` containing only the following import statement:: |
|
||||||
|
|
||||||
from .flaskr import app |
|
||||||
|
|
||||||
This import statement brings the application instance into the top-level |
|
||||||
of the application package. When it is time to run the application, the |
|
||||||
Flask development server needs the location of the app instance. This |
|
||||||
import statement simplifies the location process. Without the above |
|
||||||
import statement, the export statement a few steps below would need to be |
|
||||||
``export FLASK_APP=flaskr.flaskr``. |
|
||||||
|
|
||||||
At this point you should be able to install the application. As usual, it |
|
||||||
is recommended to install your Flask application within a `virtualenv`_. |
|
||||||
With that said, from the ``flaskr/`` directory, go ahead and install the |
|
||||||
application with:: |
|
||||||
|
|
||||||
pip install --editable . |
|
||||||
|
|
||||||
The above installation command assumes that it is run within the projects |
|
||||||
root directory, ``flaskr/``. The ``editable`` flag allows editing |
|
||||||
source code without having to reinstall the Flask app each time you make |
|
||||||
changes. The flaskr app is now installed in your virtualenv (see output |
|
||||||
of ``pip freeze``). |
|
||||||
|
|
||||||
With that out of the way, you should be able to start up the application. |
|
||||||
Do this on Mac or Linux with the following commands in ``flaskr/``:: |
|
||||||
|
|
||||||
export FLASK_APP=flaskr |
|
||||||
export FLASK_ENV=development |
|
||||||
flask run |
|
||||||
|
|
||||||
(In case you are on Windows you need to use ``set`` instead of ``export``). |
|
||||||
Exporting ``FLASK_ENV=development`` turns on all development features |
|
||||||
such as enabling the interactive debugger. |
|
||||||
|
|
||||||
*Never leave debug mode activated in a production system*, because it will |
|
||||||
allow users to execute code on the server! |
|
||||||
|
|
||||||
You will see a message telling you that server has started along with |
|
||||||
the address at which you can access it in a browser. |
|
||||||
|
|
||||||
When you head over to the server in your browser, you will get a 404 error |
|
||||||
because we don't have any views yet. That will be addressed a little later, |
|
||||||
but first, you should get the database working. |
|
||||||
|
|
||||||
.. admonition:: Externally Visible Server |
|
||||||
|
|
||||||
Want your server to be publicly available? Check out the |
|
||||||
:ref:`externally visible server <public-server>` section for more |
|
||||||
information. |
|
||||||
|
|
||||||
Continue with :ref:`tutorial-dbcon`. |
|
||||||
|
|
||||||
.. _Click: http://click.pocoo.org |
|
||||||
.. _Python Packaging Guide: https://packaging.python.org |
|
||||||
.. _virtualenv: https://virtualenv.pypa.io |
|
@ -1,25 +0,0 @@ |
|||||||
.. _tutorial-schema: |
|
||||||
|
|
||||||
Step 1: Database Schema |
|
||||||
======================= |
|
||||||
|
|
||||||
In this step, you will create the database schema. Only a single table is |
|
||||||
needed for this application and it will only support SQLite. All you need to do |
|
||||||
is put the following contents into a file named :file:`schema.sql` in the |
|
||||||
:file:`flaskr/flaskr` folder: |
|
||||||
|
|
||||||
.. sourcecode:: sql |
|
||||||
|
|
||||||
drop table if exists entries; |
|
||||||
create table entries ( |
|
||||||
id integer primary key autoincrement, |
|
||||||
title text not null, |
|
||||||
'text' text not null |
|
||||||
); |
|
||||||
|
|
||||||
This schema consists of a single table called ``entries``. 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. |
|
||||||
|
|
||||||
Continue with :ref:`tutorial-setup`. |
|
@ -1,101 +0,0 @@ |
|||||||
.. _tutorial-setup: |
|
||||||
|
|
||||||
Step 2: Application Setup Code |
|
||||||
============================== |
|
||||||
|
|
||||||
Next, we will create the application module, :file:`flaskr.py`. Just like the |
|
||||||
:file:`schema.sql` file you created in the previous step, this file should be |
|
||||||
placed inside of the :file:`flaskr/flaskr` folder. |
|
||||||
|
|
||||||
For this tutorial, all the Python code we use will be put into this file |
|
||||||
(except for one line in ``__init__.py``, and any testing or optional files you |
|
||||||
decide to create). |
|
||||||
|
|
||||||
The first several lines of code in the application module are the needed import |
|
||||||
statements. After that there will be a few lines of configuration code. |
|
||||||
|
|
||||||
For small applications like ``flaskr``, it is possible to drop the configuration |
|
||||||
directly into the module. However, a cleaner solution is to create a separate |
|
||||||
``.py`` file, load that, and import the values from there. |
|
||||||
|
|
||||||
Here are the import statements (in :file:`flaskr.py`):: |
|
||||||
|
|
||||||
import os |
|
||||||
import sqlite3 |
|
||||||
|
|
||||||
from flask import (Flask, request, session, g, redirect, url_for, abort, |
|
||||||
render_template, flash) |
|
||||||
|
|
||||||
The next couple lines will create the actual application instance and |
|
||||||
initialize it with the config from the same file in :file:`flaskr.py`:: |
|
||||||
|
|
||||||
app = Flask(__name__) # create the application instance :) |
|
||||||
app.config.from_object(__name__) # load config from this file , flaskr.py |
|
||||||
|
|
||||||
# Load default config and override config from an environment variable |
|
||||||
app.config.update( |
|
||||||
DATABASE=os.path.join(app.root_path, 'flaskr.db'), |
|
||||||
SECRET_KEY=b'_5#y2L"F4Q8z\n\xec]/', |
|
||||||
USERNAME='admin', |
|
||||||
PASSWORD='default' |
|
||||||
) |
|
||||||
app.config.from_envvar('FLASKR_SETTINGS', silent=True) |
|
||||||
|
|
||||||
In the above code, the :class:`~flask.Config` object works similarly to a |
|
||||||
dictionary, so it can be updated with new values. |
|
||||||
|
|
||||||
.. admonition:: Database Path |
|
||||||
|
|
||||||
Operating systems know the concept of a current working directory for |
|
||||||
each process. Unfortunately, you cannot depend on this in web |
|
||||||
applications because you might have more than one application in the |
|
||||||
same process. |
|
||||||
|
|
||||||
For this reason the ``app.root_path`` attribute can be used to |
|
||||||
get the path to the application. Together with the ``os.path`` module, |
|
||||||
files can then easily be found. In this example, we place the |
|
||||||
database right next to it. |
|
||||||
|
|
||||||
For a real-world application, it's recommended to use |
|
||||||
:ref:`instance-folders` instead. |
|
||||||
|
|
||||||
Usually, it is a good idea to load a separate, environment-specific |
|
||||||
configuration file. Flask allows you to import multiple configurations and it |
|
||||||
will use the setting defined in the last import. This enables robust |
|
||||||
configuration setups. :meth:`~flask.Config.from_envvar` can help achieve |
|
||||||
this. :: |
|
||||||
|
|
||||||
app.config.from_envvar('FLASKR_SETTINGS', silent=True) |
|
||||||
|
|
||||||
If you want to do this (not required for this tutorial) simply define the |
|
||||||
environment variable :envvar:`FLASKR_SETTINGS` that points to a config file |
|
||||||
to be loaded. The silent switch just tells Flask to not complain if no such |
|
||||||
environment key is set. |
|
||||||
|
|
||||||
In addition to that, you can use the :meth:`~flask.Config.from_object` |
|
||||||
method on the config object and provide it with an import name of a |
|
||||||
module. Flask will then initialize the variable from that module. Note |
|
||||||
that in all cases, only variable names that are uppercase are considered. |
|
||||||
|
|
||||||
The :data:`SECRET_KEY` is needed to keep the client-side sessions secure. |
|
||||||
Choose that key wisely and as hard to guess and complex as possible. |
|
||||||
|
|
||||||
Lastly, add a method that allows for easy connections to the specified |
|
||||||
database. :: |
|
||||||
|
|
||||||
def connect_db(): |
|
||||||
"""Connects to the specific database.""" |
|
||||||
|
|
||||||
rv = sqlite3.connect(app.config['DATABASE']) |
|
||||||
rv.row_factory = sqlite3.Row |
|
||||||
return rv |
|
||||||
|
|
||||||
This 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. |
|
||||||
You can create a simple database connection through SQLite and then tell |
|
||||||
it to use the :class:`sqlite3.Row` object to represent rows. This allows |
|
||||||
the rows to be treated as if they were dictionaries instead of tuples. |
|
||||||
|
|
||||||
In the next section you will see how to run the application. |
|
||||||
|
|
||||||
Continue with :ref:`tutorial-packaging`. |
|
@ -0,0 +1,72 @@ |
|||||||
|
Static Files |
||||||
|
============ |
||||||
|
|
||||||
|
The authentication views and templates work, but they look very plain |
||||||
|
right now. Some `CSS`_ can be added to add style to the HTML layout you |
||||||
|
constructed. The style won't change, so it's a *static* file rather than |
||||||
|
a template. |
||||||
|
|
||||||
|
Flask automatically adds a ``static`` view that takes a path relative |
||||||
|
to the ``flaskr/static`` directory and serves it. The ``base.html`` |
||||||
|
template already has a link to the ``style.css`` file: |
||||||
|
|
||||||
|
.. code-block:: html+jinja |
||||||
|
|
||||||
|
{{ url_for('static', filename='style.css') }} |
||||||
|
|
||||||
|
Besides CSS, other types of static files might be files with JavaScript |
||||||
|
functions, or a logo image. They are all placed under the |
||||||
|
``flaskr/static`` directory and referenced with |
||||||
|
``url_for('static', filename='...')``. |
||||||
|
|
||||||
|
This tutorial isn't focused on how to write CSS, so you can just copy |
||||||
|
the following into the ``flaskr/static/style.css`` file: |
||||||
|
|
||||||
|
.. code-block:: css |
||||||
|
:caption: ``flaskr/static/style.css`` |
||||||
|
|
||||||
|
html { font-family: sans-serif; background: #eee; padding: 1rem; } |
||||||
|
body { max-width: 960px; margin: 0 auto; background: white; } |
||||||
|
h1 { font-family: serif; color: #377ba8; margin: 1rem 0; } |
||||||
|
a { color: #377ba8; } |
||||||
|
hr { border: none; border-top: 1px solid lightgray; } |
||||||
|
nav { background: lightgray; display: flex; align-items: center; padding: 0 0.5rem; } |
||||||
|
nav h1 { flex: auto; margin: 0; } |
||||||
|
nav h1 a { text-decoration: none; padding: 0.25rem 0.5rem; } |
||||||
|
nav ul { display: flex; list-style: none; margin: 0; padding: 0; } |
||||||
|
nav ul li a, nav ul li span, header .action { display: block; padding: 0.5rem; } |
||||||
|
.content { padding: 0 1rem 1rem; } |
||||||
|
.content > header { border-bottom: 1px solid lightgray; display: flex; align-items: flex-end; } |
||||||
|
.content > header h1 { flex: auto; margin: 1rem 0 0.25rem 0; } |
||||||
|
.flash { margin: 1em 0; padding: 1em; background: #cae6f6; border: 1px solid #377ba8; } |
||||||
|
.post > header { display: flex; align-items: flex-end; font-size: 0.85em; } |
||||||
|
.post > header > div:first-of-type { flex: auto; } |
||||||
|
.post > header h1 { font-size: 1.5em; margin-bottom: 0; } |
||||||
|
.post .about { color: slategray; font-style: italic; } |
||||||
|
.post .body { white-space: pre-line; } |
||||||
|
.content:last-child { margin-bottom: 0; } |
||||||
|
.content form { margin: 1em 0; display: flex; flex-direction: column; } |
||||||
|
.content label { font-weight: bold; margin-bottom: 0.5em; } |
||||||
|
.content input, .content textarea { margin-bottom: 1em; } |
||||||
|
.content textarea { min-height: 12em; resize: vertical; } |
||||||
|
input.danger { color: #cc2f2e; } |
||||||
|
input[type=submit] { align-self: start; min-width: 10em; } |
||||||
|
|
||||||
|
You can find a less compact version of ``style.css`` in the |
||||||
|
:gh:`example code <examples/tutorial/flaskr/static/style.css>`. |
||||||
|
|
||||||
|
Go to http://127.0.0.1/auth/login and the page should look like the |
||||||
|
screenshot below. |
||||||
|
|
||||||
|
.. image:: flaskr_login.png |
||||||
|
:align: center |
||||||
|
:class: screenshot |
||||||
|
:alt: screenshot of login page |
||||||
|
|
||||||
|
You can read more about CSS from `Mozilla's documentation <CSS_>`_. If |
||||||
|
you change a static file, refresh the browser page. If the change |
||||||
|
doesn't show up, try clearing your browser's cache. |
||||||
|
|
||||||
|
.. _CSS: https://developer.mozilla.org/docs/Web/CSS |
||||||
|
|
||||||
|
Continue to :doc:`blog`. |
@ -1,113 +1,187 @@ |
|||||||
.. _tutorial-templates: |
.. currentmodule:: flask |
||||||
|
|
||||||
|
Templates |
||||||
|
========= |
||||||
|
|
||||||
|
You've written the authentication views for your application, but if |
||||||
|
you're running the server and try to go to any of the URLs, you'll see a |
||||||
|
``TemplateNotFound`` error. That's because the views are calling |
||||||
|
:func:`render_template`, but you haven't written the templates yet. |
||||||
|
The template files will be stored in the ``templates`` directory inside |
||||||
|
the ``flaskr`` package. |
||||||
|
|
||||||
|
Templates are files that contain static data as well as placeholders |
||||||
|
for dynamic data. A template is rendered with specific data to produce a |
||||||
|
final document. Flask uses the `Jinja`_ template library to render |
||||||
|
templates. |
||||||
|
|
||||||
|
In your application, you will use templates to render `HTML`_ which |
||||||
|
will display in the user's browser. In Flask, Jinja is configured to |
||||||
|
*autoescape* any data that is rendered in HTML templates. This means |
||||||
|
that it's safe to render user input; any characters they've entered that |
||||||
|
could mess with the HTML, such as ``<`` and ``>`` will be *escaped* with |
||||||
|
*safe* values that look the same in the browser but don't cause unwanted |
||||||
|
effects. |
||||||
|
|
||||||
|
Jinja looks and behaves mostly like Python. Special delimiters are used |
||||||
|
to distinguish Jinja syntax from the static data in the template. |
||||||
|
Anything between ``{{`` and ``}}`` is an expression that will be output |
||||||
|
to the final document. ``{%`` and ``%}`` denotes a control flow |
||||||
|
statement like ``if`` and ``for``. Unlike Python, blocks are denoted |
||||||
|
by start and end tags rather than indentation since static text within |
||||||
|
a block could change indentation. |
||||||
|
|
||||||
|
.. _Jinja: http://jinja.pocoo.org/docs/templates/ |
||||||
|
.. _HTML: https://developer.mozilla.org/docs/Web/HTML |
||||||
|
|
||||||
|
|
||||||
|
The Base Layout |
||||||
|
--------------- |
||||||
|
|
||||||
|
Each page in the application will have the same basic layout around a |
||||||
|
different body. Instead of writing the entire HTML structure in each |
||||||
|
template, each template will *extend* a base template and override |
||||||
|
specific sections. |
||||||
|
|
||||||
|
.. code-block:: html+jinja |
||||||
|
:caption: ``flaskr/templates/base.html`` |
||||||
|
|
||||||
Step 7: The Templates |
<!doctype html> |
||||||
===================== |
<title>{% block title %}{% endblock %} - Flaskr</title> |
||||||
|
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}"> |
||||||
|
<nav> |
||||||
|
<h1>Flaskr</h1> |
||||||
|
<ul> |
||||||
|
{% if g.user %} |
||||||
|
<li><span>{{ g.user['username'] }}</span> |
||||||
|
<li><a href="{{ url_for('auth.logout') }}">Log Out</a> |
||||||
|
{% else %} |
||||||
|
<li><a href="{{ url_for('auth.register') }}">Register</a> |
||||||
|
<li><a href="{{ url_for('auth.login') }}">Log In</a> |
||||||
|
{% endif %} |
||||||
|
</ul> |
||||||
|
</nav> |
||||||
|
<section class="content"> |
||||||
|
<header> |
||||||
|
{% block header %}{% endblock %} |
||||||
|
</header> |
||||||
|
{% for message in get_flashed_messages() %} |
||||||
|
<div class="flash">{{ message }}</div> |
||||||
|
{% endfor %} |
||||||
|
{% block content %}{% endblock %} |
||||||
|
</section> |
||||||
|
|
||||||
Now it is time to start working on the templates. As you may have |
:data:`g` is automatically available in templates. Based on if |
||||||
noticed, if you make requests with the app running, you will get |
``g.user`` is set (from ``load_logged_in_user``), either the username |
||||||
an exception that Flask cannot find the templates. The templates |
and a log out link are displayed, otherwise links to register and log in |
||||||
are using `Jinja2`_ syntax and have autoescaping enabled by |
are displayed. :func:`url_for` is also automatically available, and is |
||||||
default. This means that unless you mark a value in the code with |
used to generate URLs to views instead of writing them out manually. |
||||||
: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 |
After the page title, and before the content, the template loops over |
||||||
the layout of the website in all pages. |
each message returned by :func:`get_flashed_messages`. You used |
||||||
|
:func:`flash` in the views to show error messages, and this is the code |
||||||
|
that will display them. |
||||||
|
|
||||||
Create the follwing three HTML files and place them in the |
There are three blocks defined here that will be overridden in the other |
||||||
:file:`templates` folder: |
templates: |
||||||
|
|
||||||
.. _Jinja2: http://jinja.pocoo.org/docs/templates |
#. ``{% block title %}`` will change the title displayed in the |
||||||
|
browser's tab and window title. |
||||||
|
|
||||||
layout.html |
#. ``{% block header %}`` is similar to ``title`` but will change the |
||||||
----------- |
title displayed on the page. |
||||||
|
|
||||||
This template contains the HTML skeleton, the header and a link to log in |
#. ``{% block content %}`` is where the content of each page goes, such |
||||||
(or log out if the user was already logged in). It also displays the |
as the login form or a blog post. |
||||||
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 |
The base template is directly in the ``templates`` directory. To keep |
||||||
you can use that to check if the user is logged in or not. Note that in |
the others organized, the templates for a blueprint will be placed in a |
||||||
Jinja you can access missing attributes and items of objects / dicts which |
directory with the same name as the blueprint. |
||||||
makes the following code work, even if there is no ``'logged_in'`` key in |
|
||||||
the session: |
|
||||||
|
|
||||||
.. sourcecode:: html+jinja |
|
||||||
|
|
||||||
<!doctype html> |
Register |
||||||
<title>Flaskr</title> |
-------- |
||||||
<link rel=stylesheet type=text/css href="{{ url_for('static', filename='style.css') }}"> |
|
||||||
<div class=page> |
.. code-block:: html+jinja |
||||||
<h1>Flaskr</h1> |
:caption: ``flaskr/templates/auth/register.html`` |
||||||
<div class=metanav> |
|
||||||
{% if not session.logged_in %} |
{% extends 'base.html' %} |
||||||
<a href="{{ url_for('login') }}">log in</a> |
|
||||||
{% else %} |
{% block header %} |
||||||
<a href="{{ url_for('logout') }}">log out</a> |
<h1>{% block title %}Register{% endblock %}</h1> |
||||||
{% endif %} |
{% endblock %} |
||||||
</div> |
|
||||||
{% for message in get_flashed_messages() %} |
{% block content %} |
||||||
<div class=flash>{{ message }}</div> |
<form method="post"> |
||||||
{% endfor %} |
<label for="username">Username</label> |
||||||
{% block body %}{% endblock %} |
<input name="username" id="username" required> |
||||||
</div> |
<label for="password">Password</label> |
||||||
|
<input type="password" name="password" id="password" required> |
||||||
show_entries.html |
<input type="submit" value="Register"> |
||||||
----------------- |
</form> |
||||||
|
{% endblock %} |
||||||
This template extends the :file:`layout.html` template from above to display the |
|
||||||
messages. Note that the ``for`` loop iterates over the messages we passed |
``{% extends 'base.html' %}`` tells Jinja that this template should |
||||||
in with the :func:`~flask.render_template` function. Notice that the form is |
replace the blocks from the base template. All the rendered content must |
||||||
configured to submit to the `add_entry` view function and use ``POST`` as |
appear inside ``{% block %}`` tags that override blocks from the base |
||||||
HTTP method: |
template. |
||||||
|
|
||||||
.. sourcecode:: html+jinja |
A useful pattern used here is to place ``{% block title %}`` inside |
||||||
|
``{% block header %}``. This will set the title block and then output |
||||||
{% extends "layout.html" %} |
the value of it into the header block, so that both the window and page |
||||||
{% block body %} |
share the same title without writing it twice. |
||||||
{% if session.logged_in %} |
|
||||||
<form action="{{ url_for('add_entry') }}" method=post class=add-entry> |
The ``input`` tags are using the ``required`` attribute here. This tells |
||||||
<dl> |
the browser not to submit the form until those fields are filled in. If |
||||||
<dt>Title: |
the user is using an older browser that doesn't support that attribute, |
||||||
<dd><input type=text size=30 name=title> |
or if they are using something besides a browser to make requests, you |
||||||
<dt>Text: |
still want to validate the data in the Flask view. It's important to |
||||||
<dd><textarea name=text rows=5 cols=40></textarea> |
always fully validate the data on the server, even if the client does |
||||||
<dd><input type=submit value=Share> |
some validation as well. |
||||||
</dl> |
|
||||||
</form> |
|
||||||
{% endif %} |
Log In |
||||||
<ul class=entries> |
------ |
||||||
{% for entry in entries %} |
|
||||||
<li><h2>{{ entry.title }}</h2>{{ entry.text|safe }}</li> |
This is identical to the register template except for the title and |
||||||
{% else %} |
submit button. |
||||||
<li><em>Unbelievable. No entries here so far</em></li> |
|
||||||
{% endfor %} |
.. code-block:: html+jinja |
||||||
</ul> |
:caption: ``flaskr/templates/auth/login.html`` |
||||||
|
|
||||||
|
{% extends 'base.html' %} |
||||||
|
|
||||||
|
{% block header %} |
||||||
|
<h1>{% block title %}Log In{% endblock %}</h1> |
||||||
{% endblock %} |
{% endblock %} |
||||||
|
|
||||||
login.html |
{% block content %} |
||||||
---------- |
<form method="post"> |
||||||
|
<label for="username">Username</label> |
||||||
This is the login template, which basically just displays a form to allow |
<input name="username" id="username" required> |
||||||
the user to login: |
<label for="password">Password</label> |
||||||
|
<input type="password" name="password" id="password" required> |
||||||
.. sourcecode:: html+jinja |
<input type="submit" value="Log In"> |
||||||
|
|
||||||
{% 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> |
</form> |
||||||
{% endblock %} |
{% endblock %} |
||||||
|
|
||||||
Continue with :ref:`tutorial-css`. |
|
||||||
|
Register A User |
||||||
|
--------------- |
||||||
|
|
||||||
|
Now that the authentication templates are written, you can register a |
||||||
|
user. Make sure the server is still running (``flask run`` if it's not), |
||||||
|
then go to http://127.0.0.1:5000/auth/register. |
||||||
|
|
||||||
|
Try clicking the "Register" button without filling out the form and see |
||||||
|
that the browser shows an error message. Try removing the ``required`` |
||||||
|
attributes from the ``register.html`` template and click "Register" |
||||||
|
again. Instead of the browser showing an error, the page will reload and |
||||||
|
the error from :func:`flash` in the view will be shown. |
||||||
|
|
||||||
|
Fill out a username and password and you'll be redirected to the login |
||||||
|
page. Try entering an incorrect username, or the correct username and |
||||||
|
incorrect password. If you log in you'll get an error because there's |
||||||
|
no ``index`` view to redirect to yet. |
||||||
|
|
||||||
|
Continue to :doc:`static`. |
||||||
|
@ -1,96 +0,0 @@ |
|||||||
.. _tutorial-testing: |
|
||||||
|
|
||||||
Bonus: Testing the Application |
|
||||||
============================== |
|
||||||
|
|
||||||
Now that you have finished the application and everything works as |
|
||||||
expected, it's probably not a bad idea to add automated tests to simplify |
|
||||||
modifications in the future. The application above is used as a basic |
|
||||||
example of how to perform unit testing in the :ref:`testing` section of the |
|
||||||
documentation. Go there to see how easy it is to test Flask applications. |
|
||||||
|
|
||||||
Adding tests to flaskr |
|
||||||
---------------------- |
|
||||||
|
|
||||||
Assuming you have seen the :ref:`testing` section and have either written |
|
||||||
your own tests for ``flaskr`` or have followed along with the examples |
|
||||||
provided, you might be wondering about ways to organize the project. |
|
||||||
|
|
||||||
One possible and recommended project structure is:: |
|
||||||
|
|
||||||
flaskr/ |
|
||||||
flaskr/ |
|
||||||
__init__.py |
|
||||||
static/ |
|
||||||
templates/ |
|
||||||
tests/ |
|
||||||
test_flaskr.py |
|
||||||
setup.py |
|
||||||
MANIFEST.in |
|
||||||
|
|
||||||
For now go ahead a create the :file:`tests/` directory as well as the |
|
||||||
:file:`test_flaskr.py` file. |
|
||||||
|
|
||||||
Running the tests |
|
||||||
----------------- |
|
||||||
|
|
||||||
At this point you can run the tests. Here ``pytest`` will be used. |
|
||||||
|
|
||||||
.. note:: Make sure that ``pytest`` is installed in the same virtualenv |
|
||||||
as flaskr. Otherwise ``pytest`` test will not be able to import the |
|
||||||
required components to test the application:: |
|
||||||
|
|
||||||
pip install -e . |
|
||||||
pip install pytest |
|
||||||
|
|
||||||
Run and watch the tests pass, within the top-level :file:`flaskr/` |
|
||||||
directory as:: |
|
||||||
|
|
||||||
pytest |
|
||||||
|
|
||||||
Testing + setuptools |
|
||||||
-------------------- |
|
||||||
|
|
||||||
One way to handle testing is to integrate it with ``setuptools``. Here |
|
||||||
that requires adding a couple of lines to the :file:`setup.py` file and |
|
||||||
creating a new file :file:`setup.cfg`. One benefit of running the tests |
|
||||||
this way is that you do not have to install ``pytest``. Go ahead and |
|
||||||
update the :file:`setup.py` file to contain:: |
|
||||||
|
|
||||||
from setuptools import setup |
|
||||||
|
|
||||||
setup( |
|
||||||
name='flaskr', |
|
||||||
packages=['flaskr'], |
|
||||||
include_package_data=True, |
|
||||||
install_requires=[ |
|
||||||
'flask', |
|
||||||
], |
|
||||||
setup_requires=[ |
|
||||||
'pytest-runner', |
|
||||||
], |
|
||||||
tests_require=[ |
|
||||||
'pytest', |
|
||||||
], |
|
||||||
) |
|
||||||
|
|
||||||
Now create :file:`setup.cfg` in the project root (alongside |
|
||||||
:file:`setup.py`):: |
|
||||||
|
|
||||||
[aliases] |
|
||||||
test=pytest |
|
||||||
|
|
||||||
Now you can run:: |
|
||||||
|
|
||||||
python setup.py test |
|
||||||
|
|
||||||
This calls on the alias created in :file:`setup.cfg` which in turn runs |
|
||||||
``pytest`` via ``pytest-runner``, as the :file:`setup.py` script has |
|
||||||
been called. (Recall the `setup_requires` argument in :file:`setup.py`) |
|
||||||
Following the standard rules of test-discovery your tests will be |
|
||||||
found, run, and hopefully pass. |
|
||||||
|
|
||||||
This is one possible way to run and manage testing. Here ``pytest`` is |
|
||||||
used, but there are other options such as ``nose``. Integrating testing |
|
||||||
with ``setuptools`` is convenient because it is not necessary to actually |
|
||||||
download ``pytest`` or any other testing framework one might use. |
|
@ -0,0 +1,561 @@ |
|||||||
|
.. currentmodule:: flask |
||||||
|
|
||||||
|
Test Coverage |
||||||
|
============= |
||||||
|
|
||||||
|
Writing unit tests for your application lets you check that the code |
||||||
|
you wrote works the way you expect. Flask provides a test client that |
||||||
|
simulates requests to the application and returns the response data. |
||||||
|
|
||||||
|
You should test as much of your code as possible. Code in functions only |
||||||
|
runs when the function is called, and code in branches, such as ``if`` |
||||||
|
blocks, only runs when the condition is met. You want to make sure that |
||||||
|
each function is tested with data that covers each branch. |
||||||
|
|
||||||
|
The closer you get to 100% coverage, the more comfortable you can be |
||||||
|
that making a change won't unexpectedly change other behavior. However, |
||||||
|
100% coverage doesn't guarantee that your application doesn't have bugs. |
||||||
|
In particular, it doesn't test how the user interacts with the |
||||||
|
application in the browser. Despite this, test coverage is an important |
||||||
|
tool to use during development. |
||||||
|
|
||||||
|
.. note:: |
||||||
|
This is being introduced late in the tutorial, but in your future |
||||||
|
projects you should test as you develop. |
||||||
|
|
||||||
|
You'll use `pytest`_ and `coverage`_ to test and measure your code. |
||||||
|
Install them both: |
||||||
|
|
||||||
|
.. code-block:: none |
||||||
|
|
||||||
|
pip install pytest coverage |
||||||
|
|
||||||
|
.. _pytest: https://pytest.readthedocs.io/ |
||||||
|
.. _coverage: https://coverage.readthedocs.io/ |
||||||
|
|
||||||
|
|
||||||
|
Setup and Fixtures |
||||||
|
------------------ |
||||||
|
|
||||||
|
The test code is located in the ``tests`` directory. This directory is |
||||||
|
*next to* the ``flaskr`` package, not inside it. The |
||||||
|
``tests/conftest.py`` file contains setup functions called *fixtures* |
||||||
|
that each test will use. Tests are in Python modules that start with |
||||||
|
``test_``, and each test function in those modules also starts with |
||||||
|
``test_``. |
||||||
|
|
||||||
|
Each test will create a new temporary database file and populate some |
||||||
|
data that will be used in the tests. Write a SQL file to insert that |
||||||
|
data. |
||||||
|
|
||||||
|
.. code-block:: sql |
||||||
|
:caption: ``tests/data.sql`` |
||||||
|
|
||||||
|
INSERT INTO user (username, password) |
||||||
|
VALUES |
||||||
|
('test', 'pbkdf2:sha256:50000$TCI4GzcX$0de171a4f4dac32e3364c7ddc7c14f3e2fa61f2d17574483f7ffbb431b4acb2f'), |
||||||
|
('other', 'pbkdf2:sha256:50000$kJPKsz6N$d2d4784f1b030a9761f5ccaeeaca413f27f2ecb76d6168407af962ddce849f79'); |
||||||
|
|
||||||
|
INSERT INTO post (title, body, author_id, created) |
||||||
|
VALUES |
||||||
|
('test title', 'test' || x'0a' || 'body', 1, '2018-01-01 00:00:00'); |
||||||
|
|
||||||
|
The ``app`` fixture will call the factory and pass ``test_config`` to |
||||||
|
configure the application and database for testing instead of using your |
||||||
|
local development configuration. |
||||||
|
|
||||||
|
.. code-block:: python |
||||||
|
:caption: ``tests/conftest.py`` |
||||||
|
|
||||||
|
import os |
||||||
|
import tempfile |
||||||
|
|
||||||
|
import pytest |
||||||
|
from flaskr import create_app |
||||||
|
from flaskr.db import get_db, init_db |
||||||
|
|
||||||
|
with open(os.path.join(os.path.dirname(__file__), 'data.sql'), 'rb') as f: |
||||||
|
_data_sql = f.read().decode('utf8') |
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture |
||||||
|
def app(): |
||||||
|
db_fd, db_path = tempfile.mkstemp() |
||||||
|
|
||||||
|
app = create_app({ |
||||||
|
'TESTING': True, |
||||||
|
'DATABASE': db_path, |
||||||
|
}) |
||||||
|
|
||||||
|
with app.app_context(): |
||||||
|
init_db() |
||||||
|
get_db().executescript(_data_sql) |
||||||
|
|
||||||
|
yield app |
||||||
|
|
||||||
|
os.close(db_fd) |
||||||
|
os.unlink(db_path) |
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture |
||||||
|
def client(app): |
||||||
|
return app.test_client() |
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture |
||||||
|
def runner(app): |
||||||
|
return app.test_cli_runner() |
||||||
|
|
||||||
|
:func:`tempfile.mkstemp` creates and opens a temporary file, returning |
||||||
|
the file object and the path to it. The ``DATABASE`` path is |
||||||
|
overridden so it points to this temporary path instead of the instance |
||||||
|
folder. After setting the path, the database tables are created and the |
||||||
|
test data is inserted. After the test is over, the temporary file is |
||||||
|
closed and removed. |
||||||
|
|
||||||
|
:data:`TESTING` tells Flask that the app is in test mode. Flask changes |
||||||
|
some internal behavior so it's easier to test, and other extensions can |
||||||
|
also use the flag to make testing them easier. |
||||||
|
|
||||||
|
The ``client`` fixture calls |
||||||
|
:meth:`app.test_client() <Flask.test_client>` with the application |
||||||
|
object created by the ``app`` fixture. Tests will use the client to make |
||||||
|
requests to the application without running the server. |
||||||
|
|
||||||
|
The ``runner`` fixture is similar to ``client``. |
||||||
|
:meth:`app.test_cli_runner() <Flask.test_cli_runner>` creates a runner |
||||||
|
that can call the Click commands registered with the application. |
||||||
|
|
||||||
|
Pytest uses fixtures by matching their function names with the names |
||||||
|
of arguments in the test functions. For example, the ``test_hello`` |
||||||
|
function you'll write next takes a ``client`` argument. Pytest matches |
||||||
|
that with the ``client`` fixture function, calls it, and passes the |
||||||
|
returned value to the test function. |
||||||
|
|
||||||
|
|
||||||
|
Factory |
||||||
|
------- |
||||||
|
|
||||||
|
There's not much to test about the factory itself. Most of the code will |
||||||
|
be executed for each test already, so if something fails the other tests |
||||||
|
will notice. |
||||||
|
|
||||||
|
The only behavior that can change is passing test config. If config is |
||||||
|
not passed, there should be some default configuration, otherwise the |
||||||
|
configuration should be overridden. |
||||||
|
|
||||||
|
.. code-block:: python |
||||||
|
:caption: ``tests/test_factory.py`` |
||||||
|
|
||||||
|
from flaskr import create_app |
||||||
|
|
||||||
|
|
||||||
|
def test_config(): |
||||||
|
assert not create_app().testing |
||||||
|
assert create_app({'TESTING': True}).testing |
||||||
|
|
||||||
|
|
||||||
|
def test_hello(client): |
||||||
|
response = client.get('/hello') |
||||||
|
assert response.data == b'Hello, World!' |
||||||
|
|
||||||
|
You added the ``hello`` route as an example when writing the factory at |
||||||
|
the beginning of the tutorial. It returns "Hello, World!", so the test |
||||||
|
checks that the response data matches. |
||||||
|
|
||||||
|
|
||||||
|
Database |
||||||
|
-------- |
||||||
|
|
||||||
|
Within an application context, ``get_db`` should return the same |
||||||
|
connection each time it's called. After the context, the connection |
||||||
|
should be closed. |
||||||
|
|
||||||
|
.. code-block:: python |
||||||
|
:caption: ``tests/test_db.py`` |
||||||
|
|
||||||
|
import sqlite3 |
||||||
|
|
||||||
|
import pytest |
||||||
|
from flaskr.db import get_db |
||||||
|
|
||||||
|
|
||||||
|
def test_get_close_db(app): |
||||||
|
with app.app_context(): |
||||||
|
db = get_db() |
||||||
|
assert db is get_db() |
||||||
|
|
||||||
|
with pytest.raises(sqlite3.ProgrammingError) as e: |
||||||
|
db.execute('SELECT 1') |
||||||
|
|
||||||
|
assert 'closed' in str(e) |
||||||
|
|
||||||
|
The ``init-db`` command should call the ``init_db`` function and output |
||||||
|
a message. |
||||||
|
|
||||||
|
.. code-block:: python |
||||||
|
:caption: ``tests/test_db.py`` |
||||||
|
|
||||||
|
def test_init_db_command(runner, monkeypatch): |
||||||
|
class Recorder(object): |
||||||
|
called = False |
||||||
|
|
||||||
|
def fake_init_db(): |
||||||
|
Recorder.called = True |
||||||
|
|
||||||
|
monkeypatch.setattr('flaskr.db.init_db', fake_init_db) |
||||||
|
result = runner.invoke(args=['init-db']) |
||||||
|
assert 'Initialized' in result.output |
||||||
|
assert Recorder.called |
||||||
|
|
||||||
|
This test uses Pytest's ``monkeypatch`` fixture to replace the |
||||||
|
``init_db`` function with one that records that it's been called. The |
||||||
|
``runner`` fixture you wrote above is used to call the ``init-db`` |
||||||
|
command by name. |
||||||
|
|
||||||
|
|
||||||
|
Authentication |
||||||
|
-------------- |
||||||
|
|
||||||
|
For most of the views, a user needs to be logged in. The easiest way to |
||||||
|
do this in tests is to make a ``POST`` request to the ``login`` view |
||||||
|
with the client. Rather than writing that out every time, you can write |
||||||
|
a class with methods to do that, and use a fixture to pass it the client |
||||||
|
for each test. |
||||||
|
|
||||||
|
.. code-block:: python |
||||||
|
:caption: ``tests/conftest.py`` |
||||||
|
|
||||||
|
class AuthActions(object): |
||||||
|
def __init__(self, client): |
||||||
|
self._client = client |
||||||
|
|
||||||
|
def login(self, username='test', password='test'): |
||||||
|
return self._client.post( |
||||||
|
'/auth/login', |
||||||
|
data={'username': username, 'password': password} |
||||||
|
) |
||||||
|
|
||||||
|
def logout(self): |
||||||
|
return self._client.get('/auth/logout') |
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture |
||||||
|
def auth(client): |
||||||
|
return AuthActions(client) |
||||||
|
|
||||||
|
With the ``auth`` fixture, you can call ``auth.login()`` in a test to |
||||||
|
log in as the ``test`` user, which was inserted as part of the test |
||||||
|
data in the ``app`` fixture. |
||||||
|
|
||||||
|
The ``register`` view should render successfully on ``GET``. On ``POST`` |
||||||
|
with valid form data, it should redirect to the login URL and the user's |
||||||
|
data should be in the database. Invalid data should display error |
||||||
|
messages. |
||||||
|
|
||||||
|
.. code-block:: python |
||||||
|
:caption: ``tests/test_auth.py`` |
||||||
|
|
||||||
|
import pytest |
||||||
|
from flask import g, session |
||||||
|
from flaskr.db import get_db |
||||||
|
|
||||||
|
|
||||||
|
def test_register(client, app): |
||||||
|
assert client.get('/auth/register').status_code == 200 |
||||||
|
response = client.post( |
||||||
|
'/auth/register', data={'username': 'a', 'password': 'a'} |
||||||
|
) |
||||||
|
assert 'http://localhost/auth/login' == response.headers['Location'] |
||||||
|
|
||||||
|
with app.app_context(): |
||||||
|
assert get_db().execute( |
||||||
|
"select * from user where username = 'a'", |
||||||
|
).fetchone() is not None |
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(('username', 'password', 'message'), ( |
||||||
|
('', '', b'Username is required.'), |
||||||
|
('a', '', b'Password is required.'), |
||||||
|
('test', 'test', b'already registered'), |
||||||
|
)) |
||||||
|
def test_register_validate_input(client, username, password, message): |
||||||
|
response = client.post( |
||||||
|
'/auth/register', |
||||||
|
data={'username': username, 'password': password} |
||||||
|
) |
||||||
|
assert message in response.data |
||||||
|
|
||||||
|
:meth:`client.get() <werkzeug.test.Client.get>` makes a ``GET`` request |
||||||
|
and returns the :class:`Response` object returned by Flask. Similarly, |
||||||
|
:meth:`client.post() <werkzeug.test.Client.post>` makes a ``POST`` |
||||||
|
request, converting the ``data`` dict into form data. |
||||||
|
|
||||||
|
To test that the page renders successfully, a simple request is made and |
||||||
|
checked for a ``200 OK`` :attr:`~Response.status_code`. If |
||||||
|
rendering failed, Flask would return a ``500 Internal Server Error`` |
||||||
|
code. |
||||||
|
|
||||||
|
:attr:`~Response.headers` will have a ``Location`` header with the login |
||||||
|
URL when the register view redirects to the login view. |
||||||
|
|
||||||
|
:attr:`~Response.data` contains the body of the response as bytes. If |
||||||
|
you expect a certain value to render on the page, check that it's in |
||||||
|
``data``. Bytes must be compared to bytes. If you want to compare |
||||||
|
Unicode text, use :meth:`get_data(as_text=True) <werkzeug.wrappers.BaseResponse.get_data>` |
||||||
|
instead. |
||||||
|
|
||||||
|
``pytest.mark.parametrize`` tells Pytest to run the same test function |
||||||
|
with different arguments. You use it here to test different invalid |
||||||
|
input and error messages without writing the same code three times. |
||||||
|
|
||||||
|
The tests for the ``login`` view are very similar to those for |
||||||
|
``register``. Rather than testing the data in the database, |
||||||
|
:data:`session` should have ``user_id`` set after logging in. |
||||||
|
|
||||||
|
.. code-block:: python |
||||||
|
:caption: ``tests/test_auth.py`` |
||||||
|
|
||||||
|
def test_login(client, auth): |
||||||
|
assert client.get('/auth/login').status_code == 200 |
||||||
|
response = auth.login() |
||||||
|
assert response.headers['Location'] == 'http://localhost/' |
||||||
|
|
||||||
|
with client: |
||||||
|
client.get('/') |
||||||
|
assert session['user_id'] == 1 |
||||||
|
assert g.user['username'] == 'test' |
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(('username', 'password', 'message'), ( |
||||||
|
('a', 'test', b'Incorrect username.'), |
||||||
|
('test', 'a', b'Incorrect password.'), |
||||||
|
)) |
||||||
|
def test_login_validate_input(auth, username, password, message): |
||||||
|
response = auth.login(username, password) |
||||||
|
assert message in response.data |
||||||
|
|
||||||
|
Using ``client`` in a ``with`` block allows accessing context variables |
||||||
|
such as :data:`session` after the response is returned. Normally, |
||||||
|
accessing ``session`` outside of a request would raise an error. |
||||||
|
|
||||||
|
Testing ``logout`` is the opposite of ``login``. :data:`session` should |
||||||
|
not contain ``user_id`` after logging out. |
||||||
|
|
||||||
|
.. code-block:: python |
||||||
|
:caption: ``tests/test_auth.py`` |
||||||
|
|
||||||
|
def test_logout(client, auth): |
||||||
|
auth.login() |
||||||
|
|
||||||
|
with client: |
||||||
|
auth.logout() |
||||||
|
assert 'user_id' not in session |
||||||
|
|
||||||
|
|
||||||
|
Blog |
||||||
|
---- |
||||||
|
|
||||||
|
All the blog views use the ``auth`` fixture you wrote earlier. Call |
||||||
|
``auth.login()`` and subsequent requests from the client will be logged |
||||||
|
in as the ``test`` user. |
||||||
|
|
||||||
|
The ``index`` view should display information about the post that was |
||||||
|
added with the test data. When logged in as the author, there should be |
||||||
|
a link to edit the post. |
||||||
|
|
||||||
|
You can also test some more authentication behavior while testing the |
||||||
|
``index`` view. When not logged in, each page shows links to log in or |
||||||
|
register. When logged in, there's a link to log out. |
||||||
|
|
||||||
|
.. code-block:: python |
||||||
|
:caption: ``tests/test_blog.py`` |
||||||
|
|
||||||
|
import pytest |
||||||
|
from flaskr.db import get_db |
||||||
|
|
||||||
|
|
||||||
|
def test_index(client, auth): |
||||||
|
response = client.get('/') |
||||||
|
assert b"Log In" in response.data |
||||||
|
assert b"Register" in response.data |
||||||
|
|
||||||
|
auth.login() |
||||||
|
response = client.get('/') |
||||||
|
assert b'Log Out' in response.data |
||||||
|
assert b'test title' in response.data |
||||||
|
assert b'by test on 2018-01-01' in response.data |
||||||
|
assert b'test\nbody' in response.data |
||||||
|
assert b'href="/1/update"' in response.data |
||||||
|
|
||||||
|
A user must be logged in to access the ``create``, ``update``, and |
||||||
|
``delete`` views. The logged in user must be the author of the post to |
||||||
|
access ``update`` and ``delete``, otherwise a ``403 Forbidden`` status |
||||||
|
is returned. If a ``post`` with the given ``id`` doesn't exist, |
||||||
|
``update`` and ``delete`` should return ``404 Not Found``. |
||||||
|
|
||||||
|
.. code-block:: python |
||||||
|
:caption: ``tests/test_blog.py`` |
||||||
|
|
||||||
|
@pytest.mark.parametrize('path', ( |
||||||
|
'/create', |
||||||
|
'/1/update', |
||||||
|
'/1/delete', |
||||||
|
)) |
||||||
|
def test_login_required(client, path): |
||||||
|
response = client.post(path) |
||||||
|
assert response.headers['Location'] == 'http://localhost/auth/login' |
||||||
|
|
||||||
|
|
||||||
|
def test_author_required(app, client, auth): |
||||||
|
# change the post author to another user |
||||||
|
with app.app_context(): |
||||||
|
db = get_db() |
||||||
|
db.execute('UPDATE post SET author_id = 2 WHERE id = 1') |
||||||
|
db.commit() |
||||||
|
|
||||||
|
auth.login() |
||||||
|
# current user can't modify other user's post |
||||||
|
assert client.post('/1/update').status_code == 403 |
||||||
|
assert client.post('/1/delete').status_code == 403 |
||||||
|
# current user doesn't see edit link |
||||||
|
assert b'href="/1/update"' not in client.get('/').data |
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('path', ( |
||||||
|
'/2/update', |
||||||
|
'/2/delete', |
||||||
|
)) |
||||||
|
def test_exists_required(client, auth, path): |
||||||
|
auth.login() |
||||||
|
assert client.post(path).status_code == 404 |
||||||
|
|
||||||
|
The ``create`` and ``update`` views should render and return a |
||||||
|
``200 OK`` status for a ``GET`` request. When valid data is sent in a |
||||||
|
``POST`` request, ``create`` should insert the new post data into the |
||||||
|
database, and ``update`` should modify the existing data. Both pages |
||||||
|
should show an error message on invalid data. |
||||||
|
|
||||||
|
.. code-block:: python |
||||||
|
:caption: ``tests/test_blog.py`` |
||||||
|
|
||||||
|
def test_create(client, auth, app): |
||||||
|
auth.login() |
||||||
|
assert client.get('/create').status_code == 200 |
||||||
|
client.post('/create', data={'title': 'created', 'body': ''}) |
||||||
|
|
||||||
|
with app.app_context(): |
||||||
|
db = get_db() |
||||||
|
count = db.execute('SELECT COUNT(id) FROM post').fetchone()[0] |
||||||
|
assert count == 2 |
||||||
|
|
||||||
|
|
||||||
|
def test_update(client, auth, app): |
||||||
|
auth.login() |
||||||
|
assert client.get('/1/update').status_code == 200 |
||||||
|
client.post('/1/update', data={'title': 'updated', 'body': ''}) |
||||||
|
|
||||||
|
with app.app_context(): |
||||||
|
db = get_db() |
||||||
|
post = db.execute('SELECT * FROM post WHERE id = 1').fetchone() |
||||||
|
assert post['title'] == 'updated' |
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('path', ( |
||||||
|
'/create', |
||||||
|
'/1/update', |
||||||
|
)) |
||||||
|
def test_create_update_validate(client, auth, path): |
||||||
|
auth.login() |
||||||
|
response = client.post(path, data={'title': '', 'body': ''}) |
||||||
|
assert b'Title is required.' in response.data |
||||||
|
|
||||||
|
The ``delete`` view should redirect to the index URL and the post should |
||||||
|
no longer exist in the database. |
||||||
|
|
||||||
|
.. code-block:: python |
||||||
|
:caption: ``tests/test_blog.py`` |
||||||
|
|
||||||
|
def test_delete(client, auth, app): |
||||||
|
auth.login() |
||||||
|
response = client.post('/1/delete') |
||||||
|
assert response.headers['Location'] == 'http://localhost/' |
||||||
|
|
||||||
|
with app.app_context(): |
||||||
|
db = get_db() |
||||||
|
post = db.execute('SELECT * FROM post WHERE id = 1').fetchone() |
||||||
|
assert post is None |
||||||
|
|
||||||
|
|
||||||
|
Running the Tests |
||||||
|
----------------- |
||||||
|
|
||||||
|
Some extra configuration, which is not required but makes running |
||||||
|
tests with coverage less verbose, can be added to the project's |
||||||
|
``setup.cfg`` file. |
||||||
|
|
||||||
|
.. code-block:: none |
||||||
|
:caption: ``setup.cfg`` |
||||||
|
|
||||||
|
[tool:pytest] |
||||||
|
testpaths = tests |
||||||
|
|
||||||
|
[coverage:run] |
||||||
|
branch = True |
||||||
|
source = |
||||||
|
flaskr |
||||||
|
|
||||||
|
To run the tests, use the ``pytest`` command. It will find and run all |
||||||
|
the test functions you've written. |
||||||
|
|
||||||
|
.. code-block:: none |
||||||
|
|
||||||
|
pytest |
||||||
|
|
||||||
|
========================= test session starts ========================== |
||||||
|
platform linux -- Python 3.6.4, pytest-3.5.0, py-1.5.3, pluggy-0.6.0 |
||||||
|
rootdir: /home/user/Projects/flask-tutorial, inifile: setup.cfg |
||||||
|
collected 23 items |
||||||
|
|
||||||
|
tests/test_auth.py ........ [ 34%] |
||||||
|
tests/test_blog.py ............ [ 86%] |
||||||
|
tests/test_db.py .. [ 95%] |
||||||
|
tests/test_factory.py .. [100%] |
||||||
|
|
||||||
|
====================== 24 passed in 0.64 seconds ======================= |
||||||
|
|
||||||
|
If any tests fail, pytest will show the error that was raised. You can |
||||||
|
run ``pytest -v`` to get a list of each test function rather than dots. |
||||||
|
|
||||||
|
To measure the code coverage of your tests, use the ``coverage`` command |
||||||
|
to run pytest instead of running it directly. |
||||||
|
|
||||||
|
.. code-block:: none |
||||||
|
|
||||||
|
coverage run -m pytest |
||||||
|
|
||||||
|
You can either view a simple coverage report in the terminal: |
||||||
|
|
||||||
|
.. code-block:: none |
||||||
|
|
||||||
|
coverage report |
||||||
|
|
||||||
|
Name Stmts Miss Branch BrPart Cover |
||||||
|
------------------------------------------------------ |
||||||
|
flaskr/__init__.py 21 0 2 0 100% |
||||||
|
flaskr/auth.py 54 0 22 0 100% |
||||||
|
flaskr/blog.py 54 0 16 0 100% |
||||||
|
flaskr/db.py 24 0 4 0 100% |
||||||
|
------------------------------------------------------ |
||||||
|
TOTAL 153 0 44 0 100% |
||||||
|
|
||||||
|
An HTML report allows you to see which lines were covered in each file: |
||||||
|
|
||||||
|
.. code-block:: none |
||||||
|
|
||||||
|
coverage html |
||||||
|
|
||||||
|
This generates files in the ``htmlcov`` directory. Open |
||||||
|
``htmlcov/index.html`` in your browser to see the report. |
||||||
|
|
||||||
|
Continue to :doc:`deploy`. |
@ -1,118 +1,301 @@ |
|||||||
.. _tutorial-views: |
.. currentmodule:: flask |
||||||
|
|
||||||
Step 6: The View Functions |
Blueprints and Views |
||||||
========================== |
==================== |
||||||
|
|
||||||
Now that the database connections are working, you can start writing the |
A view function is the code you write to respond to requests to your |
||||||
view functions. You will need four of them; Show Entries, Add New Entry, |
application. Flask uses patterns to match the incoming request URL to |
||||||
Login and Logout. Add the following code snipets to :file:`flaskr.py`. |
the view that should handle it. The view returns data that Flask turns |
||||||
|
into an outgoing response. Flask can also go the other direction and |
||||||
Show Entries |
generate a URL to a view based on its name and arguments. |
||||||
------------ |
|
||||||
|
|
||||||
This view shows all the entries stored in the database. It listens on the |
Create a Blueprint |
||||||
root of the application and will select title and text from the database. |
------------------ |
||||||
The one with the highest id (the newest entry) will be on top. The rows |
|
||||||
returned from the cursor look a bit like dictionaries because we are using |
A :class:`Blueprint` is a way to organize a group of related views and |
||||||
the :class:`sqlite3.Row` row factory. |
other code. Rather than registering views and other code directly with |
||||||
|
an application, they are registered with a blueprint. Then the blueprint |
||||||
The view function will pass the entries to the :file:`show_entries.html` |
is registered with the application when it is available in the factory |
||||||
template and return the rendered one:: |
function. |
||||||
|
|
||||||
@app.route('/') |
Flaskr will have two blueprints, one for authentication functions and |
||||||
def show_entries(): |
one for the blog posts functions. The code for each blueprint will go |
||||||
db = get_db() |
in a separate module. Since the blog needs to know about authentication, |
||||||
cur = db.execute('select title, text from entries order by id desc') |
you'll write the authentication one first. |
||||||
entries = cur.fetchall() |
|
||||||
return render_template('show_entries.html', entries=entries) |
.. code-block:: python |
||||||
|
:caption: ``flaskr/auth.py`` |
||||||
Add New Entry |
|
||||||
------------- |
import functools |
||||||
|
|
||||||
This view lets the user add new entries if they are logged in. This only |
from flask import ( |
||||||
responds to ``POST`` requests; the actual form is shown on the |
Blueprint, flash, g, redirect, render_template, request, session, url_for |
||||||
`show_entries` page. If everything worked out well, it will |
) |
||||||
:func:`~flask.flash` an information message to the next request and |
from werkzeug.security import check_password_hash, generate_password_hash |
||||||
redirect back to the `show_entries` page:: |
|
||||||
|
from flaskr.db import get_db |
||||||
@app.route('/add', methods=['POST']) |
|
||||||
def add_entry(): |
bp = Blueprint('auth', __name__, url_prefix='/auth') |
||||||
if not session.get('logged_in'): |
|
||||||
abort(401) |
This creates a :class:`Blueprint` named ``'auth'``. Like the application |
||||||
db = get_db() |
object, the blueprint needs to know where it's defined, so ``__name__`` |
||||||
db.execute('insert into entries (title, text) values (?, ?)', |
is passed as the second argument. The ``url_prefix`` will be prepended |
||||||
[request.form['title'], request.form['text']]) |
to all the URLs associated with the blueprint. |
||||||
db.commit() |
|
||||||
flash('New entry was successfully posted') |
Import and register the blueprint from the factory using |
||||||
return redirect(url_for('show_entries')) |
:meth:`app.register_blueprint() <Flask.register_blueprint>`. Place the |
||||||
|
new code at the end of the factory function before returning the app. |
||||||
Note that this view checks that the user is logged in (that is, if the |
|
||||||
`logged_in` key is present in the session and ``True``). |
.. code-block:: python |
||||||
|
:caption: ``flaskr/__init__.py`` |
||||||
.. admonition:: Security Note |
|
||||||
|
def create_app(): |
||||||
Be sure to use question marks when building SQL statements, as done in the |
app = ... |
||||||
example above. Otherwise, your app will be vulnerable to SQL injection when |
# existing code omitted |
||||||
you use string formatting to build SQL statements. |
|
||||||
See :ref:`sqlite3` for more. |
from . import auth |
||||||
|
app.register_blueprint(auth.bp) |
||||||
Login and Logout |
|
||||||
---------------- |
return app |
||||||
|
|
||||||
These functions are used to sign the user in and out. Login checks the |
The authentication blueprint will have views to register new users and |
||||||
username and password against the ones from the configuration and sets the |
to log in and log out. |
||||||
`logged_in` key for 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 addition, a message is flashed that informs the user that he or |
The First View: Register |
||||||
she was logged in successfully. If an error occurred, the template is |
------------------------ |
||||||
notified about that, and the user is asked again:: |
|
||||||
|
When the user visits the ``/auth/register`` URL, the ``register`` view |
||||||
@app.route('/login', methods=['GET', 'POST']) |
will return `HTML`_ with a form for them to fill out. When they submit |
||||||
|
the form, it will validate their input and either show the form again |
||||||
|
with an error message or create the new user and go to the login page. |
||||||
|
|
||||||
|
.. _HTML: https://developer.mozilla.org/docs/Web/HTML |
||||||
|
|
||||||
|
For now you will just write the view code. On the next page, you'll |
||||||
|
write templates to generate the HTML form. |
||||||
|
|
||||||
|
.. code-block:: python |
||||||
|
:caption: ``flaskr/auth.py`` |
||||||
|
|
||||||
|
@bp.route('/register', methods=('GET', 'POST')) |
||||||
|
def register(): |
||||||
|
if request.method == 'POST': |
||||||
|
username = request.form['username'] |
||||||
|
password = request.form['password'] |
||||||
|
db = get_db() |
||||||
|
error = None |
||||||
|
|
||||||
|
if not username: |
||||||
|
error = 'Username is required.' |
||||||
|
elif not password: |
||||||
|
error = 'Password is required.' |
||||||
|
elif db.execute( |
||||||
|
'SELECT id FROM user WHERE username = ?', (username,) |
||||||
|
).fetchone() is not None: |
||||||
|
error = 'User {} is already registered.'.format(username) |
||||||
|
|
||||||
|
if error is None: |
||||||
|
db.execute( |
||||||
|
'INSERT INTO user (username, password) VALUES (?, ?)', |
||||||
|
(username, generate_password_hash(password)) |
||||||
|
) |
||||||
|
db.commit() |
||||||
|
return redirect(url_for('auth.login')) |
||||||
|
|
||||||
|
flash(error) |
||||||
|
|
||||||
|
return render_template('auth/register.html') |
||||||
|
|
||||||
|
Here's what the ``register`` view function is doing: |
||||||
|
|
||||||
|
#. :meth:`@bp.route <Blueprint.route>` associates the URL ``/register`` |
||||||
|
with the ``register`` view function. When Flask receives a request |
||||||
|
to ``/auth/register``, it will call the ``register`` view and use |
||||||
|
the return value as the response. |
||||||
|
|
||||||
|
#. If the user submitted the form, |
||||||
|
:attr:`request.method <Request.method>` will be ``'POST'``. In this |
||||||
|
case, start validating the input. |
||||||
|
|
||||||
|
#. :attr:`request.form <Request.form>` is a special type of |
||||||
|
:class:`dict` mapping submitted form keys and values. The user will |
||||||
|
input their ``username`` and ``password``. |
||||||
|
|
||||||
|
#. Validate that ``username`` and ``password`` are not empty. |
||||||
|
|
||||||
|
#. Validate that ``username`` is not already registered by querying the |
||||||
|
database and checking if a result is returned. |
||||||
|
:meth:`db.execute <sqlite3.Connection.execute>` takes a SQL query |
||||||
|
with ``?`` placeholders for any user input, and a tuple of values |
||||||
|
to replace the placeholders with. The database library will take |
||||||
|
care of escaping the values so you are not vulnerable to a |
||||||
|
*SQL injection attack*. |
||||||
|
|
||||||
|
:meth:`~sqlite3.Cursor.fetchone` returns one row from the query. |
||||||
|
If the query returned no results, it returns ``None``. Later, |
||||||
|
:meth:`~sqlite3.Cursor.fetchall` is used, which returns a list of |
||||||
|
all results. |
||||||
|
|
||||||
|
#. If validation succeeds, insert the new user data into the database. |
||||||
|
For security, passwords should never be stored in the database |
||||||
|
directly. Instead, |
||||||
|
:func:`~werkzeug.security.generate_password_hash` is used to |
||||||
|
securely hash the password, and that hash is stored. Since this |
||||||
|
query modifies data, :meth:`db.commit() <sqlite3.Connection.commit>` |
||||||
|
needs to be called afterwards to save the changes. |
||||||
|
|
||||||
|
#. After storing the user, they are redirected to the login page. |
||||||
|
:func:`url_for` generates the URL for the login view based on its |
||||||
|
name. This is preferable to writing the URL directly as it allows |
||||||
|
you to change the URL later without changing all code that links to |
||||||
|
it. :func:`redirect` generates a redirect response to the generated |
||||||
|
URL. |
||||||
|
|
||||||
|
#. If validation fails, the error is shown to the user. :func:`flash` |
||||||
|
stores messages that can be retrieved when rendering the template. |
||||||
|
|
||||||
|
#. When the user initially navigates to ``auth/register``, or |
||||||
|
there was an validation error, an HTML page with the registration |
||||||
|
form should be shown. :func:`render_template` will render a template |
||||||
|
containing the HTML, which you'll write in the next step of the |
||||||
|
tutorial. |
||||||
|
|
||||||
|
|
||||||
|
Login |
||||||
|
----- |
||||||
|
|
||||||
|
This view follows the same pattern as the ``register`` view above. |
||||||
|
|
||||||
|
.. code-block:: python |
||||||
|
:caption: ``flaskr/auth.py`` |
||||||
|
|
||||||
|
@bp.route('/login', methods=('GET', 'POST')) |
||||||
def login(): |
def login(): |
||||||
error = None |
|
||||||
if request.method == 'POST': |
if request.method == 'POST': |
||||||
if request.form['username'] != app.config['USERNAME']: |
username = request.form['username'] |
||||||
error = 'Invalid username' |
password = request.form['password'] |
||||||
elif request.form['password'] != app.config['PASSWORD']: |
db = get_db() |
||||||
error = 'Invalid password' |
error = None |
||||||
else: |
user = db.execute( |
||||||
session['logged_in'] = True |
'SELECT * FROM user WHERE username = ?', (username,) |
||||||
flash('You were logged in') |
).fetchone() |
||||||
return redirect(url_for('show_entries')) |
|
||||||
return render_template('login.html', error=error) |
if user is None: |
||||||
|
error = 'Incorrect username.' |
||||||
The `logout` function, on the other hand, removes that key from the session |
elif not check_password_hash(user['password'], password): |
||||||
again. There is a neat trick here: if you use the :meth:`~dict.pop` method |
error = 'Incorrect password.' |
||||||
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 |
if error is None: |
||||||
key is not in there. This is helpful because now it is not necessary to |
session.clear() |
||||||
check if the user was logged in. |
session['user_id'] = user['id'] |
||||||
|
return redirect(url_for('index')) |
||||||
:: |
|
||||||
|
flash(error) |
||||||
@app.route('/logout') |
|
||||||
|
return render_template('auth/login.html') |
||||||
|
|
||||||
|
There are a few differences from the ``register`` view: |
||||||
|
|
||||||
|
#. The user is queried first and stored in a variable for later use. |
||||||
|
|
||||||
|
#. :func:`~werkzeug.security.check_password_hash` hashes the submitted |
||||||
|
password in the same way as the stored hash and securely compares |
||||||
|
them. If they match, the password is valid. |
||||||
|
|
||||||
|
#. :data:`session` is a :class:`dict` that stores data across requests. |
||||||
|
When validation succeeds, the user's ``id`` is stored in a new |
||||||
|
session. The data is stored in a *cookie* that is sent to the |
||||||
|
browser, and the browser then sends it back with subsequent requests. |
||||||
|
Flask securely *signs* the data so that it can't be tampered with. |
||||||
|
|
||||||
|
Now that the user's ``id`` is stored in the :data:`session`, it will be |
||||||
|
available on subsequent requests. At the beginning of each request, if |
||||||
|
a user is logged in their information should be loaded and made |
||||||
|
available to other views. |
||||||
|
|
||||||
|
.. code-block:: python |
||||||
|
:caption: ``flaskr/auth.py`` |
||||||
|
|
||||||
|
@bp.before_app_request |
||||||
|
def load_logged_in_user(): |
||||||
|
user_id = session.get('user_id') |
||||||
|
|
||||||
|
if user_id is None: |
||||||
|
g.user = None |
||||||
|
else: |
||||||
|
g.user = get_db().execute( |
||||||
|
'SELECT * FROM user WHERE id = ?', (user_id,) |
||||||
|
).fetchone() |
||||||
|
|
||||||
|
:meth:`bp.before_app_request() <Blueprint.before_app_request>` registers |
||||||
|
a function that runs before the view function, no matter what URL is |
||||||
|
requested. ``load_logged_in_user`` checks if a user id is stored in the |
||||||
|
:data:`session` and gets that user's data from the database, storing it |
||||||
|
on :data:`g.user <g>`, which lasts for the length of the request. If |
||||||
|
there is no user id, or if the id doesn't exist, ``g.user`` will be |
||||||
|
``None``. |
||||||
|
|
||||||
|
|
||||||
|
Logout |
||||||
|
------ |
||||||
|
|
||||||
|
To log out, you need to remove the user id from the :data:`session`. |
||||||
|
Then ``load_logged_in_user`` won't load a user on subsequent requests. |
||||||
|
|
||||||
|
.. code-block:: python |
||||||
|
:caption: ``flaskr/auth.py`` |
||||||
|
|
||||||
|
@bp.route('/logout') |
||||||
def logout(): |
def logout(): |
||||||
session.pop('logged_in', None) |
session.clear() |
||||||
flash('You were logged out') |
return redirect(url_for('index')) |
||||||
return redirect(url_for('show_entries')) |
|
||||||
|
|
||||||
|
Require Authentication in Other Views |
||||||
|
------------------------------------- |
||||||
|
|
||||||
|
Creating, editing, and deleting blog posts will require a user to be |
||||||
|
logged in. A *decorator* can be used to check this for each view it's |
||||||
|
applied to. |
||||||
|
|
||||||
|
.. code-block:: python |
||||||
|
:caption: ``flaskr/auth.py`` |
||||||
|
|
||||||
|
def login_required(view): |
||||||
|
@functools.wraps(view) |
||||||
|
def wrapped_view(**kwargs): |
||||||
|
if g.user is None: |
||||||
|
return redirect(url_for('auth.login')) |
||||||
|
|
||||||
|
return view(**kwargs) |
||||||
|
|
||||||
.. admonition:: Security Note |
return wrapped_view |
||||||
|
|
||||||
Passwords should never be stored in plain text in a production |
This decorator returns a new view function that wraps the original view |
||||||
system. This tutorial uses plain text passwords for simplicity. If you |
it's applied to. The new function checks if a user is loaded and |
||||||
plan to release a project based off this tutorial out into the world, |
redirects to the login page otherwise. If a user is loaded the original |
||||||
passwords should be both `hashed and salted`_ before being stored in a |
view is called and continues normally. You'll use this decorator when |
||||||
database or file. |
writing the blog views. |
||||||
|
|
||||||
Fortunately, there are Flask extensions for the purpose of |
Endpoints and URLs |
||||||
hashing passwords and verifying passwords against hashes, so adding |
------------------ |
||||||
this functionality is fairly straight forward. There are also |
|
||||||
many general python libraries that can be used for hashing. |
|
||||||
|
|
||||||
You can find a list of recommended Flask extensions |
The :func:`url_for` function generates the URL to a view based on a name |
||||||
`here <http://flask.pocoo.org/extensions/>`_ |
and arguments. The name associated with a view is also called the |
||||||
|
*endpoint*, and by default it's the same as the name of the view |
||||||
|
function. |
||||||
|
|
||||||
|
For example, the ``hello()`` view that was added to the app |
||||||
|
factory earlier in the tutorial has the name ``'hello'`` and can be |
||||||
|
linked to with ``url_for('hello')``. If it took an argument, which |
||||||
|
you'll see later, it would be linked to using |
||||||
|
``url_for('hello', who='World')``. |
||||||
|
|
||||||
Continue with :ref:`tutorial-templates`. |
When using a blueprint, the name of the blueprint is prepended to the |
||||||
|
name of the function, so the endpoint for the ``login`` function you |
||||||
|
wrote above is ``'auth.login'`` because you added it to the ``'auth'`` |
||||||
|
blueprint. |
||||||
|
|
||||||
.. _hashed and salted: https://blog.codinghorror.com/youre-probably-storing-passwords-incorrectly/ |
Continue to :doc:`templates`. |
||||||
|
@ -1,19 +0,0 @@ |
|||||||
# -*- coding: utf-8 -*- |
|
||||||
""" |
|
||||||
Blueprint Example |
|
||||||
~~~~~~~~~~~~~~~~~ |
|
||||||
|
|
||||||
:copyright: © 2010 by the Pallets team. |
|
||||||
:license: BSD, see LICENSE for more details. |
|
||||||
""" |
|
||||||
|
|
||||||
from flask import Flask |
|
||||||
from simple_page.simple_page import simple_page |
|
||||||
|
|
||||||
app = Flask(__name__) |
|
||||||
app.register_blueprint(simple_page) |
|
||||||
# Blueprint can be registered many times |
|
||||||
app.register_blueprint(simple_page, url_prefix='/pages') |
|
||||||
|
|
||||||
if __name__=='__main__': |
|
||||||
app.run() |
|
@ -1,13 +0,0 @@ |
|||||||
from flask import Blueprint, render_template, abort |
|
||||||
from jinja2 import TemplateNotFound |
|
||||||
|
|
||||||
simple_page = Blueprint('simple_page', __name__, |
|
||||||
template_folder='templates') |
|
||||||
|
|
||||||
@simple_page.route('/', defaults={'page': 'index'}) |
|
||||||
@simple_page.route('/<page>') |
|
||||||
def show(page): |
|
||||||
try: |
|
||||||
return render_template('pages/%s.html' % page) |
|
||||||
except TemplateNotFound: |
|
||||||
abort(404) |
|
@ -1,5 +0,0 @@ |
|||||||
{% extends "pages/layout.html" %} |
|
||||||
|
|
||||||
{% block body %} |
|
||||||
Hello |
|
||||||
{% endblock %} |
|
@ -1,5 +0,0 @@ |
|||||||
{% extends "pages/layout.html" %} |
|
||||||
|
|
||||||
{% block body %} |
|
||||||
Blueprint example page |
|
||||||
{% endblock %} |
|
@ -1,20 +0,0 @@ |
|||||||
<!doctype html> |
|
||||||
<title>Simple Page Blueprint</title> |
|
||||||
<div class="page"> |
|
||||||
<h1>This is blueprint example</h1> |
|
||||||
<p> |
|
||||||
A simple page blueprint is registered under / and /pages |
|
||||||
you can access it using this URLs: |
|
||||||
<ul> |
|
||||||
<li><a href="{{ url_for('simple_page.show', page='hello') }}">/hello</a> |
|
||||||
<li><a href="{{ url_for('simple_page.show', page='world') }}">/world</a> |
|
||||||
</ul> |
|
||||||
<p> |
|
||||||
Also you can register the same blueprint under another path |
|
||||||
<ul> |
|
||||||
<li><a href="/pages/hello">/pages/hello</a> |
|
||||||
<li><a href="/pages/world">/pages/world</a> |
|
||||||
</ul> |
|
||||||
|
|
||||||
{% block body %}{% endblock %} |
|
||||||
</div> |
|
@ -1,4 +0,0 @@ |
|||||||
{% extends "pages/layout.html" %} |
|
||||||
{% block body %} |
|
||||||
World |
|
||||||
{% endblock %} |
|
@ -1,35 +0,0 @@ |
|||||||
# -*- coding: utf-8 -*- |
|
||||||
""" |
|
||||||
Blueprint Example Tests |
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~ |
|
||||||
|
|
||||||
:copyright: © 2010 by the Pallets team. |
|
||||||
:license: BSD, see LICENSE for more details. |
|
||||||
""" |
|
||||||
|
|
||||||
import pytest |
|
||||||
|
|
||||||
import blueprintexample |
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture |
|
||||||
def client(): |
|
||||||
return blueprintexample.app.test_client() |
|
||||||
|
|
||||||
|
|
||||||
def test_urls(client): |
|
||||||
r = client.get('/') |
|
||||||
assert r.status_code == 200 |
|
||||||
|
|
||||||
r = client.get('/hello') |
|
||||||
assert r.status_code == 200 |
|
||||||
|
|
||||||
r = client.get('/world') |
|
||||||
assert r.status_code == 200 |
|
||||||
|
|
||||||
# second blueprint instance |
|
||||||
r = client.get('/pages/hello') |
|
||||||
assert r.status_code == 200 |
|
||||||
|
|
||||||
r = client.get('/pages/world') |
|
||||||
assert r.status_code == 200 |
|
@ -1,40 +0,0 @@ |
|||||||
/ Flaskr / |
|
||||||
|
|
||||||
a minimal blog application |
|
||||||
|
|
||||||
|
|
||||||
~ What is Flaskr? |
|
||||||
|
|
||||||
A sqlite powered thumble blog application |
|
||||||
|
|
||||||
~ How do I use it? |
|
||||||
|
|
||||||
1. edit the configuration in the factory.py file or |
|
||||||
export a FLASKR_SETTINGS environment variable |
|
||||||
pointing to a configuration file or pass in a |
|
||||||
dictionary with config values using the create_app |
|
||||||
function. |
|
||||||
|
|
||||||
2. install the app from the root of the project directory |
|
||||||
|
|
||||||
pip install --editable . |
|
||||||
|
|
||||||
3. instruct flask to use the right application |
|
||||||
|
|
||||||
export FLASK_APP="flaskr.factory:create_app()" |
|
||||||
|
|
||||||
4. initialize the database with this command: |
|
||||||
|
|
||||||
flask initdb |
|
||||||
|
|
||||||
5. now you can run flaskr: |
|
||||||
|
|
||||||
flask run |
|
||||||
|
|
||||||
the application will greet you on |
|
||||||
http://localhost:5000/ |
|
||||||
|
|
||||||
~ Is it tested? |
|
||||||
|
|
||||||
You betcha. Run `python setup.py test` to see |
|
||||||
the tests pass. |
|
@ -1,85 +0,0 @@ |
|||||||
# -*- coding: utf-8 -*- |
|
||||||
""" |
|
||||||
Flaskr |
|
||||||
~~~~~~ |
|
||||||
|
|
||||||
A microblog example application written as Flask tutorial with |
|
||||||
Flask and sqlite3. |
|
||||||
|
|
||||||
:copyright: © 2010 by the Pallets team. |
|
||||||
:license: BSD, see LICENSE for more details. |
|
||||||
""" |
|
||||||
|
|
||||||
from sqlite3 import dbapi2 as sqlite3 |
|
||||||
from flask import Blueprint, request, session, g, redirect, url_for, abort, \ |
|
||||||
render_template, flash, current_app |
|
||||||
|
|
||||||
|
|
||||||
# create our blueprint :) |
|
||||||
bp = Blueprint('flaskr', __name__) |
|
||||||
|
|
||||||
|
|
||||||
def connect_db(): |
|
||||||
"""Connects to the specific database.""" |
|
||||||
rv = sqlite3.connect(current_app.config['DATABASE']) |
|
||||||
rv.row_factory = sqlite3.Row |
|
||||||
return rv |
|
||||||
|
|
||||||
|
|
||||||
def init_db(): |
|
||||||
"""Initializes the database.""" |
|
||||||
db = get_db() |
|
||||||
with current_app.open_resource('schema.sql', mode='r') as f: |
|
||||||
db.cursor().executescript(f.read()) |
|
||||||
db.commit() |
|
||||||
|
|
||||||
|
|
||||||
def get_db(): |
|
||||||
"""Opens a new database connection if there is none yet for the |
|
||||||
current application context. |
|
||||||
""" |
|
||||||
if not hasattr(g, 'sqlite_db'): |
|
||||||
g.sqlite_db = connect_db() |
|
||||||
return g.sqlite_db |
|
||||||
|
|
||||||
|
|
||||||
@bp.route('/') |
|
||||||
def show_entries(): |
|
||||||
db = get_db() |
|
||||||
cur = db.execute('select title, text from entries order by id desc') |
|
||||||
entries = cur.fetchall() |
|
||||||
return render_template('show_entries.html', entries=entries) |
|
||||||
|
|
||||||
|
|
||||||
@bp.route('/add', methods=['POST']) |
|
||||||
def add_entry(): |
|
||||||
if not session.get('logged_in'): |
|
||||||
abort(401) |
|
||||||
db = get_db() |
|
||||||
db.execute('insert into entries (title, text) values (?, ?)', |
|
||||||
[request.form['title'], request.form['text']]) |
|
||||||
db.commit() |
|
||||||
flash('New entry was successfully posted') |
|
||||||
return redirect(url_for('flaskr.show_entries')) |
|
||||||
|
|
||||||
|
|
||||||
@bp.route('/login', methods=['GET', 'POST']) |
|
||||||
def login(): |
|
||||||
error = None |
|
||||||
if request.method == 'POST': |
|
||||||
if request.form['username'] != current_app.config['USERNAME']: |
|
||||||
error = 'Invalid username' |
|
||||||
elif request.form['password'] != current_app.config['PASSWORD']: |
|
||||||
error = 'Invalid password' |
|
||||||
else: |
|
||||||
session['logged_in'] = True |
|
||||||
flash('You were logged in') |
|
||||||
return redirect(url_for('flaskr.show_entries')) |
|
||||||
return render_template('login.html', error=error) |
|
||||||
|
|
||||||
|
|
||||||
@bp.route('/logout') |
|
||||||
def logout(): |
|
||||||
session.pop('logged_in', None) |
|
||||||
flash('You were logged out') |
|
||||||
return redirect(url_for('flaskr.show_entries')) |
|
@ -1,64 +0,0 @@ |
|||||||
# -*- coding: utf-8 -*- |
|
||||||
""" |
|
||||||
Flaskr |
|
||||||
~~~~~~ |
|
||||||
|
|
||||||
A microblog example application written as Flask tutorial with |
|
||||||
Flask and sqlite3. |
|
||||||
|
|
||||||
:copyright: © 2010 by the Pallets team. |
|
||||||
:license: BSD, see LICENSE for more details. |
|
||||||
""" |
|
||||||
|
|
||||||
import os |
|
||||||
from flask import Flask, g |
|
||||||
from werkzeug.utils import find_modules, import_string |
|
||||||
from flaskr.blueprints.flaskr import init_db |
|
||||||
|
|
||||||
|
|
||||||
def create_app(config=None): |
|
||||||
app = Flask('flaskr') |
|
||||||
|
|
||||||
app.config.update(dict( |
|
||||||
DATABASE=os.path.join(app.root_path, 'flaskr.db'), |
|
||||||
DEBUG=True, |
|
||||||
SECRET_KEY=b'_5#y2L"F4Q8z\n\xec]/', |
|
||||||
USERNAME='admin', |
|
||||||
PASSWORD='default' |
|
||||||
)) |
|
||||||
app.config.update(config or {}) |
|
||||||
app.config.from_envvar('FLASKR_SETTINGS', silent=True) |
|
||||||
|
|
||||||
register_blueprints(app) |
|
||||||
register_cli(app) |
|
||||||
register_teardowns(app) |
|
||||||
|
|
||||||
return app |
|
||||||
|
|
||||||
|
|
||||||
def register_blueprints(app): |
|
||||||
"""Register all blueprint modules |
|
||||||
|
|
||||||
Reference: Armin Ronacher, "Flask for Fun and for Profit" PyBay 2016. |
|
||||||
""" |
|
||||||
for name in find_modules('flaskr.blueprints'): |
|
||||||
mod = import_string(name) |
|
||||||
if hasattr(mod, 'bp'): |
|
||||||
app.register_blueprint(mod.bp) |
|
||||||
return None |
|
||||||
|
|
||||||
|
|
||||||
def register_cli(app): |
|
||||||
@app.cli.command('initdb') |
|
||||||
def initdb_command(): |
|
||||||
"""Creates the database tables.""" |
|
||||||
init_db() |
|
||||||
print('Initialized the database.') |
|
||||||
|
|
||||||
|
|
||||||
def register_teardowns(app): |
|
||||||
@app.teardown_appcontext |
|
||||||
def close_db(error): |
|
||||||
"""Closes the database again at the end of the request.""" |
|
||||||
if hasattr(g, 'sqlite_db'): |
|
||||||
g.sqlite_db.close() |
|
@ -1,6 +0,0 @@ |
|||||||
drop table if exists entries; |
|
||||||
create table entries ( |
|
||||||
id integer primary key autoincrement, |
|
||||||
title text not null, |
|
||||||
'text' text not null |
|
||||||
); |
|
@ -1,18 +0,0 @@ |
|||||||
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; } |
|
@ -1,17 +0,0 @@ |
|||||||
<!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('flaskr.login') }}">log in</a> |
|
||||||
{% else %} |
|
||||||
<a href="{{ url_for('flaskr.logout') }}">log out</a> |
|
||||||
{% endif %} |
|
||||||
</div> |
|
||||||
{% for message in get_flashed_messages() %} |
|
||||||
<div class="flash">{{ message }}</div> |
|
||||||
{% endfor %} |
|
||||||
{% block body %}{% endblock %} |
|
||||||
</div> |
|
@ -1,14 +0,0 @@ |
|||||||
{% extends "layout.html" %} |
|
||||||
{% block body %} |
|
||||||
<h2>Login</h2> |
|
||||||
{% if error %}<p class="error"><strong>Error:</strong> {{ error }}{% endif %} |
|
||||||
<form action="{{ url_for('flaskr.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 %} |
|
@ -1,21 +0,0 @@ |
|||||||
{% extends "layout.html" %} |
|
||||||
{% block body %} |
|
||||||
{% if session.logged_in %} |
|
||||||
<form action="{{ url_for('flaskr.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 }}</li> |
|
||||||
{% else %} |
|
||||||
<li><em>Unbelievable. No entries here so far</em></li> |
|
||||||
{% endfor %} |
|
||||||
</ul> |
|
||||||
{% endblock %} |
|
@ -1,27 +0,0 @@ |
|||||||
# -*- coding: utf-8 -*- |
|
||||||
""" |
|
||||||
Flaskr Tests |
|
||||||
~~~~~~~~~~~~ |
|
||||||
|
|
||||||
Tests the Flaskr application. |
|
||||||
|
|
||||||
:copyright: © 2010 by the Pallets team. |
|
||||||
:license: BSD, see LICENSE for more details. |
|
||||||
""" |
|
||||||
|
|
||||||
from setuptools import setup, find_packages |
|
||||||
|
|
||||||
setup( |
|
||||||
name='flaskr', |
|
||||||
packages=find_packages(), |
|
||||||
include_package_data=True, |
|
||||||
install_requires=[ |
|
||||||
'flask', |
|
||||||
], |
|
||||||
setup_requires=[ |
|
||||||
'pytest-runner', |
|
||||||
], |
|
||||||
tests_require=[ |
|
||||||
'pytest', |
|
||||||
], |
|
||||||
) |
|
@ -1,83 +0,0 @@ |
|||||||
# -*- coding: utf-8 -*- |
|
||||||
""" |
|
||||||
Flaskr Tests |
|
||||||
~~~~~~~~~~~~ |
|
||||||
|
|
||||||
Tests the Flaskr application. |
|
||||||
|
|
||||||
:copyright: © 2010 by the Pallets team. |
|
||||||
:license: BSD, see LICENSE for more details. |
|
||||||
""" |
|
||||||
|
|
||||||
import os |
|
||||||
import tempfile |
|
||||||
import pytest |
|
||||||
from flaskr.factory import create_app |
|
||||||
from flaskr.blueprints.flaskr import init_db |
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture |
|
||||||
def app(): |
|
||||||
db_fd, db_path = tempfile.mkstemp() |
|
||||||
config = { |
|
||||||
'DATABASE': db_path, |
|
||||||
'TESTING': True, |
|
||||||
} |
|
||||||
app = create_app(config=config) |
|
||||||
|
|
||||||
with app.app_context(): |
|
||||||
init_db() |
|
||||||
yield app |
|
||||||
|
|
||||||
os.close(db_fd) |
|
||||||
os.unlink(db_path) |
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture |
|
||||||
def client(app): |
|
||||||
return app.test_client() |
|
||||||
|
|
||||||
|
|
||||||
def login(client, username, password): |
|
||||||
return client.post('/login', data=dict( |
|
||||||
username=username, |
|
||||||
password=password |
|
||||||
), follow_redirects=True) |
|
||||||
|
|
||||||
|
|
||||||
def logout(client): |
|
||||||
return client.get('/logout', follow_redirects=True) |
|
||||||
|
|
||||||
|
|
||||||
def test_empty_db(client): |
|
||||||
"""Start with a blank database.""" |
|
||||||
rv = client.get('/') |
|
||||||
assert b'No entries here so far' in rv.data |
|
||||||
|
|
||||||
|
|
||||||
def test_login_logout(client, app): |
|
||||||
"""Make sure login and logout works""" |
|
||||||
rv = login(client, app.config['USERNAME'], |
|
||||||
app.config['PASSWORD']) |
|
||||||
assert b'You were logged in' in rv.data |
|
||||||
rv = logout(client) |
|
||||||
assert b'You were logged out' in rv.data |
|
||||||
rv = login(client,app.config['USERNAME'] + 'x', |
|
||||||
app.config['PASSWORD']) |
|
||||||
assert b'Invalid username' in rv.data |
|
||||||
rv = login(client, app.config['USERNAME'], |
|
||||||
app.config['PASSWORD'] + 'x') |
|
||||||
assert b'Invalid password' in rv.data |
|
||||||
|
|
||||||
|
|
||||||
def test_messages(client, app): |
|
||||||
"""Test that messages work""" |
|
||||||
login(client, app.config['USERNAME'], |
|
||||||
app.config['PASSWORD']) |
|
||||||
rv = client.post('/add', data=dict( |
|
||||||
title='<Hello>', |
|
||||||
text='<strong>HTML</strong> allowed here' |
|
||||||
), follow_redirects=True) |
|
||||||
assert b'No entries here so far' not in rv.data |
|
||||||
assert b'<Hello>' in rv.data |
|
||||||
assert b'<strong>HTML</strong> allowed here' in rv.data |
|
@ -1,29 +0,0 @@ |
|||||||
# -*- coding: utf-8 -*- |
|
||||||
""" |
|
||||||
jQuery Example |
|
||||||
~~~~~~~~~~~~~~ |
|
||||||
|
|
||||||
A simple application that shows how Flask and jQuery get along. |
|
||||||
|
|
||||||
:copyright: © 2010 by the Pallets team. |
|
||||||
:license: BSD, see LICENSE for more details. |
|
||||||
""" |
|
||||||
|
|
||||||
from flask import Flask, jsonify, render_template, request |
|
||||||
app = Flask(__name__) |
|
||||||
|
|
||||||
|
|
||||||
@app.route('/_add_numbers') |
|
||||||
def add_numbers(): |
|
||||||
"""Add two numbers server side, ridiculous but well...""" |
|
||||||
a = request.args.get('a', 0, type=int) |
|
||||||
b = request.args.get('b', 0, type=int) |
|
||||||
return jsonify(result=a + b) |
|
||||||
|
|
||||||
|
|
||||||
@app.route('/') |
|
||||||
def index(): |
|
||||||
return render_template('index.html') |
|
||||||
|
|
||||||
if __name__ == '__main__': |
|
||||||
app.run() |
|
@ -1,33 +0,0 @@ |
|||||||
{% extends "layout.html" %} |
|
||||||
{% block body %} |
|
||||||
<script type="text/javascript"> |
|
||||||
$(function() { |
|
||||||
var submit_form = function(e) { |
|
||||||
$.getJSON($SCRIPT_ROOT + '/_add_numbers', { |
|
||||||
a: $('input[name="a"]').val(), |
|
||||||
b: $('input[name="b"]').val() |
|
||||||
}, function(data) { |
|
||||||
$('#result').text(data.result); |
|
||||||
$('input[name=a]').focus().select(); |
|
||||||
}); |
|
||||||
return false; |
|
||||||
}; |
|
||||||
|
|
||||||
$('a#calculate').bind('click', submit_form); |
|
||||||
|
|
||||||
$('input[type=text]').bind('keydown', function(e) { |
|
||||||
if (e.keyCode == 13) { |
|
||||||
submit_form(e); |
|
||||||
} |
|
||||||
}); |
|
||||||
|
|
||||||
$('input[name=a]').focus(); |
|
||||||
}); |
|
||||||
</script> |
|
||||||
<h1>jQuery Example</h1> |
|
||||||
<p> |
|
||||||
<input type="text" size="5" name="a"> + |
|
||||||
<input type="text" size="5" name="b"> = |
|
||||||
<span id="result">?</span> |
|
||||||
<p><a href=# id="calculate">calculate server side</a> |
|
||||||
{% endblock %} |
|
@ -1,8 +0,0 @@ |
|||||||
<!doctype html> |
|
||||||
<title>jQuery Example</title> |
|
||||||
<script type="text/javascript" |
|
||||||
src="http://ajax.googleapis.com/ajax/libs/jquery/1.4.2/jquery.min.js"></script> |
|
||||||
<script type="text/javascript"> |
|
||||||
var $SCRIPT_ROOT = {{ request.script_root|tojson|safe }}; |
|
||||||
</script> |
|
||||||
{% block body %}{% endblock %} |
|
@ -1,3 +0,0 @@ |
|||||||
graft minitwit/templates |
|
||||||
graft minitwit/static |
|
||||||
include minitwit/schema.sql |
|
@ -1,39 +0,0 @@ |
|||||||
|
|
||||||
/ MiniTwit / |
|
||||||
|
|
||||||
because writing todo lists is not fun |
|
||||||
|
|
||||||
|
|
||||||
~ What is MiniTwit? |
|
||||||
|
|
||||||
A SQLite and Flask powered twitter clone |
|
||||||
|
|
||||||
~ How do I use it? |
|
||||||
|
|
||||||
1. edit the configuration in the minitwit.py file or |
|
||||||
export an MINITWIT_SETTINGS environment variable |
|
||||||
pointing to a configuration file. |
|
||||||
|
|
||||||
2. install the app from the root of the project directory |
|
||||||
|
|
||||||
pip install --editable . |
|
||||||
|
|
||||||
3. tell flask about the right application: |
|
||||||
|
|
||||||
export FLASK_APP=minitwit |
|
||||||
|
|
||||||
4. fire up a shell and run this: |
|
||||||
|
|
||||||
flask initdb |
|
||||||
|
|
||||||
5. now you can run minitwit: |
|
||||||
|
|
||||||
flask run |
|
||||||
|
|
||||||
the application will greet you on |
|
||||||
http://localhost:5000/ |
|
||||||
|
|
||||||
~ Is it tested? |
|
||||||
|
|
||||||
You betcha. Run the `python setup.py test` file to |
|
||||||
see the tests pass. |
|
@ -1,256 +0,0 @@ |
|||||||
# -*- coding: utf-8 -*- |
|
||||||
""" |
|
||||||
MiniTwit |
|
||||||
~~~~~~~~ |
|
||||||
|
|
||||||
A microblogging application written with Flask and sqlite3. |
|
||||||
|
|
||||||
:copyright: © 2010 by the Pallets team. |
|
||||||
:license: BSD, see LICENSE for more details. |
|
||||||
""" |
|
||||||
|
|
||||||
import time |
|
||||||
from sqlite3 import dbapi2 as sqlite3 |
|
||||||
from hashlib import md5 |
|
||||||
from datetime import datetime |
|
||||||
from flask import Flask, request, session, url_for, redirect, \ |
|
||||||
render_template, abort, g, flash, _app_ctx_stack |
|
||||||
from werkzeug import check_password_hash, generate_password_hash |
|
||||||
|
|
||||||
|
|
||||||
# configuration |
|
||||||
DATABASE = '/tmp/minitwit.db' |
|
||||||
PER_PAGE = 30 |
|
||||||
DEBUG = True |
|
||||||
SECRET_KEY = b'_5#y2L"F4Q8z\n\xec]/' |
|
||||||
|
|
||||||
# create our little application :) |
|
||||||
app = Flask('minitwit') |
|
||||||
app.config.from_object(__name__) |
|
||||||
app.config.from_envvar('MINITWIT_SETTINGS', silent=True) |
|
||||||
|
|
||||||
|
|
||||||
def get_db(): |
|
||||||
"""Opens a new database connection if there is none yet for the |
|
||||||
current application context. |
|
||||||
""" |
|
||||||
top = _app_ctx_stack.top |
|
||||||
if not hasattr(top, 'sqlite_db'): |
|
||||||
top.sqlite_db = sqlite3.connect(app.config['DATABASE']) |
|
||||||
top.sqlite_db.row_factory = sqlite3.Row |
|
||||||
return top.sqlite_db |
|
||||||
|
|
||||||
|
|
||||||
@app.teardown_appcontext |
|
||||||
def close_database(exception): |
|
||||||
"""Closes the database again at the end of the request.""" |
|
||||||
top = _app_ctx_stack.top |
|
||||||
if hasattr(top, 'sqlite_db'): |
|
||||||
top.sqlite_db.close() |
|
||||||
|
|
||||||
|
|
||||||
def init_db(): |
|
||||||
"""Initializes the database.""" |
|
||||||
db = get_db() |
|
||||||
with app.open_resource('schema.sql', mode='r') as f: |
|
||||||
db.cursor().executescript(f.read()) |
|
||||||
db.commit() |
|
||||||
|
|
||||||
|
|
||||||
@app.cli.command('initdb') |
|
||||||
def initdb_command(): |
|
||||||
"""Creates the database tables.""" |
|
||||||
init_db() |
|
||||||
print('Initialized the database.') |
|
||||||
|
|
||||||
|
|
||||||
def query_db(query, args=(), one=False): |
|
||||||
"""Queries the database and returns a list of dictionaries.""" |
|
||||||
cur = get_db().execute(query, args) |
|
||||||
rv = cur.fetchall() |
|
||||||
return (rv[0] if rv else None) if one else rv |
|
||||||
|
|
||||||
|
|
||||||
def get_user_id(username): |
|
||||||
"""Convenience method to look up the id for a username.""" |
|
||||||
rv = query_db('select user_id from user where username = ?', |
|
||||||
[username], one=True) |
|
||||||
return rv[0] if rv else None |
|
||||||
|
|
||||||
|
|
||||||
def format_datetime(timestamp): |
|
||||||
"""Format a timestamp for display.""" |
|
||||||
return datetime.utcfromtimestamp(timestamp).strftime('%Y-%m-%d @ %H:%M') |
|
||||||
|
|
||||||
|
|
||||||
def gravatar_url(email, size=80): |
|
||||||
"""Return the gravatar image for the given email address.""" |
|
||||||
return 'https://www.gravatar.com/avatar/%s?d=identicon&s=%d' % \ |
|
||||||
(md5(email.strip().lower().encode('utf-8')).hexdigest(), size) |
|
||||||
|
|
||||||
|
|
||||||
@app.before_request |
|
||||||
def before_request(): |
|
||||||
g.user = None |
|
||||||
if 'user_id' in session: |
|
||||||
g.user = query_db('select * from user where user_id = ?', |
|
||||||
[session['user_id']], one=True) |
|
||||||
|
|
||||||
|
|
||||||
@app.route('/') |
|
||||||
def timeline(): |
|
||||||
"""Shows a users timeline or if no user is logged in it will |
|
||||||
redirect to the public timeline. This timeline shows the user's |
|
||||||
messages as well as all the messages of followed users. |
|
||||||
""" |
|
||||||
if not g.user: |
|
||||||
return redirect(url_for('public_timeline')) |
|
||||||
return render_template('timeline.html', messages=query_db(''' |
|
||||||
select message.*, user.* from message, user |
|
||||||
where message.author_id = user.user_id and ( |
|
||||||
user.user_id = ? or |
|
||||||
user.user_id in (select whom_id from follower |
|
||||||
where who_id = ?)) |
|
||||||
order by message.pub_date desc limit ?''', |
|
||||||
[session['user_id'], session['user_id'], PER_PAGE])) |
|
||||||
|
|
||||||
|
|
||||||
@app.route('/public') |
|
||||||
def public_timeline(): |
|
||||||
"""Displays the latest messages of all users.""" |
|
||||||
return render_template('timeline.html', messages=query_db(''' |
|
||||||
select message.*, user.* from message, user |
|
||||||
where message.author_id = user.user_id |
|
||||||
order by message.pub_date desc limit ?''', [PER_PAGE])) |
|
||||||
|
|
||||||
|
|
||||||
@app.route('/<username>') |
|
||||||
def user_timeline(username): |
|
||||||
"""Display's a users tweets.""" |
|
||||||
profile_user = query_db('select * from user where username = ?', |
|
||||||
[username], one=True) |
|
||||||
if profile_user is None: |
|
||||||
abort(404) |
|
||||||
followed = False |
|
||||||
if g.user: |
|
||||||
followed = query_db('''select 1 from follower where |
|
||||||
follower.who_id = ? and follower.whom_id = ?''', |
|
||||||
[session['user_id'], profile_user['user_id']], |
|
||||||
one=True) is not None |
|
||||||
return render_template('timeline.html', messages=query_db(''' |
|
||||||
select message.*, user.* from message, user where |
|
||||||
user.user_id = message.author_id and user.user_id = ? |
|
||||||
order by message.pub_date desc limit ?''', |
|
||||||
[profile_user['user_id'], PER_PAGE]), followed=followed, |
|
||||||
profile_user=profile_user) |
|
||||||
|
|
||||||
|
|
||||||
@app.route('/<username>/follow') |
|
||||||
def follow_user(username): |
|
||||||
"""Adds the current user as follower of the given user.""" |
|
||||||
if not g.user: |
|
||||||
abort(401) |
|
||||||
whom_id = get_user_id(username) |
|
||||||
if whom_id is None: |
|
||||||
abort(404) |
|
||||||
db = get_db() |
|
||||||
db.execute('insert into follower (who_id, whom_id) values (?, ?)', |
|
||||||
[session['user_id'], whom_id]) |
|
||||||
db.commit() |
|
||||||
flash('You are now following "%s"' % username) |
|
||||||
return redirect(url_for('user_timeline', username=username)) |
|
||||||
|
|
||||||
|
|
||||||
@app.route('/<username>/unfollow') |
|
||||||
def unfollow_user(username): |
|
||||||
"""Removes the current user as follower of the given user.""" |
|
||||||
if not g.user: |
|
||||||
abort(401) |
|
||||||
whom_id = get_user_id(username) |
|
||||||
if whom_id is None: |
|
||||||
abort(404) |
|
||||||
db = get_db() |
|
||||||
db.execute('delete from follower where who_id=? and whom_id=?', |
|
||||||
[session['user_id'], whom_id]) |
|
||||||
db.commit() |
|
||||||
flash('You are no longer following "%s"' % username) |
|
||||||
return redirect(url_for('user_timeline', username=username)) |
|
||||||
|
|
||||||
|
|
||||||
@app.route('/add_message', methods=['POST']) |
|
||||||
def add_message(): |
|
||||||
"""Registers a new message for the user.""" |
|
||||||
if 'user_id' not in session: |
|
||||||
abort(401) |
|
||||||
if request.form['text']: |
|
||||||
db = get_db() |
|
||||||
db.execute('''insert into message (author_id, text, pub_date) |
|
||||||
values (?, ?, ?)''', (session['user_id'], request.form['text'], |
|
||||||
int(time.time()))) |
|
||||||
db.commit() |
|
||||||
flash('Your message was recorded') |
|
||||||
return redirect(url_for('timeline')) |
|
||||||
|
|
||||||
|
|
||||||
@app.route('/login', methods=['GET', 'POST']) |
|
||||||
def login(): |
|
||||||
"""Logs the user in.""" |
|
||||||
if g.user: |
|
||||||
return redirect(url_for('timeline')) |
|
||||||
error = None |
|
||||||
if request.method == 'POST': |
|
||||||
user = query_db('''select * from user where |
|
||||||
username = ?''', [request.form['username']], one=True) |
|
||||||
if user is None: |
|
||||||
error = 'Invalid username' |
|
||||||
elif not check_password_hash(user['pw_hash'], |
|
||||||
request.form['password']): |
|
||||||
error = 'Invalid password' |
|
||||||
else: |
|
||||||
flash('You were logged in') |
|
||||||
session['user_id'] = user['user_id'] |
|
||||||
return redirect(url_for('timeline')) |
|
||||||
return render_template('login.html', error=error) |
|
||||||
|
|
||||||
|
|
||||||
@app.route('/register', methods=['GET', 'POST']) |
|
||||||
def register(): |
|
||||||
"""Registers the user.""" |
|
||||||
if g.user: |
|
||||||
return redirect(url_for('timeline')) |
|
||||||
error = None |
|
||||||
if request.method == 'POST': |
|
||||||
if not request.form['username']: |
|
||||||
error = 'You have to enter a username' |
|
||||||
elif not request.form['email'] or \ |
|
||||||
'@' not in request.form['email']: |
|
||||||
error = 'You have to enter a valid email address' |
|
||||||
elif not request.form['password']: |
|
||||||
error = 'You have to enter a password' |
|
||||||
elif request.form['password'] != request.form['password2']: |
|
||||||
error = 'The two passwords do not match' |
|
||||||
elif get_user_id(request.form['username']) is not None: |
|
||||||
error = 'The username is already taken' |
|
||||||
else: |
|
||||||
db = get_db() |
|
||||||
db.execute('''insert into user ( |
|
||||||
username, email, pw_hash) values (?, ?, ?)''', |
|
||||||
[request.form['username'], request.form['email'], |
|
||||||
generate_password_hash(request.form['password'])]) |
|
||||||
db.commit() |
|
||||||
flash('You were successfully registered and can login now') |
|
||||||
return redirect(url_for('login')) |
|
||||||
return render_template('register.html', error=error) |
|
||||||
|
|
||||||
|
|
||||||
@app.route('/logout') |
|
||||||
def logout(): |
|
||||||
"""Logs the user out.""" |
|
||||||
flash('You were logged out') |
|
||||||
session.pop('user_id', None) |
|
||||||
return redirect(url_for('public_timeline')) |
|
||||||
|
|
||||||
|
|
||||||
# add some filters to jinja |
|
||||||
app.jinja_env.filters['datetimeformat'] = format_datetime |
|
||||||
app.jinja_env.filters['gravatar'] = gravatar_url |
|
@ -1,21 +0,0 @@ |
|||||||
drop table if exists user; |
|
||||||
create table user ( |
|
||||||
user_id integer primary key autoincrement, |
|
||||||
username text not null, |
|
||||||
email text not null, |
|
||||||
pw_hash text not null |
|
||||||
); |
|
||||||
|
|
||||||
drop table if exists follower; |
|
||||||
create table follower ( |
|
||||||
who_id integer, |
|
||||||
whom_id integer |
|
||||||
); |
|
||||||
|
|
||||||
drop table if exists message; |
|
||||||
create table message ( |
|
||||||
message_id integer primary key autoincrement, |
|
||||||
author_id integer not null, |
|
||||||
text text not null, |
|
||||||
pub_date integer |
|
||||||
); |
|
@ -1,178 +0,0 @@ |
|||||||
body { |
|
||||||
background: #CAECE9; |
|
||||||
font-family: 'Trebuchet MS', sans-serif; |
|
||||||
font-size: 14px; |
|
||||||
} |
|
||||||
|
|
||||||
a { |
|
||||||
color: #26776F; |
|
||||||
} |
|
||||||
|
|
||||||
a:hover { |
|
||||||
color: #333; |
|
||||||
} |
|
||||||
|
|
||||||
input[type="text"], |
|
||||||
input[type="password"] { |
|
||||||
background: white; |
|
||||||
border: 1px solid #BFE6E2; |
|
||||||
padding: 2px; |
|
||||||
font-family: 'Trebuchet MS', sans-serif; |
|
||||||
font-size: 14px; |
|
||||||
-moz-border-radius: 2px; |
|
||||||
-webkit-border-radius: 2px; |
|
||||||
color: #105751; |
|
||||||
} |
|
||||||
|
|
||||||
input[type="submit"] { |
|
||||||
background: #105751; |
|
||||||
border: 1px solid #073B36; |
|
||||||
padding: 1px 3px; |
|
||||||
font-family: 'Trebuchet MS', sans-serif; |
|
||||||
font-size: 14px; |
|
||||||
font-weight: bold; |
|
||||||
-moz-border-radius: 2px; |
|
||||||
-webkit-border-radius: 2px; |
|
||||||
color: white; |
|
||||||
} |
|
||||||
|
|
||||||
div.page { |
|
||||||
background: white; |
|
||||||
border: 1px solid #6ECCC4; |
|
||||||
width: 700px; |
|
||||||
margin: 30px auto; |
|
||||||
} |
|
||||||
|
|
||||||
div.page h1 { |
|
||||||
background: #6ECCC4; |
|
||||||
margin: 0; |
|
||||||
padding: 10px 14px; |
|
||||||
color: white; |
|
||||||
letter-spacing: 1px; |
|
||||||
text-shadow: 0 0 3px #24776F; |
|
||||||
font-weight: normal; |
|
||||||
} |
|
||||||
|
|
||||||
div.page div.navigation { |
|
||||||
background: #DEE9E8; |
|
||||||
padding: 4px 10px; |
|
||||||
border-top: 1px solid #ccc; |
|
||||||
border-bottom: 1px solid #eee; |
|
||||||
color: #888; |
|
||||||
font-size: 12px; |
|
||||||
letter-spacing: 0.5px; |
|
||||||
} |
|
||||||
|
|
||||||
div.page div.navigation a { |
|
||||||
color: #444; |
|
||||||
font-weight: bold; |
|
||||||
} |
|
||||||
|
|
||||||
div.page h2 { |
|
||||||
margin: 0 0 15px 0; |
|
||||||
color: #105751; |
|
||||||
text-shadow: 0 1px 2px #ccc; |
|
||||||
} |
|
||||||
|
|
||||||
div.page div.body { |
|
||||||
padding: 10px; |
|
||||||
} |
|
||||||
|
|
||||||
div.page div.footer { |
|
||||||
background: #eee; |
|
||||||
color: #888; |
|
||||||
padding: 5px 10px; |
|
||||||
font-size: 12px; |
|
||||||
} |
|
||||||
|
|
||||||
div.page div.followstatus { |
|
||||||
border: 1px solid #ccc; |
|
||||||
background: #E3EBEA; |
|
||||||
-moz-border-radius: 2px; |
|
||||||
-webkit-border-radius: 2px; |
|
||||||
padding: 3px; |
|
||||||
font-size: 13px; |
|
||||||
} |
|
||||||
|
|
||||||
div.page ul.messages { |
|
||||||
list-style: none; |
|
||||||
margin: 0; |
|
||||||
padding: 0; |
|
||||||
} |
|
||||||
|
|
||||||
div.page ul.messages li { |
|
||||||
margin: 10px 0; |
|
||||||
padding: 5px; |
|
||||||
background: #F0FAF9; |
|
||||||
border: 1px solid #DBF3F1; |
|
||||||
-moz-border-radius: 5px; |
|
||||||
-webkit-border-radius: 5px; |
|
||||||
min-height: 48px; |
|
||||||
} |
|
||||||
|
|
||||||
div.page ul.messages p { |
|
||||||
margin: 0; |
|
||||||
} |
|
||||||
|
|
||||||
div.page ul.messages li img { |
|
||||||
float: left; |
|
||||||
padding: 0 10px 0 0; |
|
||||||
} |
|
||||||
|
|
||||||
div.page ul.messages li small { |
|
||||||
font-size: 0.9em; |
|
||||||
color: #888; |
|
||||||
} |
|
||||||
|
|
||||||
div.page div.twitbox { |
|
||||||
margin: 10px 0; |
|
||||||
padding: 5px; |
|
||||||
background: #F0FAF9; |
|
||||||
border: 1px solid #94E2DA; |
|
||||||
-moz-border-radius: 5px; |
|
||||||
-webkit-border-radius: 5px; |
|
||||||
} |
|
||||||
|
|
||||||
div.page div.twitbox h3 { |
|
||||||
margin: 0; |
|
||||||
font-size: 1em; |
|
||||||
color: #2C7E76; |
|
||||||
} |
|
||||||
|
|
||||||
div.page div.twitbox p { |
|
||||||
margin: 0; |
|
||||||
} |
|
||||||
|
|
||||||
div.page div.twitbox input[type="text"] { |
|
||||||
width: 585px; |
|
||||||
} |
|
||||||
|
|
||||||
div.page div.twitbox input[type="submit"] { |
|
||||||
width: 70px; |
|
||||||
margin-left: 5px; |
|
||||||
} |
|
||||||
|
|
||||||
ul.flashes { |
|
||||||
list-style: none; |
|
||||||
margin: 10px 10px 0 10px; |
|
||||||
padding: 0; |
|
||||||
} |
|
||||||
|
|
||||||
ul.flashes li { |
|
||||||
background: #B9F3ED; |
|
||||||
border: 1px solid #81CEC6; |
|
||||||
-moz-border-radius: 2px; |
|
||||||
-webkit-border-radius: 2px; |
|
||||||
padding: 4px; |
|
||||||
font-size: 13px; |
|
||||||
} |
|
||||||
|
|
||||||
div.error { |
|
||||||
margin: 10px 0; |
|
||||||
background: #FAE4E4; |
|
||||||
border: 1px solid #DD6F6F; |
|
||||||
-moz-border-radius: 2px; |
|
||||||
-webkit-border-radius: 2px; |
|
||||||
padding: 4px; |
|
||||||
font-size: 13px; |
|
||||||
} |
|
@ -1,32 +0,0 @@ |
|||||||
<!doctype html> |
|
||||||
<title>{% block title %}Welcome{% endblock %} | MiniTwit</title> |
|
||||||
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='style.css') }}"> |
|
||||||
<div class="page"> |
|
||||||
<h1>MiniTwit</h1> |
|
||||||
<div class="navigation"> |
|
||||||
{% if g.user %} |
|
||||||
<a href="{{ url_for('timeline') }}">my timeline</a> | |
|
||||||
<a href="{{ url_for('public_timeline') }}">public timeline</a> | |
|
||||||
<a href="{{ url_for('logout') }}">sign out [{{ g.user.username }}]</a> |
|
||||||
{% else %} |
|
||||||
<a href="{{ url_for('public_timeline') }}">public timeline</a> | |
|
||||||
<a href="{{ url_for('register') }}">sign up</a> | |
|
||||||
<a href="{{ url_for('login') }}">sign in</a> |
|
||||||
{% endif %} |
|
||||||
</div> |
|
||||||
{% with flashes = get_flashed_messages() %} |
|
||||||
{% if flashes %} |
|
||||||
<ul class="flashes"> |
|
||||||
{% for message in flashes %} |
|
||||||
<li>{{ message }} |
|
||||||
{% endfor %} |
|
||||||
</ul> |
|
||||||
{% endif %} |
|
||||||
{% endwith %} |
|
||||||
<div class="body"> |
|
||||||
{% block body %}{% endblock %} |
|
||||||
</div> |
|
||||||
<div class="footer"> |
|
||||||
MiniTwit — A Flask Application |
|
||||||
</div> |
|
||||||
</div> |
|
@ -1,16 +0,0 @@ |
|||||||
{% extends "layout.html" %} |
|
||||||
{% block title %}Sign In{% endblock %} |
|
||||||
{% block body %} |
|
||||||
<h2>Sign In</h2> |
|
||||||
{% if error %}<div class="error"><strong>Error:</strong> {{ error }}</div>{% endif %} |
|
||||||
<form action="" method="post"> |
|
||||||
<dl> |
|
||||||
<dt>Username: |
|
||||||
<dd><input type="text" name="username" size="30" value="{{ request.form.username }}"> |
|
||||||
<dt>Password: |
|
||||||
<dd><input type="password" name="password" size="30"> |
|
||||||
</dl> |
|
||||||
<div class="actions"><input type="submit" value="Sign In"></div> |
|
||||||
</form> |
|
||||||
{% endblock %} |
|
||||||
|
|
@ -1,19 +0,0 @@ |
|||||||
{% extends "layout.html" %} |
|
||||||
{% block title %}Sign Up{% endblock %} |
|
||||||
{% block body %} |
|
||||||
<h2>Sign Up</h2> |
|
||||||
{% if error %}<div class="error"><strong>Error:</strong> {{ error }}</div>{% endif %} |
|
||||||
<form action="" method="post"> |
|
||||||
<dl> |
|
||||||
<dt>Username: |
|
||||||
<dd><input type="text" name="username" size="30" value="{{ request.form.username }}"> |
|
||||||
<dt>E-Mail: |
|
||||||
<dd><input type="text" name="email" size="30" value="{{ request.form.email }}"> |
|
||||||
<dt>Password: |
|
||||||
<dd><input type="password" name="password" size="30"> |
|
||||||
<dt>Password <small>(repeat)</small>: |
|
||||||
<dd><input type="password" name="password2" size="30"> |
|
||||||
</dl> |
|
||||||
<div class="actions"><input type="submit" value="Sign Up"></div> |
|
||||||
</form> |
|
||||||
{% endblock %} |
|
@ -1,49 +0,0 @@ |
|||||||
{% extends "layout.html" %} |
|
||||||
{% block title %} |
|
||||||
{% if request.endpoint == 'public_timeline' %} |
|
||||||
Public Timeline |
|
||||||
{% elif request.endpoint == 'user_timeline' %} |
|
||||||
{{ profile_user.username }}'s Timeline |
|
||||||
{% else %} |
|
||||||
My Timeline |
|
||||||
{% endif %} |
|
||||||
{% endblock %} |
|
||||||
{% block body %} |
|
||||||
<h2>{{ self.title() }}</h2> |
|
||||||
{% if g.user %} |
|
||||||
{% if request.endpoint == 'user_timeline' %} |
|
||||||
<div class="followstatus"> |
|
||||||
{% if g.user.user_id == profile_user.user_id %} |
|
||||||
This is you! |
|
||||||
{% elif followed %} |
|
||||||
You are currently following this user. |
|
||||||
<a class="unfollow" href="{{ url_for('unfollow_user', username=profile_user.username) |
|
||||||
}}">Unfollow user</a>. |
|
||||||
{% else %} |
|
||||||
You are not yet following this user. |
|
||||||
<a class="follow" href="{{ url_for('follow_user', username=profile_user.username) |
|
||||||
}}">Follow user</a>. |
|
||||||
{% endif %} |
|
||||||
</div> |
|
||||||
{% elif request.endpoint == 'timeline' %} |
|
||||||
<div class="twitbox"> |
|
||||||
<h3>What's on your mind {{ g.user.username }}?</h3> |
|
||||||
<form action="{{ url_for('add_message') }}" method="post"> |
|
||||||
<p><input type="text" name="text" size="60"><!-- |
|
||||||
--><input type="submit" value="Share"> |
|
||||||
</form> |
|
||||||
</div> |
|
||||||
{% endif %} |
|
||||||
{% endif %} |
|
||||||
<ul class="messages"> |
|
||||||
{% for message in messages %} |
|
||||||
<li><img src="{{ message.email|gravatar(size=48) }}"><p> |
|
||||||
<strong><a href="{{ url_for('user_timeline', username=message.username) |
|
||||||
}}">{{ message.username }}</a></strong> |
|
||||||
{{ message.text }} |
|
||||||
<small>— {{ message.pub_date|datetimeformat }}</small> |
|
||||||
{% else %} |
|
||||||
<li><em>There's no message so far.</em> |
|
||||||
{% endfor %} |
|
||||||
</ul> |
|
||||||
{% endblock %} |
|
@ -1,16 +0,0 @@ |
|||||||
from setuptools import setup |
|
||||||
|
|
||||||
setup( |
|
||||||
name='minitwit', |
|
||||||
packages=['minitwit'], |
|
||||||
include_package_data=True, |
|
||||||
install_requires=[ |
|
||||||
'flask', |
|
||||||
], |
|
||||||
setup_requires=[ |
|
||||||
'pytest-runner', |
|
||||||
], |
|
||||||
tests_require=[ |
|
||||||
'pytest', |
|
||||||
], |
|
||||||
) |
|
@ -1,150 +0,0 @@ |
|||||||
# -*- coding: utf-8 -*- |
|
||||||
""" |
|
||||||
MiniTwit Tests |
|
||||||
~~~~~~~~~~~~~~ |
|
||||||
|
|
||||||
Tests the MiniTwit application. |
|
||||||
|
|
||||||
:copyright: © 2010 by the Pallets team. |
|
||||||
:license: BSD, see LICENSE for more details. |
|
||||||
""" |
|
||||||
|
|
||||||
import os |
|
||||||
import tempfile |
|
||||||
import pytest |
|
||||||
from minitwit import minitwit |
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture |
|
||||||
def client(): |
|
||||||
db_fd, minitwit.app.config['DATABASE'] = tempfile.mkstemp() |
|
||||||
client = minitwit.app.test_client() |
|
||||||
with minitwit.app.app_context(): |
|
||||||
minitwit.init_db() |
|
||||||
|
|
||||||
yield client |
|
||||||
|
|
||||||
os.close(db_fd) |
|
||||||
os.unlink(minitwit.app.config['DATABASE']) |
|
||||||
|
|
||||||
|
|
||||||
def register(client, username, password, password2=None, email=None): |
|
||||||
"""Helper function to register a user""" |
|
||||||
if password2 is None: |
|
||||||
password2 = password |
|
||||||
if email is None: |
|
||||||
email = username + '@example.com' |
|
||||||
return client.post('/register', data={ |
|
||||||
'username': username, |
|
||||||
'password': password, |
|
||||||
'password2': password2, |
|
||||||
'email': email, |
|
||||||
}, follow_redirects=True) |
|
||||||
|
|
||||||
|
|
||||||
def login(client, username, password): |
|
||||||
"""Helper function to login""" |
|
||||||
return client.post('/login', data={ |
|
||||||
'username': username, |
|
||||||
'password': password |
|
||||||
}, follow_redirects=True) |
|
||||||
|
|
||||||
|
|
||||||
def register_and_login(client, username, password): |
|
||||||
"""Registers and logs in in one go""" |
|
||||||
register(client, username, password) |
|
||||||
return login(client, username, password) |
|
||||||
|
|
||||||
|
|
||||||
def logout(client): |
|
||||||
"""Helper function to logout""" |
|
||||||
return client.get('/logout', follow_redirects=True) |
|
||||||
|
|
||||||
|
|
||||||
def add_message(client, text): |
|
||||||
"""Records a message""" |
|
||||||
rv = client.post('/add_message', data={'text': text}, |
|
||||||
follow_redirects=True) |
|
||||||
if text: |
|
||||||
assert b'Your message was recorded' in rv.data |
|
||||||
return rv |
|
||||||
|
|
||||||
|
|
||||||
def test_register(client): |
|
||||||
"""Make sure registering works""" |
|
||||||
rv = register(client, 'user1', 'default') |
|
||||||
assert b'You were successfully registered ' \ |
|
||||||
b'and can login now' in rv.data |
|
||||||
rv = register(client, 'user1', 'default') |
|
||||||
assert b'The username is already taken' in rv.data |
|
||||||
rv = register(client, '', 'default') |
|
||||||
assert b'You have to enter a username' in rv.data |
|
||||||
rv = register(client, 'meh', '') |
|
||||||
assert b'You have to enter a password' in rv.data |
|
||||||
rv = register(client, 'meh', 'x', 'y') |
|
||||||
assert b'The two passwords do not match' in rv.data |
|
||||||
rv = register(client, 'meh', 'foo', email='broken') |
|
||||||
assert b'You have to enter a valid email address' in rv.data |
|
||||||
|
|
||||||
|
|
||||||
def test_login_logout(client): |
|
||||||
"""Make sure logging in and logging out works""" |
|
||||||
rv = register_and_login(client, 'user1', 'default') |
|
||||||
assert b'You were logged in' in rv.data |
|
||||||
rv = logout(client) |
|
||||||
assert b'You were logged out' in rv.data |
|
||||||
rv = login(client, 'user1', 'wrongpassword') |
|
||||||
assert b'Invalid password' in rv.data |
|
||||||
rv = login(client, 'user2', 'wrongpassword') |
|
||||||
assert b'Invalid username' in rv.data |
|
||||||
|
|
||||||
|
|
||||||
def test_message_recording(client): |
|
||||||
"""Check if adding messages works""" |
|
||||||
register_and_login(client, 'foo', 'default') |
|
||||||
add_message(client, 'test message 1') |
|
||||||
add_message(client, '<test message 2>') |
|
||||||
rv = client.get('/') |
|
||||||
assert b'test message 1' in rv.data |
|
||||||
assert b'<test message 2>' in rv.data |
|
||||||
|
|
||||||
|
|
||||||
def test_timelines(client): |
|
||||||
"""Make sure that timelines work""" |
|
||||||
register_and_login(client, 'foo', 'default') |
|
||||||
add_message(client, 'the message by foo') |
|
||||||
logout(client) |
|
||||||
register_and_login(client, 'bar', 'default') |
|
||||||
add_message(client, 'the message by bar') |
|
||||||
rv = client.get('/public') |
|
||||||
assert b'the message by foo' in rv.data |
|
||||||
assert b'the message by bar' in rv.data |
|
||||||
|
|
||||||
# bar's timeline should just show bar's message |
|
||||||
rv = client.get('/') |
|
||||||
assert b'the message by foo' not in rv.data |
|
||||||
assert b'the message by bar' in rv.data |
|
||||||
|
|
||||||
# now let's follow foo |
|
||||||
rv = client.get('/foo/follow', follow_redirects=True) |
|
||||||
assert b'You are now following "foo"' in rv.data |
|
||||||
|
|
||||||
# we should now see foo's message |
|
||||||
rv = client.get('/') |
|
||||||
assert b'the message by foo' in rv.data |
|
||||||
assert b'the message by bar' in rv.data |
|
||||||
|
|
||||||
# but on the user's page we only want the user's message |
|
||||||
rv = client.get('/bar') |
|
||||||
assert b'the message by foo' not in rv.data |
|
||||||
assert b'the message by bar' in rv.data |
|
||||||
rv = client.get('/foo') |
|
||||||
assert b'the message by foo' in rv.data |
|
||||||
assert b'the message by bar' not in rv.data |
|
||||||
|
|
||||||
# now unfollow and check if that worked |
|
||||||
rv = client.get('/foo/unfollow', follow_redirects=True) |
|
||||||
assert b'You are no longer following "foo"' in rv.data |
|
||||||
rv = client.get('/') |
|
||||||
assert b'the message by foo' not in rv.data |
|
||||||
assert b'the message by bar' in rv.data |
|
@ -1,10 +0,0 @@ |
|||||||
from setuptools import setup |
|
||||||
|
|
||||||
setup( |
|
||||||
name='yourapplication', |
|
||||||
packages=['yourapplication'], |
|
||||||
include_package_data=True, |
|
||||||
install_requires=[ |
|
||||||
'flask', |
|
||||||
], |
|
||||||
) |
|
@ -1,21 +0,0 @@ |
|||||||
# -*- coding: utf-8 -*- |
|
||||||
""" |
|
||||||
Larger App Tests |
|
||||||
~~~~~~~~~~~~~~~~ |
|
||||||
|
|
||||||
:copyright: © 2010 by the Pallets team. |
|
||||||
:license: BSD, see LICENSE for more details. |
|
||||||
""" |
|
||||||
|
|
||||||
from yourapplication import app |
|
||||||
import pytest |
|
||||||
|
|
||||||
@pytest.fixture |
|
||||||
def client(): |
|
||||||
app.config['TESTING'] = True |
|
||||||
client = app.test_client() |
|
||||||
return client |
|
||||||
|
|
||||||
def test_index(client): |
|
||||||
rv = client.get('/') |
|
||||||
assert b"Hello World!" in rv.data |
|
@ -1,13 +0,0 @@ |
|||||||
# -*- coding: utf-8 -*- |
|
||||||
""" |
|
||||||
yourapplication |
|
||||||
~~~~~~~~~~~~~~~ |
|
||||||
|
|
||||||
:copyright: © 2010 by the Pallets team. |
|
||||||
:license: BSD, see LICENSE for more details. |
|
||||||
""" |
|
||||||
|
|
||||||
from flask import Flask |
|
||||||
app = Flask('yourapplication') |
|
||||||
|
|
||||||
import yourapplication.views |
|
@ -1,14 +0,0 @@ |
|||||||
# -*- coding: utf-8 -*- |
|
||||||
""" |
|
||||||
yourapplication.views |
|
||||||
~~~~~~~~~~~~~~~~~~~~~ |
|
||||||
|
|
||||||
:copyright: © 2010 by the Pallets team. |
|
||||||
:license: BSD, see LICENSE for more details. |
|
||||||
""" |
|
||||||
|
|
||||||
from yourapplication import app |
|
||||||
|
|
||||||
@app.route('/') |
|
||||||
def index(): |
|
||||||
return 'Hello World!' |
|
@ -0,0 +1,14 @@ |
|||||||
|
venv/ |
||||||
|
*.pyc |
||||||
|
__pycache__/ |
||||||
|
instance/ |
||||||
|
.cache/ |
||||||
|
.pytest_cache/ |
||||||
|
.coverage |
||||||
|
htmlcov/ |
||||||
|
dist/ |
||||||
|
build/ |
||||||
|
*.egg-info/ |
||||||
|
.idea/ |
||||||
|
*.swp |
||||||
|
*~ |
@ -0,0 +1,31 @@ |
|||||||
|
Copyright © 2010 by the Pallets team. |
||||||
|
|
||||||
|
Some rights reserved. |
||||||
|
|
||||||
|
Redistribution and use in source and binary forms of the software as |
||||||
|
well as documentation, with or without modification, are permitted |
||||||
|
provided that the following conditions are met: |
||||||
|
|
||||||
|
* Redistributions of source code must retain the above copyright notice, |
||||||
|
this list of conditions and the following disclaimer. |
||||||
|
|
||||||
|
* Redistributions in binary form must reproduce the above copyright |
||||||
|
notice, this list of conditions and the following disclaimer in the |
||||||
|
documentation and/or other materials provided with the distribution. |
||||||
|
|
||||||
|
* Neither the name of the copyright holder nor the names of its |
||||||
|
contributors may be used to endorse or promote products derived from |
||||||
|
this software without specific prior written permission. |
||||||
|
|
||||||
|
THIS SOFTWARE AND DOCUMENTATION IS PROVIDED BY THE COPYRIGHT HOLDERS AND |
||||||
|
CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, |
||||||
|
BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND |
||||||
|
FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE |
||||||
|
COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, |
||||||
|
INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT |
||||||
|
NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF |
||||||
|
USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON |
||||||
|
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
||||||
|
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF |
||||||
|
THIS SOFTWARE AND DOCUMENTATION, EVEN IF ADVISED OF THE POSSIBILITY OF |
||||||
|
SUCH DAMAGE. |
@ -1,3 +1,6 @@ |
|||||||
graft flaskr/templates |
include LICENSE |
||||||
graft flaskr/static |
|
||||||
include flaskr/schema.sql |
include flaskr/schema.sql |
||||||
|
graft flaskr/static |
||||||
|
graft flaskr/templates |
||||||
|
graft tests |
||||||
|
global-exclude *.pyc |
@ -0,0 +1,76 @@ |
|||||||
|
Flaskr |
||||||
|
====== |
||||||
|
|
||||||
|
The basic blog app built in the Flask `tutorial`_. |
||||||
|
|
||||||
|
.. _tutorial: http://flask.pocoo.org/docs/tutorial/ |
||||||
|
|
||||||
|
|
||||||
|
Install |
||||||
|
------- |
||||||
|
|
||||||
|
**Be sure to use the same version of the code as the version of the docs |
||||||
|
you're reading.** You probably want the latest tagged version, but the |
||||||
|
default Git version is the master branch. :: |
||||||
|
|
||||||
|
# clone the repository |
||||||
|
git clone https://github.com/pallets/flask |
||||||
|
cd flask |
||||||
|
# checkout the correct version |
||||||
|
git tag # shows the tagged versions |
||||||
|
git checkout latest-tag-found-above |
||||||
|
cd examples/tutorial |
||||||
|
|
||||||
|
Create a virtualenv and activate it:: |
||||||
|
|
||||||
|
python3 -m venv venv |
||||||
|
. venv/bin/activate |
||||||
|
|
||||||
|
Or on Windows cmd:: |
||||||
|
|
||||||
|
py -3 -m venv venv |
||||||
|
venv\Scripts\activate.bat |
||||||
|
|
||||||
|
Install Flaskr:: |
||||||
|
|
||||||
|
pip install -e . |
||||||
|
|
||||||
|
Or if you are using the master branch, install Flask from source before |
||||||
|
installing Flaskr:: |
||||||
|
|
||||||
|
pip install -e ../.. |
||||||
|
pip install -e . |
||||||
|
|
||||||
|
|
||||||
|
Run |
||||||
|
--- |
||||||
|
|
||||||
|
:: |
||||||
|
|
||||||
|
export FLASK_APP=flaskr |
||||||
|
export FLASK_ENV=development |
||||||
|
flask run |
||||||
|
|
||||||
|
Or on Windows cmd:: |
||||||
|
|
||||||
|
set FLASK_APP=flaskr |
||||||
|
set FLASK_ENV=development |
||||||
|
flask run |
||||||
|
|
||||||
|
Open http://127.0.0.1:5000 in a browser. |
||||||
|
|
||||||
|
|
||||||
|
Test |
||||||
|
---- |
||||||
|
|
||||||
|
:: |
||||||
|
|
||||||
|
pip install pytest |
||||||
|
pytest |
||||||
|
|
||||||
|
Run with coverage report:: |
||||||
|
|
||||||
|
pip install pytest coverage |
||||||
|
coverage run -m pytest |
||||||
|
coverage report |
||||||
|
coverage html # open htmlcov/index.html in a browser |
@ -0,0 +1,48 @@ |
|||||||
|
import os |
||||||
|
|
||||||
|
from flask import Flask |
||||||
|
|
||||||
|
|
||||||
|
def create_app(test_config=None): |
||||||
|
"""Create and configure an instance of the Flask application.""" |
||||||
|
app = Flask(__name__, instance_relative_config=True) |
||||||
|
app.config.from_mapping( |
||||||
|
# a default secret that should be overridden by instance config |
||||||
|
SECRET_KEY='dev', |
||||||
|
# store the database in the instance folder |
||||||
|
DATABASE=os.path.join(app.instance_path, 'flaskr.sqlite'), |
||||||
|
) |
||||||
|
|
||||||
|
if test_config is None: |
||||||
|
# load the instance config, if it exists, when not testing |
||||||
|
app.config.from_pyfile('config.py', silent=True) |
||||||
|
else: |
||||||
|
# load the test config if passed in |
||||||
|
app.config.update(test_config) |
||||||
|
|
||||||
|
# ensure the instance folder exists |
||||||
|
try: |
||||||
|
os.makedirs(app.instance_path) |
||||||
|
except OSError: |
||||||
|
pass |
||||||
|
|
||||||
|
@app.route('/hello') |
||||||
|
def hello(): |
||||||
|
return 'Hello, World!' |
||||||
|
|
||||||
|
# register the database commands |
||||||
|
from flaskr import db |
||||||
|
db.init_app(app) |
||||||
|
|
||||||
|
# apply the blueprints to the app |
||||||
|
from flaskr import auth, blog |
||||||
|
app.register_blueprint(auth.bp) |
||||||
|
app.register_blueprint(blog.bp) |
||||||
|
|
||||||
|
# make url_for('index') == url_for('blog.index') |
||||||
|
# in another app, you might define a separate main index here with |
||||||
|
# app.route, while giving the blog blueprint a url_prefix, but for |
||||||
|
# the tutorial the blog will be the main index |
||||||
|
app.add_url_rule('/', endpoint='index') |
||||||
|
|
||||||
|
return app |
@ -0,0 +1,108 @@ |
|||||||
|
import functools |
||||||
|
|
||||||
|
from flask import ( |
||||||
|
Blueprint, flash, g, redirect, render_template, request, session, url_for |
||||||
|
) |
||||||
|
from werkzeug.security import check_password_hash, generate_password_hash |
||||||
|
|
||||||
|
from flaskr.db import get_db |
||||||
|
|
||||||
|
bp = Blueprint('auth', __name__, url_prefix='/auth') |
||||||
|
|
||||||
|
|
||||||
|
def login_required(view): |
||||||
|
"""View decorator that redirects anonymous users to the login page.""" |
||||||
|
@functools.wraps(view) |
||||||
|
def wrapped_view(**kwargs): |
||||||
|
if g.user is None: |
||||||
|
return redirect(url_for('auth.login')) |
||||||
|
|
||||||
|
return view(**kwargs) |
||||||
|
|
||||||
|
return wrapped_view |
||||||
|
|
||||||
|
|
||||||
|
@bp.before_app_request |
||||||
|
def load_logged_in_user(): |
||||||
|
"""If a user id is stored in the session, load the user object from |
||||||
|
the database into ``g.user``.""" |
||||||
|
user_id = session.get('user_id') |
||||||
|
|
||||||
|
if user_id is None: |
||||||
|
g.user = None |
||||||
|
else: |
||||||
|
g.user = get_db().execute( |
||||||
|
'SELECT * FROM user WHERE id = ?', (user_id,) |
||||||
|
).fetchone() |
||||||
|
|
||||||
|
|
||||||
|
@bp.route('/register', methods=('GET', 'POST')) |
||||||
|
def register(): |
||||||
|
"""Register a new user. |
||||||
|
|
||||||
|
Validates that the username is not already taken. Hashes the |
||||||
|
password for security. |
||||||
|
""" |
||||||
|
if request.method == 'POST': |
||||||
|
username = request.form['username'] |
||||||
|
password = request.form['password'] |
||||||
|
db = get_db() |
||||||
|
error = None |
||||||
|
|
||||||
|
if not username: |
||||||
|
error = 'Username is required.' |
||||||
|
elif not password: |
||||||
|
error = 'Password is required.' |
||||||
|
elif db.execute( |
||||||
|
'SELECT id FROM user WHERE username = ?', (username,) |
||||||
|
).fetchone() is not None: |
||||||
|
error = 'User {} is already registered.'.format(username) |
||||||
|
|
||||||
|
if error is None: |
||||||
|
# the name is available, store it in the database and go to |
||||||
|
# the login page |
||||||
|
db.execute( |
||||||
|
'INSERT INTO user (username, password) VALUES (?, ?)', |
||||||
|
(username, generate_password_hash(password)) |
||||||
|
) |
||||||
|
db.commit() |
||||||
|
return redirect(url_for('auth.login')) |
||||||
|
|
||||||
|
flash(error) |
||||||
|
|
||||||
|
return render_template('auth/register.html') |
||||||
|
|
||||||
|
|
||||||
|
@bp.route('/login', methods=('GET', 'POST')) |
||||||
|
def login(): |
||||||
|
"""Log in a registered user by adding the user id to the session.""" |
||||||
|
if request.method == 'POST': |
||||||
|
username = request.form['username'] |
||||||
|
password = request.form['password'] |
||||||
|
db = get_db() |
||||||
|
error = None |
||||||
|
user = db.execute( |
||||||
|
'SELECT * FROM user WHERE username = ?', (username,) |
||||||
|
).fetchone() |
||||||
|
|
||||||
|
if user is None: |
||||||
|
error = 'Incorrect username.' |
||||||
|
elif not check_password_hash(user['password'], password): |
||||||
|
error = 'Incorrect password.' |
||||||
|
|
||||||
|
if error is None: |
||||||
|
# store the user id in a new session and return to the index |
||||||
|
session.clear() |
||||||
|
session['user_id'] = user['id'] |
||||||
|
return redirect(url_for('index')) |
||||||
|
|
||||||
|
flash(error) |
||||||
|
|
||||||
|
return render_template('auth/login.html') |
||||||
|
|
||||||
|
|
||||||
|
@bp.route('/logout') |
||||||
|
def logout(): |
||||||
|
"""Clear the current session, including the stored user id.""" |
||||||
|
session.clear() |
||||||
|
return redirect(url_for('index')) |
@ -0,0 +1,119 @@ |
|||||||
|
from flask import ( |
||||||
|
Blueprint, flash, g, redirect, render_template, request, url_for |
||||||
|
) |
||||||
|
from werkzeug.exceptions import abort |
||||||
|
|
||||||
|
from flaskr.auth import login_required |
||||||
|
from flaskr.db import get_db |
||||||
|
|
||||||
|
bp = Blueprint('blog', __name__) |
||||||
|
|
||||||
|
|
||||||
|
@bp.route('/') |
||||||
|
def index(): |
||||||
|
"""Show all the posts, most recent first.""" |
||||||
|
db = get_db() |
||||||
|
posts = db.execute( |
||||||
|
'SELECT p.id, title, body, created, author_id, username' |
||||||
|
' FROM post p JOIN user u ON p.author_id = u.id' |
||||||
|
' ORDER BY created DESC' |
||||||
|
).fetchall() |
||||||
|
return render_template('blog/index.html', posts=posts) |
||||||
|
|
||||||
|
|
||||||
|
def get_post(id, check_author=True): |
||||||
|
"""Get a post and its author by id. |
||||||
|
|
||||||
|
Checks that the id exists and optionally that the current user is |
||||||
|
the author. |
||||||
|
|
||||||
|
:param id: id of post to get |
||||||
|
:param check_author: require the current user to be the author |
||||||
|
:return: the post with author information |
||||||
|
:raise 404: if a post with the given id doesn't exist |
||||||
|
:raise 403: if the current user isn't the author |
||||||
|
""" |
||||||
|
post = get_db().execute( |
||||||
|
'SELECT p.id, title, body, created, author_id, username' |
||||||
|
' FROM post p JOIN user u ON p.author_id = u.id' |
||||||
|
' WHERE p.id = ?', |
||||||
|
(id,) |
||||||
|
).fetchone() |
||||||
|
|
||||||
|
if post is None: |
||||||
|
abort(404, "Post id {0} doesn't exist.".format(id)) |
||||||
|
|
||||||
|
if check_author and post['author_id'] != g.user['id']: |
||||||
|
abort(403) |
||||||
|
|
||||||
|
return post |
||||||
|
|
||||||
|
|
||||||
|
@bp.route('/create', methods=('GET', 'POST')) |
||||||
|
@login_required |
||||||
|
def create(): |
||||||
|
"""Create a new post for the current user.""" |
||||||
|
if request.method == 'POST': |
||||||
|
title = request.form['title'] |
||||||
|
body = request.form['body'] |
||||||
|
error = None |
||||||
|
|
||||||
|
if not title: |
||||||
|
error = 'Title is required.' |
||||||
|
|
||||||
|
if error is not None: |
||||||
|
flash(error) |
||||||
|
else: |
||||||
|
db = get_db() |
||||||
|
db.execute( |
||||||
|
'INSERT INTO post (title, body, author_id)' |
||||||
|
' VALUES (?, ?, ?)', |
||||||
|
(title, body, g.user['id']) |
||||||
|
) |
||||||
|
db.commit() |
||||||
|
return redirect(url_for('blog.index')) |
||||||
|
|
||||||
|
return render_template('blog/create.html') |
||||||
|
|
||||||
|
|
||||||
|
@bp.route('/<int:id>/update', methods=('GET', 'POST')) |
||||||
|
@login_required |
||||||
|
def update(id): |
||||||
|
"""Update a post if the current user is the author.""" |
||||||
|
post = get_post(id) |
||||||
|
|
||||||
|
if request.method == 'POST': |
||||||
|
title = request.form['title'] |
||||||
|
body = request.form['body'] |
||||||
|
error = None |
||||||
|
|
||||||
|
if not title: |
||||||
|
error = 'Title is required.' |
||||||
|
|
||||||
|
if error is not None: |
||||||
|
flash(error) |
||||||
|
else: |
||||||
|
db = get_db() |
||||||
|
db.execute( |
||||||
|
'UPDATE post SET title = ?, body = ? WHERE id = ?', |
||||||
|
(title, body, id) |
||||||
|
) |
||||||
|
db.commit() |
||||||
|
return redirect(url_for('blog.index')) |
||||||
|
|
||||||
|
return render_template('blog/update.html', post=post) |
||||||
|
|
||||||
|
|
||||||
|
@bp.route('/<int:id>/delete', methods=('POST',)) |
||||||
|
@login_required |
||||||
|
def delete(id): |
||||||
|
"""Delete a post. |
||||||
|
|
||||||
|
Ensures that the post exists and that the logged in user is the |
||||||
|
author of the post. |
||||||
|
""" |
||||||
|
get_post(id) |
||||||
|
db = get_db() |
||||||
|
db.execute('DELETE FROM post WHERE id = ?', (id,)) |
||||||
|
db.commit() |
||||||
|
return redirect(url_for('blog.index')) |
@ -0,0 +1,54 @@ |
|||||||
|
import sqlite3 |
||||||
|
|
||||||
|
import click |
||||||
|
from flask import current_app, g |
||||||
|
from flask.cli import with_appcontext |
||||||
|
|
||||||
|
|
||||||
|
def get_db(): |
||||||
|
"""Connect to the application's configured database. The connection |
||||||
|
is unique for each request and will be reused if this is called |
||||||
|
again. |
||||||
|
""" |
||||||
|
if 'db' not in g: |
||||||
|
g.db = sqlite3.connect( |
||||||
|
current_app.config['DATABASE'], |
||||||
|
detect_types=sqlite3.PARSE_DECLTYPES |
||||||
|
) |
||||||
|
g.db.row_factory = sqlite3.Row |
||||||
|
|
||||||
|
return g.db |
||||||
|
|
||||||
|
|
||||||
|
def close_db(e=None): |
||||||
|
"""If this request connected to the database, close the |
||||||
|
connection. |
||||||
|
""" |
||||||
|
db = g.pop('db', None) |
||||||
|
|
||||||
|
if db is not None: |
||||||
|
db.close() |
||||||
|
|
||||||
|
|
||||||
|
def init_db(): |
||||||
|
"""Clear existing data and create new tables.""" |
||||||
|
db = get_db() |
||||||
|
|
||||||
|
with current_app.open_resource('schema.sql') as f: |
||||||
|
db.executescript(f.read().decode('utf8')) |
||||||
|
|
||||||
|
|
||||||
|
@click.command('init-db') |
||||||
|
@with_appcontext |
||||||
|
def init_db_command(): |
||||||
|
"""Clear existing data and create new tables.""" |
||||||
|
init_db() |
||||||
|
click.echo('Initialized the database.') |
||||||
|
|
||||||
|
|
||||||
|
def init_app(app): |
||||||
|
"""Register database functions with the Flask app. This is called by |
||||||
|
the application factory. |
||||||
|
""" |
||||||
|
app.teardown_appcontext(close_db) |
||||||
|
app.cli.add_command(init_db_command) |
@ -0,0 +1,20 @@ |
|||||||
|
-- Initialize the database. |
||||||
|
-- Drop any existing data and create empty tables. |
||||||
|
|
||||||
|
DROP TABLE IF EXISTS user; |
||||||
|
DROP TABLE IF EXISTS post; |
||||||
|
|
||||||
|
CREATE TABLE user ( |
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT, |
||||||
|
username TEXT UNIQUE NOT NULL, |
||||||
|
password TEXT NOT NULL |
||||||
|
); |
||||||
|
|
||||||
|
CREATE TABLE post ( |
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT, |
||||||
|
author_id INTEGER NOT NULL, |
||||||
|
created TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, |
||||||
|
title TEXT NOT NULL, |
||||||
|
body TEXT NOT NULL, |
||||||
|
FOREIGN KEY (author_id) REFERENCES user (id) |
||||||
|
); |
@ -0,0 +1,134 @@ |
|||||||
|
html { |
||||||
|
font-family: sans-serif; |
||||||
|
background: #eee; |
||||||
|
padding: 1rem; |
||||||
|
} |
||||||
|
|
||||||
|
body { |
||||||
|
max-width: 960px; |
||||||
|
margin: 0 auto; |
||||||
|
background: white; |
||||||
|
} |
||||||
|
|
||||||
|
h1, h2, h3, h4, h5, h6 { |
||||||
|
font-family: serif; |
||||||
|
color: #377ba8; |
||||||
|
margin: 1rem 0; |
||||||
|
} |
||||||
|
|
||||||
|
a { |
||||||
|
color: #377ba8; |
||||||
|
} |
||||||
|
|
||||||
|
hr { |
||||||
|
border: none; |
||||||
|
border-top: 1px solid lightgray; |
||||||
|
} |
||||||
|
|
||||||
|
nav { |
||||||
|
background: lightgray; |
||||||
|
display: flex; |
||||||
|
align-items: center; |
||||||
|
padding: 0 0.5rem; |
||||||
|
} |
||||||
|
|
||||||
|
nav h1 { |
||||||
|
flex: auto; |
||||||
|
margin: 0; |
||||||
|
} |
||||||
|
|
||||||
|
nav h1 a { |
||||||
|
text-decoration: none; |
||||||
|
padding: 0.25rem 0.5rem; |
||||||
|
} |
||||||
|
|
||||||
|
nav ul { |
||||||
|
display: flex; |
||||||
|
list-style: none; |
||||||
|
margin: 0; |
||||||
|
padding: 0; |
||||||
|
} |
||||||
|
|
||||||
|
nav ul li a, nav ul li span, header .action { |
||||||
|
display: block; |
||||||
|
padding: 0.5rem; |
||||||
|
} |
||||||
|
|
||||||
|
.content { |
||||||
|
padding: 0 1rem 1rem; |
||||||
|
} |
||||||
|
|
||||||
|
.content > header { |
||||||
|
border-bottom: 1px solid lightgray; |
||||||
|
display: flex; |
||||||
|
align-items: flex-end; |
||||||
|
} |
||||||
|
|
||||||
|
.content > header h1 { |
||||||
|
flex: auto; |
||||||
|
margin: 1rem 0 0.25rem 0; |
||||||
|
} |
||||||
|
|
||||||
|
.flash { |
||||||
|
margin: 1em 0; |
||||||
|
padding: 1em; |
||||||
|
background: #cae6f6; |
||||||
|
border: 1px solid #377ba8; |
||||||
|
} |
||||||
|
|
||||||
|
.post > header { |
||||||
|
display: flex; |
||||||
|
align-items: flex-end; |
||||||
|
font-size: 0.85em; |
||||||
|
} |
||||||
|
|
||||||
|
.post > header > div:first-of-type { |
||||||
|
flex: auto; |
||||||
|
} |
||||||
|
|
||||||
|
.post > header h1 { |
||||||
|
font-size: 1.5em; |
||||||
|
margin-bottom: 0; |
||||||
|
} |
||||||
|
|
||||||
|
.post .about { |
||||||
|
color: slategray; |
||||||
|
font-style: italic; |
||||||
|
} |
||||||
|
|
||||||
|
.post .body { |
||||||
|
white-space: pre-line; |
||||||
|
} |
||||||
|
|
||||||
|
.content:last-child { |
||||||
|
margin-bottom: 0; |
||||||
|
} |
||||||
|
|
||||||
|
.content form { |
||||||
|
margin: 1em 0; |
||||||
|
display: flex; |
||||||
|
flex-direction: column; |
||||||
|
} |
||||||
|
|
||||||
|
.content label { |
||||||
|
font-weight: bold; |
||||||
|
margin-bottom: 0.5em; |
||||||
|
} |
||||||
|
|
||||||
|
.content input, .content textarea { |
||||||
|
margin-bottom: 1em; |
||||||
|
} |
||||||
|
|
||||||
|
.content textarea { |
||||||
|
min-height: 12em; |
||||||
|
resize: vertical; |
||||||
|
} |
||||||
|
|
||||||
|
input.danger { |
||||||
|
color: #cc2f2e; |
||||||
|
} |
||||||
|
|
||||||
|
input[type=submit] { |
||||||
|
align-self: start; |
||||||
|
min-width: 10em; |
||||||
|
} |
@ -0,0 +1,15 @@ |
|||||||
|
{% extends 'base.html' %} |
||||||
|
|
||||||
|
{% block header %} |
||||||
|
<h1>{% block title %}Log In{% endblock %}</h1> |
||||||
|
{% endblock %} |
||||||
|
|
||||||
|
{% block content %} |
||||||
|
<form method="post"> |
||||||
|
<label for="username">Username</label> |
||||||
|
<input name="username" id="username" required> |
||||||
|
<label for="password">Password</label> |
||||||
|
<input type="password" name="password" id="password" required> |
||||||
|
<input type="submit" value="Log In"> |
||||||
|
</form> |
||||||
|
{% endblock %} |
@ -0,0 +1,15 @@ |
|||||||
|
{% extends 'base.html' %} |
||||||
|
|
||||||
|
{% block header %} |
||||||
|
<h1>{% block title %}Register{% endblock %}</h1> |
||||||
|
{% endblock %} |
||||||
|
|
||||||
|
{% block content %} |
||||||
|
<form method="post"> |
||||||
|
<label for="username">Username</label> |
||||||
|
<input name="username" id="username" required> |
||||||
|
<label for="password">Password</label> |
||||||
|
<input type="password" name="password" id="password" required> |
||||||
|
<input type="submit" value="Register"> |
||||||
|
</form> |
||||||
|
{% endblock %} |
@ -0,0 +1,24 @@ |
|||||||
|
<!doctype html> |
||||||
|
<title>{% block title %}{% endblock %} - Flaskr</title> |
||||||
|
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}"> |
||||||
|
<nav> |
||||||
|
<h1><a href="{{ url_for('index') }}">Flaskr</a></h1> |
||||||
|
<ul> |
||||||
|
{% if g.user %} |
||||||
|
<li><span>{{ g.user['username'] }}</span> |
||||||
|
<li><a href="{{ url_for('auth.logout') }}">Log Out</a> |
||||||
|
{% else %} |
||||||
|
<li><a href="{{ url_for('auth.register') }}">Register</a> |
||||||
|
<li><a href="{{ url_for('auth.login') }}">Log In</a> |
||||||
|
{% endif %} |
||||||
|
</ul> |
||||||
|
</nav> |
||||||
|
<section class="content"> |
||||||
|
<header> |
||||||
|
{% block header %}{% endblock %} |
||||||
|
</header> |
||||||
|
{% for message in get_flashed_messages() %} |
||||||
|
<div class="flash">{{ message }}</div> |
||||||
|
{% endfor %} |
||||||
|
{% block content %}{% endblock %} |
||||||
|
</section> |
@ -0,0 +1,15 @@ |
|||||||
|
{% extends 'base.html' %} |
||||||
|
|
||||||
|
{% block header %} |
||||||
|
<h1>{% block title %}New Post{% endblock %}</h1> |
||||||
|
{% endblock %} |
||||||
|
|
||||||
|
{% block content %} |
||||||
|
<form method="post"> |
||||||
|
<label for="title">Title</label> |
||||||
|
<input name="title" id="title" value="{{ request.form['title'] }}" required> |
||||||
|
<label for="body">Body</label> |
||||||
|
<textarea name="body" id="body">{{ request.form['body'] }}</textarea> |
||||||
|
<input type="submit" value="Save"> |
||||||
|
</form> |
||||||
|
{% endblock %} |
@ -0,0 +1,28 @@ |
|||||||
|
{% extends 'base.html' %} |
||||||
|
|
||||||
|
{% block header %} |
||||||
|
<h1>{% block title %}Posts{% endblock %}</h1> |
||||||
|
{% if g.user %} |
||||||
|
<a class="action" href="{{ url_for('blog.create') }}">New</a> |
||||||
|
{% endif %} |
||||||
|
{% endblock %} |
||||||
|
|
||||||
|
{% block content %} |
||||||
|
{% for post in posts %} |
||||||
|
<article class="post"> |
||||||
|
<header> |
||||||
|
<div> |
||||||
|
<h1>{{ post['title'] }}</h1> |
||||||
|
<div class="about">by {{ post['username'] }} on {{ post['created'].strftime('%Y-%m-%d') }}</div> |
||||||
|
</div> |
||||||
|
{% if g.user['id'] == post['author_id'] %} |
||||||
|
<a class="action" href="{{ url_for('blog.update', id=post['id']) }}">Edit</a> |
||||||
|
{% endif %} |
||||||
|
</header> |
||||||
|
<p class="body">{{ post['body'] }}</p> |
||||||
|
</article> |
||||||
|
{% if not loop.last %} |
||||||
|
<hr> |
||||||
|
{% endif %} |
||||||
|
{% endfor %} |
||||||
|
{% endblock %} |
@ -0,0 +1,19 @@ |
|||||||
|
{% extends 'base.html' %} |
||||||
|
|
||||||
|
{% block header %} |
||||||
|
<h1>{% block title %}Edit "{{ post['title'] }}"{% endblock %}</h1> |
||||||
|
{% endblock %} |
||||||
|
|
||||||
|
{% block content %} |
||||||
|
<form method="post"> |
||||||
|
<label for="title">Title</label> |
||||||
|
<input name="title" id="title" value="{{ request.form['title'] or post['title'] }}" required> |
||||||
|
<label for="body">Body</label> |
||||||
|
<textarea name="body" id="body">{{ request.form['body'] or post['body'] }}</textarea> |
||||||
|
<input type="submit" value="Save"> |
||||||
|
</form> |
||||||
|
<hr> |
||||||
|
<form action="{{ url_for('blog.delete', id=post['id']) }}" method="post"> |
||||||
|
<input class="danger" type="submit" value="Delete" onclick="return confirm('Are you sure?');"> |
||||||
|
</form> |
||||||
|
{% endblock %} |
@ -0,0 +1,13 @@ |
|||||||
|
[metadata] |
||||||
|
license_file = LICENSE |
||||||
|
|
||||||
|
[bdist_wheel] |
||||||
|
universal = False |
||||||
|
|
||||||
|
[tool:pytest] |
||||||
|
testpaths = tests |
||||||
|
|
||||||
|
[coverage:run] |
||||||
|
branch = True |
||||||
|
source = |
||||||
|
flaskr |
@ -0,0 +1,23 @@ |
|||||||
|
import io |
||||||
|
|
||||||
|
from setuptools import find_packages, setup |
||||||
|
|
||||||
|
with io.open('README.rst', 'rt', encoding='utf8') as f: |
||||||
|
readme = f.read() |
||||||
|
|
||||||
|
setup( |
||||||
|
name='flaskr', |
||||||
|
version='1.0.0', |
||||||
|
url='http://flask.pocoo.org/docs/tutorial/', |
||||||
|
license='BSD', |
||||||
|
maintainer='Pallets team', |
||||||
|
maintainer_email='contact@palletsprojects.com', |
||||||
|
description='The basic blog app built in the Flask tutorial.', |
||||||
|
long_description=readme, |
||||||
|
packages=find_packages(), |
||||||
|
include_package_data=True, |
||||||
|
zip_safe=False, |
||||||
|
install_requires=[ |
||||||
|
'flask', |
||||||
|
], |
||||||
|
) |
@ -0,0 +1,64 @@ |
|||||||
|
import os |
||||||
|
import tempfile |
||||||
|
|
||||||
|
import pytest |
||||||
|
from flaskr import create_app |
||||||
|
from flaskr.db import get_db, init_db |
||||||
|
|
||||||
|
# read in SQL for populating test data |
||||||
|
with open(os.path.join(os.path.dirname(__file__), 'data.sql'), 'rb') as f: |
||||||
|
_data_sql = f.read().decode('utf8') |
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture |
||||||
|
def app(): |
||||||
|
"""Create and configure a new app instance for each test.""" |
||||||
|
# create a temporary file to isolate the database for each test |
||||||
|
db_fd, db_path = tempfile.mkstemp() |
||||||
|
# create the app with common test config |
||||||
|
app = create_app({ |
||||||
|
'TESTING': True, |
||||||
|
'DATABASE': db_path, |
||||||
|
}) |
||||||
|
|
||||||
|
# create the database and load test data |
||||||
|
with app.app_context(): |
||||||
|
init_db() |
||||||
|
get_db().executescript(_data_sql) |
||||||
|
|
||||||
|
yield app |
||||||
|
|
||||||
|
# close and remove the temporary database |
||||||
|
os.close(db_fd) |
||||||
|
os.unlink(db_path) |
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture |
||||||
|
def client(app): |
||||||
|
"""A test client for the app.""" |
||||||
|
return app.test_client() |
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture |
||||||
|
def runner(app): |
||||||
|
"""A test runner for the app's Click commands.""" |
||||||
|
return app.test_cli_runner() |
||||||
|
|
||||||
|
|
||||||
|
class AuthActions(object): |
||||||
|
def __init__(self, client): |
||||||
|
self._client = client |
||||||
|
|
||||||
|
def login(self, username='test', password='test'): |
||||||
|
return self._client.post( |
||||||
|
'/auth/login', |
||||||
|
data={'username': username, 'password': password} |
||||||
|
) |
||||||
|
|
||||||
|
def logout(self): |
||||||
|
return self._client.get('/auth/logout') |
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture |
||||||
|
def auth(client): |
||||||
|
return AuthActions(client) |
@ -0,0 +1,8 @@ |
|||||||
|
INSERT INTO user (username, password) |
||||||
|
VALUES |
||||||
|
('test', 'pbkdf2:sha256:50000$TCI4GzcX$0de171a4f4dac32e3364c7ddc7c14f3e2fa61f2d17574483f7ffbb431b4acb2f'), |
||||||
|
('other', 'pbkdf2:sha256:50000$kJPKsz6N$d2d4784f1b030a9761f5ccaeeaca413f27f2ecb76d6168407af962ddce849f79'); |
||||||
|
|
||||||
|
INSERT INTO post (title, body, author_id, created) |
||||||
|
VALUES |
||||||
|
('test title', 'test' || x'0a' || 'body', 1, '2018-01-01 00:00:00'); |
@ -0,0 +1,66 @@ |
|||||||
|
import pytest |
||||||
|
from flask import g, session |
||||||
|
from flaskr.db import get_db |
||||||
|
|
||||||
|
|
||||||
|
def test_register(client, app): |
||||||
|
# test that viewing the page renders without template errors |
||||||
|
assert client.get('/auth/register').status_code == 200 |
||||||
|
|
||||||
|
# test that successful registration redirects to the login page |
||||||
|
response = client.post( |
||||||
|
'/auth/register', data={'username': 'a', 'password': 'a'} |
||||||
|
) |
||||||
|
assert 'http://localhost/auth/login' == response.headers['Location'] |
||||||
|
|
||||||
|
# test that the user was inserted into the database |
||||||
|
with app.app_context(): |
||||||
|
assert get_db().execute( |
||||||
|
"select * from user where username = 'a'", |
||||||
|
).fetchone() is not None |
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(('username', 'password', 'message'), ( |
||||||
|
('', '', b'Username is required.'), |
||||||
|
('a', '', b'Password is required.'), |
||||||
|
('test', 'test', b'already registered'), |
||||||
|
)) |
||||||
|
def test_register_validate_input(client, username, password, message): |
||||||
|
response = client.post( |
||||||
|
'/auth/register', |
||||||
|
data={'username': username, 'password': password} |
||||||
|
) |
||||||
|
assert message in response.data |
||||||
|
|
||||||
|
|
||||||
|
def test_login(client, auth): |
||||||
|
# test that viewing the page renders without template errors |
||||||
|
assert client.get('/auth/login').status_code == 200 |
||||||
|
|
||||||
|
# test that successful login redirects to the index page |
||||||
|
response = auth.login() |
||||||
|
assert response.headers['Location'] == 'http://localhost/' |
||||||
|
|
||||||
|
# login request set the user_id in the session |
||||||
|
# check that the user is loaded from the session |
||||||
|
with client: |
||||||
|
client.get('/') |
||||||
|
assert session['user_id'] == 1 |
||||||
|
assert g.user['username'] == 'test' |
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(('username', 'password', 'message'), ( |
||||||
|
('a', 'test', b'Incorrect username.'), |
||||||
|
('test', 'a', b'Incorrect password.'), |
||||||
|
)) |
||||||
|
def test_login_validate_input(auth, username, password, message): |
||||||
|
response = auth.login(username, password) |
||||||
|
assert message in response.data |
||||||
|
|
||||||
|
|
||||||
|
def test_logout(client, auth): |
||||||
|
auth.login() |
||||||
|
|
||||||
|
with client: |
||||||
|
auth.logout() |
||||||
|
assert 'user_id' not in session |
@ -0,0 +1,92 @@ |
|||||||
|
import pytest |
||||||
|
from flaskr.db import get_db |
||||||
|
|
||||||
|
|
||||||
|
def test_index(client, auth): |
||||||
|
response = client.get('/') |
||||||
|
assert b"Log In" in response.data |
||||||
|
assert b"Register" in response.data |
||||||
|
|
||||||
|
auth.login() |
||||||
|
response = client.get('/') |
||||||
|
assert b'test title' in response.data |
||||||
|
assert b'by test on 2018-01-01' in response.data |
||||||
|
assert b'test\nbody' in response.data |
||||||
|
assert b'href="/1/update"' in response.data |
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('path', ( |
||||||
|
'/create', |
||||||
|
'/1/update', |
||||||
|
'/1/delete', |
||||||
|
)) |
||||||
|
def test_login_required(client, path): |
||||||
|
response = client.post(path) |
||||||
|
assert response.headers['Location'] == 'http://localhost/auth/login' |
||||||
|
|
||||||
|
|
||||||
|
def test_author_required(app, client, auth): |
||||||
|
# change the post author to another user |
||||||
|
with app.app_context(): |
||||||
|
db = get_db() |
||||||
|
db.execute('UPDATE post SET author_id = 2 WHERE id = 1') |
||||||
|
db.commit() |
||||||
|
|
||||||
|
auth.login() |
||||||
|
# current user can't modify other user's post |
||||||
|
assert client.post('/1/update').status_code == 403 |
||||||
|
assert client.post('/1/delete').status_code == 403 |
||||||
|
# current user doesn't see edit link |
||||||
|
assert b'href="/1/update"' not in client.get('/').data |
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('path', ( |
||||||
|
'/2/update', |
||||||
|
'/2/delete', |
||||||
|
)) |
||||||
|
def test_exists_required(client, auth, path): |
||||||
|
auth.login() |
||||||
|
assert client.post(path).status_code == 404 |
||||||
|
|
||||||
|
|
||||||
|
def test_create(client, auth, app): |
||||||
|
auth.login() |
||||||
|
assert client.get('/create').status_code == 200 |
||||||
|
client.post('/create', data={'title': 'created', 'body': ''}) |
||||||
|
|
||||||
|
with app.app_context(): |
||||||
|
db = get_db() |
||||||
|
count = db.execute('SELECT COUNT(id) FROM post').fetchone()[0] |
||||||
|
assert count == 2 |
||||||
|
|
||||||
|
|
||||||
|
def test_update(client, auth, app): |
||||||
|
auth.login() |
||||||
|
assert client.get('/1/update').status_code == 200 |
||||||
|
client.post('/1/update', data={'title': 'updated', 'body': ''}) |
||||||
|
|
||||||
|
with app.app_context(): |
||||||
|
db = get_db() |
||||||
|
post = db.execute('SELECT * FROM post WHERE id = 1').fetchone() |
||||||
|
assert post['title'] == 'updated' |
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('path', ( |
||||||
|
'/create', |
||||||
|
'/1/update', |
||||||
|
)) |
||||||
|
def test_create_update_validate(client, auth, path): |
||||||
|
auth.login() |
||||||
|
response = client.post(path, data={'title': '', 'body': ''}) |
||||||
|
assert b'Title is required.' in response.data |
||||||
|
|
||||||
|
|
||||||
|
def test_delete(client, auth, app): |
||||||
|
auth.login() |
||||||
|
response = client.post('/1/delete') |
||||||
|
assert response.headers['Location'] == 'http://localhost/' |
||||||
|
|
||||||
|
with app.app_context(): |
||||||
|
db = get_db() |
||||||
|
post = db.execute('SELECT * FROM post WHERE id = 1').fetchone() |
||||||
|
assert post is None |
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue