Blog Functionality
To make life easier all blog related routes will sit inside their own file (blueprint) and then be registered with the app. This makes it easier to seperate code by functionality rather than having one large file with all routes.
Create a Blueprint
Creating a blueprint is as simple as creating a new file and adding the following code.
"""Blog routes."""from flask import Blueprint, redirect, render_template, request, url_forfrom werkzeug.exceptions import abort
from flaskapp.db import get_db
bp = Blueprint("blog", __name__)
With our blueprint created we can now register it with the app. The index
route in our blueprint is also going to be the main page of our website so we will set a url rule to link it to /
.
def create_app(): app = ... # existing code omitted
from . import blog
app.register_blueprint(blog.bp) app.add_url_rule('/', endpoint='index')
return app
Routes
For our blog multiple different routes are needed, each serving one of the already created templates. The routes will also need to interact with the database to get the required data to then fill the template.
Index route
Our main page needs to show a preview of all posts and the amount of comments on each post.
@bp.route("/")def index(): """Main view page.""" db = get_db() posts = db.execute( "SELECT post.id, post.title, SUBSTR(post.body, 1, 400) AS body, post.created, " "count(comment.id) AS comment_amount FROM post LEFT JOIN comment ON " "comment.post_id=post.id group by post.id ORDER BY post.created DESC", ).fetchall() return render_template("blog/index.html", posts=posts)
-
To only show a preview of the post we use the
SUBSTR
function to only get the first 400 characters of the post body. -
To get the amount of comments on each post we use a
LEFT JOIN
to join thecomment
table to thepost
table. This will return all posts even if they have no comments. We then use thecount
function to count the amount of comments on each post. theLEFT JOIN
command adds the contents of thecomment
table into our query. -
So that all posts are in the correct order we use the
ORDER BY
command to order the posts by their creation date.
Display post
With our index page displaying a preview of all posts we now need to create a page to display the full post and all comments on that post. To keep things organised we will seperate the code to get the post and the code to get the comments into seperate functions. This page will also handle adding comments to the post.
def get_post(id): """Get a blog post.
Args: id (int): the id of the post to get.
Returns: the post if found otherwise redirects the user to a 404. """ post = ( get_db() .execute( "SELECT id, title, body, created FROM post WHERE id = ?", (id,), ) .fetchone() )
if post is None: abort(404, f"Post id {id} doesn't exist.")
return post
def get_comments(id): """Gets all comments on a post.
Args: id (int): the id of the post to get.
Returns: A list of all comments if found. """ post = ( get_db() .execute( "SELECT id, body, created FROM comment WHERE post_id = ?", (id,), ) .fetchall() )
return post
@bp.route("/<int:id>/post", methods=("GET", "POST"))def post(id): """Post page with comments and the ability to add comments.""" error = None if request.method == "POST": body = request.form["body"]
if len(body) < 10: error = "Comment is too short"
if error is None: db = get_db() db.execute( "INSERT INTO comment (body, post_id) VALUES (?, ?)", ( body, id, ), ) db.commit() return redirect(f"/{id}/post") post = get_post(id) comments = get_comments(id) return render_template( "blog/post.html", post=post, comments=comments, error=error, )
-
The route of
/<int:id>/post
means that the route will be/1/post
for example. Theint
part of the route means that the id will be converted to an integer before being passed to the function. -
As this page has both
GET
andPOST
methods we need to check which method is being used. If the method isPOST
then we know that the user is trying to add a comment to the post. We then get the comment from the form and check that it is at least 10 characters long. If the comment is valid we then add it to the database and redirect the user back to the post page.
Creating posts
Similar to creating comments creating posts functions the same way, when you initially load the page it will be a GET
request and when you submit the form it will be a POST
request. Validation is done to make sure that the title is not empty.
@bp.route("/create", methods=("GET", "POST"))def create(): """Create post page.""" error = None if request.method == "POST": title = request.form["title"] body = request.form["body"]
if not title: error = "Title is required."
if error is None: db = get_db() db.execute( "INSERT INTO post (title, body) VALUES (?, ?)", (title, body), ) db.commit() return redirect(url_for("blog.index"))
return render_template("blog/create.html", error=error)
Searching posts
Searching posts is done by using the LIKE
command in SQL. This command allows you to search for a string within a string. The %
character is used as a wildcard so that the search will return any post that contains the search query. Any results are then passed to the template to be displayed.
@bp.route("/search")def search(): """Search page.""" query = request.args.get("query") db = get_db() results = db.execute( ( "SELECT id, title, SUBSTR(body, 1, 400) AS body, created FROM post " "WHERE title LIKE ? OR body LIKE ?" ), (f"%{query}%", f"%{query}%"), ).fetchall() return render_template("blog/search.html", posts=results, query=query)