Blog Blueprint

You’ll use the same techniques you learned about when writing theauthentication blueprint to write the blog blueprint. The blog shouldlist all posts, allow logged in users to create posts, and allow theauthor of a post to edit or delete it.

As you implement each view, keep the development server running. As yousave your changes, try going to the URL in your browser and testing themout.

The Blueprint

Define the blueprint and register it in the application factory.

flaskr/blog.py
fromflaskimport(Blueprint,flash,g,redirect,render_template,request,url_for)fromwerkzeug.exceptionsimportabortfromflaskr.authimportlogin_requiredfromflaskr.dbimportget_dbbp=Blueprint('blog',__name__)

Import and register the blueprint from the factory usingapp.register_blueprint(). Place thenew code at the end of the factory function before returning the app.

flaskr/__init__.py
defcreate_app():app=...# existing code omittedfrom.importblogapp.register_blueprint(blog.bp)app.add_url_rule('/',endpoint='index')returnapp

Unlike the auth blueprint, the blog blueprint does not have aurl_prefix. So theindex view will be at/, thecreateview 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 theindex view defined below will beblog.index. Some of the authentication views referred to a plainindex endpoint.app.add_url_rule()associates the endpoint name'index' with the/ url so thaturl_for('index') orurl_for('blog.index') will both work,generating the same/ URL either way.

In another application you might give the blog blueprint aurl_prefix and define a separateindex view in the applicationfactory, similar to thehello view. Then theindex andblog.index endpoints and URLs would be different.

Index

The index will show all of the posts, most recent first. AJOIN isused so that the author information from theuser table isavailable in the result.

flaskr/blog.py
@bp.route('/')defindex():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()returnrender_template('blog/index.html',posts=posts)
flaskr/templates/blog/index.html
{%extends'base.html'%}{%blockheader%}<h1>{%blocktitle%}Posts{%endblock%}</h1>{%ifg.user%}<aclass="action"href="{{url_for('blog.create')}}">New</a>{%endif%}{%endblock%}{%blockcontent%}{%forpostinposts%}<articleclass="post"><header><div><h1>{{post['title']}}</h1><divclass="about">by{{post['username']}} on{{post['created'].strftime('%Y-%m-%d')}}</div></div>{%ifg.user['id']==post['author_id']%}<aclass="action"href="{{url_for('blog.update',id=post['id'])}}">Edit</a>{%endif%}</header><pclass="body">{{post['body']}}</p></article>{%ifnotloop.last%}<hr>{%endif%}{%endfor%}{%endblock%}

When a user is logged in, theheader block adds a link to thecreate view. When the user is the author of a post, they’ll see an“Edit” link to theupdate view for that post.loop.last is aspecial variable available insideJinja for loops. It’s used todisplay a line after each post except the last one, to visually separatethem.

Create

Thecreate view works the same as the authregister view. Eitherthe form is displayed, or the posted data is validated and the post isadded to the database or an error is shown.

Thelogin_required decorator you wrote earlier is used on the blogviews. A user must be logged in to visit these views, otherwise theywill be redirected to the login page.

flaskr/blog.py
@bp.route('/create',methods=('GET','POST'))@login_requireddefcreate():ifrequest.method=='POST':title=request.form['title']body=request.form['body']error=Noneifnottitle:error='Title is required.'iferrorisnotNone:flash(error)else:db=get_db()db.execute('INSERT INTO post (title, body, author_id)'' VALUES (?, ?, ?)',(title,body,g.user['id']))db.commit()returnredirect(url_for('blog.index'))returnrender_template('blog/create.html')
flaskr/templates/blog/create.html
{%extends'base.html'%}{%blockheader%}<h1>{%blocktitle%}New Post{%endblock%}</h1>{%endblock%}{%blockcontent%}<formmethod="post"><labelfor="title">Title</label><inputname="title"id="title"value="{{request.form['title']}}"required><labelfor="body">Body</label><textareaname="body"id="body">{{request.form['body']}}</textarea><inputtype="submit"value="Save"></form>{%endblock%}

Update

Both theupdate anddelete views will need to fetch apostbyid and check if the author matches the logged in user. To avoidduplicating code, you can write a function to get thepost and callit from each view.

flaskr/blog.py
defget_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()ifpostisNone:abort(404,f"Post id{id} doesn't exist.")ifcheck_authorandpost['author_id']!=g.user['id']:abort(403)returnpost

abort() will raise a special exception that returns an HTTP statuscode. It takes an optional message to show with the error, otherwise adefault message is used.404 means “Not Found”, and403 means“Forbidden”. (401 means “Unauthorized”, but you redirect to thelogin page instead of returning that status.)

Thecheck_author argument is defined so that the function can beused to get apost without checking the author. This would be usefulif you wrote a view to show an individual post on a page, where the userdoesn’t matter because they’re not modifying the post.

flaskr/blog.py
@bp.route('/<int:id>/update',methods=('GET','POST'))@login_requireddefupdate(id):post=get_post(id)ifrequest.method=='POST':title=request.form['title']body=request.form['body']error=Noneifnottitle:error='Title is required.'iferrorisnotNone:flash(error)else:db=get_db()db.execute('UPDATE post SET title = ?, body = ?'' WHERE id = ?',(title,body,id))db.commit()returnredirect(url_for('blog.index'))returnrender_template('blog/update.html',post=post)

Unlike the views you’ve written so far, theupdate function takesan argument,id. That corresponds to the<int:id> in the route.A real URL will look like/1/update. Flask will capture the1,ensure it’s anint, and pass it as theid argument. If youdon’t specifyint: and instead do<id>, it will be a string.To generate a URL to the update page,url_for() needs to be passedtheid so it knows what to fill in:url_for('blog.update',id=post['id']). This is also in theindex.html file above.

Thecreate andupdate views look very similar. The maindifference is that theupdate view uses apost object and anUPDATE query instead of anINSERT. With some clever refactoring,you could use one view and template for both actions, but for thetutorial it’s clearer to keep them separate.

flaskr/templates/blog/update.html
{%extends'base.html'%}{%blockheader%}<h1>{%blocktitle%}Edit "{{post['title']}}"{%endblock%}</h1>{%endblock%}{%blockcontent%}<formmethod="post"><labelfor="title">Title</label><inputname="title"id="title"value="{{request.form['title']orpost['title']}}"required><labelfor="body">Body</label><textareaname="body"id="body">{{request.form['body']orpost['body']}}</textarea><inputtype="submit"value="Save"></form><hr><formaction="{{url_for('blog.delete',id=post['id'])}}"method="post"><inputclass="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 thecurrent page (/<id>/update). The other form contains only a buttonand specifies anaction attribute that posts to the delete viewinstead. The button uses some JavaScript to show a confirmation dialogbefore submitting.

The pattern{{request.form['title']orpost['title']}} is used tochoose what data appears in the form. When the form hasn’t beensubmitted, the originalpost data appears, but if invalid form datawas posted you want to display that so the user can fix the error, sorequest.form is used instead.request is another variablethat’s automatically available in templates.

Delete

The delete view doesn’t have its own template, the delete button is partofupdate.html and posts to the/<id>/delete URL. Since thereis no template, it will only handle thePOST method and then redirectto theindex view.

flaskr/blog.py
@bp.route('/<int:id>/delete',methods=('POST',))@login_requireddefdelete(id):get_post(id)db=get_db()db.execute('DELETE FROM post WHERE id = ?',(id,))db.commit()returnredirect(url_for('blog.index'))

Congratulations, you’ve now finished writing your application! Take sometime to try out everything in the browser. However, there’s still moreto do before the project is complete.

Continue toMake the Project Installable.