Django站内消息通知

1、安装Notifications

站内通知使用django-notifications-hq第三方库。执行如下命令安装django-notifications-hq:

  1. pip install django-notifications-hq

执行命令后,安装3个库。对应名称和版本如下,若你测试代码有问题,请参考最新帮助文档或源码:

1)django-model-utils=3.0.0

2)django-notifications-hq=1.2

3)jsonfield=2.0.1

可以在Python安装目录Lib/site-packages找到notifications。以下开发基本都是查看notifications源码和其Github的帮助

接着,打开Django项目的settings.py文件,在INSTALLED_APPS加入该应用:

  1. INSTALLED_APPS = [
  2. … 其他省略不写

  3. ‘notifications’,
  4. ]

再更新数据库,由于notifications已经makemigrations了,直接migrate更新同步数据库:

  1. python manage.py migrate notifications

再打开urls.py总路由设置,添加notifications的urls(貌似不加也行,我没有使用到)

  1. url(r’^notifications/‘, include(‘notifications.urls’)),

2、评论或回复时发送消息通知

当然,不止在评论或回复时才发送消息通知。可以在任何地方发送消息通知,例如用户注册成功、用户第一次登录等等。主要看你的需求,基本原理都一样,我以django-comments库评论或回复作为例子。相关的django-comments开发可参考Django评论库开发专题

此处不建议直接修改评论库提交评论的代码,可使用signals机制处理消息通知。

signals是Django一套信号机制,模型对象操作会产生一系列的信号。例如保存前、保存后。Django自动监控到这些信号会执行对应的代码。故,打开django-comments库的signals.py文件,在其中添加评论提交之后的处理代码。

django-comments库的路径同样在Python安装目录的Lib/site-packages中。由于我对该库修改比较多,已经复制全部代码到我的Django项目中。打开signals.py文件,可发现已经定义好了3个signals信号器。

  1. coding:utf-8

  2. from django.dispatch import Signal
  3. comment_will_be_posted = Signal(providing_args=[“comment”, “request”])
  4. comment_was_posted = Signal(providing_args=[“comment”, “request”])
  5. comment_was_flagged = Signal(providing_args=[“comment”, “flag”, “created”, “request”])

其中,comment_was_posted是评论保存之后监控的信号。我们将使用该信号,在该文件添加如下代码:

  1. coding:utf-8

  2. from django.dispatch import receiver
  3. from django.shortcuts import get_object_or_404
  4. from notifications.signals import notify
  5. try:
  6. from django.apps import apps
  7. except ImportError:
  8. from django.db import models as apps
  9. from .models import Comment
  10. from . import get_model
  11. @receiver(comment_was_posted, sender=Comment)
  12. def send_message(sender, **kwargs):
  13. 获取相关数据

  14. print(kwargs)

  15. comment = kwargs[‘comment’]
  16. request = kwargs[‘request’]
  17. user = comment.user
  18. username = user.first_name or user.username
  19. 获取评论的对象

  20. data = request.POST.copy()
  21. ctype = data.get(“content_type”)
  22. object_pk = data.get(“object_pk”)
  23. model = apps.get_model(*ctype.split(“.”, 1))
  24. target = model._default_manager.using(None).get(pk=object_pk)
  25. 判断是评论还是回复,设置消息标题

  26. if int(comment.root_id) == 0:
  27. 评论对象(博客,专题)

  28. content_object = comment.content_type.get_object_for_this_type(id=object_pk)
  29. recipient = content_object.author # 被评论时,通知文章作者
  30. verb = u’[%s] 评论你了’ % username
  31. else:
  32. 被回复

  33. reply_to = get_object_or_404(get_model(), id=comment.reply_to)
  34. recipient = reply_to.user # 被回复时,通知评论者
  35. verb = u’[%s] 回复你了’ % username
  36. 发送消息(level: ‘success’, ‘info’, ‘warning’, ‘error’)

  37. message = {}
  38. message[‘recipient’] = recipient # 消息接收人
  39. message[‘verb’] = verb # 消息标题
  40. message[‘description’] = comment.comment # 评论详细内容
  41. message[‘target’] = target # 目标对象
  42. message[‘action_object’] = comment # 评论记录
  43. notify.send(user, **message)

