rewrite tutorial docs and example
This commit is contained in:
parent
16d83d6bb4
commit
c3dd7b8e4c
103 changed files with 3327 additions and 2224 deletions
|
|
@ -75,7 +75,7 @@ First time setup
|
|||
.. _latest version of git: https://git-scm.com/downloads
|
||||
.. _username: https://help.github.com/articles/setting-your-username-in-git/
|
||||
.. _email: https://help.github.com/articles/setting-your-email-in-git/
|
||||
.. _Fork: https://github.com/pallets/flask/pull/2305#fork-destination-box
|
||||
.. _Fork: https://github.com/pallets/flask/fork
|
||||
.. _Clone: https://help.github.com/articles/fork-a-repo/#step-2-create-a-local-clone-of-your-fork
|
||||
|
||||
Start coding
|
||||
|
|
|
|||
BIN
docs/_static/flaskr.png
vendored
BIN
docs/_static/flaskr.png
vendored
Binary file not shown.
|
Before Width: | Height: | Size: 65 KiB |
39
docs/conf.py
39
docs/conf.py
|
|
@ -11,13 +11,13 @@
|
|||
# All configuration values have a default; values that are commented out
|
||||
# serve to show the default.
|
||||
from __future__ import print_function
|
||||
|
||||
import datetime
|
||||
import os
|
||||
import sys
|
||||
import pkg_resources
|
||||
import time
|
||||
import datetime
|
||||
|
||||
from sphinx.application import Sphinx
|
||||
import pkg_resources
|
||||
|
||||
BUILD_DATE = datetime.datetime.utcfromtimestamp(int(os.environ.get('SOURCE_DATE_EPOCH', time.time())))
|
||||
|
||||
|
|
@ -300,7 +300,7 @@ unwrap_decorators()
|
|||
del unwrap_decorators
|
||||
|
||||
|
||||
def setup(app: Sphinx):
|
||||
def setup(app):
|
||||
def cut_module_meta(app, what, name, obj, options, lines):
|
||||
"""Remove metadata from autodoc output."""
|
||||
if what != 'module':
|
||||
|
|
@ -312,3 +312,34 @@ def setup(app: Sphinx):
|
|||
]
|
||||
|
||||
app.connect('autodoc-process-docstring', cut_module_meta)
|
||||
|
||||
def github_link(
|
||||
name, rawtext, text, lineno, inliner,
|
||||
options=None, content=None
|
||||
):
|
||||
app = inliner.document.settings.env.app
|
||||
release = app.config.release
|
||||
base_url = 'https://github.com/pallets/flask/tree/'
|
||||
|
||||
if text.endswith('>'):
|
||||
words, text = text[:-1].rsplit('<', 1)
|
||||
words = words.strip()
|
||||
else:
|
||||
words = None
|
||||
|
||||
if release.endswith('dev'):
|
||||
url = '{0}master/{1}'.format(base_url, text)
|
||||
else:
|
||||
url = '{0}{1}/{2}'.format(base_url, release, text)
|
||||
|
||||
if words is None:
|
||||
words = url
|
||||
|
||||
from docutils.nodes import reference
|
||||
from docutils.parsers.rst.roles import set_classes
|
||||
options = options or {}
|
||||
set_classes(options)
|
||||
node = reference(rawtext, words, refuri=url, **options)
|
||||
return [node], []
|
||||
|
||||
app.add_role('gh', github_link)
|
||||
|
|
|
|||
|
|
@ -104,6 +104,8 @@ On Windows:
|
|||
|
||||
\Python27\Scripts\virtualenv.exe venv
|
||||
|
||||
.. _install-activate-env:
|
||||
|
||||
Activate the environment
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
|
|
|
|||
|
|
@ -162,7 +162,5 @@ explanation of the little bit of code above:
|
|||
argument. Note that we can use the `$SCRIPT_ROOT` variable here that
|
||||
we set earlier.
|
||||
|
||||
If you don't get the whole picture, download the `sourcecode
|
||||
for this example
|
||||
<https://github.com/pallets/flask/tree/master/examples/jqueryexample>`_
|
||||
from GitHub.
|
||||
If you don't get the whole picture, download the :gh:`sourcecode
|
||||
for this example <examples/jqueryexample>`.
|
||||
|
|
|
|||
|
|
@ -17,9 +17,8 @@ this::
|
|||
login.html
|
||||
...
|
||||
|
||||
If you find yourself stuck on something, feel free
|
||||
to take a look at the source code for this example.
|
||||
You'll find `the full src for this example here`_.
|
||||
The :ref:`tutorial <tutorial>` is structured this way, see the
|
||||
:gh:`example code <examples/tutorial>`.
|
||||
|
||||
Simple Packages
|
||||
---------------
|
||||
|
|
@ -134,7 +133,6 @@ You should then end up with something like that::
|
|||
|
||||
|
||||
.. _working-with-modules:
|
||||
.. _the full src for this example here: https://github.com/pallets/flask/tree/master/examples/patterns/largerapp
|
||||
|
||||
Working with Blueprints
|
||||
-----------------------
|
||||
|
|
|
|||
|
|
@ -28,10 +28,7 @@ The Application
|
|||
|
||||
First, we need an application to test; we will use the application from
|
||||
the :ref:`tutorial`. If you don't have that application yet, get the
|
||||
source code from `the examples`_.
|
||||
|
||||
.. _the examples:
|
||||
https://github.com/pallets/flask/tree/master/examples/flaskr/
|
||||
source code from :gh:`the examples <examples/tutorial>`.
|
||||
|
||||
The Testing Skeleton
|
||||
--------------------
|
||||
|
|
|
|||
336
docs/tutorial/blog.rst
Normal file
336
docs/tutorial/blog.rst
Normal file
|
|
@ -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`.
|
||||
213
docs/tutorial/database.rst
Normal file
213
docs/tutorial/database.rst
Normal file
|
|
@ -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`
|
||||
121
docs/tutorial/deploy.rst
Normal file
121
docs/tutorial/deploy.rst
Normal file
|
|
@ -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`.
|
||||
177
docs/tutorial/factory.rst
Normal file
177
docs/tutorial/factory.rst
Normal file
|
|
@ -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`.
|
||||
BIN
docs/tutorial/flaskr_edit.png
Normal file
BIN
docs/tutorial/flaskr_edit.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 13 KiB |
BIN
docs/tutorial/flaskr_index.png
Normal file
BIN
docs/tutorial/flaskr_index.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 11 KiB |
BIN
docs/tutorial/flaskr_login.png
Normal file
BIN
docs/tutorial/flaskr_login.png
Normal file
Binary file not shown.
|
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/
|
||||
|
|
@ -3,31 +3,64 @@
|
|||
Tutorial
|
||||
========
|
||||
|
||||
Learn by example to develop an application with Python and Flask.
|
||||
|
||||
In this tutorial, we will create a simple blogging application. It only
|
||||
supports one user, only allows text entries, and has no feeds or comments.
|
||||
|
||||
While very simple, this example still features everything you need to get
|
||||
started. In addition to Flask, we will use SQLite for the database, which is
|
||||
built-in to Python, so there is nothing else you need.
|
||||
|
||||
If you want the full source code in advance or for comparison, check out
|
||||
the `example source`_.
|
||||
|
||||
.. _example source: https://github.com/pallets/flask/tree/master/examples/flaskr/
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
:caption: Contents:
|
||||
:maxdepth: 1
|
||||
|
||||
introduction
|
||||
folders
|
||||
schema
|
||||
setup
|
||||
packaging
|
||||
dbcon
|
||||
dbinit
|
||||
views
|
||||
templates
|
||||
css
|
||||
testing
|
||||
layout
|
||||
factory
|
||||
database
|
||||
views
|
||||
templates
|
||||
static
|
||||
blog
|
||||
install
|
||||
tests
|
||||
deploy
|
||||
next
|
||||
|
||||
This tutorial will walk you through creating a basic blog application
|
||||
called Flaskr. Users will be able to register, log in, create posts,
|
||||
and edit or delete their own posts. You will be able to package and
|
||||
install the application on other computers.
|
||||
|
||||
.. image:: flaskr_index.png
|
||||
:align: center
|
||||
:class: screenshot
|
||||
:alt: screenshot of index page
|
||||
|
||||
It's assumed that you're already familiar with Python. The `official
|
||||
tutorial`_ in the Python docs is a great way to learn or review first.
|
||||
|
||||
.. _official tutorial: https://docs.python.org/3/tutorial/
|
||||
|
||||
While it's designed to give a good starting point, the tutorial doesn't
|
||||
cover all of Flask's features. Check out the :ref:`quickstart` for an
|
||||
overview of what Flask can do, then dive into the docs to find out more.
|
||||
The tutorial only uses what's provided by Flask and Python. In another
|
||||
project, you might decide to use :ref:`extensions` or other libraries to
|
||||
make some tasks simpler.
|
||||
|
||||
.. image:: flaskr_login.png
|
||||
:align: center
|
||||
:class: screenshot
|
||||
:alt: screenshot of login page
|
||||
|
||||
Flask is flexible. It doesn't require you to use any particular project
|
||||
or code layout. However, when first starting, it's helpful to use a more
|
||||
structured approach. This means that the tutorial will require a bit of
|
||||
boilerplate up front, but it's done to avoid many common pitfalls that
|
||||
new developers encounter, and it creates a project that's easy to expand
|
||||
on. Once you become more comfortable with Flask, you can step out of
|
||||
this structure and take full advantage of Flask's flexibility.
|
||||
|
||||
.. image:: flaskr_edit.png
|
||||
:align: center
|
||||
:class: screenshot
|
||||
:alt: screenshot of login page
|
||||
|
||||
:gh:`The tutorial project is available as an example in the Flask
|
||||
repository <examples/tutorial>`, if you want to compare your project
|
||||
with the final product as you follow the tutorial.
|
||||
|
||||
Continue to :doc:`layout`.
|
||||
|
|
|
|||
113
docs/tutorial/install.rst
Normal file
113
docs/tutorial/install.rst
Normal file
|
|
@ -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/
|
||||
110
docs/tutorial/layout.rst
Normal file
110
docs/tutorial/layout.rst
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
Project Layout
|
||||
==============
|
||||
|
||||
Create a project directory and enter it:
|
||||
|
||||
.. code-block:: none
|
||||
|
||||
mkdir flask-tutorial
|
||||
cd flask-tutorial
|
||||
|
||||
Then follow the :doc:`installation instructions </installation>` to set
|
||||
up a Python virtual environment and install Flask for your project.
|
||||
|
||||
The tutorial will assume you're working from the ``flask-tutorial``
|
||||
directory from now on. The file names at the top of each code block are
|
||||
relative to this directory.
|
||||
|
||||
----
|
||||
|
||||
A Flask application can be as simple as a single file.
|
||||
|
||||
.. code-block:: python
|
||||
:caption: ``hello.py``
|
||||
|
||||
from flask import Flask
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
|
||||
@app.route('/')
|
||||
def hello():
|
||||
return 'Hello, World!'
|
||||
|
||||
However, as a project get bigger, it becomes overwhelming to keep all
|
||||
the code in one file. Python projects use *packages* to organize code
|
||||
into multiple modules that can be imported where needed, and the
|
||||
tutorial will do this as well.
|
||||
|
||||
The project directory will contain:
|
||||
|
||||
* ``flaskr/``, a Python package containing your application code and
|
||||
files.
|
||||
* ``tests/``, a directory containing test modules.
|
||||
* ``venv/``, a Python virtual environment where Flask and other
|
||||
dependencies are installed.
|
||||
* Installation files telling Python how to install your project.
|
||||
* Version control config, such as `git`_. You should make a habit of
|
||||
using some type of version control for all your projects, no matter
|
||||
the size.
|
||||
* Any other project files you might add in the future.
|
||||
|
||||
.. _git: https://git-scm.com/
|
||||
|
||||
By the end, your project layout will look like this:
|
||||
|
||||
.. code-block:: none
|
||||
|
||||
/home/user/Projects/flask-tutorial
|
||||
├── flaskr/
|
||||
│ ├── __init__.py
|
||||
│ ├── db.py
|
||||
│ ├── schema.sql
|
||||
│ ├── auth.py
|
||||
│ ├── blog.py
|
||||
│ ├── templates/
|
||||
│ │ ├── base.html
|
||||
│ │ ├── auth/
|
||||
│ │ │ ├── login.html
|
||||
│ │ │ └── register.html
|
||||
│ │ └── blog/
|
||||
│ │ ├── create.html
|
||||
│ │ ├── index.html
|
||||
│ │ └── update.html
|
||||
│ └── static/
|
||||
│ └── style.css
|
||||
├── tests/
|
||||
│ ├── conftest.py
|
||||
│ ├── data.sql
|
||||
│ ├── test_factory.py
|
||||
│ ├── test_db.py
|
||||
│ ├── test_auth.py
|
||||
│ └── test_blog.py
|
||||
├── venv/
|
||||
├── setup.py
|
||||
└── MANIFEST.in
|
||||
|
||||
If you're using version control, the following files that are generated
|
||||
while running your project should be ignored. There may be other files
|
||||
based on the editor you use. In general, ignore files that you didn't
|
||||
write. For example, with git:
|
||||
|
||||
.. code-block:: none
|
||||
:caption: ``.gitignore``
|
||||
|
||||
venv/
|
||||
|
||||
*.pyc
|
||||
__pycache__/
|
||||
|
||||
instance/
|
||||
|
||||
.pytest_cache/
|
||||
.coverage
|
||||
htmlcov/
|
||||
|
||||
dist/
|
||||
build/
|
||||
*.egg-info/
|
||||
|
||||
Continue to :doc:`factory`.
|
||||
38
docs/tutorial/next.rst
Normal file
38
docs/tutorial/next.rst
Normal file
|
|
@ -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`.
|
||||
72
docs/tutorial/static.rst
Normal file
72
docs/tutorial/static.rst
Normal file
|
|
@ -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
|
||||
|
||||
Step 7: The Templates
|
||||
=====================
|
||||
Templates
|
||||
=========
|
||||
|
||||
Now it is time to start working on the templates. As you may have
|
||||
noticed, if you make requests with the app running, you will get
|
||||
an exception that Flask cannot find the templates. The templates
|
||||
are using `Jinja2`_ syntax and have autoescaping enabled by
|
||||
default. This means that unless you mark a value in the code with
|
||||
:class:`~flask.Markup` or with the ``|safe`` filter in the template,
|
||||
Jinja2 will ensure that special characters such as ``<`` or ``>`` are
|
||||
escaped with their XML equivalents.
|
||||
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.
|
||||
|
||||
We are also using template inheritance which makes it possible to reuse
|
||||
the layout of the website in all pages.
|
||||
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.
|
||||
|
||||
Create the follwing three HTML files and place them in the
|
||||
:file:`templates` folder:
|
||||
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.
|
||||
|
||||
.. _Jinja2: http://jinja.pocoo.org/docs/templates
|
||||
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.
|
||||
|
||||
layout.html
|
||||
-----------
|
||||
.. _Jinja: http://jinja.pocoo.org/docs/templates/
|
||||
.. _HTML: https://developer.mozilla.org/docs/Web/HTML
|
||||
|
||||
This template contains the HTML skeleton, the header and a link to log in
|
||||
(or log out if the user was already logged in). It also displays the
|
||||
flashed messages if there are any. The ``{% block body %}`` block can be
|
||||
replaced by a block of the same name (``body``) in a child template.
|
||||
|
||||
The :class:`~flask.session` dict is available in the template as well and
|
||||
you can use that to check if the user is logged in or not. Note that in
|
||||
Jinja you can access missing attributes and items of objects / dicts which
|
||||
makes the following code work, even if there is no ``'logged_in'`` key in
|
||||
the session:
|
||||
The Base Layout
|
||||
---------------
|
||||
|
||||
.. sourcecode:: html+jinja
|
||||
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``
|
||||
|
||||
<!doctype html>
|
||||
<title>Flaskr</title>
|
||||
<link rel=stylesheet type=text/css href="{{ url_for('static', filename='style.css') }}">
|
||||
<div class=page>
|
||||
<title>{% block title %}{% endblock %} - Flaskr</title>
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
|
||||
<nav>
|
||||
<h1>Flaskr</h1>
|
||||
<div class=metanav>
|
||||
{% if not session.logged_in %}
|
||||
<a href="{{ url_for('login') }}">log in</a>
|
||||
{% else %}
|
||||
<a href="{{ url_for('logout') }}">log out</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% for message in get_flashed_messages() %}
|
||||
<div class=flash>{{ message }}</div>
|
||||
{% endfor %}
|
||||
{% block body %}{% endblock %}
|
||||
</div>
|
||||
|
||||
show_entries.html
|
||||
-----------------
|
||||
|
||||
This template extends the :file:`layout.html` template from above to display the
|
||||
messages. Note that the ``for`` loop iterates over the messages we passed
|
||||
in with the :func:`~flask.render_template` function. Notice that the form is
|
||||
configured to submit to the `add_entry` view function and use ``POST`` as
|
||||
HTTP method:
|
||||
|
||||
.. sourcecode:: html+jinja
|
||||
|
||||
{% extends "layout.html" %}
|
||||
{% block body %}
|
||||
{% if session.logged_in %}
|
||||
<form action="{{ url_for('add_entry') }}" method=post class=add-entry>
|
||||
<dl>
|
||||
<dt>Title:
|
||||
<dd><input type=text size=30 name=title>
|
||||
<dt>Text:
|
||||
<dd><textarea name=text rows=5 cols=40></textarea>
|
||||
<dd><input type=submit value=Share>
|
||||
</dl>
|
||||
</form>
|
||||
{% endif %}
|
||||
<ul class=entries>
|
||||
{% for entry in entries %}
|
||||
<li><h2>{{ entry.title }}</h2>{{ entry.text|safe }}</li>
|
||||
{% else %}
|
||||
<li><em>Unbelievable. No entries here so far</em></li>
|
||||
{% endfor %}
|
||||
<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>
|
||||
|
||||
:data:`g` is automatically available in templates. Based on if
|
||||
``g.user`` is set (from ``load_logged_in_user``), either the username
|
||||
and a log out link are displayed, otherwise links to register and log in
|
||||
are displayed. :func:`url_for` is also automatically available, and is
|
||||
used to generate URLs to views instead of writing them out manually.
|
||||
|
||||
After the page title, and before the content, the template loops over
|
||||
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.
|
||||
|
||||
There are three blocks defined here that will be overridden in the other
|
||||
templates:
|
||||
|
||||
#. ``{% block title %}`` will change the title displayed in the
|
||||
browser's tab and window title.
|
||||
|
||||
#. ``{% block header %}`` is similar to ``title`` but will change the
|
||||
title displayed on the page.
|
||||
|
||||
#. ``{% block content %}`` is where the content of each page goes, such
|
||||
as the login form or a blog post.
|
||||
|
||||
The base template is directly in the ``templates`` directory. To keep
|
||||
the others organized, the templates for a blueprint will be placed in a
|
||||
directory with the same name as the blueprint.
|
||||
|
||||
|
||||
Register
|
||||
--------
|
||||
|
||||
.. code-block:: html+jinja
|
||||
:caption: ``flaskr/templates/auth/register.html``
|
||||
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block header %}
|
||||
<h1>{% block title %}Register{% endblock %}</h1>
|
||||
{% endblock %}
|
||||
|
||||
login.html
|
||||
----------
|
||||
|
||||
This is the login template, which basically just displays a form to allow
|
||||
the user to login:
|
||||
|
||||
.. sourcecode:: html+jinja
|
||||
|
||||
{% extends "layout.html" %}
|
||||
{% block body %}
|
||||
<h2>Login</h2>
|
||||
{% if error %}<p class=error><strong>Error:</strong> {{ error }}{% endif %}
|
||||
<form action="{{ url_for('login') }}" method=post>
|
||||
<dl>
|
||||
<dt>Username:
|
||||
<dd><input type=text name=username>
|
||||
<dt>Password:
|
||||
<dd><input type=password name=password>
|
||||
<dd><input type=submit value=Login>
|
||||
</dl>
|
||||
{% 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 %}
|
||||
|
||||
Continue with :ref:`tutorial-css`.
|
||||
``{% extends 'base.html' %}`` tells Jinja that this template should
|
||||
replace the blocks from the base template. All the rendered content must
|
||||
appear inside ``{% block %}`` tags that override blocks from the base
|
||||
template.
|
||||
|
||||
A useful pattern used here is to place ``{% block title %}`` inside
|
||||
``{% block header %}``. This will set the title block and then output
|
||||
the value of it into the header block, so that both the window and page
|
||||
share the same title without writing it twice.
|
||||
|
||||
The ``input`` tags are using the ``required`` attribute here. This tells
|
||||
the browser not to submit the form until those fields are filled in. If
|
||||
the user is using an older browser that doesn't support that attribute,
|
||||
or if they are using something besides a browser to make requests, you
|
||||
still want to validate the data in the Flask view. It's important to
|
||||
always fully validate the data on the server, even if the client does
|
||||
some validation as well.
|
||||
|
||||
|
||||
Log In
|
||||
------
|
||||
|
||||
This is identical to the register template except for the title and
|
||||
submit button.
|
||||
|
||||
.. code-block:: html+jinja
|
||||
:caption: ``flaskr/templates/auth/login.html``
|
||||
|
||||
{% 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 %}
|
||||
|
||||
|
||||
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.
|
||||
561
docs/tutorial/tests.rst
Normal file
561
docs/tutorial/tests.rst
Normal file
|
|
@ -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
|
||||
view functions. You will need four of them; Show Entries, Add New Entry,
|
||||
Login and Logout. Add the following code snipets to :file:`flaskr.py`.
|
||||
A view function is the code you write to respond to requests to your
|
||||
application. Flask uses patterns to match the incoming request URL to
|
||||
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
|
||||
generate a URL to a view based on its name and arguments.
|
||||
|
||||
Show Entries
|
||||
------------
|
||||
|
||||
This view shows all the entries stored in the database. It listens on the
|
||||
root of the application and will select title and text from the database.
|
||||
The one with the highest id (the newest entry) will be on top. The rows
|
||||
returned from the cursor look a bit like dictionaries because we are using
|
||||
the :class:`sqlite3.Row` row factory.
|
||||
Create a Blueprint
|
||||
------------------
|
||||
|
||||
The view function will pass the entries to the :file:`show_entries.html`
|
||||
template and return the rendered one::
|
||||
A :class:`Blueprint` is a way to organize a group of related views and
|
||||
other code. Rather than registering views and other code directly with
|
||||
an application, they are registered with a blueprint. Then the blueprint
|
||||
is registered with the application when it is available in the factory
|
||||
function.
|
||||
|
||||
@app.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)
|
||||
Flaskr will have two blueprints, one for authentication functions and
|
||||
one for the blog posts functions. The code for each blueprint will go
|
||||
in a separate module. Since the blog needs to know about authentication,
|
||||
you'll write the authentication one first.
|
||||
|
||||
Add New Entry
|
||||
-------------
|
||||
.. code-block:: python
|
||||
:caption: ``flaskr/auth.py``
|
||||
|
||||
This view lets the user add new entries if they are logged in. This only
|
||||
responds to ``POST`` requests; the actual form is shown on the
|
||||
`show_entries` page. If everything worked out well, it will
|
||||
:func:`~flask.flash` an information message to the next request and
|
||||
redirect back to the `show_entries` page::
|
||||
import functools
|
||||
|
||||
@app.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('show_entries'))
|
||||
from flask import (
|
||||
Blueprint, flash, g, redirect, render_template, request, session, url_for
|
||||
)
|
||||
from werkzeug.security import check_password_hash, generate_password_hash
|
||||
|
||||
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``).
|
||||
from flaskr.db import get_db
|
||||
|
||||
.. admonition:: Security Note
|
||||
bp = Blueprint('auth', __name__, url_prefix='/auth')
|
||||
|
||||
Be sure to use question marks when building SQL statements, as done in the
|
||||
example above. Otherwise, your app will be vulnerable to SQL injection when
|
||||
you use string formatting to build SQL statements.
|
||||
See :ref:`sqlite3` for more.
|
||||
This creates a :class:`Blueprint` named ``'auth'``. Like the application
|
||||
object, the blueprint needs to know where it's defined, so ``__name__``
|
||||
is passed as the second argument. The ``url_prefix`` will be prepended
|
||||
to all the URLs associated with the blueprint.
|
||||
|
||||
Login and Logout
|
||||
----------------
|
||||
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.
|
||||
|
||||
These functions are used to sign the user in and out. Login checks the
|
||||
username and password against the ones from the configuration and sets the
|
||||
`logged_in` key 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
|
||||
she was logged in successfully. If an error occurred, the template is
|
||||
notified about that, and the user is asked again::
|
||||
.. code-block:: python
|
||||
:caption: ``flaskr/__init__.py``
|
||||
|
||||
@app.route('/login', methods=['GET', 'POST'])
|
||||
def login():
|
||||
error = None
|
||||
def create_app():
|
||||
app = ...
|
||||
# existing code omitted
|
||||
|
||||
from . import auth
|
||||
app.register_blueprint(auth.bp)
|
||||
|
||||
return app
|
||||
|
||||
The authentication blueprint will have views to register new users and
|
||||
to log in and log out.
|
||||
|
||||
|
||||
The First View: Register
|
||||
------------------------
|
||||
|
||||
When the user visits the ``/auth/register`` URL, the ``register`` view
|
||||
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':
|
||||
if request.form['username'] != app.config['USERNAME']:
|
||||
error = 'Invalid username'
|
||||
elif request.form['password'] != app.config['PASSWORD']:
|
||||
error = 'Invalid password'
|
||||
else:
|
||||
session['logged_in'] = True
|
||||
flash('You were logged in')
|
||||
return redirect(url_for('show_entries'))
|
||||
return render_template('login.html', error=error)
|
||||
username = request.form['username']
|
||||
password = request.form['password']
|
||||
db = get_db()
|
||||
error = None
|
||||
|
||||
The `logout` function, on the other hand, removes that key from the session
|
||||
again. There is a neat trick here: if you use the :meth:`~dict.pop` method
|
||||
of the dict and pass a second parameter to it (the default), the method
|
||||
will delete the key from the dictionary if present or do nothing when that
|
||||
key is not in there. This is helpful because now it is not necessary to
|
||||
check if the user was logged in.
|
||||
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'))
|
||||
|
||||
@app.route('/logout')
|
||||
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():
|
||||
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:
|
||||
session.clear()
|
||||
session['user_id'] = user['id']
|
||||
return redirect(url_for('index'))
|
||||
|
||||
flash(error)
|
||||
|
||||
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():
|
||||
session.pop('logged_in', None)
|
||||
flash('You were logged out')
|
||||
return redirect(url_for('show_entries'))
|
||||
|
||||
.. admonition:: Security Note
|
||||
|
||||
Passwords should never be stored in plain text in a production
|
||||
system. This tutorial uses plain text passwords for simplicity. If you
|
||||
plan to release a project based off this tutorial out into the world,
|
||||
passwords should be both `hashed and salted`_ before being stored in a
|
||||
database or file.
|
||||
|
||||
Fortunately, there are Flask extensions for the purpose of
|
||||
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
|
||||
`here <http://flask.pocoo.org/extensions/>`_
|
||||
session.clear()
|
||||
return redirect(url_for('index'))
|
||||
|
||||
|
||||
Continue with :ref:`tutorial-templates`.
|
||||
Require Authentication in Other Views
|
||||
-------------------------------------
|
||||
|
||||
.. _hashed and salted: https://blog.codinghorror.com/youre-probably-storing-passwords-incorrectly/
|
||||
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)
|
||||
|
||||
return wrapped_view
|
||||
|
||||
This decorator returns a new view function that wraps the original view
|
||||
it's applied to. The new function checks if a user is loaded and
|
||||
redirects to the login page otherwise. If a user is loaded the original
|
||||
view is called and continues normally. You'll use this decorator when
|
||||
writing the blog views.
|
||||
|
||||
Endpoints and URLs
|
||||
------------------
|
||||
|
||||
The :func:`url_for` function generates the URL to a view based on a name
|
||||
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')``.
|
||||
|
||||
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.
|
||||
|
||||
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
|
||||
2
examples/flaskr/.gitignore
vendored
2
examples/flaskr/.gitignore
vendored
|
|
@ -1,2 +0,0 @@
|
|||
flaskr.db
|
||||
.eggs/
|
||||
|
|
@ -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,2 +0,0 @@
|
|||
[aliases]
|
||||
test=pytest
|
||||
|
|
@ -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 %}
|
||||
2
examples/minitwit/.gitignore
vendored
2
examples/minitwit/.gitignore
vendored
|
|
@ -1,2 +0,0 @@
|
|||
minitwit.db
|
||||
.eggs/
|
||||
|
|
@ -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 +0,0 @@
|
|||
from .minitwit import app
|
||||
|
|
@ -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,2 +0,0 @@
|
|||
[aliases]
|
||||
test=pytest
|
||||
|
|
@ -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!'
|
||||
14
examples/tutorial/.gitignore
vendored
Normal file
14
examples/tutorial/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
venv/
|
||||
*.pyc
|
||||
__pycache__/
|
||||
instance/
|
||||
.cache/
|
||||
.pytest_cache/
|
||||
.coverage
|
||||
htmlcov/
|
||||
dist/
|
||||
build/
|
||||
*.egg-info/
|
||||
.idea/
|
||||
*.swp
|
||||
*~
|
||||
31
examples/tutorial/LICENSE
Normal file
31
examples/tutorial/LICENSE
Normal file
|
|
@ -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
|
||||
graft flaskr/static
|
||||
include LICENSE
|
||||
include flaskr/schema.sql
|
||||
graft flaskr/static
|
||||
graft flaskr/templates
|
||||
graft tests
|
||||
global-exclude *.pyc
|
||||
76
examples/tutorial/README.rst
Normal file
76
examples/tutorial/README.rst
Normal file
|
|
@ -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
|
||||
48
examples/tutorial/flaskr/__init__.py
Normal file
48
examples/tutorial/flaskr/__init__.py
Normal file
|
|
@ -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
|
||||
108
examples/tutorial/flaskr/auth.py
Normal file
108
examples/tutorial/flaskr/auth.py
Normal file
|
|
@ -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'))
|
||||
119
examples/tutorial/flaskr/blog.py
Normal file
119
examples/tutorial/flaskr/blog.py
Normal file
|
|
@ -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'))
|
||||
54
examples/tutorial/flaskr/db.py
Normal file
54
examples/tutorial/flaskr/db.py
Normal file
|
|
@ -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)
|
||||
20
examples/tutorial/flaskr/schema.sql
Normal file
20
examples/tutorial/flaskr/schema.sql
Normal file
|
|
@ -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)
|
||||
);
|
||||
134
examples/tutorial/flaskr/static/style.css
Normal file
134
examples/tutorial/flaskr/static/style.css
Normal file
|
|
@ -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;
|
||||
}
|
||||
15
examples/tutorial/flaskr/templates/auth/login.html
Normal file
15
examples/tutorial/flaskr/templates/auth/login.html
Normal file
|
|
@ -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 %}
|
||||
15
examples/tutorial/flaskr/templates/auth/register.html
Normal file
15
examples/tutorial/flaskr/templates/auth/register.html
Normal file
|
|
@ -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 %}
|
||||
24
examples/tutorial/flaskr/templates/base.html
Normal file
24
examples/tutorial/flaskr/templates/base.html
Normal file
|
|
@ -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>
|
||||
15
examples/tutorial/flaskr/templates/blog/create.html
Normal file
15
examples/tutorial/flaskr/templates/blog/create.html
Normal file
|
|
@ -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 %}
|
||||
28
examples/tutorial/flaskr/templates/blog/index.html
Normal file
28
examples/tutorial/flaskr/templates/blog/index.html
Normal file
|
|
@ -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 %}
|
||||
19
examples/tutorial/flaskr/templates/blog/update.html
Normal file
19
examples/tutorial/flaskr/templates/blog/update.html
Normal file
|
|
@ -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 %}
|
||||
13
examples/tutorial/setup.cfg
Normal file
13
examples/tutorial/setup.cfg
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
[metadata]
|
||||
license_file = LICENSE
|
||||
|
||||
[bdist_wheel]
|
||||
universal = False
|
||||
|
||||
[tool:pytest]
|
||||
testpaths = tests
|
||||
|
||||
[coverage:run]
|
||||
branch = True
|
||||
source =
|
||||
flaskr
|
||||
23
examples/tutorial/setup.py
Normal file
23
examples/tutorial/setup.py
Normal file
|
|
@ -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',
|
||||
],
|
||||
)
|
||||
64
examples/tutorial/tests/conftest.py
Normal file
64
examples/tutorial/tests/conftest.py
Normal file
|
|
@ -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)
|
||||
8
examples/tutorial/tests/data.sql
Normal file
8
examples/tutorial/tests/data.sql
Normal file
|
|
@ -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');
|
||||
66
examples/tutorial/tests/test_auth.py
Normal file
66
examples/tutorial/tests/test_auth.py
Normal file
|
|
@ -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
|
||||
92
examples/tutorial/tests/test_blog.py
Normal file
92
examples/tutorial/tests/test_blog.py
Normal file
|
|
@ -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…
Add table
Add a link
Reference in a new issue