编辑功能使用到了ckeditor的MathJax组件。ajax提交评论可以不用刷新浏览器。

1、变化的部分

2、上代码:

ul.blog-types,ul.blog-dates {
list-style-type: none;
} div.blog:not(:last-child) {
margin-bottom: 2em;
padding-bottom: 1em;
border-bottom: 1px solid #eee;
} div.blog h3 {
margin-top: 0.5em;
} div.blog-info p {
margin-bottom:;
}
div.blog-info p span{
margin-right: 10px;
} div.blog-info-description {
list-style-type: none;
margin-bottom: 1em;
} ul.blog-info-description li {
display: inline-block;
margin-right: 1em;
} div.paginator {
text-align: center;
} div.container {
max-width: 80%;
} div.comment-area{
margin-top: 2em;
} h3.comment-area-title{
border-bottom: 1px solid #ccc;
padding-bottom: 0.4em;
} div.django-ckeditor-widget {
width: 100%;
}

blog.css

{# 引用模板 #}
{% extends 'base.html' %}
{% load staticfiles %} {% block header_extends %}
<link rel="stylesheet" href="{% static 'blog/blog.css' %}">
{# 处理公式 #}
<script src='https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.5/MathJax.js?config=TeX-MML-AM_CHTML'
async></script>
<script type="text/javascript" src="{% static "ckeditor/ckeditor-init.js" %}"></script>
<script type="text/javascript" src="{% static "ckeditor/ckeditor/ckeditor.js" %}"></script>
{% endblock %} {# 标题 #}
{% block title %}
{{ blog.title }}
{% endblock %} {# 内容#}
{% block content %}
<div class="container">
<div class="row">
<div class="col-10 offset-1">
<ul class="blog-info-description">
<h3>{{ blog.title }}</h3>
<li>作者:{{ blog.author }}</li>
{# 时间过滤器让时间按照自己需要的格式过滤 #}
<li>发布日期:{{ blog.created_time|date:"Y-m-d H:i:s" }}</li>
<li>分类:
<a href="{% url 'blogs_with_type' blog.blog_type.pk %}">
{{ blog.blog_type }}
</a>
</li>
<li>阅读({{ blog.get_read_num }})</li>
</ul>
<p class="blog-content">{{ blog.content|safe }}</p> <p>上一篇:
{% if previous_blog %}
<a href="{% url 'blog_detail' previous_blog.pk %}">{{ previous_blog.title }}</a>
{% else %}
<span>没有了</span>
{% endif %}
</p>
<p>下一篇:
{% if next_blog %}
<a href="{% url 'blog_detail' next_blog.pk %}">{{ next_blog.title }}</a>
{% else %}
<span>没有了</span>
{% endif %}
</p>
</div>
</div>
<div class="row">
<div class="col-10 offset-1">
<div class="comment-area">
<h3 class="comment-area-title">提交评论</h3>
{% if user.is_authenticated %}
<form id="comment-form" action="{% url 'update_comment' %}" method="post"
style="overflow: hidden">
{% csrf_token %}
<label for="form-control">{{ user.username }},欢迎评论~</label>
{% for field in comment_form %}
{{ field }}
{% endfor %}
<span id="comment-error" class="text-danger float-left"></span>
<input type="submit" value="评论" class="btn btn-primary float-right">
</form>
{% else %}
您尚未登录,登录之后方可评论
{# 提交登录的时候带上从哪里访问的路径 #}
<a class="btn btn-primary" href="{% url 'login' %}?from={{ request.get_full_path }}">登录</a>
<span> or </span>
<a class="btn-danger btn" href="{% url 'register' %}?from={{ request.get_full_path }}">注册</a>
{% endif %}
</div>
<div class="-comment-area">
<h3 class="comment-area-title">评论列表</h3>
<div id="comment-list">
{% for comment in comments %}
<div>
{{ comment.user.username }}
{{ comment.comment_time|date:"Y-m-d H:i:s" }}
{{ comment.text|safe }}
</div>
{% empty %} {% endfor %}
</div> </div>
</div>
</div>
</div>
{% endblock %} {% block js %}
<script>
$('#comment-form').submit(function () {
// 获取错误框
let comment_error = $('#comment-error');
comment_error.text(''); // 更新数据到textarea
CKEDITOR.instances['id_text'].updateElement();
let comment_text = CKEDITOR.instances['id_text'].document.getBody().getText().trim();
// 判断是否为空
if (!(CKEDITOR.instances['id_text'].document.getBody().find('img')['$'].length !== 0 || comment_text !== '')) {
// 显示错误信息
comment_error.text('评论内容不能为空');
return false;
}
//异步提交
$.ajax({
url: "{% url 'update_comment' %}",
type: 'POST',
data: $(this).serialize(),// 序列化表单值
cache: false, // 关闭缓存
success: function (data) { if (data['status'] === 'SUCCESS') {
console.log(data);
// 插入数据
// es6写法
let comment_html = `<div>${data["username"]} (${data["comment_time"]}): ${data["text"]}</div>`;
$('#comment-list').prepend(comment_html);
// 清空编辑框的内容
CKEDITOR.instances['id_text'].setData('');
} else {
// 显示错误信息
comment_error.text(data['message'])
}
},
error: function (xhr) {
console.log(xhr);
}
});
return false;
})
</script> <script>
$(".nav-blog").addClass("active").siblings().removeClass("active");
</script>
{% endblock %}

blog_detail.html

{# 引用模板 #}
{% extends 'base.html' %}
{% load staticfiles %} {% block header_extends %}
<link rel="stylesheet" href="{% static 'blog/blog.css' %}">
{# 处理公式 #}
<script src='https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.5/MathJax.js?config=TeX-MML-AM_CHTML'
async></script>
<script type="text/javascript" src="{% static "ckeditor/ckeditor-init.js" %}"></script>
<script type="text/javascript" src="{% static "ckeditor/ckeditor/ckeditor.js" %}"></script>
{% endblock %} {# 标题 #}
{% block title %}
{{ blog.title }}
{% endblock %} {# 内容#}
{% block content %}
<div class="container">
<div class="row">
<div class="col-10 offset-1">
<ul class="blog-info-description">
<h3>{{ blog.title }}</h3>
<li>作者:{{ blog.author }}</li>
{# 时间过滤器让时间按照自己需要的格式过滤 #}
<li>发布日期:{{ blog.created_time|date:"Y-m-d H:i:s" }}</li>
<li>分类:
<a href="{% url 'blogs_with_type' blog.blog_type.pk %}">
{{ blog.blog_type }}
</a>
</li>
<li>阅读({{ blog.get_read_num }})</li>
</ul>
<p class="blog-content">{{ blog.content|safe }}</p> <p>上一篇:
{% if previous_blog %}
<a href="{% url 'blog_detail' previous_blog.pk %}">{{ previous_blog.title }}</a>
{% else %}
<span>没有了</span>
{% endif %}
</p>
<p>下一篇:
{% if next_blog %}
<a href="{% url 'blog_detail' next_blog.pk %}">{{ next_blog.title }}</a>
{% else %}
<span>没有了</span>
{% endif %}
</p>
</div>
</div>
<div class="row">
<div class="col-10 offset-1">
<div class="comment-area">
<h3 class="comment-area-title">提交评论</h3>
{% if user.is_authenticated %}
<form id="comment-form" action="{% url 'update_comment' %}" method="post"
style="overflow: hidden">
{% csrf_token %}
<label for="form-control">{{ user.username }},欢迎评论~</label>
{% for field in comment_form %}
{{ field }}
{% endfor %}
<span id="comment-error" class="text-danger float-left"></span>
<input type="submit" value="评论" class="btn btn-primary float-right">
</form>
{% else %}
您尚未登录,登录之后方可评论
{# 提交登录的时候带上从哪里访问的路径 #}
<a class="btn btn-primary" href="{% url 'login' %}?from={{ request.get_full_path }}">登录</a>
<span> or </span>
<a class="btn-danger btn" href="{% url 'register' %}?from={{ request.get_full_path }}">注册</a>
{% endif %}
</div>
<div class="-comment-area">
<h3 class="comment-area-title">评论列表</h3>
<div id="comment-list">
{% for comment in comments %}
<div>
{{ comment.user.username }}
{{ comment.comment_time|date:"Y-m-d H:i:s" }}
{{ comment.text|safe }}
</div>
{% empty %} {% endfor %}
</div> </div>
</div>
</div>
</div>
{% endblock %} {% block js %}
<script>
$('#comment-form').submit(function () {
// 获取错误框
let comment_error = $('#comment-error');
comment_error.text(''); // 更新数据到textarea
CKEDITOR.instances['id_text'].updateElement();
let comment_text = CKEDITOR.instances['id_text'].document.getBody().getText().trim();
// 判断是否为空
if (!(CKEDITOR.instances['id_text'].document.getBody().find('img')['$'].length !== 0 || comment_text !== '')) {
// 显示错误信息
comment_error.text('评论内容不能为空');
return false;
}
//异步提交
$.ajax({
url: "{% url 'update_comment' %}",
type: 'POST',
data: $(this).serialize(),// 序列化表单值
cache: false, // 关闭缓存
success: function (data) { if (data['status'] === 'SUCCESS') {
console.log(data);
// 插入数据
// es6写法
let comment_html = `<div>${data["username"]} (${data["comment_time"]}): ${data["text"]}</div>`;
$('#comment-list').prepend(comment_html);
// 清空编辑框的内容
CKEDITOR.instances['id_text'].setData('');
} else {
// 显示错误信息
comment_error.text(data['message'])
}
},
error: function (xhr) {
console.log(xhr);
}
});
return false;
})
</script> <script>
$(".nav-blog").addClass("active").siblings().removeClass("active");
</script>
{% endblock %}

blog下的views.py

# -*- coding: utf-8 -*-
# @Time : 18-11-20 下午10:47
# @Author : Felix Wang from django import forms
from django.contrib.contenttypes.models import ContentType
from django.db.models import ObjectDoesNotExist
from ckeditor.widgets import CKEditorWidget class CommentForm(forms.Form):
content_type = forms.CharField(widget=forms.HiddenInput)
object_id = forms.IntegerField(widget=forms.HiddenInput)
text = forms.CharField(widget=CKEditorWidget(config_name='comment_ckeditor'),
error_messages={'required': '评论内容不能为空'}) def __init__(self, *args, **kwargs):
if 'user' in kwargs:
self.user = kwargs.pop('user')
super().__init__(*args, **kwargs) # 表单验证
def clean(self):
# 判断用户是否登录
if self.user.is_authenticated:
self.cleaned_data['user'] = self.user
else:
raise forms.ValidationError('用户尚未登录') content_type = self.cleaned_data['content_type']
object_id = self.cleaned_data['object_id']
try:
model_class = ContentType.objects.get(model=content_type).model_class()
model_obj = model_class.objects.get(pk=object_id)
self.cleaned_data['content_object'] = model_obj
except ObjectDoesNotExist as e:
raise forms.ValidationError('评论对象不存在') return self.cleaned_data

comment下的forms.py

from django.shortcuts import render, reverse, redirect
from django.http import JsonResponse
from .models import Comment
from django.contrib.contenttypes.models import ContentType
from .forms import CommentForm
import re
import copy def update_commit(requests):
comment_form = CommentForm(requests.POST, user=requests.user)
if comment_form.is_valid():
comment = Comment()
comment.user = comment_form.cleaned_data['user']
comment.text = comment_form.cleaned_data['text']
comment.content_object = comment_form.cleaned_data['content_object']
comment.save()
# 返回数据
data = {
'status': 'SUCCESS',
'username': comment.user.username,
'comment_time': comment.comment_time.strftime('%Y-%m-%d %H:%M:%S'),
'text': comment.text.strip(),
}
else:
data = {
'status': 'ERROR',
'message': list(comment_form.errors.values())[0][0],
}
return JsonResponse(data)

comment下的biews.py

# -*- coding: utf-8 -*-
# @Time : 18-11-20 下午8:10
# @Author : Felix Wang from django import forms
from django.contrib import auth
from django.contrib.auth.models import User class LoginForm(forms.Form):
username = forms.CharField(label='用户名', required=True,
widget=forms.TextInput(attrs={'class': 'form-control', 'placeholder': '请输入用户名'}))
# widget指定input标签类型
password = forms.CharField(label='密码',
widget=forms.PasswordInput(attrs={'class': 'form-control', 'placeholder': '请输入密码'})) def clean(self): # 验证数据
username = self.cleaned_data['username']
password = self.cleaned_data['password']
user = auth.authenticate(username=username, password=password)
if user is None:
raise forms.ValidationError('用户名或密码错误')
self.cleaned_data['user'] = user # 将验证过的user放入clean_data
return self.cleaned_data class RegisterForm(forms.Form):
# 用户名字段
username = forms.CharField(label='用户名',
max_length=30,
min_length=3,
required=True,
widget=forms.TextInput(attrs={'class': 'form-control', 'placeholder': '请输入用户名'}))
# 邮箱字段
email = forms.EmailField(label='邮箱',
min_length=3,
required=True,
widget=forms.EmailInput(attrs={'class': 'form-control', 'placeholder': '请输入邮箱'}))
# 密码字段
password = forms.CharField(label='密码',
min_length=6,
required=True,
widget=forms.PasswordInput(
attrs={'class': 'form-control', 'placeholder': '请输入密码'}))
# 再次输入密码
password_again = forms.CharField(label='确认密码',
min_length=6,
required=True,
widget=forms.PasswordInput(
attrs={'class': 'form-control', 'placeholder': '请再输入一次密码'})) def clean_username(self):
username = self.cleaned_data['username']
if User.objects.filter(username=username).exists():
raise forms.ValidationError('用户名已存在')
return username def clean_email(self):
email = self.cleaned_data['email']
if User.objects.filter(email=email).exists():
raise forms.ValidationError('邮箱已存在') return email def clean_password_again(self):
password = self.cleaned_data['password']
password_again = self.cleaned_data['password_again']
if password != password_again:
raise forms.ValidationError('两次输入的密码不一致')
return password_again

myblog下的forms.py

"""
Django settings for myblog project. Generated by 'django-admin startproject' using Django 2.1.3. For more information on this file, see
https://docs.djangoproject.com/en/2.1/topics/settings/ For the full list of settings and their values, see
https://docs.djangoproject.com/en/2.1/ref/settings/
""" import os # Build paths inside the project like this: os.path.join(BASE_DIR, ...)
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) # Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/2.1/howto/deployment/checklist/ # SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = 'ea+kzo_5k^6r7micfg@lar1(rfdc08@b4*+w5d11=0mp1p5ngr' # SECURITY WARNING: don't run with debug turned on in production!2.
DEBUG = True ALLOWED_HOSTS = ['*'] # Application definition INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'ckeditor',
'ckeditor_uploader',
'blog.apps.BlogConfig', # 将自己创建的app添加到设置中
'read_statistics.apps.ReadStatisticsConfig', # 注册阅读统计app
'comment.apps.CommentConfig', # 注册评论 ] MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
'blog.middleware.mymiddleware.My404', # 添加自己的中间件
] ROOT_URLCONF = 'myblog.urls' TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [
os.path.join(BASE_DIR, 'templates'),
],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
] WSGI_APPLICATION = 'myblog.wsgi.application' # Database
# https://docs.djangoproject.com/en/2.1/ref/settings/#databases DATABASES = {
# 'default': {
# 'ENGINE': 'django.db.backends.sqlite3',
# 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
# }
'default': {
'ENGINE': 'django.db.backends.mysql',
'NAME': 'myblogs', # 要连接的数据库,连接前需要创建好
'USER': 'root', # 连接数据库的用户名
'PASSWORD': 'felixwang', # 连接数据库的密码
'HOST': '127.0.0.1', # 连接主机,默认本级
'PORT': 3306 # 端口 默认3306
}
} # Password validation
# https://docs.djangoproject.com/en/2.1/ref/settings/#auth-password-validators AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
},
] # Internationalization
# https://docs.djangoproject.com/en/2.1/topics/i18n/ # LANGUAGE_CODE = 'en-us'
# 语言
LANGUAGE_CODE = 'zh-hans' # TIME_ZONE = 'UTC'
# 时区
TIME_ZONE = 'Asia/Shanghai' USE_I18N = True USE_L10N = True # 不考虑时区
USE_TZ = False # Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/2.1/howto/static-files/ STATIC_URL = '/static/'
STATICFILES_DIRS = [
os.path.join(BASE_DIR, "static")
] # media
MEDIA_URL = '/media/'
MEDIA_ROOT = os.path.join(BASE_DIR, 'media') # 配置ckeditor
CKEDITOR_UPLOAD_PATH = 'upload/' # 自定义参数
EACH_PAGE_BLOGS_NUMBER = 7 # 设置数据库缓存
CACHES = {
'default': {
'BACKEND': 'django.core.cache.backends.db.DatabaseCache',
'LOCATION': 'my_read_num_cache_table',
}
} # ckeditor 代码高亮,以及公式
CKEDITOR_CONFIGS = {
'default': {
'skin': 'moono',
'tabSpaces': 4,
'mathJaxLib': 'https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.5/MathJax.js?config=TeX-MML-AM_CHTML',
'toolbar': (
['div', 'Source', '-', 'Save', 'NewPage', 'Preview', '-', 'Templates'],
['Cut', 'Copy', 'Paste', 'PasteText', 'PasteFromWord', '-', 'Print',
'SpellChecker', 'Scayt'],
['Undo', 'Redo', '-', 'Find', 'Replace', '-', 'SelectAll', 'RemoveFormat',
'-', 'Maximize', 'ShowBlocks', '-', "CodeSnippet", 'Mathjax', 'Subscript',
'Superscript'],
['Form', 'Checkbox', 'Radio', 'TextField', 'Textarea', 'Select', 'Button',
'ImageButton', 'HiddenField'],
['Bold', 'Italic', 'Underline', 'Strike', '-'],
['NumberedList', 'BulletedList', '-', 'Outdent', 'Indent', 'Blockquote'],
['JustifyLeft', 'JustifyCenter', 'JustifyRight', 'JustifyBlock'],
['Link', 'Unlink', 'Anchor'],
['Image', 'Flash', 'Table', 'HorizontalRule', 'Smiley', 'SpecialChar',
'PageBreak'], ['Styles', 'Format', 'Font', 'FontSize'],
['TextColor', 'BGColor'],),
'extraPlugins': ','.join([
'codesnippet',
'mathjax',
'dialog',
'dialogui',
'lineutils',
]),
},
'comment_ckeditor': {
'toolbar': 'custom',
'toolbar_custom': [
['Bold', 'Italic', 'Underline', 'Strike', 'Subscript', 'Superscript'],
['TextColor', 'BGColor', 'RemoveFormat'],
['NumberedList', 'BulletedList'],
['Link', 'Unlink'],
['Smiley', 'SpecialChar', 'Blockquote'],
],
'width': 'auto',
'height': '',
'tabSpace': 4,
'removePlugins': 'elementspath',
'resize_enabled': False,
}
}

settings.py

05-22 20:54