论坛首页 编程语言技术论坛

使用Django的 signals 和 contenttypes 实现新鲜事功能

浏览 17874 次
该帖已经被评为精华帖
作者 正文
   发表时间:2008-07-22  

看到很多SNS网站上,像校内,5G都有一个很棒的功能,就是登录之后在自己的首页,可以看到自己好友最新发生的动态。于是想到使用django其实可以非常简单的实现这个功能,并且效果比现在SNS网站所用的更好。

 

总体来说这个功能就是在用户发生某个动作的时候将其记录下来,我不知道别人是怎么实现的,也许是直接在发生那个动作的代码块里写死,也许是使用数据库的触发器之类,但是在django中,一个很简单的方法的就是使用signals。

什么是signals:

最初看到signals是在django的The database API文档,当保存一个object的时候,会发生下面的这些事情(原文):

What happens when you save?

When you save an object, Django performs the following steps:

  1. Emit a ``pre_save`` signal. This provides a notification that an object is about to be saved. You can register a listener that will be invoked whenever this signal is emitted. (These signals are not yet documented.)

  2. Pre-process the data. Each field on the object is asked to perform any automated data modification that the field may need to perform.

    Most fields do no pre-processing — the field data is kept as-is. Pre-processing is only used on fields that have special behavior. For example, if your model has a DateField with auto_now=True, the pre-save phase will alter the data in the object to ensure that the date field contains the current date stamp. (Our documentation doesn’t yet include a list of all the fields with this “special behavior.”)

  3. Prepare the data for the database. Each field is asked to provide its current value in a data type that can be written to the database.

    Most fields require no data preparation. Simple data types, such as integers and strings, are ‘ready to write’ as a Python object. However, more complex data types often require some modification.

    For example, DateFields use a Python datetime object to store data. Databases don’t store datetime objects, so the field value must be converted into an ISO-compliant date string for insertion into the database.

  4. Insert the data into the database. The pre-processed, prepared data is then composed into an SQL statement for insertion into the database.

  5. Emit a ``post_save`` signal. As with the pre_save signal, this is used to provide notification that an object has been successfully saved. (These signals are not yet documented.)

简单来说就是当django保存一个object的时候会发出一系列的signals,可以通过对这些signals注册listener,从而在相应的signals发出时执行一定的代码。关于signals的文档django还没有整理出来,不过找了些资料来看了看,不算很难懂,基本使用还是很简单的。

使用signals来监听用户的动作有很多好处,1、不管这个动作是发生在什么页面,甚至在很多页面都可以发生这个动作,都只需要写一次代码来监听保存object这个动作就可以了。2、可以完全不修改原来的代码就可以添加监听signals的功能。3、你几乎可以在signals监听代码里写任何代码,包括做一些判断是不是第一次发生此动作还是一个修改行为等等。

 

鉴于本人表达能力有限,如果看到这里感觉稀里糊涂,可以考虑先看一下本文底部所列出的一些资料获取更多信息。

 

现在需要面对的就是第二个问题,监听到用户动作之后如何完整有效地保存用户这一动作,保存一个字符串来描述这个动作?通过外键来指向某个表中的某条记录?

保存一个字符串来描述这个动作是一个很高效的方法,但是缺乏灵活性,比如用户发表了一篇日志,但是很快又删了,如何把这个多余的新鲜事记录找出来同时删除?想修改已经保存了的字符串描述该怎么办?

而通过外键来指向某个表中的某条记录虽然可以解决删除问题,但是需要为不同类型的动作各自添加一张表来对应,如果以后有新的功能实现需要添加新表,如何可以简单来进行扩展呢?

 

而这些django的 contenttypes framework就可以很好的解决。

什么是contenttypes framework(原文):

Django includes a “contenttypes” application that can track all of the models installed in your Django-powered project, providing a high-level, generic interface for working with your models.

这句话听上去很难理解,不过对于新鲜事这个功能来说就是使用Generic relations来产生一个特殊的外键,它不像models.ForeignKey那样,必须指定一个Model来作为它指向的对象。Generic relations可以指向任何Model对象,有点像C语言中 void* 指针。

 

