The blog blueprint will use the same techniques you learned writing the authentication 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.
The Blog Blueprint
Create the blog blueprint
Create 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__)
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. Register the blueprint
Import and register the blueprint in the factory (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
app.add_url_rule() associates the endpoint name 'index' with the / url so that url_for('index') or url_for('blog.index') will both work.
Index View
The index will show all posts, most recent first. A JOIN is used so that the author information from the user table is available in the result.
@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)
Create the template flaskr/templates/blog/index.html:
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 %}
Key features:
- When a user is logged in, the header shows a link to create new posts
- When the user is the author of a post, they see an “Edit” link
loop.last is a special Jinja variable that’s used to display a line after each post except the last one
Create View
The create view works the same as the auth register view. Either the form is displayed, or the posted data is validated and added to the database or an error is shown.
The login_required decorator 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.
@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')
Create the template flaskr/templates/blog/create.html:
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 View
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, write a helper function:
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, f"Post id {id} doesn't exist.")
if check_author and post['author_id'] != g.user['id']:
abort(403)
return post
abort() will raise a special exception that returns an HTTP status code:
404 means “Not Found”
403 means “Forbidden”
401 means “Unauthorized” (but we redirect to login instead)
Now implement the update view:
@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)
Key features:
- The
update function takes an id argument that corresponds to <int:id> in the route
- A real URL will look like
/1/update
- Flask will capture the
1, ensure it’s an int, and pass it as the id argument
Create the template flaskr/templates/blog/update.html:
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 edited data to the current page (
/<id>/update)
- The second contains only a button and posts to the delete view
The pattern {{ request.form['title'] or post['title'] }} chooses what data appears in the form:
- When the form hasn’t been submitted, the original
post data appears
- If invalid form data was posted, display that so the user can fix the error
Delete View
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:
@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'))
Test the Blog
Start the server
flask --app flaskr run --debug
Log in
Log in with the user you created earlier.
Create a post
Click “New” and create a blog post. You should be redirected to the index showing your new post.
Edit a post
Click “Edit” on your post, modify it, and save.
Delete a post
Click “Delete” on the edit page and confirm. The post should be removed.
Congratulations! You’ve now finished writing your application. There’s still more to do before the project is complete - you need to make it installable and add tests.
Complete Blog Module
Here’s the complete flaskr/blog.py file:
from flask import Blueprint
from flask import flash
from flask import g
from flask import redirect
from flask import render_template
from flask import request
from flask import url_for
from werkzeug.exceptions import abort
from .auth import login_required
from .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, f"Post id {id} doesn't exist.")
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"))