1.构建关注系统

本章将学习如何构建关注系统,同时创建一个用户活动流。读者将会看到Django信号的工作方式,并将Redis中的快速I/O存储整合至项目中,进而存储各项视图。
本节将在项目中构建关注系统,用户之间可彼此关注,并跟踪其他用户在平台上的共享内容。这里,用户间的关系表示为多对多关系。

一、利用中间模型创建多对多关系

在第5章通过向某一关系模型中添加ManyToManyField创建了多对多关系,同时令Django针对这一关系生成了数据库表。该方案适用于大多数场合,但有时需要针对该关系创建中间模型。

中间模型的使用原因

当希望存储针对该关系的附加信息时,则需要构建一个中间模型。例如,关系创建的日期,或者描述关系本质的某个字段。
中间模型的使用包含以下两个原因:

  1. 使用Django提供的User模型,但并不对其加以修改。
  2. 需要存储创建关系的时间。

创建用户关系的中间模型

# 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()。用户需要创建或删除中间模型实例。

数据库迁移

Pasted image 20241219002317.png
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,运行下列代码进行测试:
Pasted image 20241221223358.png

3.模板

在account应用程序的templates/account/目录中添加下列目录和文件
Pasted image 20241221223536.png

<!--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 ,对应的用户列表如下图所示。
Pasted image 20241221224338.png

[!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不支持多行标签。
再次打开浏览器单击某个用户(该用户收藏了某些图像),如下图所示显示了某些配置细节内容。
Pasted image 20241221225113.png

三、构建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 %}

4.效果

Pasted image 20241221230420.png

© 版权声明
THE END
喜欢就支持一下吧
点赞9 分享