Forms and Favorites Feature
In this chapter, you will learn to handle user POST requests, establish many-to-many relationships, and implement the "favorite articles" feature.
Data model design for the favorites feature
The core of the favorites feature is:A user can favorite multiple posts, and a post can be favorited by multiple users.。
This is a typical many-to-many (ManyToMany) relationship.
We don't need to create a new model — Django's User model is built-in; we just need to add a ManyToManyField pointing to User in the Post model.
Example
from django.contrib.auth.models import User
class Post(models.Model):
# ... existing fields ...
# Collector: ManyToManyField automatically creates an intermediate table
favorites = models.ManyToManyField(
User,
related_name='favorite_posts', # Reverse query: user.favorite_posts.all()
verbose_name='collector',
blank=True
)
After modifying the model, generate and run the migration:
(venv) $ python manage.py makemigrations (venv) $ python manage.py migrate
View logic for favoriting and unfavoriting
Example
from django.shortcuts import render, redirect, get_object_or_404
from django.contrib.auth.decorators import login_required
from django.contrib import messages
from .models import Post
# login_required decorator: when an unauthenticated user accesses this view, automatically redirect to the login page
@login_required
def toggle_favorite(request, pk):
"""Toggle favorite status: favorited → unfavorite, not favorited → favorite"""
post = get_object_or_404(Post, pk=pk)
# Check whether the current user has favorited
if post.favorites.filter(id=request.user.id).exists():
# Favorited → Unfavorite
post.favorites.remove(request.user)
messages.info(request, f'Unfavorited "{post.title}"')
else:
# Not favorited → Add to favorites
post.favorites.add(request.user)
messages.success(request, f'Favorited "{post.title}"')
# Redirect back to current page (HTTP_REFERER is the URL of the source page)
referer = request.META.get('HTTP_REFERER', '/')
return redirect(referer)
ManyToMany operation method quick reference
| Methods | Function | Example |
|---|---|---|
| add(obj) | Add association | post.favorites.add(user) |
| remove(obj) | Remove association | post.favorites.remove(user) |
| all() | Query all associated objects | post.favorites.all() |
| filter(...) | Filter with conditions | post.favorites.filter(id=1) |
| exists() | Determine whether there is a matching record | post.favorites.filter(id=1).exists() |
| clear() | Clear All Associations | post.favorites.clear() |
Configure Favorites Route
Example
urlpatterns = [
# ... existing routes ...
path('post/<int:pk>/favorite/', views.toggle_favorite, name='toggle_favorite'),
]
Add a favorites button to the detail page
Example
{% extends 'blog/base.html' %}
{% block title %}{{ post.title }} - EXAMPLE Blog{% endblock %}
{% block content %}
<article class="post-view">
<span class="category-tag">{{ post.category.name }}</span>
<div class="post-header">
<h1>{{ post.title }}</h1>
<!-- Favorite button: Only visible to logged-in users -->
{% if user.is_authenticated %}
<form method="post" action="{% url 'toggle_favorite' post.pk %}">
{% csrf_token %}
<button type="submit" class="fav-btn">
{% if user in post.favorites.all %}
♥favorited
{% else %}
♡ Favorite
{% endif %}
</button>
</form>
{% endif %}
</div>
<time>{{ post.created_at|date:"Y-m-d" }}</time>
<div class="content">{{ post.content|safe }}</div>
<a href="{% url 'index' %}" class="back-link">← Back to Home</a>
</article>
{% endblock %}
Also add a favorites button to the article cards on the homepage:
Example
<div class="article-card">
<div class="card-content">
<span class="card-category">{{ post.category.name }}</span>
<h3>{{ post.title }}</h3>
<p>{{ post.summary|truncatechars:80 }}</p>
<div class="card-footer">
<span class="card-date">{{ post.created_at|date:"Y-m-d" }}</span>
{% if user.is_authenticated %}
<form method="post" action="{% url 'toggle_favorite' post.pk %}" class="fav-form">
{% csrf_token %}
<button type="submit" class="fav-btn-small">
{% if user in post.favorites.all %}♥{% else %}♡{% endif %}
</button>
</form>
{% endif %}
</div>
</div>
</div>
Why does the favorite button use a form instead of an <a> tag? Because the favorite operation modifies the database and is a POST request. According to REST conventions, read operations use GET, write operations use POST. More importantly, POST requests carry
csrf_token...to prevent malicious websites from forging requests.
Favorites List Page
Display the list of favorited articles for logged-in users.
Example
@login_required
def favorites(request):
"""Current user's favorites list"""
# Use related_name to reverse query the user's favorited articles
posts = request.user.favorite_posts.all().order_by('-created_at')
return render(request, 'blog/favorites.html', {
'posts': posts,
'title': 'My Favorites - EXAMPLE Blog'
})
Add a route in urls.py, and add a "My Favorites" link in the navigation bar.
Favorites Button Style
Example
.post-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
}
.fav-btn, .fav-btn-small {
background: none;
border: none;
font-size: 18px;
cursor: pointer;
color: #e74c3c;
padding: 4px 8px;
transition: transform 0.2s;
}
.fav-btn:hover, .fav-btn-small:hover {
transform: scale(1.2);
}
.fav-btn-small {
font-size: 16px;
}
.fav-form {
display: inline;
}
.card-footer {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 10px;
}
Chapter summary
In this chapter, you mastered user interaction handling in Django: ManyToManyField to establish many-to-many relationships, POST requests to handle forms, csrf_token for security protection, login_required decorator to protect views, and request.user to determine the current user's identity.
The favorite feature allows users to mark articles they like and view them in their personal favorites list.
other extensions