In the previous article, we implemented full user login and session management for our FastAPI blog. Now, the server can "remember" a user's login status and protect pages that require authentication.
Since we can now distinguish between logged-in users and guests, it's the perfect time to add interactive features to our blog.
In this article, we will add a fundamental yet crucial feature to our blog: a comment system.
Specifically, we will implement the following functionalities:
- Display a list of comments below each post.
- Allow logged-in users to post comments on articles.
Step 1: Create the Data Model for Comments
Just like our Post
and User
models, our Comment
model needs its own data model and relationships with Post
and User
.
1. Create the Comment Model
Open the models.py
file, add the Comment
model, and update the User
and Post
models to establish a bi-directional relationship.
# models.py
import uuid
from datetime import datetime
from typing import Optional, List
from sqlmodel import Field, SQLModel, Relationship
class User(SQLModel, table=True):
id: Optional[uuid.UUID] = Field(default_factory=uuid.uuid4, primary_key=True)
username: str = Field(unique=True, index=True)
password: str
# Add a one-to-many relationship with Comment
comments: List["Comment"] = Relationship(back_populates="user")
class Post(SQLModel, table=True):
id: Optional[uuid.UUID] = Field(default_factory=uuid.uuid4, primary_key=True)
title: str
content: str
createdAt: datetime = Field(default_factory=datetime.utcnow, nullable=False)
# Add a one-to-many relationship with Comment
comments: List["Comment"] = Relationship(back_populates="post")
class Comment(SQLModel, table=True):
id: Optional[uuid.UUID] = Field(default_factory=uuid.uuid4, primary_key=True)
content: str
createdAt: datetime = Field(default_factory=datetime.utcnow, nullable=False)
# Define foreign keys, linking to the Post and User tables
postId: uuid.UUID = Field(foreign_key="post.id")
userId: uuid.UUID = Field(foreign_key="user.id")
# Define many-to-one relationships
post: Post = Relationship(back_populates="comments")
user: User = Relationship(back_populates="comments")
Here, we've done three things:
- Created the
Comment
model, which includescontent
,createdAt
, and the foreign keyspostId
anduserId
that point to thepost
anduser
tables. - Used
Relationship
in theComment
model to define its "many-to-one" relationship withPost
andUser
. - Added the reverse "one-to-many" relationship
comments
to theUser
andPost
models. This allows us to easily retrieve all comments for aPost
object or all comments made by aUser
object.
Because we configured the create_db_and_tables
function in main.py
, it will automatically detect the SQLModel
models and create or update the corresponding database tables when the application starts. We don't need to execute SQL manually.
If you need to execute SQL manually, and your database is created on Leapcell,
you can easily execute SQL statements through its graphical interface. Just go to the Database management page on the website, paste the above statements into the SQL interface, and execute them.
Step 2: Implement the Business Logic for Comments
Next, we will create functions to handle comment creation and queries.
Create a new file comments_service.py
in the project's root directory to store the business logic related to comments.
# comments_service.py
import uuid
from typing import List
from sqlmodel import Session, select
from models import Comment
def get_comments_by_post_id(post_id: uuid.UUID, session: Session) -> List[Comment]:
"""Finds all comments for a given post ID and sorts them by creation time in ascending order."""
statement = select(Comment).where(Comment.postId == post_id).order_by(Comment.createdAt)
comments = session.exec(statement).all()
return comments
def create_comment(content: str, user_id: uuid.UUID, post_id: uuid.UUID, session: Session) -> Comment:
"""Creates a new comment."""
new_comment = Comment(content=content, userId=user_id, postId=post_id)
session.add(new_comment)
session.commit()
session.refresh(new_comment)
return new_comment
The get_comments_by_post_id
function is used to fetch all comments under a post. create_comment
is used to save a new comment to the database. Because we have correctly set up the Relationship
in our models, we can later conveniently access the commenter's username in the template using comment.user.username
.
Step 3: Create Routes for Submitting and Displaying Comments
Now, we need to integrate the comment functionality into the post page. This requires two parts: a backend route to receive user-submitted comments, and an update to the post detail page route to display the comments.
1. Create the Comment Route
Create a new file comments.py
in the routers
folder:
# routers/comments.py
import uuid
from fastapi import APIRouter, Depends, Form
from fastapi.responses import RedirectResponse
from sqlmodel import Session
from database import get_session
import comments_service
from auth_dependencies import login_required
router = APIRouter()
@router.post("/posts/{post_id}/comments")
def create_comment_for_post(
post_id: uuid.UUID,
content: str = Form(...),
user: dict = Depends(login_required), # Dependency injection to ensure the user is logged in
session: Session = Depends(get_session)
):
# The user ID from the session is a string, needs to be converted to a UUID type
user_id = uuid.UUID(user["id"])
comments_service.create_comment(
content=content, user_id=user_id, post_id=post_id, session=session
)
# After a successful comment, redirect back to the post page
return RedirectResponse(url=f"/posts/{post_id}", status_code=302)
This route only handles POST
requests. We use the login_required
dependency we created earlier to protect it, ensuring only logged-in users can post comments. After a comment is successfully created, the page redirects back to the original post detail page.
2. Update the Main Application and Post Routes
First, let's mount the comment router we just created in main.py
.
# main.py
# ... other imports
from routers import posts, users, auth, comments # import the comments router
# ...
app = FastAPI(lifespan=lifespan)
# ...
# Include routers
app.include_router(posts.router)
app.include_router(users.router)
app.include_router(auth.router)
app.include_router(comments.router) # Mount the comments router
Next, modify the get_post_by_id
function in routers/posts.py
so that when it renders the post detail page, it also fetches and passes all the comments for that post.
# routers/posts.py
# ... imports ...
from auth_dependencies import get_user_from_session
import comments_service # Import the comment service
# ... router and templates definition ...
# ... other routes ...
@router.get("/posts/{post_id}", response_class=HTMLResponse)
def get_post_by_id(
request: Request,
post_id: uuid.UUID,
session: Session = Depends(get_session),
user: dict | None = Depends(get_user_from_session)
):
post = session.get(Post, post_id)
# Get all comments for this post
comments = comments_service.get_comments_by_post_id(post_id, session)
# Pass post, user, and comments together to the template
return templates.TemplateResponse(
"post.html",
{
"request": request,
"post": post,
"title": post.title,
"user": user,
"comments": comments # New
}
)
Step 4: Update the Frontend View
The final step is to modify the template file to display the comment list and the comment form.
Open templates/post.html
, and add the following code below the post content and above the back link:
<div class="post-content">{{ post.content | replace('\n', '<br>') | safe }}</div>
</article>
<section class="comments-section">
<h3>Comments</h3>
<div class="comment-list">
{% if comments %}
{% for comment in comments %}
<div class="comment-item">
<p class="comment-content">{{ comment.content }}</p>
<small>
By <strong>{{ comment.user.username }}</strong> on {{ comment.createdAt.strftime('%Y-%m-%d') }}
</small>
</div>
{% endfor %}
{% else %}
<p>No comments yet. Be the first to comment!</p>
{% endif %}
</div>
{% if user %}
<form action="/posts/{{ post.id }}/comments" method="POST" class="comment-form">
<h4>Leave a Comment</h4>
<div class="form-group">
<textarea name="content" rows="4" placeholder="Write your comment here..." required></textarea>
</div>
<button type="submit">Submit Comment</button>
</form>
{% else %}
<p><a href="/auth/login">Login</a> to leave a comment.</p>
{% endif %}
</section>
<a href="/" class="back-link">← Back to Home</a>
{% include "_footer.html" %}
- We use
{% for comment in comments %}
to loop through and display all the comments. Throughcomment.user.username
, we can directly show the commenter's username. - We use
{% if user %}
to check if the user is logged in. If they are, the comment form is displayed; otherwise, a login link is shown.
To make the page look better, you can add some styles to public/css/style.css
:
/* ... other styles ... */
.comments-section {
margin-top: 3rem;
border-top: 1px solid #eee;
padding-top: 2rem;
}
.comment-list .comment-item {
background: #f9f9f9;
border: 1px solid #ddd;
padding: 1rem;
border-radius: 5px;
margin-bottom: 1rem;
}
.comment-content {
margin-top: 0;
}
.comment-item small {
color: #666;
}
.comment-form {
margin-top: 2rem;
}
.comment-form textarea {
width: 100%;
padding: 0.5rem;
margin-bottom: 1rem;
border: 1px solid #ccc;
border-radius: 4px;
}
Running and Testing
Now, restart your application:
uvicorn main:app --reload
Open your browser and navigate to any post's detail page. You will see the new comments section.
Type something into the comment box and submit it. After the page refreshes, you will see the comment you just posted in the comment list.
Congratulations, you have successfully added a comment system to your blog!
Of course, the current comment functionality is still quite basic. In the next article, we will continue to enhance this feature by implementing logic for authors to reply to comments, taking the blog's interactivity to the next level.
Follow us on X: @LeapcellHQ
Related Posts:
Top comments (0)