Django搭建个人博客:用django-notifications实现消息通知

2701阅读 · 35评论 · 2019/05/18发布   前往评论

凭借你勤奋的写作,拜读你文章的用户越来越多,他们的评论也分散在众多的文章之中。作为博主,读者的留言肯定是要都看的;而读者给你留言,自然也希望得到回复。

怎么将未读的留言呈现给正确的用户呢?总不能用户自己去茫茫文章中寻找吧,那也太蠢了。给评论增加通知功能就是很流行的解决方案:比如微信朋友圈留言的通知、新浪微博留言的通知、以及各种社交平台的“小红点”。

本篇将以django-notifications为基础,非常高效的搭建一个简易的通知系统。

发送通知

前面的步骤我们已经很熟悉了。

首先安装django-notifications

(env) > pip install django-notifications-hq

注册app:

my_blog/settings.py

...
INSTALLED_APPS = [
    ...
    'notifications',
    ...
]
...

在根路由中安装路径:

my_blog/urls.py

...
import notifications.urls

urlpatterns = [
    ...
    path('inbox/notifications/', include(notifications.urls, namespace='notifications')),
    ...
]
...

注意这里的notifications.urls没有像之前一样用字符串,是为了确保模块安装到正确的命名空间中。

数据迁移:

(env) > python manage.py migrate

app就安装好了。

接下来你就可以在项目的任何地方发送通知了!像这样:

from notifications.signals import notify

notify.send(actor, recipient, verb, target, action_object)

其中的参数释义:

  • actor:发送通知的对象
  • recipient:接收通知的对象
  • verb:动词短语
  • target:链接到动作的对象(可选)
  • action_object:执行通知的对象(可选)

有点绕,举个栗子:杜赛 (actor)Django搭建个人博客 (target) 中对 (recipient) 发表了 (verb) 评论 (action_object)

因为我们想要在用户发表评论的时候发送通知,因此修改一下发表评论的视图:

comments/views.py

...
from notifications.signals import notify
from django.contrib.auth.models import User

...
def post_comment(...):
    ...

    # 已有代码,创建新回复
    if comment_form.is_valid():
        ...

        # 已有代码,二级回复
        if parent_comment_id:
            ...

            # 新增代码,给其他用户发送通知
            if not parent_comment.user.is_superuser:
                notify.send(
                    request.user,
                    recipient=parent_comment.user,
                    verb='回复了你',
                    target=article,
                    action_object=new_comment,
                )

            return HttpResponse('200 OK')

        new_comment.save()

        # 新增代码,给管理员发送通知
        if not request.user.is_superuser:
            notify.send(
                    request.user,
                    recipient=User.objects.filter(is_superuser=1),
                    verb='回复了你',
                    target=article,
                    action_object=new_comment,
                )

        return redirect(article)
...

增加了两条notify的语句,分别位于两个if语句中:

  • 第一个notify:用户之间可以互相评论,因此需要发送通知。if语句是为了防止管理员收到重复的通知。
  • 第二个notify:普通用户回复时给管理员发送通知。

普通用户回复普通用户时,这段代码是不会发送通知给管理员的。如果需要发送给管理员,在第一个 notify 的 recipient 里以列表形式,将被回复用户和管理员添加进去即可。感谢 @囸義使鍺 反馈。

其他的代码没有变化,注意位置不要错就行了。你可以试着发送几条评论,然后打开SQLiteStudio,查看notifications_notification表中的数据变化。

有效代码实际上只有4行,我们就完成了创建、发送通知的功能!相信你已经逐渐体会到运用第三方库带来的便利了。这就是站在了“巨人们”的肩膀上。

小红点

后台创建通知的逻辑已经写好了,但是如果不能在前端显示出来,那也没起到作用。

而前端显示消息通知比较流行的是“小红点”,流行得都已经泛滥了,尽管很多软件其实根本就不需要。另一种形式是消息徽章,即一个红色方框中带有消息条目的计数。这两种方式都会用到博客页面中。

在位置的选择上,header是很合适的,因为它在博客的所有位置都会显示,很符合通知本身的定位。

因此修改header.html

templates/header.html