这样关于保存用户所产生的这个动作,比如用户写了一片日志,我们就可以使用Generic relations来指向某个Model实例比如Post,而那个Post实例才真正保存着关于用户动作的完整信息,即Post实例本身就是保存动作信息最好的地方。这样我们就可以通过存取Post实例里面的字段来描述用户的那个动作了,需要什么信息就往那里面去取。而且使用Generic relations的另外一个好处就是在删除了Post实例后,相应的新鲜事实例也会自动删除。

 

说了这么多,还是直接来看代码更实际些,看看到底怎么来实现吧。

假如有一个叫Post的Model,以及一个用来记录用户事件的Model定义如下:

 

# -*- coding: utf-8 -*-
from django.db import models
from django.contrib.auth.models import User
from django.contrib.contenttypes.models import ContentType
from django.contrib.contenttypes import generic

class Post(models.Model):
    author = models.ForeignKey(User)
    title = models.CharField(max_length=255)
    content = models.TextField()
    created = models.DateTimeField(u'发表时间', auto_now_add = True)
    updated = models.DateTimeField(u'最后修改时间', auto_now = True)


    def __unicode__(self):
        return self.title


class Event(models.Model):
    user = models.ForeignKey(User)
    content_type = models.ForeignKey(ContentType)
    object_id = models.PositiveIntegerField()
    
    event = generic.GenericForeignKey('content_type', 'object_id')
    
    created = models.DateTimeField(u'事件发生时间', auto_now_add = True)
    
    def __unicode__(self):
        return  self.user.username + u'的事件'

    

 

 Post这个Model很普通没什么好说的,Event这个Model有3个很特殊的字段,content_type是一个普通的外键指向一个叫ContentType的Model,而ContentType这个Model就很特殊了,文档描述是

Instances of ContentType represent and store information about the models installed in your project, and new instances of ContentType are automatically created whenever new models are installed.

 

就是说一个ContentType 实例 存储了 某个Model 的一些信息,通过这些信息就可以还原出那个Model。其实ContentType 的存储的信息也非常简单,其定义如下:

 

class ContentType(models.Model):
    name = models.CharField(max_length=100)
    app_label = models.CharField(max_length=100)
    model = models.CharField(_('python model class name'), max_length=100)
    objects = ContentTypeManager()

 

而通过app_label和model这2个字段,使用django.db.models.get_model这个方法就可以找出原来所对应的Model。而有了原来的Model的定义,再通过使用主键,就可以找到这个Model所对应的某条记录了,object_id这个字段正是用来存储这个主键的,

所以content_type和object_id这两个字段加起来就可以表达在一个project中所存在的所有Model的某个实例了。当然我们最终并不需要直接和这两个字段打交道,而是通过另外一个看起来很奇快的字段,generic.GenericForeignKey是一个特殊的外键,可以指向任何Model的实例,在这里就可以通过这个字段来指向类似Post这样保存着用户动作信息的Model实例。

 

定义完了Model之后就是来写一下signals部分的代码了,将原来的文件修改成如下这样:

 

 

# -*- coding: utf-8 -*-
from django.db import models
from django.contrib.auth.models import User
from django.contrib.contenttypes.models import ContentType
from django.contrib.contenttypes import generic
from django.db.models import signals
from django.dispatch import dispatcher

class Post(models.Model):
    author = models.ForeignKey(User)
    title = models.CharField(max_length=255)
    content = models.TextField()
    created = models.DateTimeField(u'发表时间', auto_now_add = True)
    updated = models.DateTimeField(u'最后修改时间', auto_now = True)
    
    events = generic.GenericRelation('Event')

    def __unicode__(self):
        return self.title
    
    def description(self):
        return u'%s 发表了日志《%s》' % (self.author, self.title)
    
    class Admin:
        pass
    



class Event(models.Model):
    user = models.ForeignKey(User)
    content_type = models.ForeignKey(ContentType)
    object_id = models.PositiveIntegerField()
    
    event = generic.GenericForeignKey('content_type', 'object_id')
    
    created = models.DateTimeField(u'事件发生时间', auto_now_add = True)
    
    def __unicode__(self):
        return  u"%s的事件: %s" % (self.user, self.description())
    
    def description(self):
        return self.event.description()
    
    class Admin:
        pass


