本章将学习如何构建关注系统,同时创建一个用户活动流。读者将会看到Django信号的工作方式,并将Redis中的快速I/O存储整合至项目中,进而存储各项视图。
本节将在项目中构建关注系统,用户之间可彼此关注,并跟踪其他用户在平台上的共享内容。这里,用户间的关系表示为多对多关系。
一、利用中间模型创建多对多关系
在第5章通过向某一关系模型中添加ManyToManyField创建了多对多关系,同时令Django针对这一关系生成了数据库表。该方案适用于大多数场合,但有时需要针对该关系创建中间模型。
中间模型的使用原因
当希望存储针对该关系的附加信息时,则需要构建一个中间模型。例如,关系创建的日期,或者描述关系本质的某个字段。
中间模型的使用包含以下两个原因:
- 使用Django提供的User模型,但并不对其加以修改。
- 需要存储创建关系的时间。
创建用户关系的中间模型
# account/models.py
class Contact(models.Model):
user_from = models.ForeignKey('auth.User', on_delete=models.CASCADE, related_name='user_form') # 创建对应关系的用户Foreignkey
user_to = models.ForeignKey('auth.User', on_delete=models.CASCADE, related_name='user_to') # 被关注用户的ForeignKey
created = models.DateTimeField(auto_now_add=True,db_index=True) # 存储创建对应关系的时间值
class Meta:
ordering = ['-created']
def __str__(self):
return f'Contact from {self.user_from} to {self.user_to}'
数据库索引将在ForeignKey字段上自动被创建。然后使用db_index=True针对已生成的字段创建数据库索引。当通过该字段对QuerySets进行排序时,这可有效地改善查询性能。
多对多关系添加附加字段
方法一、常规模型方法
当采用ORM时,对于两个用户间的关注问题(如user1和user2),可生成一个关系,如下所示:
user1 = User.objects.get(id=1)
user2 = User.objects.get(id=2)
Contact.objects.create(user_from=user1, user_to=user2)
对于Contact模型,关系管理器rel_from_set和rel_to_set将返回一个QuerySet。为了访问源自User模型的关系的终端,User应包含ManyToManyField,如下所示:
following = models.ManyToManyField('self', through='Contact', symmetrical=False, related_name='followers')
上述代码通知Django针对当前关系使用自定义中间模型,即向ManyToManyField添加through=Contact,这是User模型至其自身的多对多关系。我们引用了ManyToManyField字段中的self针对同一模型生成了一个关系。
[!info] 当需要在多对多关系中使用附加字段时,可针对关系各端利用ForeignKey创建一个自定义模型。可将ManyToManyField添加至某一关系模型中并通知Django,应将中间模型置入through参数中进而对其加以使用。
方法二、Django模型修改方法
如果user模型表示为当前应用程序中的部分内容,可将上一个字段添加至模型中。然而,我们无法直接修改User类,其原因在于该类隶属于django.contrib.auth应用程序。
这里将采用一种稍微不同的方案,即向User模型中动态地添加该字段。
# account/models.py
from django.contrib.auth import get_user_model
# 添加到文件最后
User_model = get_user_model()
User_model.add_to_class('following', models.ManyToManyField('self', through=Contact, related_name='followers', symmetrical=False))
上述代码解释如下:
- 通过Django提供的
get_user_model()
通过函数检索User模型。 - 使用Django模型的
add_to_class()
方法,以对User模型设置猴子补丁(monkey patch)。并不推荐,但在这里可以避免创建自定义用户模型,并保留Django内建User模型的全部优点。 - 此外,通过基于user.followers.all()和user.following.all()的Django ORM,简化了关系对象的检索方式。使用Contact中间模型,避免了涉及额外数据库连接的复杂查询。如果是在自定义用户模型Profile中定义了关系,则会出现这种情况。
- 多对多关系表,可通过Contact模型创建。对于User模型来说,动态添加的ManyToManyField不会对其产生数据库变化。
- 当对模型自身定义ManyToManyField时,Django强制该关系呈对称状态。使用
symmetrical=False
可定义为非对称关系(也就是说,我关注了你。并不意味着你会自动关注我)。
[!warning] 当针对多对多关系使用中间模型时,一些相关的管理器方法将会被禁用,如add()、create()或remove()。用户需要创建或删除中间模型实例。
数据库迁移
Contact模型与数据库同步后,进而可创建用户间的关系。然而,当前网站尚无法浏览用户,或查看特定的用户配置内容。下面针对User模型构建列表和详细视图。
二、针对用户配置创建列表和详细视图
1.视图
# account/views.py
from django.shortcuts import get_object_or_404
from django.contrib.auth.models import User
@login_required
def user_list(request):
users = User.objects.filter(is_active=True)
return render(request,
'account/user/list.html',
{'section': 'people',
'users': users})
@login_required
def user_detail(request, username):
user = get_object_or_404(User,
username=username,
is_active=True)
return render(request,
'account/user/detail.html',
{'section': 'people',
'user': user})
以上代码解释如下:
- user_list视图用于获取处于活动状态的全部用户。Django User模型使用了is_active标记,表明当前用户账户是否处于活动状态。
- user_detail视图使用了get_object_or_404()快捷方式以及给定的用户名检索活动用户,如果不存在该账号下的活动用户,则该视图将返回HTTP404响应结果。
2.URL
# account/urls.py
urlpatterns = [
# ...
path('users/', views.user_list, name='user_list'),
path('users/<username>/', views.user_detail, name='user_detail'),
]
此处使用了user_detail URL路径针对用户生成规范URL。之前已经在模型中定义了get_absolute_url()方法,并针对每个对象返回规范URL。另一种指定模型URL的方式是向当前项目中添加ABSOLUTE_URL_OVERRIEDS设置,如下所示:
# settings.py
from django.urls import reverse_lazy
ABSOLUTE_URL_OVERRIDES = {
'auth.user': lambda u: reverse_lazy('user_detail',
args=[u.username])
}
Django将向ABSOLUTE_URL_OVERRIDES设置中出现的任意模型动态地添加get_absolte_url()方法,该方法针对设置中的既定模型返回对应的URL。对于当前用户,此处返回了user_detail URL,进而可在User实例上使用get_absolute_url(),并检索其对应的URL。
利用python manage.py shell命令打开Python Shell,运行下列代码进行测试:
3.模板
在account应用程序的templates/account/目录中添加下列目录和文件
<!--account/user/list.html-->
{% extends "base.html" %}
{% load thumbnail %}
{% block title %}People{% endblock %}
{% block content %}
<h1>People</h1>
<div id="people-list">
{% for user in users %}
<div class="user">
<a href="{{ user.get_absolute_url }}">
<img src="{% thumbnail user.profile.photo 180x180 %}">
</a>
<div class="info">
<a href="{{ user.get_absolute_url }}" class="title">
{{ user.get_full_name }}
</a>
</div>
</div>
{% endfor %}
</div>
{% endblock %}
上述模板可列出站点中的全部活动用户。随后,可遍历既定用户,使用easy-thumbnails的{% thumbnail %}模板标签,并生成配置图像缩略图。
<!--base.html-->
<li {% if section == "people" %}class="selected"{% endif %}>
<a href="{% url "user_list" %}">People</a>
</li>
启动开发服务器,并在浏览器中打开 http://127.0.0.1:8000/account/users ,对应的用户列表如下图所示。
[!warning] 如果在生成缩略图时遇到任何困难,可向settings.py文件中添加THUMANAIL_DEBUG=True,以便在Shell中查看调试信息。
<!--account/user/detail.html-->
{% extends "base.html" %}
{% load thumbnail %}
{% block title %}{{ user.get_full_name }}{% endblock %}
{% block content %}
<h1>{{ user.get_full_name }}</h1>
<div class="profile-info">
<img src="{% thumbnail user.profile.photo 180x180 %}" class="user-detail">
</div>
{% with total_followers=user.followers.count %}
<span class="count">
<span class="total">{{ total_followers }}</span>
follower{{ total_followers|pluralize }}
</span>
<a href="#" data-id="{{ user.id }}" data-action="{% if request.user in user.followers.all %}un{% endif %}follow" class="follow button">
{% if request.user not in user.followers.all %}
Follow
{% else %}
Unfollow
{% endif %}
</a>
<div id="image-list" class="image-container">
{% include "images/image/list_ajax.html" with images=user.images_created.all %}
</div>
{% endwith %}
{% endblock %}
此外应确保模板标签未被划分为多行,Django不支持多行标签。
再次打开浏览器单击某个用户(该用户收藏了某些图像),如下图所示显示了某些配置细节内容。
三、构建AJAX视图以关注用户
1.视图
# account/views.py
from django.http import JsonResponse
from django.views.decorators.http import require_POST
from common.decorators import ajax_required # 自定义的装饰器,检查请求是否为AJAX请求
from .models import Contact
@ajax_required
@require_POST
@login_required
def user_follow(request):
user_id = request.POST.get('id')
action = request.POST.get('action')
if user_id and action:
try:
user = User.objects.get(id=user_id)
if action == 'follow':
Contact.objects.get_or_create(user_from=request.user,
user_to=user)
else:
Contact.objects.filter(user_from=request.user,
user_to=user).delete()
return JsonResponse({'status':'ok'})
except User.DoesNotExist:
return JsonResponse({'status':'error'})
return JsonResponse({'status':'error'})
user_follow视图类似于之前创建的image_like视图。由于针对用户的多对多关系使用了自定义中间模型,因而无法提供ManyToManyField中自动管理器的add()和remove()默认方法。这里使用了中间模型Contact生成或删除用户关系。
2.URL
path('users/follow/', views.user_follow, name='user_follow'),
此处须确保上述路径置于user_detail URL路径之前。否则,针对/users/follow/的请求将与user_detail的正则表达式相匹配,进而执行该视图。
[!info] Django针对每个路径(以出现顺序为准)检测所请求的URL,并在首次匹配处停止。
3.模板
<!--account/user/detail.html 的</body>前添加如下代码-->
{% block domready %}
$('a.follow').click(function(e){
e.preventDefault();
$.post('{% url "user_follow" %}',
{
id: $(this).data('id'),
action: $(this).data('action')
},
function(data){
if (data['status'] == 'ok') {
var previous_action = $('a.follow').data('action');
// toggle data-action
$('a.follow').data('action',
previous_action == 'follow' ? 'unfollow' : 'follow');
// toggle link text
$('a.follow').text(
previous_action == 'follow' ? 'Unfollow' : 'Follow');
// update total followers
var previous_followers = parseInt(
$('span.count .total').text());
$('span.count .total').text(previous_action == 'follow' ?
previous_followers + 1 : previous_followers - 1);
}
}
);
});
{% endblock %}