Django中模拟微博timeline

相信大家都用过微博,我们在微博的timeline中,常常可以看到文字、音乐、视频等等的混合。所有的内容都聚合在一起显示出来。具体的实现未可知,但是在Django中,我们有比较优雅的方式。那就是使用contenttypes模块来实现。

现在我们假设对于一条微博只有三种方式(其他的很容易拓展):文本,音乐以及视频。我们先定义三个简单的Model。(注意到Music和Video只要简单继承Tweet类即可。)

from django.db import models
from django.contrib.auth.models import User

class Tweet(models.Model):
    user = models.ForeignKey(User) # 这里就用Django自带的User
    content = models.CharField(max_length=140) # 内容,最多140字
    pic_url = models.URLField(null=True, blank=True)
    created = models.DateTimeField(auto_now_add=True)
    
    class Meta:
        ordering = ['-created',]
        
    def __unicode__(self):
        return self.content
    
class Music(Tweet):
    music_url = models.URLField(verify_exists=False)
    
class Video(Tweet):
    video_url = models.URLField(verify_exists=False)

为了使无论什么类型的微博都显示在timeline中,我们就需要单独用一张表来存放用户的行为(event)。作者在添加一条微博的时候,就会自动记录用户的事件。记录用户事件的表中,我们当然要记录用户的微博类型,以及如何找到这个对象。但是这里我们就有三个model,这个时候Django中的contenttypes framework就派上了用场。官方文档在这里。其中的核心类是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所在的app名称,model是Model的类名,name是Model的友好名称,即Model的元类中的verbose_name属性。通过这几个字段,就可以还原出这个Model来。

于是,我们在记录事件的Model中用一个字段表示ContentType,这样我们就能指向任意一个Model。这点颇像C中的void*指针。

让我们来看看Event Model的代码:

from django.contrib.contenttypes.models import ContentType
from django.contrib.contenttypes import generic

class Event(models.Model):
    user = models.ForeignKey(User)
    content_type = models.ForeignKey(ContentType)
    object_id = models.PositiveIntegerField()
    event_object = generic.GenericForeignKey('content_type', 'object_id')

user字段用来指向事件的发起者。conten_type表示任意一个Model,而通过object_id,我们就可以得到这个实例,也就是event_object的作用(GenericForeignKey),这点可以参考官方文档中GenericForeignKey类的说明

我们在各个Model中再添加一行(本例只需在Tweet中添加即可):

events = generic.GenericRelation('Event')  

这样,当一个Model实例被删除时,Event中的相关记录也就会被删除。

在结束了这些之后,我们就需要在用户添加一篇微博的时候,来记录这个动作。可能有人就会想到重写Model中的save方法。没错,这样确实可以实现。但是如果有多个类,我们就需要在每个类中都重写这个方法。而且,这么写会破坏原有Model的行为。其实有着更好的方法来实现:使用Signals(官方文档)。

如Signals名字——信号所言,Django中包含一个“Signal dispatcher”(信号调度员,姑且这么翻译吧),其实就是让某个动作(发送者senders)能够让别的应用(receivers)捕捉到。比如说一个Model在保存之前会出发pre_save信号,来让别的程序捕捉。这个模块在多个代码片段捕捉同一个事件的时候就很有用。Signals模块的实现是和Django中内置的生产者消费者模块相关的。当然这个不必深究。

Django提供以下内置信号:

  • django.db.models.signals.pre_save & django.db.models.signals.post_save:分别在一个Model的save()方法之前和之后触发。
  • django.db.models.signals.pre_delete & django.db.models.signals.post_delete:分别在一个Model的delete()方法之前和之后触发。
  • django.db.models.signals.m2m_changed:当一个Model中的ManyToManyField改变时触发。
  • django.core.signals.request_started & django.core.signals.request_finished:在django的http请求开始和结束时触发。

在我们的例子里,我们只需要监听几个Model的post_save信号:

from django.db.models.signals import post_save

def post_save_callback(sender, instance, **kwargs):
    '''sender是信号发送者(Model),instance是其实例'''
    obj = instance
    event = Event(user=obj.user, event_object=obj)
    event.save()
        
# 添加dispatch_uid保证每次事件只会被记录一次
post_save.connect(post_save_callback, sender=Tweet, dispatch_uid="my_unique_identifier")

