2.构建通用活动流应用程序

许多社交网站均会向用户显示活动流,进而可跟踪其他用户在当前平台上的活动。活动流是某个或一组用户执行的近期活动列表。例如,Fackbook的News Feed(新闻提要)即为一个活动流,相关动作包括:用户X收藏了图像Y,或者用户X关注了用户Y。
本节将尝试构建一个活动流应用程序,以使每个用户可看到其所关注的用户的近期交互行为。对此,需要定义一个模型,以保存站点用户执行的各种动作,同时提供一种较为简单的方式向提要中添加相关动作。

一、初始配置

1.创建actions应用

python manage.py startapp actions

2.注册应用

# setting.py
INSTALLED_APPS = [
	# ...
    'actions.apps.ActionsConfig',
]

3.创建action模型

# action/views.py
from django.db import models

class Action(models.Model):
    user = models.ForeignKey('auth.User',
                             related_name='actions',
                             db_index=True,
                             on_delete=models.CASCADE)
    verb = models.CharField(max_length=255)   # 用户执行的相关动作
    created = models.DateTimeField(auto_now_add=True,
                                   db_index=True)
    class Meta:
        ordering = ('-created',)

利用上述模型,仅可设置诸如“用户X执行了某项活动”这一类操作。此外我们还需要一个附加的ForeginKey字段,进而保存涉及target对象的活动,如用户X收藏了图像Y,或者用户X当前关注了用户Y。如前所述,常规的ForeignKey仅执行一个模型。相反,我们需要一种方法使活动的target对象成为现有模型的实例,这也是Django内容类型框架的用武之地。

二、使用contenttypes框架

Django设置了位于django.contrib.contenttypes的contenttypes框架,该应用程序可跟踪安装在项目中的全部模型,并提供了一个通用接口与模型进行交互!
当利用startproject命令创建项目时,默认状态下,django.contrib.contenttypes应用程序包含于INSTALLED_APPS设置中。同时,该应用还可供其他contrib包使用,如验证框架和管理应用程序。

1.ContentType实例

# settings.py
INSTALLED_APPS = [
    'account.apps.AccountConfig',
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',   # 创建项目时,默认已安装此应用
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'social_django',
    'django_extensions',
    'easy_thumbnails',
    'images.apps.ImagesConfig',
    'actions.apps.ActionsConfig',
]

[!info] contenttypes应用程序包含了ContentType模型。该模型实例体现了应用程序的实际模型,当在项目中安装新模型时,将自动生成新的ContentType实例。ContentType模型包含以下字段:

  • app_label 表示当前模型所属的应用程序名称,并自动从模型Meta选项中app_label属性中得到。如Image模型隶属于images应用程序。
  • model 表示模型类的名称。
  • name表示人们可读的模型名称,并自动从模型Meta选项的verbose_namej属性中获得。

2.与ContentType实例对象进行交互

运行python manage.py shell命令打开shell,通过执行通过app_label和model的查询,可获得与特定模型对应的ContentType对象,如下所示:
Pasted image 20241221233417.png

三、向模型中添加通用关系

1.3个字段

在通用关系中,ContentType对象扮演的角色用于指向用于关系的模型。对此,需要3个字段设置模型的通用关系,如下所示:

  • ContentType的ForeginKey字段,表示为当前关系的模型。
  • 存储关系对象的主键的字段,通常是PositiveIntegerField以匹配Django的主键字段。
  • 在上述两个字段的基础上,用于定义和管理通用关系的字段。ContentTypes框架对此提供了一个GenericForeignKey字段。
# actions/models.py
from django.db import models
from django.contrib.contenttypes.models import ContentType
from django.contrib.contenttypes.fields import GenericForeignKey

class Action(models.Model):
    user = models.ForeignKey('auth.User',related_name='actions',db_index=True,on_delete=models.CASCADE)
    verb = models.CharField(max_length=255)
    
   # 添加3个字段来设置action模型的通用关系
    target_ct = models.ForeignKey(ContentType,blank=True,null=True,related_name='target_obj',on_delete=models.CASCADE)   # 外键,指向ContentType模型
    target_id = models.PositiveIntegerField(null=True,blank=True,db_index=True)  # 一个存储关系对象主键的PositiveIntegerField
    target = GenericForeignKey('target_ct', 'target_id')  # 在上述两个字段基础上,表示为关系对象的GenericForeginKey字段

    created = models.DateTimeField(auto_now_add=True,db_index=True)
    class Meta:
        ordering = ('-created',)

Django并不会针对GenericForeignKey在数据库中生成任何字段,仅target_ct和target_id映射至数据库字段中。这两个字段均包含了blank=True和null=True属性。因而在保存Action对象时无须使用到target对象。

2.数据迁移

python manage.py makemigrations
python manage.py migrate

3.将Action添加到管理站点中

# actions/admin.py
from django.contrib import admin
from .models import Action

@admin.register(Action)
class ActionAdmin(admin.ModelAdmin):
    list_display = ('user', 'verb', 'target', 'created')
    list_filter = ('created',)
    search_fields = ('verb',)

启动开发服务器,在浏览器中打开 http://127.0.0.1:8000/admin/actions/action/add 。此时,页面中创建了一个新的Action模型,如下图所示:
Pasted image 20241221235231.png
在上图可看到,仅显示了映射至实际数据库字段的target_ct和target_id字段,GenericForeignKey字段并未在表单中显示。
target_ct字段可选择Django项目中任意注册后的模型。另外,还可对内容类型进行限制,并通过target_ct字段中的limit_choices_to属性从限定的模型集中进行选择,limit_choices_to属性可将ForeignKey字段的内容限制为某个特定的数值集。

