Python Web开发学习

Django Signals(信号)监听模型或某一字段变化

2019-05-21  本文已影响2人  吾星喵

欢迎访问我的博客查看 我的博客

Django信号

Django 提供一个“信号分发器”,允许解耦的应用在框架的其它地方发生操作时会被通知到。 简单来说,信号允许特定的sender通知一组receiver某些操作已经发生。 这在多处代码和同一事件有关联的情况下很有用。

Django提供一组内建的信号,允许用户的代码获得Django特定操作的通知。 它们包含一些有用的通知:

在模型 save()方法调用之前或之后发送。

在模型delete()方法或查询集的delete() 方法调用之前或之后发送。

模型上的ManyToManyField 修改时发送。

Django开始或完成HTTP请求时发送。

示例

示例分析:现有一篇博客,其中正文会包含很多图片,然而在我们创建文章时,没有事先创建好id的情况下,无法将这些图片关联到对应的文章,一旦当文章中的图片变动时,我们想从图片数据库、磁盘中删除这些图片,就会比较麻烦。所以引入信号,在发布的文章保存时,查找正文中用到的图片,将其关联到该文章。

当然可以改变一下思路,当用户点击创建博客时,通过ajax向后台提交一个创建博客的请求(那么model中需要设置其他字段可为为null才行),该请求会返回博客的id,进入编辑页面;当编辑博客中提交的图片,就把该id一并传入后台,关联到图片对象中;保存博客相当于就是更新该id对应的对象了。

模型数据库设计

下面用普通的方法,用信号实现。

models

class Article(models.Model):
    title = models.CharField(max_length=100, verbose_name='标题')
    author = models.ForeignKey(UserProfile, related_name='blog_articles', blank=True, null=True, on_delete=models.SET_NULL, verbose_name='作者')
    content = models.TextField(blank=True, null=True, verbose_name='正文')
    # 省略部分字段

    def __str__(self):
        return self.title

    class Meta:
        ordering = ['-publish_time', ]  # 按照发布时间降序,旧的时间在后,也就是新发布的博客放在前面
        verbose_name = '博客文章'
        verbose_name_plural = '文章列表'


# 图片存储
class BlogImage(models.Model):
    article = models.ForeignKey(Article, blank=True, null=True, on_delete=models.CASCADE, related_name='blog_images', verbose_name='关联文章')
    title = models.CharField(max_length=50, null=True, blank=True, verbose_name='标题')
    image = models.ImageField(upload_to='blog/images/%Y/%m', blank=True, null=True, verbose_name='图片')

    class Meta:
        verbose_name = '博客图集'
        verbose_name_plural = verbose_name

    def __str__(self):
        return self.title

由于编辑博客中富文本或Markdown(我这使用)编辑器上传的图片基本都是ajax异步上传,该方式图片没有关联到文章

匹配正文中的图片字符串保存路径

写一个函数,用于正则匹配正文中的Markdown格式图片文本

# apps/blog/tools/manage_image_resources.py
from blog.models import BlogImage


def get_content_image_instance(article_instance):
    """
    正则匹配文中的图片字符串,获取其中的本地位置,返回所有的与该文章相关的图片对象
    :param article_instance:
    :return:
    """
    # 例如
    # ![](/media/blog/images/2018/10/BLOG_20181008_221449_65.jpg)
    # [![423423](/media/blog/images/2018/10/BLOG_20181008_221459_67.jpg "423423")](http://432 "423423")
    # ![BLOG_20181008_221702_36](/media/blog/images/2018/10/BLOG_20181008_221702_36.png "博客图集BLOG_20181008_221702_36.png")
    # [![](/media/blog/images/2018/10/BLOG_20181008_221953_62.jpg)](http://68768)
    # ![BLOG_20181008_223948_94](/media/blog/images/2018/10/BLOG_20181008_223948_94.png "博客图集BLOG_20181008_223948_94.png")
    results = re.findall(r'!\[(.*?)\]\(/media/(.*?)\)', article_instance.content)
    blog_images = []
    # print(results)
    # for r in results:
    #     print(r)
    # print(len(results))
    for result in results:
        # print(result[1])
        # if ' ' in result[1]:
        #     image_url = result[1].split()[0]  # 'blog/images/2018/10/BLOG_20181008_221702_36.png "博客图集BLOG_20181008_221702_36.png"'
        # else:
        #     image_url = result[1]  # 'blog/images/2018/10/BLOG_20181008_221449_65.jpg'
        # print(image_url)
        image = BlogImage.objects.filter(image=result[1].split()[0])  # 获取博客图片链接字符串
        if image:
            image = image.first()  # 假定唯一
            blog_images.append(image)
    return blog_images

传入文章实例,获取正文中的图片对象。

创建信号处理函数,创建、更新文章执行

在应用下创建 signals.py 文件,用于放置处理函数

# apps/blog/signals.py

import hashlib
import os
from django.db.models.signals import post_save
from django.dispatch import receiver
from .models import Article
from .tools.manage_image_resources import get_content_image_instance