这部分的代码是整个站内消息通知的核心。一部分一部分拆分讲解。

首先,signals的结构。receiver是绑定处理信号的方法,sender是该信号的发送者。基本结构如下:

  1. @receiver(comment_was_posted, sender=Comment)
  2. def send_message(sender, **kwargs):
  3. print(kwargs) # 打印参看有哪些参数

可以打印kwargs查看有哪些参数。或者你可以查看该库的views/comments.py文件中的post_comment方法。在该方法的末尾可看到发送信号的代码:

从上图可看到评论保存前后各发送(send)两个信号。保存之后发送的signal参数有sender、comment、request。我们可以根据comment和request得到我们所需的数据。

在signals中获取被评论的对象就是通过comment获取,当然该代码不是我写的,参考comments.py的post_comments方法。

至于判断评论还是回复这部分代码可以忽略,这个是我修改django-comments库加入回复功能

最后部分的代码,notify.send同样使用了signals。使用notifications的signals,可打开notifications源码查看。而前面的message中的数据都是notify所需的数据。这些参数不是都必须的,可根据自己项目的实际需求使用。记录target是为了知道评论哪篇博客;记录action_object是为了将评论和消息一一对应,才可根据评论对象找到对应的消息对象。

3、获取消息

上面的参数recipient是希望谁接到通知。notifications是和Django的用户系统绑定。若settings设置了AUTH_USER_MODEL,也自动使用自定义的用户系统。可通过User获取该用户相关的消息,例如:

  1. user = request.user
  2. user.notifications.all() # 全部消息
  3. user.notifications.unread() #未读消息
  4. user.notifications.read() #已读消息

还可在模版中使用模版标签获得未读消息数:

  1. {% load notifications_tags %}
  2. {% notifications_unread as unread_count %}
  3. 你有{{unread_count}}条未读消息

现需要将未读消息显示在导航栏的用户名旁边,如下所示:

问题我网站判断用户的登录状态是通过ajax加载页面之后判断的,非直接在底层模版中用模版标签判断。若同样在页面加载之后再通过ajax异步获取消息会很麻烦,代码耦合性较高。而模版页面用使用request.user,需要用render或render_to_reponse + RequestContext。例如:

  1. from django.shortcuts import render_to_response
  2. from django.template import RequestContext
  3. return render_to_response(‘index.html’, data, context_instance=RequestContext(request))

以上等同于:

  1. from django.shortcuts import render
  2. return render(request, ‘index.html’, data)

