在前一篇文章里,我们已经构建了一个博客应用的模型,并利用Django的通用视图开发了博客管理后台,实现了文章的增删查改。本文将对该博客应用做出2个改进,一是实现母子类别导航,二是添加富文本编辑器CKEditor,实现图文编辑和正文显示代码。
何为母子类别导航?
我们希望用一个Category模型(如下所示)实现类似 ‘Python > Django’的母子类别导航。每个类别可能有母类别,也可能没有。一篇文章可能属于一个母类别,也可能属于一个子类别。我们希望点击Django时,能显示所有属于Django类别的文章,而点击Python时能显示所有属于Python类及其子类的文章。就这么点事,我们需要用到一种非常重要的技术,QuerySet的合并。
class Category(models.Model):
"""文章分类"""
name = models.CharField('分类名', max_length=30, unique=True)
slug = models.SlugField('slug', max_length=40)
parent_category = models.ForeignKey('self', verbose_name="父级分类", blank=True, null=True, on_delete=models.CASCADE)
def get_absolute_url(self):
return reverse('blog:category_detail', args=[self.slug])
def has_child(self):
if self.category_set.all().count() > 0:
return True
def __str__(self):
return self.name
class Meta:
ordering = ['name']
verbose_name = "分类"
verbose_name_plural = verbose_name
QuerySet的合并
查询属于某一类别的文章,我们可以使用Article.objects.filter()方法,这个方法查询的结果数据类型是QuerySet类型,而不是List类型。当一个类别有子类别时,我们需要分别查询属于每个子类的文章数据集QuerySet,然后利用union方法把它们合并,最后通过分页显示。为什么要用union方法? 因为QuerySet类型不是List类型,不能用extend或append方法。正确方法如下所示。
class CategoryDetailView(DetailView):
model = Category
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
if self.object.has_child():
articles = Article.objects.filter()
categories = self.object.category_set.all()
for category in categories:
queryset = Article.objects.filter(category=category.id).order_by('-pub_date')
articles.union(queryset)
else:
articles = Article.objects.filter(category=self.object.id).order_by('-pub_date')
paginator = Paginator(articles, 3)
page = self.request.GET.get('page')
page_obj = paginator.get_page(page)
context['page_obj'] = page_obj
context['paginator'] = paginator
context['is_paginated'] = True
return context
模板文件templates/blog/category_detail.html的代码如下所示。
{% extends "blog/base.html" %}
{% block content %}
<p>类别:
{% if category.parent_category %}
<a href="{% url 'blog:category_detail' category.parent_category.slug %}">{{ category.parent_category.name }}</a> /
{% endif %}
<a href="{% url 'blog:category_detail' category.slug %}">{{ category }}</a>
</p>
<h3>博客文章清单</h3>
{# 注释: page_obj不要改。Article可以改成自己对象 #}
{% if page_obj %}
<ul>
{% for article in page_obj %}
<li><a href="{% url 'blog:article_detail' article.id article.slug %}"> {{ article.title }}</a> {{ article.pub_date | date:"Y-m-j" }}</li>
{% endfor %}
</ul>
{# 注释: 下面代码一点也不要动 #}
{% if is_paginated %}
<ul class="pagination">
{% if page_obj.has_previous %}
<li class="page-item"><a class="page-link" href="?page={{ page_obj.previous_page_number }}">Previous</a></li>
{% else %}
<li class="page-item disabled"><span class="page-link">Previous</span></li>
{% endif %}
{% for i in paginator.page_range %}
{% if page_obj.number == i %}
<li class="page-item active"><span class="page-link"> {{ i }} <span class="sr-only">(current)</span></span></li>
{% else %}
<li class="page-item"><a class="page-link" href="?page={{ i }}">{{ i }}</a></li>
{% endif %}
{% endfor %}
{% if page_obj.has_next %}
<li class="page-item"><a class="page-link" href="?page={{ page_obj.next_page_number }}">Next</a></li>
{% else %}
<li class="page-item disabled"><span class="page-link">Next</span></li>
{% endif %}
</ul>
{% endif %}
{% else %}
{# 注释: 这里可以换成自己的对象 #}
<p>No article yet.</p>
{% endif %}
{% endblock %}
前端显示效果如下所示。点击Python基础时,能显示所有属于Python基础类别的文章列表,而点击Python时能显示所有属于Python类及其子类的文章(包括Django类)。如果你不使用QuerySet的合并,那么当你点击Python时,只会显示属于Python类的文章。
富文本编辑器CKEditor
前文里我也提到过博客正文的编辑器太过简单,不能做图文编辑,也不能显示代码。网上很多人推荐CKEditor,我也就来试了试,安装后使用效果确实不错。
安装后的效果如下所示。
如何安装使用CKEditor
网上有很多Django中使用CKEeditor的教程,都很有用。在这里我只想指出一点不同,网上教程大多是在自带后台admin里使用CKEditor,而本例是在admin外使用ckeditor。安装及设置方法如下。
1. 安装前的准备
如果你需要上传和显示图片,请先确保已安装了pillow图片库,并按文一设置STATIC和MEDIA文件夹。
2. 安装ckeditor
使用pip install django-ckeditor安装ckeditor, 在项目文件夹下(而不是app文件夹下)新建static文件夹, 使用python manage.py collectstatic下载ckeditor所需的js和css文件。
3. 设置settings.py
在settings.py里添加CKEDITOR的设置,如下所示。我们指定了图片上传文件夹"blog_uploads", 最后图片会上传到/media/blog_uploads/文件夹里。由于我们还选择了RESTRICT_BY_USER和RESTRICT_BY_DATE, 最后图片实际上传地址如下所示:
-
/media/blog_uploads/Chris/2018/09/09/img_4961.JPG
CKEDITOR_CONFIGS可以设置显示在工具栏toolbar的按钮。
CKEDITOR_UPLOAD_PATH = 'blog_uploads/'
CKEDITOR_JQUERY_URL ='https://apps.bdimg.com/libs/jquery/2.1.4/jquery.min.js'
CKEDITOR_IMAGE_BACKEND = 'pillow'
CKEDITOR_ALLOW_NONIMAGE_FILES = False
CKEDITOR_BROWSE_SHOW_DIRS = True
CKEDITOR_RESTRICT_BY_USER = True
CKEDITOR_RESTRICT_BY_DATE = True
CKEDITOR_CONFIGS = {
'default': {
'toolbar': (['Source', '-', 'Preview', '-', ],
['Cut', 'Copy', 'Paste', 'PasteText', 'PasteFromWord', '-', 'Print', 'SpellChecker', ],
['Undo', 'Redo', '-', 'Find', 'Replace', '-', 'SelectAll', 'RemoveFormat', '-',
"CodeSnippet", 'Subscript', 'Superscript'],
['NumberedList', 'BulletedList', '-', 'Blockquote'],
['Link', 'Unlink', ],
['Image', 'Table', 'HorizontalRule', 'Smiley', 'SpecialChar', ],
['Format', 'Font', 'FontSize', 'TextColor', 'BGColor', ],
['Bold', 'Italic', 'Underline', 'Strike', ],
['JustifyLeft', 'JustifyCenter', 'JustifyRight', 'JustifyBlock'],
),
'extraPlugins': 'codesnippet',
'width': 'auto',
}
}
4. 模型中使用ckeditor
我们只需将body的TextField改成RichTextUploadingField。如果你不需要上传图片,可以直接使用RichTextField。
from ckeditor_uploader.fields import RichTextUploadingField
class Article(models.Model):
"""文章模型"""
STATUS_CHOICES = (
('d', '草稿'),
('p', '发表'),
)
title = models.CharField('标题', max_length=200, unique=True)
slug = models.SlugField('slug', max_length=60, blank=True)
body = RichTextUploadingField('正文')
5. 表单中使用ckeditor
因为我们使用到了表单,所以表单的输入widget还需要改为CKEditorUploadingWidget.
from django import forms
from .models import Article
from ckeditor_uploader.widgets import CKEditorUploadingWidget
class ArticleForm(forms.ModelForm):
class Meta:
model = Article
exclude = ['author', 'views', 'slug', 'pub_date']
widgets = {
'title': forms.TextInput(attrs={'class': 'form-control'}),
'body': CKEditorUploadingWidget(attrs={'class': 'form-control'}),
'status': forms.Select(attrs={'class': 'form-control'}),
'category': forms.Select(attrs={'class': 'form-control'}),
'tags': forms.CheckboxSelectMultiple(attrs={'class': 'multi-checkbox'}),
}
6. 模板中使用{{ form.media }}调入ckeditor静态文件
模板中如果不使用{{ form.media }}调入ckeditor静态文件(js, css和图片), 那么前端你将看不到漂亮的用户界面。
<form method="POST" class="form-horizontal" role="form" action="" >
{% csrf_token %}
{{ form.media }}
{{ form }}
......
7. 修改staff_member_required装饰器变为login_required。
这一点是在admin内和admin外使用ckeditor最的不同。如果需要使用文件上传,ckeditor默认只有员工(staff member)才有这个权限。如果你需要admin外的用户也能上传图片或文件,你需要将staff_member_required装饰器改为login_required。
你需要按site-packages - > ckeditor_uploader -> templates -> urls.py的源码,把staff_member_required装饰器改为login_required。
8. 显示代码
只需要在CKEDITOR_CONFIGS中加入codesnipppet的plugin即可。
'extraPlugins': 'codesnippet',
同时找到static -> ckeditor -> ckeditor -> config.js把codesnippet注册一下。
CKEDITOR.editorConfig = function( config ) {
// Define changes to default configuration here. For example:
// config.language = 'fr';
// config.uiColor = '#AADC6E';
config.extraPlugins: "codesnippet";
};
安装好后即可通过codeshippet插入不用语言代码了,下面是python代码显示效果。
小结
本文讲解了如何实现母子类别导航,重点讲解了QuerySet的合并。我们还安装了CKEditor富文本编辑器,实现了图文编辑和代码显示功能。计划下篇教程中讲解如何添加评论和点赞功能,就看本文有没有30个赞啦。
大江狗
2018.9.10