<!-- 引入notifications的模板标签 -->
{% load notifications_tags %}
{% notifications_unread as unread_count %}

...

<!-- 已有代码,用户下拉框 -->
<li class="nav-item dropdown">
    <a class="nav-link dropdown-toggle" ...>

        <!-- 新增代码,小红点 -->
        {% if unread_count %}
            <svg viewBox="0 0 8 8"
                 width="8px"
                 height="8px">
                <circle cx="4"
                        cy="4"
                        r="4"
                        fill="#ff6b6b"
                        ></circle>
            </svg>
        {% endif %}

        {{ user.username }}
    </a>
    <!-- 已有代码,下拉框中的链接 -->
    <div class="dropdown-menu" ...>

        <!-- 新增代码,通知计数 -->
        <a class="dropdown-item" href="#">通知
            {% if unread_count %}
            <span class="badge badge-danger">{{ unread_count }}</span>
            {% endif %}
        </a>

        ...
        <a ...>退出登录</a>
    </div>
</li>

...

django-notifications自带简易的模板标签,可以在前台模板中调用重要的通知相关的对象,在顶部引入就可以使用了。比如unread_count是当前用户的未读通知的计数。

Bootstrap自带有徽章的样式,但是却没有小红点的样式(至少我没有找到),所以就只能用svg自己画了,好在也不难。

svg是绘制矢量图形的标签,这里就不展开讲了,感兴趣请自行搜索相关文章。

随便评论几条,刷新页面看一看:

效果不错。但是链接的href是空的,接下来就处理。

未读与已读

既然是通知,那么肯定能够分成”未读的“”已读的“两种。在适当的时候,未读通知又需要转换为已读的。现在我们来开发功能集中处理它。

通知是一个独立的功能,以后有可能在任何地方用到,放到评论app中似乎并不合适。

所以新建一个app:

(env) > python manage.py startapp notice

注册:

my_blog/settings.py

...
INSTALLED_APPS = [
    ...
    'notice',
]
...

根路由:

my_blog/urls.py

...
urlpatterns = [
    ...
    # notice
    path('notice/', include('notice.urls', namespace='notice')),
]
...

接下来就是视图了。之前所有的视图都是用的视图函数,这次我们更进一步,用类视图来完成。忘记什么是类视图的,回忆一下前面类的视图章节。

编写视图:

notice/views.py

from django.shortcuts import render, redirect
from django.views import View
from django.views.generic import ListView
from django.contrib.auth.mixins import LoginRequiredMixin
from article.models import ArticlePost


class CommentNoticeListView(LoginRequiredMixin, ListView):
    """通知列表"""
    # 上下文的名称
    context_object_name = 'notices'
    # 模板位置
    template_name = 'notice/list.html'
    # 登录重定向
    login_url = '/userprofile/login/'

    # 未读通知的查询集
    def get_queryset(self):
        return self.request.user.notifications.unread()


class CommentNoticeUpdateView(View):
    """更新通知状态"""
    # 处理 get 请求
    def get(self, request):
        # 获取未读消息
        notice_id = request.GET.get('notice_id')
        # 更新单条通知
        if notice_id:
            article = ArticlePost.objects.get(id=request.GET.get('article_id'))
            request.user.notifications.get(id=notice_id).mark_as_read()
            return redirect(article)
        # 更新全部通知
        else:
            request.user.notifications.mark_all_as_read()
            return redirect('notice:list')

视图共两个。

CommentNoticeListView:继承自ListView,用于展示所有的未读通知。get_queryset方法返回了传递给模板的上下文对象,unread()方法是django-notifications提供的,用于获取所有未读通知的集合。另外视图还继承了“混入类”LoginRequiredMixin,要求调用此视图必须先登录。

CommentNoticeUpdateView:继承自View,获得了如get、post等基础的方法。mark_as_read()mark_all_as_read都是模块提供的方法,用于将未读通知转换为已读。if语句用来判断转换单条还是所有未读通知。

重复:阅读有困难的同学,请重新阅读类的视图章节,或者去官方文档查阅。

接下来就是新建urls.py了,写入:

notice/urls.py

from django.urls import path
from . import views

app_name = 'notice'

