Posts CRUD

View saved

Goal for this part

Complete the core blog loop: create a post, open its detail page, edit it, and delete it. Public readers can still view posts; only the author can change or remove them.

You will practice loading rows by id, returning proper HTTP error codes, and keeping templates honest about which actions the current user may see.

What you should already know

Authentication should already work for you: login, logout, and current_user. You should also know how PostForm and database commits behave from earlier parts.

Functions that return early with errors will show up a lot here. If helper functions are comfortable for you, the ownership checks will feel natural.

Concepts: CRUD and authorization

CRUD means Create, Read, Update, and Delete—the four basic operations on stored records. A blog post is a friendly example of the same pattern used in many web apps.

Authentication answers “who are you?” Authorization answers “are you allowed to do this?” Checking that current_user owns the post before edit or delete is authorization. Skipping it would let any logged-in person alter someone else’s writing.

Walkthrough: ownership helper and edit route

A small helper loads a post by id, aborts with 404 if it is missing, and aborts with 403 if the current user is not the author. Centralizing that logic keeps create/edit/delete routes readable and consistent.

Edit binds PostForm(obj=post) so fields start with existing values. On valid submit, write the new title and content, commit, flash, and redirect to the detail page.

def get_owned_post_or_404(post_id: int) -> Post:
    post = db.session.get(Post, post_id)
    if post is None:
        abort(404)
    if post.author != current_user:
        abort(403)
    return post

Create, delete, and detail actions

Create inserts a Post tied to current_user after the form validates. Delete should use a POST form (not a casual GET link) so browsers and crawlers do not trigger removals by accident.

On the detail template, show Edit and Delete only when the viewer owns the post. Everyone else still sees the public title and body. That UI choice matches the server-side checks—never rely on hiding buttons alone.

@app.route("/post/<int:post_id>/edit", methods=["GET", "POST"])
@login_required
def edit_post(post_id):
    post = get_owned_post_or_404(post_id)
    form = PostForm(obj=post)
    if form.validate_on_submit():
        post.title = form.title.data.strip()
        post.content = form.content.data.strip()
        db.session.commit()
        flash("Post updated.", "success")
        return redirect(url_for("post_detail", post_id=post.id))
    return render_template("post_form.html", form=form, heading="Edit post")

How to run it and what you should see

Launch the 06-Posts snapshot, log in, and create a post. Open its detail page, edit the title, and confirm the change. Delete a draft you no longer want and confirm it disappears from home.

With a second account (or while logged out), open another user’s post. You should be able to read it but not see working edit/delete controls—and direct edit URLs should fail with 403 if you are the wrong user.

git clone https://github.com/michaeldunga1/fcc-flask-blog.git
cd fcc-flask-blog/06-Posts
python3 -m venv .venv
source .venv/bin/activate
pip install -r ../requirements.txt
python app.py

Common mistakes and try this

Forgetting db.session.commit() after changing fields is a classic bug: the form looks successful, but the database never updates. Another mistake is checking ownership only in the template; always enforce it in the route.

Try writing a second post, then edit only one and confirm the other stays unchanged. That quick check builds confidence before pagination adds more rows to the home feed.

  • 404 means “not found”; 403 means “found, but forbidden”
  • Strip user input before saving when it makes sense
  • Use POST for delete actions
  • Keep server-side ownership checks even if the UI hides buttons

Next: Pagination

Comments

One comment per signed-in account. Comments are saved with this page’s URL.