mirror of https://github.com/mitsuhiko/flask.git
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
336 lines
11 KiB
336 lines
11 KiB
.. 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`.
|
|
|