urlpatterns = [
    # 通知列表
    path('list/', views.CommentNoticeListView.as_view(), name='list'),
    # 更新通知状态
    path('update/', views.CommentNoticeUpdateView.as_view(), name='update'),
]

path()的第二个参数只能接收函数,因此别忘了要调用类视图的as_view()方法。

集中处理通知需要一个单独的页面。新建templates/notice/list.html模板文件:

templates/notice/list.html

{% extends "base.html" %}
{% load staticfiles %}

{% block title %}
    通知
{% endblock title %}

{% block content %}
<div class="container">
    <div class="row mt-4 ml-4">
        <a href="{% url "notice:update" %}" class="btn btn-warning" role="button">清空所有通知</a>
    </div>
    <!-- 未读通知列表 -->
    <div class="row mt-2 ml-4">
        <ul class="list-group">
            {% for notice in notices %}
                <li class="list-group-item" id="notice_link">
                    <a href="{% url "notice:update" %}?article_id={{ notice.target.id }}&notice_id={{ notice.id }}"
                       target="_blank"
                    >
                    <span style="color: #5897fb;">
                        {{ notice.actor }}
                    </span><span style="color: #01a252;">{{ notice.target }}</span> {{ notice.verb }}。
                    </a>
                    &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;{{ notice.timestamp|date:"Y/m/d H:i" }}
                </li>
            {% endfor %}
        </ul>
    </div>
</div>

<style>
    #notice_link a:link {
        color: black;
    }

    #notice_link a:visited {
        color: lightgrey;
    }
</style>
{% endblock content %}

模板中主要提供了两个功能:

  • 点击button按钮清空所有未读通知
  • 点击单个通知,将其转换为已读通知,并前往此评论所在的文章

末尾<style>标签中的伪类选择器,作用是将已经点击过的通知字体颜色转换为浅灰色,优化用户体验。

最后就是补上入口

templates/header.html

...
<a ... href="{% url "notice:list" %}">通知...</a>
...

这样就完成了。

打开服务器,用一个普通账号评论几条,再登录管理员账号并进入通知页面:

就能看到不错的效果了。实现的效果是仅展示未读通知,当然也可以在下边展示已读通知,方便用户追溯。

总结

通知功能非常的重要,特别是在你的博客成长壮大了之后。你还可以把它用在别的地方,比如每新发表一篇文章,就给你所有的“粉丝”推送一条通知,提醒他们可以来拜读了。具体如何扩展运用,就靠你脑洞大开了。

课后作业:前面的代码中,如果用户自己评论自己,同样也会收到通知。这种通知并没有必要,请修正它。

遇到困难,在教程示例代码找答案吧。





本文作者: 杜赛
发布时间: 2019年05月18日 - 19:22
最后更新: 2019年10月09日 - 21:31
知识共享许可协议   转载请保留原文链接及作者


登录 后回复

共有35条评论

avatar
ipydev.com 么么哒! 5

看到你的文章了,顺便想起来我也没差这个功能,哈哈,顺便增加了。

因为工作使用的是C++,所有没有再多的时间去研究Django了,我比较擅长的是网络爬虫、数据分析以及PyQt5开发,web开发只是满足我对于个人网站的需求

你的网站写的确实蛮好的,我基本都是复制你的代码修改的,除了前端和部分功能我自己实现了,以及服务器部署是自己写的,其他的都靠复制粘贴。很感谢博主的付出。

6个月前 回复


avatar
杜赛 [博主] ipydev.com 么么哒! 3

5个月前 回复


avatar
ipydev.com 杜赛 [博主] 么么哒! 3

加个友链吧,博主。

5个月前 回复


avatar
杜赛 [博主] ipydev.com 么么哒! 3

先加上吧,虽然感觉你网站内容还是偏少。希望以后多多更新优质内容。

过两天我的博客会有功能更新,到时候一并把你的友链添加上。

5个月前 回复


avatar
ipydev.com 杜赛 [博主] 么么哒! 3

好的,我明天下班也把你的网站加到我的主页

5个月前 回复


avatar
haha ipydev.com 么么哒! 3

5个月前 回复


avatar
pxpwoa 么么哒! 4

很详细的指导

5个月前 回复


avatar
losetemp 么么哒! 3

5个月前 回复


avatar
losetemp losetemp 么么哒! 3

5个月前 回复


avatar
杜赛 [博主] losetemp 么么哒! 3

请尽量在自己的项目中测试,不要在教程下面,会影响到其他读者

5个月前 回复


avatar
shanyonggang 么么哒! 3

博主,您好,我想问下,如何实现让某篇文章从属于某个教程?直接在写models.py时候将文章与教程关联起来么

5个月前 回复


avatar
杜赛 [博主] shanyonggang 么么哒! 4

对,把教程写为一个model,用外键把它和文章链接起来

5个月前 回复


avatar
流天 么么哒! 4

博主有没有考虑给博客添加点赞功能以及更换ckeditor默认的表情。

5个月前 回复


avatar
杜赛 [博主] 流天 么么哒! 3

点赞有考虑过,在合适的时候应该会写到教程中。以前总感觉点赞对个人博客来说意义并不大。

暂时没想过更换ckeditor的表情

5个月前 回复


avatar
流天 杜赛 [博主] 么么哒! 5

个人意见点赞可以让不登录的用户参与进来,增加交互性,感觉对网站的发展还是有用的。yes

5个月前 回复


avatar
杜赛 [博主] 流天 么么哒! 4

你说得对,我现在也是这么想的。近期有计划把点赞做到博客中来。

感谢建议~

5个月前 回复


avatar
beaock 么么哒! 4

杜老师你好

我现在想做一个用户连续签到有积分奖励的系统

可以计算连续签到多少天或者判断用户中断签到的功能

请问表单应该怎么设计啊?

或者说有没有开源的代码可以参照学习一下啊?

期待您的回复,谢谢!

5个月前 回复


avatar
杜赛 [博主] beaock 么么哒! 3

对功能要求不高就自己写吧。

  • PositiveIntegerField记录用户总积分
  • DateTimeField记录用户最近一次登录的日期
  • PositiveIntegerField记录用户连续登录的天数

然后在视图里把交互逻辑写好,基本就能实现签到功能了。

如果你想完整记录某个用户的登录情况,那就专门用一个表记录某个用户的所有登录时间,用外键把它和用户表连接起来。这种方法理解起来比较简单,缺点就是数据多了之后性能不佳。

还有一种搞法是不用外键,把所有登录时间压缩成字符串或者列表,保存在字段里。django2.1 好像有个列表字段就是干这个事情的,不过我没有去研究过,你可以看看这里:列表字段

方法挺多的,挑一个适合你的方案。

相关开源代码我不太清楚,你可能得自己找找了

5个月前 回复


avatar
zhc1208420887 么么哒! 3

你好   要实现消息弹窗 该如何实现呢

4个月前 回复


avatar
杜赛 [博主] zhc1208420887 么么哒! 3

哪种形式的弹窗?

4个月前 回复


avatar
zhc1208420887 杜赛 [博主] 么么哒! 3

不好意思,昨晚退出早了  就像message包那种   可是message需要一个请求才能弹  这有什么好的方法吗

4个月前 回复


avatar
杜赛 [博主] zhc1208420887 么么哒! 3

轮询实现吧。写一段js代码,页面每过一段时间就向后台提交一个请求,查询是否有未读通知。如果有则生成iframe窗口显示通知的内容,也就是通知弹窗了。

轮询的弊端就是会增加服务器的负担。所以其实站内通知没必要实时,在页面刷新的时候请求一次足够了

4个月前 回复


avatar
zhc1208420887 杜赛 [博主] 么么哒! 3

好的  知道了 谢谢

4个月前 回复


avatar
wanyy 么么哒! 3

您好,请问评论后数据库无变化是怎么回事呢

2个月前 回复


avatar
杜赛 [博主] wanyy 么么哒! 3

数据没保存上吧。检查视图工作是否正常,多用print

2个月前 回复


avatar
南霁姑娘 么么哒! 5

博主你好,这里的代码,是有二级评论的时候发通知,以及给管理员发通知,那一级评论没有发通知呀,是我漏掉了什么吗crying

...
from notifications.signals import notify
from django.contrib.auth.models import User

...
def post_comment(...):
    ...

    # 已有代码,创建新回复
    if comment_form.is_valid():
        ...

        # 已有代码,二级回复
        if parent_comment_id:
            ...

            # 新增代码,给其他用户发送通知
            if not parent_comment.user.is_superuser:
                notify.send(
                    request.user,
                    recipient=parent_comment.user,
                    verb='回复了你',
                    target=article,
                    action_object=new_comment,
                )

            return HttpResponse('200 OK')

        new_comment.save()

        # 新增代码,给管理员发送通知
        if not request.user.is_superuser:
            notify.send(
                    request.user,
                    recipient=User.objects.filter(is_superuser=1),
                    verb='回复了你',
                    target=article,
                    action_object=new_comment,
                )

        return redirect(article)
...

 

2个月前 回复


avatar
杜赛 [博主] 南霁姑娘 么么哒! 2

你没有漏掉东西。教程这里的逻辑就是回复了谁,谁才会收到通知(外加管理员)。如果不是直接回复一级评论的话,一级评论是不会收到通知的。你可以试着去实现这个功能。

2个月前 回复


avatar
南霁姑娘 杜赛 [博主] 么么哒! 3

哈哈您回复了我的另一个疑问,但是这里的疑问是,假如发表文章的是a,管理员是admin,用户b评论a的这篇文章(这属于一级评论),按照您前面的逻辑发消息应该是b-a,b-admin这两个通知消息,刚才那个一级评论不会进入二级评论的if判断,也就是说不会有b-a,只有b-admin这一条通知消息(目前我的数据库里就是如果一级评论,只有admin收到消息,二级评论才会admin和被回复者收到消息)

2个月前 回复


avatar
杜赛 [博主] 南霁姑娘 么么哒! 2

没问题的。这是一个博客网站,所有的一级评论一定是发送给博主的,博主又必然是网站的管理员。好好想想

2个月前 回复


avatar
fcmxmk 南霁姑娘 么么哒! 1

楼主的问题我也发现了, 刚还以为是我这边的外键绑定错了, 结果发现根本没有. 按博主的说法就是, 作者即superuser, 是唯一的, 没有"第三者", 发送给作者即发送给superuser, 所以教程里面的逻辑是正确的. 

如果博客是第三个用户(非superuser)发布的, 那按教程逻辑, 就会出现不管是谁在博客里面发布第一级回复, 消息接收的人只有superuser, 博主是收不到的, 再多加一条 if 应该就可以了.

#####

new_comment.user = request.user
blog_master = ArticlePost.objects.get(id=article_id)
blog_master_user = blog_master.author
if request.user != blog_master_user:
    notify.send(
        request.user,
        recipient=blog_master_user,
        verb='回复了你',
        target=article,
        action_object=new_comment,
    )
if parent_comment_id:

假设博客用户为b,

用户 a, b, admin 分别在博客第一级目录下留言, 提醒的用户分别为: (b, admin), (admin,), (b)

同理, 修改3处 if 的条件, 即可实现: 

1. 一级目录提醒会发送到博客主和superuser;

2. 二级目录提醒会发送到博客主;

3. 限制所有消息提醒的重复发送(教程里面回复者自己也会收到消息提醒);

12天前 回复


avatar
杜赛 [博主] fcmxmk 么么哒! 1

你说的完全正确。本教程只考虑个人博客,所以文章作者只有一个人,就是管理员。

多用户平台的逻辑就要复杂一些,读者自己去实现吧。

12天前 回复


avatar
hahah 么么哒! 1

这里通知用信号应该很好

1个月前 回复


avatar
fcmxmk 么么哒! 1

老师, 发现了一个问题. 博客删除了,  通知还在, 是不是可以做成删除博客后该博客相关的通知也清除掉

on_delete=models.CASCADE,

12天前 回复


avatar
杜赛 [博主] fcmxmk 么么哒! 1

观察很仔细。有很多细节都可以去优化,就不逐个讲了。教程只是抛砖引玉

12天前 回复


avatar
fcmxmk 杜赛 [博主] 么么哒! 1

嗯嗯, 谢谢老师, 在博客删除那里加条删除即可

Notification.objects.filter(target_object_id=id).delete()

12天前 回复