Django实战专题: 专业博客开发(2)之母子类别导航和Admin外使用富文本编辑器CKEditor
在前一篇文章里,我们已经构建了一个博客应用的模型,并利用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