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中的代码。
post_save_callback.connect(my_handler, sender=Tweet,dispatch_uid='')中加入sender 参数就不用在回调函数中判断了sender是不是Tweet实例了.
确实如此,当时疏忽了。改正了,thx。
学习啦,另外对微薄认证很赶兴趣。
我开始以为你这个博客是wordpress呢,看到底部才发现是django
恩,这个blog是自己开发的,功能相似,所以和wp就差不多。
这个方法不错,比较优雅,可以将 model 和 signal 隔离的比较清楚。
我目前使用的还是比较原始的 event log 多字段对应,准备想办法重写一下。
拜读,最近一直在迷惑这个该怎么设计,现在豁然开朗了。
我都要哭了,坚决支持Django吧。楼主也加油!!这篇我得好好消化才行,感觉有点吃撑中
我提一个问题:楼主有没有想过,如果 将Tweet和Event两个模型分开放,分别放在TApps和EApps两个应用中,那么它们之间相互如何调用??我试过了,几乎无法相互调用,也就是说Event很难成为真正的通用模块了……楼主帮忙试试,我也在试着这个问题呢~
我的 from mysite.publicapps.models import Event总是无法导入Event模型(因为Event来自另一个的publicapps,而当前是postapp的Post模型在调用它)
Event和Post模型,只要不在同一个app中,似乎就不能用!
事实上应该是可以的,你在settings中的INSTALLED_APPS中有没有添加相应的app?如果都添加了,import这个model应该是可行的。
这个实际上还是sys.path的问题,django在开始运行时会将INSTALL_APPS中的app添加到sys.path中去。
我试过了,app也加了,最后是ImportErrer报错无法加载对方的模型,不管用尽多少种引用了,还是报错,有点像你说的sys.path,但又不太像,import引用也就这几种 from mysite.xapp.models import Post|Event … … 其它地方引用都不报错,不太会就此处报错ImportErrer的。。。总之哪位高手尝试成功可以交流一下心得。我水平就这么多,先贴码给大家,哈哈,就是不知在这里贴大片代码,楼主是否允许,哈哈~~
照理说应该是可以的呀,奇了怪了。
代码可以贴出来啊,不过评论还没做高亮功能。
嗯,老秦你一定要试试,有什么结果告诉我一声啊!!~
对了,差点忘了,我的代码其实就是把 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 呢??
(望一起努力解决此问题啊,等我找到解药就在这发跟帖~)
这是一篇很牛的文章啊 秦你总结的真好:)支持一下~
还是发在这吧,上边的回复被挤扁了:
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 呢??
(望一起努力解决此问题啊,等我找到解药就在这发跟帖~)
我开始的时候也遇到了同样的问题,原因是这样写造成了循环导入,所以报错。
解决方法很简单,假设有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的,所以实际上用这种方式,不会发生你在最后一段担心的问题。
还是没有明白,class ContentType(models.Model) 这段代码要写在哪个地方,看别人的源码,readme 有列出这个,但是在源码里面没有写出来
我实现了一下这个代码,可是当保存Music时并没有添加Event动作
有在Model中添加:events = generic.GenericRelation('Event') 么?
文章很好,美中不足的就是,你没有把每段代码所属的文件下出来。
没段代码基本都明白什么意思了,但却不知道这个段代码应该放在哪个文件里面吧
是放在model.py 还是view.py
不同的model是在同一个文件里面,还是在不同的文件里面,你不介绍清楚,所以搞的人,咋一看起来很不错,明白了。但实际做起来,有点糊。所以你看上面留言都是在这个地方出问题了。主要问题就是django的信号机制,里面的每个部分的代码应该放在哪个文件里面你没介绍清楚。
给作者留言
关于作者
残阳似血(@秦续业),程序猿一枚,把梦想揣进口袋的挨踢工作者。现加入阿里云,研究僧毕业于上海交通大学软件学院ADC实验室。熟悉分布式数据分析(DataFrame并行化框架)、基于图模型的分布式数据库和并行计算、Dpark/Spark以及Python web开发(Django、tornado)等。
博客分类
搜索
点击排行
标签云
扫描访问
主题
残阳似血的微博
登录