Django实战专题: 专业博客开发(2)之母子类别导航和Admin外使用富文本编辑器CKEditor

版权声明:本文系大江狗原创,请勿直接copy应用于你的出版物或任何公众平台。 https://blog.csdn.net/weixin_42134789/article/details/82702416

在前一篇文章里,我们已经构建了一个博客应用的模型,并利用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

猜你喜欢

转载自blog.csdn.net/weixin_42134789/article/details/82702416