@receiver(post_save, sender=Article)
def blog_save_handler(sender, instance=None, created=False, **kwargs):
    """
    1、创建对象时,将id加密,保存到ecpid字段中,需要在应用/apps.py中进行重载
    2、更新对象,获取正文中的图片资源,将不存在的进行删除;
    :param sender:
    :param instance: 创建的对象
    :param created:
    :param kwargs:
    :return:
    """
    if created:
        # 加密文章对象的id,之后用这个字段访问文章
        article_id = instance.id  # instance指的就是创建的对象
        obj_id = str(article_id)
        md5 = hashlib.md5()  # 生成一个MD5对象
        md5.update(obj_id.encode('utf-8'))  # 使用md5对象里的update方法md5转换
        instance.ecpid = md5.hexdigest()  # 得到加密后的字符串
        instance.save()

        # 获取该文章中用到过的图片,添加关联文章
        images = get_content_image_instance(instance)  # 获取该文章所有的图片对象
        for image_instance in images:
            image_instance.article = instance
            image_instance.save()
    else:
        # created为False,更新操作
        # 修改该文章中变动的图片信息
        images = get_content_image_instance(instance)  # 获取该文章所有的图片对象
        old_images = instance.blog_images.all()  # 图片数据库已存在的该文章所有图片对象
        # 原不存在,新添加的有,增加
        for image_instance in images:
            if image_instance not in old_images:
                image_instance.article = instance
                image_instance.save()

        # 原存在,新添加的无,删除
        for image_instance in old_images:
            if image_instance not in images:  # 不在新提交的里面
                image_path = image_instance.image.path
                if os.path.exists(image_path):
                    print('图片存在,在磁盘中进行删除:', image_path)
                    os.remove(image_path)
                # 删除数据库中的图库实例
                image_instance.delete()

当创建博客post提交保存后,就会获取正文中的图片对象,添加文章的外键关联。

引入信号模块路径

编辑应用下的 __init__.py 添加

default_app_config = 'blog.apps.BlogConfig'

编辑 apps.py 重载BlogConfigready方法

from django.apps import AppConfig


class BlogConfig(AppConfig):
    name = 'blog'
    verbose_name = '个人博客'

    def ready(self):
        """
        在子类中重写此方法,以便在Django启动时运行代码。
        :return:
        """
        from .signals import blog_save_handler

但是,模型中任意字段变化都会被监听

Article模型中也有另外两个字段,浏览量、点赞数

class Article(models.Model):
    # ...
    views = models.PositiveIntegerField(default=0, verbose_name='浏览量')
    likes = models.PositiveIntegerField(default=0, verbose_name='点赞数')

实际上通过以上设置,当用户浏览文章、或者是点赞,都会造成该信号处理函数被调用,那么如何去只监听某个字段的变化呢?

指定被监听的字段

模型信号并没有提供针对特定字段值变化的广播功能,虽然该信号提供了 update_fields 参数,但是并不能证明在该参数中的字段名的字段值一定发生了变化,所以我们要采用一个结合 post_init 信号的变通方法。

import hashlib
import os
from django.db.models.signals import post_save, post_init
from django.dispatch import receiver
from .models import Article
from .tools.manage_image_resources import get_content_image_instance


@receiver(post_init, sender=Article)
def blog_post_init(instance, **kwargs):
    """
    缓存原始的值在 __original_name 中
    :param instance:
    :param kwargs:
    :return:
    """
    instance.__original_content = instance.content


@receiver(post_save, sender=Article)
def blog_save_handler(sender, instance=None, created=False, **kwargs):
    """
    1、创建对象时,将id加密,保存到ecpid字段中,需要在应用/apps.py中进行重载
    2、更新对象,获取正文中的图片资源,将不存在的进行删除;
    :param sender:
    :param instance: 创建的对象
    :param created:
    :param kwargs:
    :return:
    """
    if created:
        # 加密文章对象的id,之后用这个字段访问文章
        article_id = instance.id  # instance指的就是创建的对象
        obj_id = str(article_id)
        md5 = hashlib.md5()  # 生成一个MD5对象
        md5.update(obj_id.encode('utf-8'))  # 使用md5对象里的update方法md5转换
        instance.ecpid = md5.hexdigest()  # 得到加密后的字符串
        instance.save()

        # 获取该文章中用到过的图片,添加关联文章
        images = get_content_image_instance(instance)  # 获取该文章所有的图片对象
        for image_instance in images:
            image_instance.article = instance
            image_instance.save()
    else:
        # created为False,更新操作,且当缓存的正文和当前提交的正文不同时,才进行正文中的图片提取
        if instance.__original_content != instance.content:
            # 修改该文章中变动的图片信息
            images = get_content_image_instance(instance)  # 获取该文章所有的图片对象
            old_images = instance.blog_images.all()  # 图片数据库已存在的该文章所有图片对象
            # 原不存在,新添加的有,增加
            for image_instance in images:
                if image_instance not in old_images:
                    image_instance.article = instance
                    image_instance.save()

            # 原存在,新添加的无,删除
            for image_instance in old_images:
                if image_instance not in images:  # 不在新提交的里面
                    image_path = image_instance.image.path
                    if os.path.exists(image_path):
                        print('图片存在,在磁盘中进行删除:', image_path)
                        os.remove(image_path)
                    # 删除数据库中的图库实例
                    image_instance.delete()

简单的说就是在该模型广播 post_init 信号的时候,在模型对象中缓存当前的字段值;在模型广播 post_save (或 pre_save )的时候,比较该模型对象的当前的字段值与缓存的字段值,如果不相同则认为该字段值发生了变化。

以上示例就是,当博客的content字段发生变化时,才进行文中图片字符获取。

上一篇下一篇

猜你喜欢

热点阅读