表单与收藏功能
本章你将学会处理用户 POST 请求,建立多对多关系,并实现「收藏文章」功能。
收藏功能的数据模型设计
收藏功能的核心是:一个用户可以收藏多篇文章,一篇文章可以被多个用户收藏。
这是典型的多对多(ManyToMany)关系。
我们不需要新建模型——Django 的 User 模型是内置的,只需要在 Post 模型中添加一个 ManyToManyField 指向 User。
实例
# 文件路径:blog/models.py 的 Post 类中添加
from django.contrib.auth.models import User
class Post(models.Model):
# ... 已有字段 ...
# 收藏者:ManyToManyField 自动创建中间表
favorites = models.ManyToManyField(
User,
related_name='favorite_posts', # 反向查询:user.favorite_posts.all()
verbose_name='收藏者',
blank=True
)
from django.contrib.auth.models import User
class Post(models.Model):
# ... 已有字段 ...
# 收藏者:ManyToManyField 自动创建中间表
favorites = models.ManyToManyField(
User,
related_name='favorite_posts', # 反向查询:user.favorite_posts.all()
verbose_name='收藏者',
blank=True
)
修改模型后,生成并执行迁移:
(venv) $ python manage.py makemigrations (venv) $ python manage.py migrate
收藏与取消收藏的视图逻辑
实例
# 文件路径:blog/views.py 新增
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 装饰器:未登录用户访问此视图时,自动跳转到登录页
@login_required
def toggle_favorite(request, pk):
"""切换收藏状态:收藏 → 取消,未收藏 → 收藏"""
post = get_object_or_404(Post, pk=pk)
# 检查当前用户是否已收藏
if post.favorites.filter(id=request.user.id).exists():
# 已收藏 → 取消收藏
post.favorites.remove(request.user)
messages.info(request, f'已取消收藏「{post.title}」')
else:
# 未收藏 → 添加收藏
post.favorites.add(request.user)
messages.success(request, f'已收藏「{post.title}」')
# 重定向回当前页面(HTTP_REFERER 是来源页面的 URL)
referer = request.META.get('HTTP_REFERER', '/')
return redirect(referer)
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 装饰器:未登录用户访问此视图时,自动跳转到登录页
@login_required
def toggle_favorite(request, pk):
"""切换收藏状态:收藏 → 取消,未收藏 → 收藏"""
post = get_object_or_404(Post, pk=pk)
# 检查当前用户是否已收藏
if post.favorites.filter(id=request.user.id).exists():
# 已收藏 → 取消收藏
post.favorites.remove(request.user)
messages.info(request, f'已取消收藏「{post.title}」')
else:
# 未收藏 → 添加收藏
post.favorites.add(request.user)
messages.success(request, f'已收藏「{post.title}」')
# 重定向回当前页面(HTTP_REFERER 是来源页面的 URL)
referer = request.META.get('HTTP_REFERER', '/')
return redirect(referer)
ManyToMany 操作方法速查
| 方法 | 作用 | 示例 |
|---|---|---|
| add(obj) | 添加关联 | post.favorites.add(user) |
| remove(obj) | 移除关联 | post.favorites.remove(user) |
| all() | 查询所有关联对象 | post.favorites.all() |
| filter(...) | 带条件过滤 | post.favorites.filter(id=1) |
| exists() | 判断是否有匹配记录 | post.favorites.filter(id=1).exists() |
| clear() | 清除所有关联 | post.favorites.clear() |
配置收藏路由
实例
# 文件路径:blog/urls.py 追加
urlpatterns = [
# ... 已有路由 ...
path('post/<int:pk>/favorite/', views.toggle_favorite, name='toggle_favorite'),
]
urlpatterns = [
# ... 已有路由 ...
path('post/<int:pk>/favorite/', views.toggle_favorite, name='toggle_favorite'),
]
在详情页添加收藏按钮
实例
<!-- 文件路径:blog/templates/blog/post_detail.html(修改) -->
{% extends 'blog/base.html' %}
{% block title %}{{ post.title }} - RUNOOB 博客{% endblock %}
{% block content %}
<article class="post-view">
<span class="category-tag">{{ post.category.name }}</span>
<div class="post-header">
<h1>{{ post.title }}</h1>
<!-- 收藏按钮:仅登录用户可见 -->
{% 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 %}
♥ 已收藏
{% else %}
♡ 收藏
{% 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">← 返回首页</a>
</article>
{% endblock %}
{% extends 'blog/base.html' %}
{% block title %}{{ post.title }} - RUNOOB 博客{% endblock %}
{% block content %}
<article class="post-view">
<span class="category-tag">{{ post.category.name }}</span>
<div class="post-header">
<h1>{{ post.title }}</h1>
<!-- 收藏按钮:仅登录用户可见 -->
{% 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 %}
♥ 已收藏
{% else %}
♡ 收藏
{% 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">← 返回首页</a>
</article>
{% endblock %}
在首页文章卡片中也加入收藏按钮:
实例
<!-- 在 index.html 的文章卡片中加入收藏按钮(footer 区域) -->
<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>
<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>
为什么收藏按钮用 form 而不是 a 标签?因为收藏操作修改了数据库,属于 POST 请求。按照 REST 规范,读操作用 GET,写操作用 POST。更重要的是,POST 请求携带
csrf_token,防止恶意网站伪造请求。
收藏列表页面
为登录用户展示已收藏的文章列表。
实例
# 文件路径:blog/views.py 新增
@login_required
def favorites(request):
"""当前用户的收藏列表"""
# 通过 related_name 反向查询用户的收藏文章
posts = request.user.favorite_posts.all().order_by('-created_at')
return render(request, 'blog/favorites.html', {
'posts': posts,
'title': '我的收藏 - RUNOOB 博客'
})
@login_required
def favorites(request):
"""当前用户的收藏列表"""
# 通过 related_name 反向查询用户的收藏文章
posts = request.user.favorite_posts.all().order_by('-created_at')
return render(request, 'blog/favorites.html', {
'posts': posts,
'title': '我的收藏 - RUNOOB 博客'
})
在 urls.py 中添加路由,在导航栏添加「我的收藏」链接。
收藏按钮样式
实例
/* 在 base.html style 中追加 */
.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;
}
.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;
}
本章小结
本章你掌握了 Django 中的用户交互处理:ManyToManyField 建立多对多关系、POST 请求处理表单、csrf_token 安全防护、login_required 装饰器保护视图、request.user 判断当前用户身份。
收藏功能让用户可以标记喜欢的文章,并在个人收藏列表中查看。