这里的post_save_callback是回调函数,当我们需要注册一个信号的时候,它要作为Singal.connect()方法的第一参数。Django1.3中可以使用装饰器,像这样:

from django.core.signals import post_save
from django.dispatch import receiver

@receiver(post_save, sender=Tweet, dispatch_uid="my_unique_identifier")
def post_save_callback(sender, instance, **kwargs):
    '''函数内容就省略了'''

更多Signals的内容请参考官方文档。

这样就基本上大功告成了。现在,我们用一个简单的Model来表示关注和被关注的用户关系:

class FriendShip(models.Model):
    user = models.ForeignKey(User, related_name='owner')
    follower = models.ForeignKey(User, related_name='follower')

在自动admin中添加一些数据,我们就可以在views中这么写了:

def main(request):
    user = User.objects.get(username="") # 某个用户
    followers = [entry.user for entry in FriendShip.objects.filter(follower=user)] # 这个用户所关注的人
    
    events = Event.objects.filter(user__in=followers)
    
    return render_to_response('twitter/main.html', {'events': events })

最后大家可能也想到了在自动admin中的“最近的动作”也是使用contenttypes实现(所以可以理解为什么使用自动admin,需要依赖django.contrib.contenttypes这个app了)。如果感兴趣,大家可以参阅django.contrib.admin.models以及django.contrib.admin.options中的代码。

赞这篇文章

分享到

