我的网站搭建(第55天) 站内消息通知

  • 发布时间:2017年6月2日 13:54
  • 作者:杨仕航
* 该文是基于Python2.7开发的,最新Python3.x和Django2.x视频教程可以前往 >> Django2.0视频教程

有较长时间没有更新关于我的网站搭建的博客,因为我在调整代码、整合功能。有人反馈有时评论和注册收不到邮件,准备去掉注册一定要绑定邮箱的功能,并加上评论和回复之后站内消息通知功能。这些功能需要修改大量代码,任重而道远。


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方法。在该方法的末尾可看到发送信号的代码:

20170602/20170602120722488.png


从上图可看到评论保存前后各发送(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>


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

20170602/20170602130834005.png


问题我网站判断用户的登录状态是通过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}}前 &gt; </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>


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

20170602/20170602135657620.png


先看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循环中构造的链接。该链接是消息具体指向位置。

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

http://yshblog.com/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:
    comment_url = u'%s#F%s' % (src_url, comment.id)


ps:发送邮件的代码也可放到signals.py中。

上一篇:Excel常用的字符串公式

下一篇:html最后一个元素不同样式

相关专题: Django评论库开发   

评论列表

zhaixuyan05231

zhaixuyan05231

测试消息通知

2019-01-06 13:48 回复

GentleCP

GentleCP

航哥建这样一个个人网站大概要化多少时间啊

2019-05-02 15:56 回复

新的评论

清空