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

# File path: add to the Post class in blog/models.py
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

# File path: blog/views.py (newly added)
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

MethodsFunctionExample
add(obj)Add associationpost.favorites.add(user)
remove(obj)Remove associationpost.favorites.remove(user)
all()Query all associated objectspost.favorites.all()
filter(...)Filter with conditionspost.favorites.filter(id=1)
exists()Determine whether there is a matching recordpost.favorites.filter(id=1).exists()
clear()Clear All Associationspost.favorites.clear()

Configure Favorites Route

Example

# File path: append to blog/urls.py
urlpatterns = [
    # ... existing routes ...
    path('post/<int:pk>/favorite/', views.toggle_favorite, name='toggle_favorite'),
]

Add a favorites button to the detail page

Example

<!-- File path: blog/templates/blog/post_detail.html (modified) -->
{% 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 %}
                    &#x2665;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

<!-- Add a favorite button to the article card in index.html (footer area) -->
<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 %}&#x2665;{% 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 carrycsrf_token...to prevent malicious websites from forging requests.


Favorites List Page

Display the list of favorited articles for logged-in users.

Example

# File path: blog/views.py (newly added)
@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

/* Append to the style in base.html */
.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