22个评论

  1. siva

    post_save_callback.connect(my_handler, sender=Tweet,dispatch_uid='')中加入sender 参数就不用在回调函数中判断了sender是不是Tweet实例了.

  2. @大乱-BG4HKQ

    学习啦,另外对微薄认证很赶兴趣。
    我开始以为你这个博客是wordpress呢,看到底部才发现是django

  3. alswl

    这个方法不错,比较优雅,可以将 model 和 signal 隔离的比较清楚。
    我目前使用的还是比较原始的 event log 多字段对应,准备想办法重写一下。

  4. wl6179

    我都要哭了,坚决支持Django吧。楼主也加油!!这篇我得好好消化才行,感觉有点吃撑中

  5. wl6179

    我提一个问题:楼主有没有想过,如果 将Tweet和Event两个模型分开放,分别放在TApps和EApps两个应用中,那么它们之间相互如何调用??我试过了,几乎无法相互调用,也就是说Event很难成为真正的通用模块了……楼主帮忙试试,我也在试着这个问题呢~

  6. wl6179

    我的 from mysite.publicapps.models import Event总是无法导入Event模型(因为Event来自另一个的publicapps,而当前是postapp的Post模型在调用它)

  7. 秦续业 作者

    事实上应该是可以的,你在settings中的INSTALLED_APPS中有没有添加相应的app?如果都添加了,import这个model应该是可行的。
    这个实际上还是sys.path的问题,django在开始运行时会将INSTALL_APPS中的app添加到sys.path中去。

  8. wl6179

    我试过了,app也加了,最后是ImportErrer报错无法加载对方的模型,不管用尽多少种引用了,还是报错,有点像你说的sys.path,但又不太像,import引用也就这几种 from mysite.xapp.models import Post|Event … … 其它地方引用都不报错,不太会就此处报错ImportErrer的。。。总之哪位高手尝试成功可以交流一下心得。我水平就这么多,先贴码给大家,哈哈,就是不知在这里贴大片代码,楼主是否允许,哈哈~~

  9. 秦续业 作者

    照理说应该是可以的呀,奇了怪了。
    代码可以贴出来啊,不过评论还没做高亮功能。

  10. wl6179

    对了,差点忘了,我的代码其实就是把 Tweet 改成Post,和本文几乎一模一样,还是以本文为例吧比较清楚。就是把本文中的 class Tweet 放入一个叫s_s的app里,再把 class Event 放入一个叫e_s的app里:
    (即有2个app分别为s_s 和 e_s)
    1.在s_s(app)中的class Tweet:为了定义class Tweet 的 events = generic.GenericRelation( Event ),【注意没有单引号哦】,一定需要调入 e_s(另一app) 中的模型 class Event 供其引用 —— 即 from mysite.e_s.models import Event ;

    2. 同理,在e_s(另一app) 中也要调用 from mysite.e_s.models import Tweet;但此处所用之处不同,是要在此models.py下定义回调函数:# 添加dispatch_uid保证每次事件只会被记录一次
    post_save.connect( post_save_callback, sender=Tweet ) #调用Tweet!!

    3.看似完全没有问题,我运行测试的结果是 ImortError:无法相互导入 Tweet 和 Event !!!~

    其实意思就是说,Tweet 和 Event 它们无法分开(在不同的app下)分别定义,也就是无法面对未来的灵活性了,假如未来要新增会员发博客的新应用Post、新增会员做某事的新应用Xxxx……都同样要调用 Event ~ 那么,就必须把 Post、Xxxx等(也许增加到三十几种新应用)全部都放在同一个App里吗?!那就足够乱了,不好管理了……假如x年后又逐年增加到100个新应用都要调用 Event 呢??
    (望一起努力解决此问题啊,等我找到解药就在这发跟帖~)

  11. wl6179

    还是发在这吧,上边的回复被挤扁了:
    wl6179 说:
    2012年 四月17日 3:08 p.m.

    对了,差点忘了,我的代码其实就是把 Tweet 改成Post,和本文几乎一模一样,还是以本文为例吧比较清楚。就是把本文中的 class Tweet 放入一个叫s_s的app里,再把 class Event 放入一个叫e_s的app里:
    (即有2个app分别为s_s 和 e_s)
    1.在s_s(app)中的class Tweet:为了定义class Tweet 的 events = generic.GenericRelation( Event ),【注意没有单引号哦】,一定需要调入 e_s(另一app) 中的模型 class Event 供其引用 —— 即 from mysite.e_s.models import Event ;

    2. 同理,在e_s(另一app) 中也要调用 from mysite.e_s.models import Tweet;但此处所用之处不同,是要在此models.py下定义回调函数:# 添加dispatch_uid保证每次事件只会被记录一次
    post_save.connect( post_save_callback, sender=Tweet ) #调用Tweet!!

    3.看似完全没有问题,我运行测试的结果是 ImortError:无法相互导入 Tweet 和 Event !!!~

    其实意思就是说,Tweet 和 Event 它们无法分开(在不同的app下)分别定义,也就是无法面对未来的灵活性了,假如未来要新增会员发博客的新应用Post、新增会员做某事的新应用Xxxx……都同样要调用 Event ~ 那么,就必须把 Post、Xxxx等(也许增加到三十几种新应用)全部都放在同一个App里吗?!那就足够乱了,不好管理了……假如x年后又逐年增加到100个新应用都要调用 Event 呢??
    (望一起努力解决此问题啊,等我找到解药就在这发跟帖~)

  12. herock

    我开始的时候也遇到了同样的问题,原因是这样写造成了循环导入,所以报错。

    解决方法很简单,假设有a和b两个app,还有一个event的app,可以把callback方法放到event的models里,把dispatcher分别放到a和b的models。

    之后只需要在a和b的models中import进event中的Event类以及callback方法,而在event的models中,则无需import a和b中的相应类,就不会引起导入错误了。

    我现在实测是ok的,所以实际上用这种方式,不会发生你在最后一段担心的问题。

  13. bingosay

    还是没有明白,class ContentType(models.Model) 这段代码要写在哪个地方,看别人的源码,readme 有列出这个,但是在源码里面没有写出来

  14. zzlettle

    文章很好,美中不足的就是,你没有把每段代码所属的文件下出来。

    没段代码基本都明白什么意思了,但却不知道这个段代码应该放在哪个文件里面吧

    是放在model.py 还是view.py

    不同的model是在同一个文件里面,还是在不同的文件里面,你不介绍清楚,所以搞的人,咋一看起来很不错,明白了。但实际做起来,有点糊。所以你看上面留言都是在这个地方出问题了。主要问题就是django的信号机制,里面的每个部分的代码应该放在哪个文件里面你没介绍清楚。

给作者留言

关于作者

残阳似血(@秦续业),程序猿一枚,把梦想揣进口袋的挨踢工作者。现加入阿里云,研究僧毕业于上海交通大学软件学院ADC实验室。熟悉分布式数据分析(DataFrame并行化框架)、基于图模型的分布式数据库和并行计算、Dpark/Spark以及Python web开发(Django、tornado)等。

博客分类

点击排行

标签云

扫描访问

主题

残阳似血的微博