4.创建action方法

# actions/utils.py
from django.contrib.contenttypes.models import ContentType
from .models import Action

def create_action(user, verb, target=None):
	action = Action(user=user, verb=verb, target=target)
	action.save()

create_action函数可创建某些活动,并可选择地包含target对象。在代码中,可将此函数用作快捷方式 ,以向活动加入新的活动。

四、避免活动流中的重复内容

某些时候,用户可能会多次执行某项活动,如多次单击LIKE或UNLIKE按钮;或者在较短时间内多次执行同一项活动,这将导致存储、显示重复的活动。为了避免这个问题,可改进create_action以忽略重复的活动。

# actions/utils.py
import datetime
from django.utils import timezone
from django.contrib.contenttypes.models import ContentType
from .models import Action

def create_action(user, verb, target=None):
    # check for any similar action made in the last minute
    now = timezone.now()
    last_minute = now - datetime.timedelta(seconds=60)
    similar_actions = Action.objects.filter(user_id=user.id,verb= verb,created__gte=last_minute)
    if target:
        target_ct = ContentType.objects.get_for_model(target)
        similar_actions = similar_actions.filter(target_ct=target_ct,target_id=target.id)

    if not similar_actions:
        # no existing actions found
        action = Action(user=user, verb=verb, target=target)
        action.save()
        return True
    return False

以上代码解释如下:

  • 首先,使用了Django提供的timezone.now()方法获取当前时间。该方法执行与datetime.datetime.now()相同的操作,但返回一个timezone-aware对象。Django提供了一项USE_TZ设置,以启用/禁用时区功能。通过startproject命令创建的默认settings.py文件包含了USE_TZ=True。
  • 利用last_minute变量存储1分钟之前的日期时间,并检索自此起用户执行的相同操作活动。
  • 如果最后1分钟内不包含相同的操作活动,则创建Action对象。若Action对象已被创建,则返回True,否则返回False。

五、向活动流中添加用户活动

下面向视图中添加某些操作活动,并对用户创建活动流。针对下列各项交互行为,我们将存储对应的操作活动:

  • 用户收藏了一幅图像。
  • 用户喜爱某幅图像。
  • 用户创建了一个账号。
  • 用户关注了一个用户。
# images/views.py
from actions.utils import create_action

# image_create视图下添加如下代码
# new_item.save()
create_action(request.user, 'bookmarked image', new_item)

# images_like视图下添加如下代码
# image.users_like.add(request.user)
create_action(request.user, 'likes', image)
# account/views.py
from actions.utils import create_action

# register视图下添加如下代码
# Profile.objects.create(user=new_user)
create_action(new_user, 'has created an account')

# user_follow视图下添加如下代码
# Contact.objects.get_or_create(user_from=request.user,user_to=user)
create_action(request.user, 'is following', user)

从上述代码中不难发现,基于Action模型和帮助函数,可简单地将活动保存至活动流中。

六、显示活动流

最后,我们需要一种方式可针对每个用户显示活动流。对此可在用户配置中包含活动流。

# account/views.py中修改dashboard代码
from actions.models import Action

@login_required
def dashboard(request):
    # Display all actions by default
    actions = Action.objects.exclude(user=request.user)
    following_ids = request.user.following.values_list('id',flat=True)
    if following_ids:
        # If user is following others, retrieve only their actions
        actions = actions.filter(user_id__in=following_ids)
    actions = actions.select_related('user', 'user__profile').prefetch_related('target')[:10]

    return render(request,'account/dashboard.html',{'section': 'dashboard','actions': actions})

以上代码解释如下:

  • 从数据库检索所有的操作活动,但不包括当前用户执行的操作。
  • 默认状态下,将检索全部平台用户执行的最近活动。如果某个用户关注了另一个用户,将限制查询且仅检索所关注用户执行的操作活动。
  • 最后,还需要将对应结果限制为所返回的前10个操作活动。此处并未使用QuerySet中的order_by(),其原因在于返回结果仅依赖于Action模板中Meta选项提供的默认排序顺序。

七、优化涉及关系对象的QuerySet

每次检索Action对象时,通常会访问其关联的User对象,以及用户关联的Profile对象。Django ORM提供了一种简单方式。可以同时检索关系对象,从而避免了额外的数据库查询操作。

1.使用select_related()

select_related方法针对ForeignKey和OneToOne字段,其工作方式可描述为:执行一个SQL JOIN操作,并包含了SELECT语句中关系对象的字段。
当使用select_related方法时,可编辑下列代码:

actions = actions[:10]

同时向所用字段添加select_related,如下所示:

actions = actions.select_related('user','user_profile').[:10]

此处使用user_profile连接单一SQL查询中的Profile表。如果调用了select_related(),且未向其传递任何参数,将从全部ForeignKey关系中检索对象。一般情况下,总是将select_related()限制为以后要访问的关系

[!info] 谨慎使用select_related(),可以大大提高执行时间。

2.使用prefetch_related()

prefetch_related方法,适用于多对多或多对一关系,以及select_related()所支持的关系。
prefetch_related方法针对每个关系执行独立的查询,并使用python连接结果。此外,该方法还支持GenericRelation和GenericForeignKey的预取行为。

# account/views.py
actions = actions.select_related('user','user_profile').prefetch_related('target')[:10]

上述查询针对用户活动的检索以及关系对象进行了相应的优化。

八、针对操作活动创建模板

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