def post_post_save(sender, instance, signal, *args, **kwargs):
    post = instance
    if post.created == post.updated:
        event = Event(user=post.author,event = post)
        event.save()

dispatcher.connect(post_post_save, signal=signals.post_save, sender=Post)

 

 前面说到django在保存一个object的时候会发出一系列signals,在这里我们所监听的是signals.post_save这个signal,这个signal是在django保存完一个对象后发出的,利用dispatcher.connect这个函数来注册监听器,第一个参数是要执行的函数,第二个参数是要监听的signal,第三个参数是指定发送信号的Class,这里指定为Post这个Model,对其他Model所发出的signal并不会执行注册的函数。而我们所定义的需要执行的函数可以将signal所发出的一些参数定义为需要执行的函数的参数,从而在函数里面进行使用,获取更多的信息,这里使用了instance这个参数,即刚刚保存完的Model对象实例。在函数里通过比较日志发布时间和修改时间是否相等来判断是第一次发表还是修改后的保存,并且只为第一次发表这个动作创建一个相应的事件。创建事件的时候看到可以将post这个instance直接赋给generic.GenericForeignKey类型的字段,从而event实例就可以通过它来获取事件的真正信息了,在这里,如果有其他类型的Model实例,当然也可以赋值给generic.GenericForeignKey类型的字段了。

 

现在事件已经保存起来了,如何来得到事件的一个简短描述呢?可以给自己做一个假定,event这个字段所指向的Model都有一个叫做description的方法,那么我们只管调用这个方法就可以得到描述了。至于Post里面的那个description方法,可以根据需要,自己存取post实例的字段来获取信息,包括直接在描述字符串里面包含日志的url等。

 

最后有一点需要的注意的是,Post的Model定义里现在多了一个字段:

 

events = generic.GenericRelation('Event')

 