当然选择使用render,render相当于render_to_response的简写。若你代码也需要在模版页面使用request.user,最好也改成render方式。然后再模版页面判断获取未读消息数,例如:

  1. {#判断是否有登录用户#}
  2. {% if request.user.is_authenticated %}
  3. {% notifications_unread as unread_count %}
  4. 您好, {{request.user.username}}
  5. {#判断是否有未读消息#}
  6. {% ifnotequal unread_count 0 %}
  7. {{unread_count}}
  8. {% endifnotequal %}
  9. {#如果是管理员#}
  10. {% if request.user.is_superuser %}
  11. 后台管理
  12. {% endif %}
  13. 用户中心
  14. {% ifnotequal unread_count 0 %}
  15. {{unread_count}}
  16. {% endifnotequal %}
  17. 退出
  18. {% else %}
  19. 登录/注册
  20. {% endif %}

上面的{%url ‘user_info’%}是进入我网站的用户中心页面。可在其中显示未读消息和已读消息,这里简单实现,先显示最多30条未读消息。

首先需要修改或者新增user_info对应的响应方法返回未读消息。核心代码如下:

  1. user = request.user
  2. unread = user.notifications.unread()[:30]
  3. data={}
  4. data[‘unread_list’] = unread # 返回未读消息

对应的模版页面再处理unread_list,列举未读消息。

  1. 您共有{{unread_list.count}}条未读消息
  2. <a class=”btn btn-info unread_btn”
  3. href=”{%url ‘user_mark_all_read’%}”>
  4. 全部标记为已读
  5. {%for unread_item in unread_list%}
  6. {{unread_item.timesince}}前 >
  7. {{unread_item.verb}}
  8. {{unread_item.description}}
  9. {%endfor%}

这个模版页面也是我反复测试调整的结果,里面有些参数需要一一讲解。效果如下:

先看for循环部分。timesince属性是获取该消息是多久之前的消息;verb和description分别是消息的简要标题和内容;target是前面创建消息绑定的对象(博客或专题)。为了方便获取具体链接,在博客和专题的model类中分别加入获取具体对象的链接方法:

  1. from django.core.urlresolvers import reverse # url逆向解析
  2. class Blog(models.Model):
  3. 其余代码省略

  4. pass
  5. 获取博客明细链接(根据具体情况写链接解析即可)

  6. def get_url(self):
  7. return reverse(‘detailblog’, kwargs={‘id’:self.id})

大家可否发现,这个有两个链接user_mark_all_read和for循环中复杂的链接。如下讲解。

4、修改消息状态为已读

先看看上面for循环中构造的链接。该链接是消息具体指向位置。

由于我这里是评论或回复的通知消息,所以消息最终要指向评论或回复的具体位置。原本评论在邮件通知的链接如下:

  1. /subject/3#F168

号前半部分是具体页面;F168是执行评论的锚点位置,在打开页面中得到该值并定位到评论位置。

当你打开该页面,需要修改本条未读消息为已读消息状态。

而在后台我接受不到#号后面的内容。于是在链接加入GET请求的参数notification,通过该参数获取具体的消息并修改消息状态。

那什么地方处理修改消息状态呢?当然是打开具体的博客或专题的处理方法中修改。为了不重复写冗余代码,我将修改消息状态的代码写成装饰器:

  1. coding:utf-8

  2. from notifications.models import Notification
  3. 修改未读消息为已读装饰器

  4. def notifications_read(func):
  5. def wrapper(request, args, *kwargs):
  6. print(request.get_full_path())
  7. notify_key = ‘notification’
  8. if request.GET.has_key(notify_key):
  9. try:
  10. 获取消息

  11. notify_id = int(request.GET[notify_key])
  12. notify = Notification.objects.get(id=notify_id)
  13. 标记为已读

  14. notify.unread = False
  15. notify.save()
  16. except ValueError:
  17. int转换错误,不处理

  18. pass
  19. except Notification.DoesNotExist:
  20. 消息不存在,不处理

  21. pass
  22. return func(request, args, *kwargs)
  23. return wrapper

再对应的处理方法上加该装饰器,例如博客的具体页面处理方法:

  1. @notifications_read
  2. def blog_detail(request, id):
  3. 博客响应方法的代码非主要,省略

  4. pass

还有上面有个user_mark_all_read链接,该链接是将所有未读消息修改为已读消息。对应响应方法如下:

  1. coding:utf-8

  2. from django.http import HttpResponseRedirect
  3. from django.core.urlresolvers import reverse # url逆向解析
  4. def user_mark_all_read(request):
  5. user = request.user
  6. notifies = user.notifications.all()
  7. notifies.mark_all_as_read() # 标记所有未读为已读
  8. return HttpResponseRedirect(reverse(‘user_info’)) # 重定向回用户中心

此处偷了一下懒,直接重定向回用户中心页面。请根据具体项目细节写代码。

5、收尾

还有个问题,之前通过邮件发送评论通知。其中的链接也需要加入notification参数,让用户打开具体页面时修改消息状态。

这时候需要用到前面创建消息使用的action_object了。前面将评论和消息通过该对象一一对应关联,所以在发送邮件通知的时候,通过评论id获取对应的消息通知id。若你也和我使用同样的逻辑机制,可参考如下代码:

  1. from notifications.models import Notification
  2. from django.contrib.contenttypes.models import ContentType
  3. 此处已经有comment对象和具体页面的链接src_url可使用

  4. 判断评论是否有对应的消息通知(一条评论对应一条消息)

  5. comment_content_type_id = ContentType.objects.get_for_model(comment).id
  6. notifies = Notification.objects.filter(\
  7. action_object_content_type_id=comment_content_type_id, \
  8. action_object_object_id=comment.id)
  9. 构造链接

  10. if notifies.count() > 0:
  11. comment_url = u’%s?notification=%s#F%s’ % (src_url, notifies[0].id, comment.id)
  12. else: