1、安装Notifications
站内通知使用django-notifications-hq第三方库。执行如下命令安装django-notifications-hq:
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加入该应用:
- INSTALLED_APPS = [
- # ... 其他省略不写
- 'notifications',
- ]
再更新数据库,由于notifications已经makemigrations了,直接migrate更新同步数据库:
- python manage.py migrate notifications
再打开urls.py总路由设置,添加notifications的urls(貌似不加也行,我没有使用到)
- 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信号器。
- #coding:utf-8
- from django.dispatch import Signal
-
- comment_will_be_posted = Signal(providing_args=["comment", "request"])
- comment_was_posted = Signal(providing_args=["comment", "request"])
- comment_was_flagged = Signal(providing_args=["comment", "flag", "created", "request"])
其中,comment_was_posted是评论保存之后监控的信号。我们将使用该信号,在该文件添加如下代码:
- #coding:utf-8
- from django.dispatch import receiver
- from django.shortcuts import get_object_or_404
- from notifications.signals import notify
-
- try:
- from django.apps import apps
- except ImportError:
- from django.db import models as apps
-
- from .models import Comment
- from . import get_model
-
- @receiver(comment_was_posted, sender=Comment)
- def send_message(sender, **kwargs):
- # 获取相关数据
- #print(kwargs)
- comment = kwargs['comment']
- request = kwargs['request']
- user = comment.user
- username = user.first_name or user.username
-
- # 获取评论的对象
- data = request.POST.copy()
- ctype = data.get("content_type")
- object_pk = data.get("object_pk")
- model = apps.get_model(*ctype.split(".", 1))
- target = model._default_manager.using(None).get(pk=object_pk)
-
- # 判断是评论还是回复,设置消息标题
- if int(comment.root_id) == 0:
- # 评论对象(博客,专题)
- content_object = comment.content_type.get_object_for_this_type(id=object_pk)
- recipient = content_object.author # 被评论时,通知文章作者
- verb = u'[%s] 评论你了' % username
- else:
- # 被回复
- reply_to = get_object_or_404(get_model(), id=comment.reply_to)
- recipient = reply_to.user # 被回复时,通知评论者
- verb = u'[%s] 回复你了' % username
-
- # 发送消息(level: 'success', 'info', 'warning', 'error')
- message = {}
- message['recipient'] = recipient # 消息接收人
- message['verb'] = verb # 消息标题
- message['description'] = comment.comment # 评论详细内容
- message['target'] = target # 目标对象
- message['action_object'] = comment # 评论记录
- notify.send(user, **message)
这部分的代码是整个站内消息通知的核心。一部分一部分拆分讲解。
首先,signals的结构。receiver是绑定处理信号的方法,sender是该信号的发送者。基本结构如下:
- @receiver(comment_was_posted, sender=Comment)
- def send_message(sender, **kwargs):
- 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获取该用户相关的消息,例如:
- user = request.user
- user.notifications.all() # 全部消息
- user.notifications.unread() #未读消息
- user.notifications.read() #已读消息
还可在模版中使用模版标签获得未读消息数:
- {% load notifications_tags %}
- {% notifications_unread as unread_count %}
-
- <span>你有{{unread_count}}条未读消息</span>
现需要将未读消息显示在导航栏的用户名旁边,如下所示:
问题我网站判断用户的登录状态是通过ajax加载页面之后判断的,非直接在底层模版中用模版标签判断。若同样在页面加载之后再通过ajax异步获取消息会很麻烦,代码耦合性较高。而模版页面用使用request.user,需要用render或render_to_reponse + RequestContext。例如:
- from django.shortcuts import render_to_response
- from django.template import RequestContext
-
- return render_to_response('index.html', data, context_instance=RequestContext(request))
以上等同于:
- from django.shortcuts import render
-
- return render(request, 'index.html', data)
当然选择使用render,render相当于render_to_response的简写。若你代码也需要在模版页面使用request.user,最好也改成render方式。然后再模版页面判断获取未读消息数,例如:
- {#判断是否有登录用户#}
- {% if request.user.is_authenticated %}
- {% notifications_unread as unread_count %}
- <span>
- 您好, {{request.user.username}}
-
- {#判断是否有未读消息#}
- {% ifnotequal unread_count 0 %}
- <span style="background-color:#d9534f">
- {{unread_count}}
- </span>
- {% endifnotequal %}
- </span>
-
- <ul class="dropdown-menu">
- {#如果是管理员#}
- {% if request.user.is_superuser %}
- <li><a href="{%url 'admin:index'%}">后台管理</a></li>
- {% endif %}
-
- <li>
- <a href="{%url 'user_info'%}">
- 用户中心
- {% ifnotequal unread_count 0 %}
- <span style="background-color:#d9534f">
- {{unread_count}}
- </span>
- {% endifnotequal %}
- </a>
- </li>
- <li><a href="{%url 'user_logout'%}">退出</a></li>
- </ul>
-
- {% else %}
- <a href="/user/login_page">登录/注册</a>
- {% endif %}
上面的{%url 'user_info'%}是进入我网站的用户中心页面。可在其中显示未读消息和已读消息,这里简单实现,先显示最多30条未读消息。
首先需要修改或者新增user_info对应的响应方法返回未读消息。核心代码如下:
- user = request.user
- unread = user.notifications.unread()[:30]
-
- data={}
- data['unread_list'] = unread # 返回未读消息
对应的模版页面再处理unread_list,列举未读消息。
- <div class="unread_head">
- <span>您共有{{unread_list.count}}条未读消息</span>
- <a class="btn btn-info unread_btn"
- href="{%url 'user_mark_all_read'%}">
- 全部标记为已读
- </a>
- </div>
-
- <ul class="unread_list">
- {%for unread_item in unread_list%}
- <li id="unread_{{unread_item.id}}">
- <span>{{unread_item.timesince}}前 > </span>
- <a href="{{unread_item.target.get_url}}?notification={{unread_item.id}}#F{{unread_item.action_object.id}}">
- {{unread_item.verb}}
- </a>
- <p class="unread_descript">{{unread_item.description}}</p>
- </li>
- {%endfor%}
- </ul>
这个模版页面也是我反复测试调整的结果,里面有些参数需要一一讲解。效果如下:
先看for循环部分。timesince属性是获取该消息是多久之前的消息;verb和description分别是消息的简要标题和内容;target是前面创建消息绑定的对象(博客或专题)。为了方便获取具体链接,在博客和专题的model类中分别加入获取具体对象的链接方法:
- from django.core.urlresolvers import reverse # url逆向解析
-
- class Blog(models.Model):
- # 其余代码省略
- pass
-
- # 获取博客明细链接(根据具体情况写链接解析即可)
- def get_url(self):
- return reverse('detailblog', kwargs={'id':self.id})
大家可否发现,这个有两个链接user_mark_all_read和for循环中复杂的链接。如下讲解。
4、修改消息状态为已读
先看看上面for循环中构造的链接。该链接是消息具体指向位置。
由于我这里是评论或回复的通知消息,所以消息最终要指向评论或回复的具体位置。原本评论在邮件通知的链接如下:
- /subject/3#F168
#号前半部分是具体页面;F168是执行评论的锚点位置,在打开页面中得到该值并定位到评论位置。
当你打开该页面,需要修改本条未读消息为已读消息状态。
而在后台我接受不到#号后面的内容。于是在链接加入GET请求的参数notification,通过该参数获取具体的消息并修改消息状态。
那什么地方处理修改消息状态呢?当然是打开具体的博客或专题的处理方法中修改。为了不重复写冗余代码,我将修改消息状态的代码写成装饰器:
- #coding:utf-8
- from notifications.models import Notification
-
- # 修改未读消息为已读装饰器
- def notifications_read(func):
- def wrapper(request, *args, **kwargs):
- print(request.get_full_path())
- notify_key = 'notification'
- if request.GET.has_key(notify_key):
- try:
- # 获取消息
- notify_id = int(request.GET[notify_key])
- notify = Notification.objects.get(id=notify_id)
-
- # 标记为已读
- notify.unread = False
- notify.save()
- except ValueError:
- # int转换错误,不处理
- pass
- except Notification.DoesNotExist:
- # 消息不存在,不处理
- pass
- return func(request, *args, **kwargs)
- return wrapper
再对应的处理方法上加该装饰器,例如博客的具体页面处理方法:
- @notifications_read
- def blog_detail(request, id):
- # 博客响应方法的代码非主要,省略
- pass
还有上面有个user_mark_all_read链接,该链接是将所有未读消息修改为已读消息。对应响应方法如下:
- #coding:utf-8
- from django.http import HttpResponseRedirect
- from django.core.urlresolvers import reverse # url逆向解析
-
- def user_mark_all_read(request):
- user = request.user
- notifies = user.notifications.all()
- notifies.mark_all_as_read() # 标记所有未读为已读
- return HttpResponseRedirect(reverse('user_info')) # 重定向回用户中心
此处偷了一下懒,直接重定向回用户中心页面。请根据具体项目细节写代码。
5、收尾
还有个问题,之前通过邮件发送评论通知。其中的链接也需要加入notification参数,让用户打开具体页面时修改消息状态。
这时候需要用到前面创建消息使用的action_object了。前面将评论和消息通过该对象一一对应关联,所以在发送邮件通知的时候,通过评论id获取对应的消息通知id。若你也和我使用同样的逻辑机制,可参考如下代码:
- from notifications.models import Notification
- from django.contrib.contenttypes.models import ContentType
-
- # 此处已经有comment对象和具体页面的链接src_url可使用
-
- #判断评论是否有对应的消息通知(一条评论对应一条消息)
- comment_content_type_id = ContentType.objects.get_for_model(comment).id
- notifies = Notification.objects.filter(\
- action_object_content_type_id=comment_content_type_id, \
- action_object_object_id=comment.id)
-
- # 构造链接
- if notifies.count() > 0:
- comment_url = u'%s?notification=%s#F%s' % (src_url, notifies[0].id, comment.id)
- else: