django-mdeditor 后台内嵌 md 文章编辑 +Editor.md 开源项目
-
前端界面由js包处理md文章更改为后端markdown数据处理。
-
添加评论提醒功能
-
利用mdeditor包来编写前端评论功能
前端界面由js包处理md文章更改为后端数据处理。
之前使用的mdedotor的js文件来处理数据的,但是每次处理数据都会加载很长时间。并且考虑到后端处理数据可以用redis缓存。所以这里进行了一次大修改。还是保留评论区的mdeditor配置文件。
- 注释掉之前的利用js包的对文章内容的处理。
查看日志:http://boywithacoin.cn/article/django-mded...
我们新建一个article.html替换原先的,(我备份了,不知道你们有木有0.0)
其实在之前的博客中已经有介绍到,利用markdown来处理文件
- 在model中加入content_to_makrdown函数:
前置包:emoji,和markdown
def content_to_markdown(self):
# 先转换成emoji然后转换成markdown
to_emoji_content = emoji.emojize(self.content, use_aliases=True)
return markdown.markdown(to_emoji_content,
extensions=['markdown.extensions.extra', ]
)
-
修改前端界面
<div class="article-body markdown-body f-17" > {{ article.body_to_markdown|safe }} <p class="font-weight-bold text-info"> <i class="fa fa-bullhorn mx-1"></i> 原创文章,转载请注明出处:{{ request.build_absolute_uri }} </p> </div>
调用body_to_markdown函数,显示处理后的html数据
- 为了更好的显示文章内容,还添加Pygments,一种通用语法高亮显示器
pip install Pygments
在命令行中进入刚才新建的md_css目录中,输入Pygments指令:
pygmentize -S monokai -f html -a .codehilite > monokai.css
这里有一点需要注意, 生成命令中的 -a 参数需要与真实页面中的 CSS Selector 相对应,即.codehilite这个字段在有些版本中应写为.highlight。如果后面的代码高亮无效,很可能是这里出了问题。
回车后检查一下,在md_css
目录中是否自动生成了一个叫monokai.css的文件,这是一个深色背景的高亮样式文件。
接下来我们在base.html中引用这个文件:
templates/base.html
<head>
...
<link rel="stylesheet" href="{% static 'bootstrap/css/bootstrap.min.css' %}">
<!-- 引入monikai.css -->
<link rel="stylesheet" href="{% static 'md_css/monokai.css' %}">
</head>
重新启动服务器基本完成
- 自定义样式
这里转载自文章链接
很多读者注意到博主的Markdown文章样式似乎比pygments生成的样式要漂亮一些。
确实是这样的。pygments提供的样式比较基础,满足不了各位大佬千奇百怪的需求,因此需要对文章样式进行深度定制。
很多读者注意到博主的Markdown文章样式似乎比pygments生成的样式要漂亮一些。
确实是这样的。pygments提供的样式比较基础,满足不了各位大佬千奇百怪的需求,因此需要对文章样式进行深度定制。
定制的方法很多。首先你应该注意到了,pygments生成的其实就是普通的css文件而已,源码也不复杂,会一点点css基础就能看懂。你完全可以自由的改动源码,变换颜色、字间距、给代码块加圆角、增加图片阴影,都随便你。定制的方法很多。首先你应该注意到了,pygments生成的其实就是普通的css文件而已,源码也不复杂,会一点点css基础就能看懂。你完全可以自由的改动源码,变换颜色、字间距、给代码块加圆角、增加图片阴影,都随便你。
另一个经常被读者问到的是,pygments的表格样式太难看了,自己从零定制表格样式又太麻烦,怎么办?博主的处理办法偷了个懒,在页面中用Jquery动态加载了Bootstrap的表格样式,就像这样:
<script src="https://cdn.bootcss.com/jquery/3.3.1/jquery.min.js"></script>
<script>
$('div#article_body table').addClass('table table-bordered');
$('div#article_body thead').addClass('thead-light');
</script>
很方便就得到漂亮的表格。
当然只是css文件而已,我们为什么要再额外的添加包呢,秉着节省的想法。
我们可以直接调用mdeditor.css的文件
首先删除之前调用的css文件
<!-- <link href="{% static 'md_css/monokai.css' %}" rel="stylesheet" type="text/css"> -->
<!-- 文本区 -->
<link href="{% static 'mdeditor/css/editormd.css' %}" rel="stylesheet" type="text/css">
通过查看这个源码可以得知
源码:
在文章内容的外层标签添加markdown-body,class就可以使用这个css文件了。
<!-- 文章内容 -->
<div class="article-body markdown-body f-17" >
{{ article.body_to_markdown|safe }}
<p class="font-weight-bold text-info">
<i class="fa fa-bullhorn mx-1"></i>
原创文章,转载请注明出处:{{ request.build_absolute_uri }}
</p>
</div>
<div class="tag-cloud my-4">
{% for tag in article.tags.all %}
<a class="tags f-16" href="{{ tag.get_absolute_url }}">{{ tag.name }}</a>
{% endfor %}
</div>
添加评论提醒功能
添加评论功能的model,定义数据库
这里我选择在comment这个app中创建:
在里面加上
·····
class Notification(models.Model):
create_p = models.ForeignKey(settings.AUTH_USER_MODEL,on_delete=models.CASCADE,verbose_name='提示创建者',related_name='notification_create')
get_p = models.ForeignKey(settings.AUTH_USER_MODEL,on_delete=models.CASCADE,verbose_name='提示接收者',related_name='notification_get')
comment = models.ForeignKey(ArticleComment,on_delete=models.CASCADE,verbose_name='所属评论',related_name='the_comment')
create_date = models.DateTimeField('提示时间',auto_now_add=True)
is_read = models.BooleanField('是否已读',default=False)
def mark_to_read(self):
self.is_read = True
self.save(update_fields=['is_read'])
class Meta:
verbose_name = '提示信息'
verbose_name_plural = verbose_name
ordering = ['-create_date']
def __str__(self):
return '{}@了{}'.format(self.create_p,self.get_p)
添加对方@后自动生成消息提醒view函数
from django.db.models.signals import post_save
·····
#回复生成消息
def notify_handler(sender, instance, created, **kwargs):
the_article = instance.belong
create_p = instance.author
# 判断是否是第一次生成评论,后续修改评论不会再次激活信号
if created:
if instance.rep_to:
'''如果评论是一个回复评论,则同时通知给文章作者和回复的评论人,如果2者相等,则只通知一次'''
if the_article.author == instance.rep_to.author:
get_p = instance.rep_to.author
if create_p != get_p:
new_notify = Notification(create_p=create_p, get_p=get_p, comment=instance)
new_notify.save()
else:
get_p1 = the_article.author
if create_p != get_p1:
new1 = Notification(create_p=create_p, get_p=get_p1, comment=instance)
new1.save()
get_p2 = instance.rep_to.author
if create_p != get_p2:
new2 = Notification(create_p=create_p, get_p=get_p2, comment=instance)
new2.save()
else:
'''如果评论是一个一级评论而不是回复其他评论并且不是作者自评,则直接通知给文章作者'''
get_p = the_article.author
if create_p != get_p:
new_notify = Notification(create_p=create_p, get_p=get_p, comment=instance)
new_notify.save()
post_save.connect(notify_handler, sender=ArticleComment)
- 当文章评论创建的时候会自动生成消息提醒
Django 自带了一个信号调度程序允许receiver函数在某个动作出现时候去获取通知。当你需要你的代码执行某些时间的同时发生些其他时间的时候,信号非常有用。 我们还可以创建一个自定义的信号,让别人在某个事件发生的时候可以获得通知。
Django自带的models提供了几个信号,它们位于django.db.models.signals。举几个常见例子:
pre_save:调用model的save()方法前发送信号
post_save:调用model的save()方法后发送信号
pre_delete:调用model活着QuerySets的delete()方法前发送信号
post_delete:同理,调用delete()后发送信号
m2m_changed:当一个模型上的ManyToManyField字段被改变的时候发送信号
使用场景:
打个比方,我们想要获取热门图片。我们可以使用Django的聚合函数来获取图片,通过用户喜欢的数量进行排序。
from django.db.models import Count
from image.models import Image
images_by_popularity = Image.objects.annotate(
total_likes=Count('user_like')).order_by('-total_likes')
但是,我们发现,每次通过统计图片的总喜欢数量在进行排序比直接使用一个已经存储好的用统计数的字段进行排序要消耗更多的性能。我们可以给模型额外添加一个正整数字段,用非规范化的计数来提升涉及该字段查询的性能。那么,问题来了,我们该如何保持这个字段是更新过的。
编辑images应用下的models.py文件,给Image模型添加以下字段:
total_likes = models.PositiveIntegerFiled(db_index=True, default=0)
total_likes字段用来存储每张图片的总喜欢数,默认值为0。
非规范化数据在你想用它们来过滤或者排序查询集(QuerSets)的时候非常有用。
在使用非规范化字段之前我们必须先考虑其他几种提高性能的方法,例如数据库索引,最优化查询以及在开始规范化你的数据之前进行缓存。
运行以下命令将新添加的字段迁移到数据库中:
python manage.py makemigrations images
你会看到如下输出:
Migrations for 'images':
0002_image_total_likes.py:
- Add field total_likes to image
接着继续运行以下命令来应用迁移:
python manage.py migrate images
输出中会包含以下内容:
Applying images.0002_image_total_likes... OK_
我们要给m2m_changed信号附加一个receiver接收函数。
在images应用目录下创建一个命名为signals.py的新文件并给该文件添加如下代码:
from django.db.models.signals import m2m_changed
from django.dsipatch import receiver
from .models import Image
@receiver(m2m_changed, sender=Image.users_like.through)
def users_like_changed(sender, instance, **kwargs): Image.objects.filter(pk=instance.id).update(total_likes=instance.user_like.conut())
这里注意一个问题在@receiver里调用save()经实测不生效,必须要使用update强制更新某一字段
首先,我们使用receiver()装饰器将users_like_changed函数注册成一个receiver函数,然后我们将该函数附加给m2m_changed信号,并将函数与Image.user_like.through实例连接,这样,只有当m2m_changed信号被Image.user_like.through执行的时候函数才被调用。还有一个可以替代的方式来注册一个receiver函数,由使用Signal对象的connect()方法组成。
Django的信号是同步阻塞的。不要使用异步任务导致信号混乱,但是我们可以联合两者来执行异步任务当我们的代码只接受一个信号通知。
我们必须要给出一个信号来连接我们的receiver函数,只有这样它才会在信号发送的时候被调用。有一个推荐的方法来注册我们的信号是在我们的应用配置类中倒入它们到ready()方法中。Django提供一个应用注册允许我们对我们的应用进行配置和内省。
典型的应用配置类
Django允许你指定配置类给你的应用们。为了提供一个自定义配置给你的应用,创建一个继承django.apps的Appconfig类的自定义类。这个应用配置类允许你为应用存储元数据和配置并提供内省。
为了注册你的信号receiver函数,当你使用receiver()装饰器的时候,你需要倒入信号模块,这些信号模块被包含在你的应用的Appconfig类中的ready()方法中。这个方法在应用注册别完整填充的时候就调用,其他给你应用的初始化都可以包含在这个方法中。
在images应用下应该有一个app.py文件,没有的话自己创建。为该文件添加如下代码:
from django.apps import Appconfig
class ImageConfig(Appconfig):
name = 'images'
verbose_name = 'Image bookmarks'
def ready(self):
import images.signals
name属性定义该应用完整的python路径。
verbose_name属性设置了这个应用的人类可读名字,它会在管理站点中显示。
ready()方法就是我们为这个应用导入信号的地方。
现在我们需要告诉Django我们的应用配置位于哪里。编辑位于images应用目录下的init.py文件添加如下内容:
defualt_app_config = 'images.apps.ImagesConfig'
原先的查询:
images_by_popularity = Image.objects.annotate(
likes=Count('users_like')).order_by('-likes')
现在我们可以用新的查询来代替上面的查询:
images_by_popularity = Image.objects.order_by('-total_likes')
以上查询只需要很少的SQL查询性能。
有了消息,我们希望在前端界面就能够对信息进行删除、已读操作,所以还要增加其他增删改查的view函数
@login_required
@require_POST
def AddcommentView(request):
if request.is_ajax():
data = request.POST
new_user = request.user
new_content = data.get('content')
article_id = data.get('article_id')
rep_id = data.get('rep_id')
the_article = Article.objects.get(id=article_id)
if not rep_id:
new_comment = ArticleComment(author=new_user, content=new_content, belong=the_article, parent=None,
rep_to=None)
else:
new_rep_to = ArticleComment.objects.get(id=rep_id)
new_parent = new_rep_to.parent if new_rep_to.parent else new_rep_to
new_comment = ArticleComment(author=new_user, content=new_content, belong=the_article, parent=new_parent,
rep_to=new_rep_to)
new_comment.save()
new_point = '#com-' + str(new_comment.id)
return JsonResponse({'msg': '评论提交成功!', 'new_point': new_point})
return JsonResponse({'msg': '评论失败!'})
@login_required
def NotificationView(request, is_read=None):
'''展示提示消息列表'''
now_date = datetime.now()
return render(request, 'comment/notification.html', context={'is_read': is_read, 'now_date': now_date})
@login_required
@require_POST
def mark_to_read(request):
'''将一个消息标记为已读'''
if request.is_ajax():
data = request.POST
user = request.user
id = data.get('id')
info = get_object_or_404(Notification, get_p=user, id=id)
info.mark_to_read()
return JsonResponse({'msg': 'mark success'})
return JsonResponse({'msg': 'miss'})
@require_POST
def mark_to_delete(request):
'''将一个成员删除'''
if request.is_ajax():
data = request.POST
contacts = request.user
member = data.get('id')
info = get_object_or_404(Notification, contacts_p=contacts, member_p=member)
info.delete()
return JsonResponse({'msg': 'delete success'})
return JsonResponse({'msg': 'miss'})
创建完signal和其他操作函数后,需要在前端插入信息提醒,信息管理界面。
直接在base.html中加入
{% get_notifications user is_read as notifications %}
<div class="dropdown for-notification">
<button class="btn btn-secondary dropdown-toggle" type="button" id="notification"
data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<i class="fa fa-bell"></i>
<span class="count bg-danger">{% get_notifications_count user 'false' %}</span>
</button>
<div class="dropdown-menu" aria-labelledby="notification">
<a class="dropdown-item media" href="{% url 'comment:notification' %}">
<i class="fa fa-bullhorn mx-1"></i>
<p>{% get_notifications_count user 'false' %}未读消息|管理消息</p>
</a>
{% for each in notifications %}
{% if not each.is_read %}
<a class="dropdown-item media" href="{{ each.comment.belong.get_absolute_url }}#com-{{ each.comment.id }}">
<i class="fa fa-warning"></i>
<p >{{ each.create_p }}回复你:{{ each.comment.content|truncatechars:15|safe }}
</p>
</a>
{% endif %}
{% endfor %}
<!-- <a class="dropdown-item media" href="#">
<i class="fa fa-check"></i>
<p>Server #1 overloaded.</p>
</a>
<a class="dropdown-item media" href="#">
<i class="fa fa-info"></i>
<p>Server #2 overloaded.</p>
</a> -->
</div>
</div>
预览效果是这样的:
- 创建消息管理界面,都是前端的代码了
{% extends 'base.html' %}
{% load static %}
{% load comment_tags %}
{% block head_title %}个人信息推送{% endblock %}
{% block top_file %}
<link href="{% static 'comment/css/notification.css' %}?v=5" rel="stylesheet">
{% endblock %}
{% block mdeditor_contain %}
<div class="container">
<div class="row">
<div class="col-md-8 col-lg-9">
{% get_notifications user is_read as notifications %}
<ul class="cbp_tmtimeline f-16">
{% for each in notifications %}
<li>
<time class="cbp_tmtime" datetime="{{ each.create_date }}">
<span>{{ each.create_date|date:"Y/m/d"}}</span>
<span>{{ each.create_date|date:"H:i"}}</span>
</time>
<div class="cbp_tmicon"><i class="fa fa-envelope"></i></div>
<div class="cbp_tmlabel">
<h2>
<strong>{{ each.create_p }}</strong> 在
<a class="text-info" title="查看评论详情"
href="{{ each.comment.belong.get_absolute_url }}#com-{{ each.comment.id }}">
{{ each.comment.belong.title }}</a> 中@了你,并评论道:
</h2>
<p>{{ each.comment.content|truncatechars:130 }}</p>
{% if not each.is_read %}
<div class="to_read pb-1">
<button class="btn btn-success float-right rounded-0 f-16" data-id="{{ each.id }}"
data-csrf="{{ csrf_token }}" data-url="{% url 'comment:mark_to_read' %}">标为已读
</button>
</div>
{% else %}
<div class="to_delete pb-1">
<button class="btn btn-danger float-right rounded-0 f-16" data-id="{{ each.id }}"
data-csrf="{{ csrf_token }}" data-url="{% url 'comment:mark_to_delete' %}">删除信息
</button>
</div>
{% endif %}
</div>
</li>
{% empty %}
<li>
<time class="cbp_tmtime" datetime="{{ now_date }}">
<span>{{ now_date|date:"Y/m/d"}}</span>
<span>{{ now_date|date:"H:i"}}</span>
</time>
<div class="cbp_tmicon"><i class="fa fa-envelope"></i></div>
<div class="cbp_tmlabel">
<h2>你暂时没有任何推送消息!</h2>
</div>
</li>
{% endfor %}
</ul>
</div>
<div class="col-md-4 col-lg-3">
<div class="card rounded-0 border-0" id="notes-main">
<div class="card-header bg-white border-0">
<h4><strong><i class="fa fa-bell-o mr-1"></i> 提示信息</strong></h4>
</div>
<div class="card-body pt-0 url-menu">
<ul class="list-group">
{% url 'comment:notification' as all_url %}
{% url 'comment:notification_no_read' as no_read_url %}
<a class="list-group-item rounded-0 {% if request.path == all_url %}active{% endif %}"
href="{{ all_url }}">全部信息
<span class="badge float-right">{% get_notifications_count user %}</span>
</a>
<a class="list-group-item rounded-0 {% if request.path == no_read_url %}active{% endif %}"
href="{{ no_read_url }}">未读信息
<span class="badge float-right">{% get_notifications_count user 'false' %}</span>
</a>
</ul>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block end_file %}
<script src="https://cdn.bootcss.com/jquery/3.4.1/jquery.min.js"></script>
<script src="{% static 'comment/js/notification.js' %}?v=23"></script>
{% endblock %}
利用mdeditor包来编写前端评论功能
整理一下思路,评论的数据通过js文件ajax数据打包进行提交数据到后台:
js文件:
至此,我们设计前端编写评论的form。
这里@一下tendcode,很多评论的功能包括js语言我都用的他之前的博客的,赞一个。
嘤嘤嘤~ctrl cv了额
好了我们重构一下comment_form文件,添加mdeditor的编辑框:
<div id="layout" style="width: 100%;">
<!-- <header>
<h1>在线md2html工具</h1>
<p>mdeditor编辑器</p>
</header> -->
<div id="test-editormd" style="width: 100%;">
<textarea id="md_contain" style="display:none;" ></textarea>
</div>
</div>
和之前博客使用js包处理md数据的一样:
之前的文章:django-mdeditor后台内嵌md文章编辑+Editor.md开源项目
http://boywithacoin.cn/article/django-mded...
应用配置文件js,和编写配置文件:
<script src="{% static 'comment/js/notification.js' %}"></script>
<script src="{% static 'comment/js/editor.js' %}"></script>
<script src="{% static 'comment/js/activate-power.js' %}"></script>
<!-- mdeditor -->
<script src="{% static 'mdeditor/js/lib/marked.min.js' %}"></script>
<script src="{% static 'mdeditor/js/editormd.min.js' %}"></script>
<!-- 绘制序列图 -->
<script src="{% static 'mdeditor/js/lib/sequence-diagram.min.js' %}"></script>
<!-- 绘制流程图 -->
<!-- <script src="{% static 'mdeditor/js/lib/flowchart.min.js' %}"></script> -->
<!-- <script src="{% static 'mdeditor/js/lib/jquery.flowchart.min.js' %}"></script> -->
<script type="text/javascript">
$(function () {
var testEditor;
testEditor = editormd("test-editormd", {
width: "100%",
path: "{% static 'mdeditor/js/lib/' %}",
height: 250,
watch:false,
todoList: true,
codeFold: true,
searchReplace: true,
emoji: true,
taskList: true,
tocm: true, // Using [TOCM]
tex: true, // 开启科学公式TeX语言支持,默认关闭
flowChart: true, // 开启流程图支持,默认关闭
sequenceDiagram: true, // 开启时序/序列图支持,默认关闭,
toolbarIcons: function () {
// Or return editormd.toolbarModes[name]; // full, simple, mini
// Using "||" set icons align right.
return ["del", "bold","emoji", "hr", "italic", "quote", "list-ul", "list-ol","|", "link","reference-link","image","code","preformatted-text","|","table","datetime","watch"]
},
lang: {
toolbar: {
// file: "上传文件",
testIcon: "自定义按钮testIcon", // 自定义按钮的提示文本,即title属性
// testIcon2: "自定义按钮testIcon2",
undo: "撤销 (Ctrl+Z)"
}
},
// toolbarIconTexts: {
// testIcon: "测试按钮" // 如果没有图标,则可以这样直接插入内容,可以是字符串或HTML标签
// },
onload: function () {
// alert("onload");
// this.setMarkdown("### onloaded");
// console.log("onload =>", this, this.id, this.settings);
// 提交评论后定位到新评论处
if (sessionStorage.getItem('new_point'))
{
var top = $(sessionStorage.getItem('new_point')).offset().top - 100;
$('body,html').animate({ scrollTop: top }, 200);
// alert('1')
// window.location.hash = sessionStorage.getItem('new_point');
// alert('1')
sessionStorage.removeItem('new_point');
// alert('1')
}
else if(window.location.hash){
// 获取url中#后的部分
var test = window.location.hash;
$("html,body").animate({ scrollTop: $(test).offset().top-100 }, 200);
// $('body,html').animate({ scrollTop: 0 }, 200);
}
else{
$('body,html').animate({ scrollTop: 0 }, 200);
};
}
});
var emoji_tag = $("#emoji-list img");
emoji_tag.click(function () {
var e = $(this).data('emoji');
testEditor.insertValue(e);
});
// 试用mdeditor处理后端md数据,进行前端js包处理
// var testEditormdView;
// testEditormdView = editormd.markdownToHTML("test-editormd-view", {
// todoList: true,
// codeFold: true,
// searchReplace: true,
// emoji: true,
// taskList: true,
// tocm: true, // Using [TOCM]
// tex: true, // 开启科学公式TeX语言支持,默认关闭
// flowChart: true, // 开启流程图支持,默认关闭
// // tocContainer: "#custom-toc-container", // 自定义 ToC 容器层
// });
});
</script>
好了,截止至近基本上更新完成了。
本作品采用《CC 协议》,转载必须注明作者和本文链接