通过这个字段可以得到与某篇post相关联的所有事件,最重要的一点是如果没有这个字段,那么当删除一篇post的时候,与该post关联的事件是不会自动删除的。反之有这个字段就会进行自动的级联删除。

 

 现在有了以上这些,如果你将这些Model定义和signals定义,添加到自己某个app,然后到admin页面添加一篇post试试,添加完后应该会自动多出了一条对应的event记录,修改一个post不会再添加event记录,而删除一篇post会自动删除相应的event记录。(现在django的newforms-admin分支已经合并了,可能关于本文的代码admin部分的定义已经无效,如何修改,可以参考此文:http://jinhao.iteye.com/blog/218112)

 

这就是一个新鲜事功能简单的实现原理,而关于 signals 和 contenttypes 的更多资料可以参考以下这些:

官方的 contenttypes 文档:http://www.djangoproject.com/documentation/contenttypes/

contenttypes 例子:http://www.djangoproject.com/documentation/models/generic_relations/

Generic Relation在SharePlat的使用:http://blog.donews.com/limodou/archive/2006/12/31/1106217.aspx

django 的 contribs 之 contenttype:http://codeplayer.blogspot.com/2006/09/django-contribs-contenttype.html

django signals的wiki页:http://code.djangoproject.com/wiki/Signals

另一篇关于signals的文章:http://www.mercurytide.co.uk/whitepapers/django-signals/

 

 

 

08年9月6日 更新:

 

现在Django 官方的文档已经进行了重构,关于signal的文档也出来了,参见:

http://docs.djangoproject.com/en/dev/topics/signals/

http://docs.djangoproject.com/en/dev/ref/signals/

 

另外关于在回复讨论中提到的时间误差问题,看了 post_save 信号的描述之后,发现其实它会自己传一个参数 created , 以说明是新创建的还是更新的方式调用了 save() ,用这个参数显然比用时间来判断更好.

http://docs.djangoproject.com/en/dev/ref/signals/#post-save

 

   发表时间:2008-07-23  
这么好,怎么没有人用DJ做一个SNS?
0 请登录后投票
   发表时间:2008-07-23  
songk 写道
这么好,怎么没有人用DJ做一个SNS?

不知道国外有没有啊,现在国内用django做得项目不是很多,比较大型的www.haokanbu.com算一个吧。
0 请登录后投票
   发表时间:2008-07-30  
http://www.haibao.cn/ 这个应该也是用的 django 。
0 请登录后投票
   发表时间:2008-08-22  
今天遇到一个奇妙的问题,新鲜事在vista上调试没有问题,程序上传到FreeBSD服务器上新鲜事无法记录

后来发现是两个环境的时间精确度的问题,不知道是硬件还是操作系统还是软件的问题

vista上时间是这样的 2008-08-22 00:30:35.679+00

FreeBSD上时间是这样的 2008-08-22 00:27:58.618778+00

FreeBSD的时间太精确了,导致最后确定统计新鲜事的时候统计不上来,updated 和 created 两个时间最后三位不一样 :(

小修改了一下代码:
    if (post.updated - post.created).seconds  < 1 :  
        event = Event(user=post.uid,event = post)  
        event.save()  

0 请登录后投票
   发表时间:2008-08-22  
非常感谢你的帖子,我在自己的blog上也实现了新鲜事功能,下面是简单的总结:
根据网上的帖子介绍,简化说下本站新鲜事实现的方法:

目前新鲜事只跟踪文章的变化,图片有空再加上,在文章models中添加代码,确保新鲜事跟文章同步,不影响文章数据库结构
events = generic.GenericRelation('Event')
def description(self):  
    return u'%s 发表 <%s>' % (self.uid, self.title)  
 
添加新鲜事记录表
class Event(models.Model):  
    user = models.ForeignKey(User)  
    content_type = models.ForeignKey(ContentType)  
    object_id = models.PositiveIntegerField()  
      
    event = generic.GenericForeignKey('content_type', 'object_id')  
      
    created = models.DateTimeField(u'事件发生时间', auto_now_add = True)  
      
    def __unicode__(self):  
        return  u"%s" % (self.description())  
      
    def description(self):  
        return self.event.description()  
      
    class Admin:  
        pass
    class Meta:
        ordering = ['-created']
 
 
添加新鲜事监视器,我直接放在了models文件里。
from django.db.models import signals  
from django.dispatch import dispatcher

def post_post_save(sender, instance, signal, *args, **kwargs):  
    post = instance  
    #因为时间精确度不同,采用计算时间差的秒数
    if (post.updated - post.created).seconds  < 1 :
        event = Event(user=post.uid,event = post)  
        event.save()  
 
dispatcher.connect(post_post_save, signal=signals.post_save, sender=articleDB) 
0 请登录后投票
   发表时间:2008-08-22  
是啊,没发现时间误差的问题,我开始还以为updated和created是从同一个变量赋值过来的,谢谢提醒。
cougarwww 写道
今天遇到一个奇妙的问题,新鲜事在vista上调试没有问题,程序上传到FreeBSD服务器上新鲜事无法记录

后来发现是两个环境的时间精确度的问题,不知道是硬件还是操作系统还是软件的问题

vista上时间是这样的 2008-08-22 00:30:35.679+00

FreeBSD上时间是这样的 2008-08-22 00:27:58.618778+00

FreeBSD的时间太精确了,导致最后确定统计新鲜事的时候统计不上来,updated 和 created 两个时间最后三位不一样 :(

小修改了一下代码:
    if (post.updated - post.created).seconds  < 1 :  
        event = Event(user=post.uid,event = post)  
        event.save()  


0 请登录后投票
   发表时间:2008-08-27  
duka 写道
http://www.haibao.cn/ 这个应该也是用的 django 。


貌似用的php吧
0 请登录后投票
   发表时间:2008-08-30  
AOP呼!
0 请登录后投票
   发表时间:2008-08-31  
bluecrystal 写道
duka 写道
http://www.haibao.cn/ 这个应该也是用的 django 。


貌似用的php吧

http://www.haibao.cn/admin/
0 请登录后投票
论坛首页 编程语言技术版

跳转论坛:
Global site tag (gtag.js) - Google Analytics