Django实战专题: 专业博客开发(1)之内容管理后台开发

很多人学习Django都是从开发个人博客入手的,网上的教程也很多,但后台大多是基于Django自带的admin来实现文章的增删查改,而前台也只是实现了简单地展示文章列表和某篇文章详情。开发一个专业的博客显然不止是那么简单的,小编我今天就带你利用Django开发一个专业点的博客,重点放在开发内容管理后台。我会带你分析每一步的代码思路,帮你了解一个优秀的程序员应该如何思考,并解决遇到的技术问题。本文适合已具备一定Django基础知识的读者。本专题连载,总篇数未知。只有当本文点赞数大于20时,我才会开始本专题下篇文章的更新。本文开发环境为Django 2.0 + Python 3.6。

 

总体思路

我们的前台需要2个功能性页面,展示文章列表和文章详情,用户无需登录即可查看。后台需要6个功能性页面,需要用户登录后才能访问,且每个用户只能编辑或删除自己创建的文章。这8个功能性页面分别是。

  • 文章列表 - 不需要登录

  • 文章详情 - 不需要登录

  • 创建文章 - 需要登录

  • 修改文章 - 需要登录

  • 删除文章 - 需要登录

  • 查看已发布文章  - 需要登录

  • 草稿箱 - 需要登录

  • 发表文章 (由草稿变发布) - 需要登录

登录后电脑上看到的管理后台大致效果是这样的。

Django实战专题: 专业博客开发(1)之内容管理后台开发_第1张图片

手机上看效果是这样子的。

Django实战专题: 专业博客开发(1)之内容管理后台开发_第2张图片

 

项目配置settings.py

我们通过python manage.py startapp blog创建一个叫blog的APP,把它加到settings.py里INSATLLED_APP里去,如下所示。我们用户注册登录功能交给了django-allauth, 所以把allauth也进去了。如果你不了解django-allauth,强烈建议阅读django-allauth教程(1): 安装,用户注册,登录,邮箱验证和密码重置(更新)。

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'django.contrib.sites',
    'allauth',
    'allauth.account',
    'allauth.socialaccount',
    'allauth.socialaccount.providers.baidu',
    'blog',
]

 

因为我们要用到静态文件如css和图片,我们需要在settings.py里设置STATIC_URL和MEDIA。用户上传的图片会放在/media/文件夹里。

STATIC_URL = '/static/'
STATICFILES_DIRS = [os.path.join(BASE_DIR, "static"), ]

# specify media root for user uploaded files,
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
MEDIA_URL = '/media/'

整个项目的urls.py如下所示。我们把blog的urls.py也加进去了。别忘了在结尾部分加static配置。

from django.conf import settings
from django.conf.urls.static import static

urlpatterns = [
    path('admin/', admin.site.urls),
    path('accounts/', include('allauth.urls')),
    path('blog/', include('blog.urls')),
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

 

模型models.py

我们需要至少创建3个模型Article(文章), Category(类别)和Tag(标签), 其中类别与文章是单对多的关系,而标签与文章是多对多的关系。我们的Article模型如下所示,包括了文章状态(草稿还是发表), 文章创建,修改和发表日期,以及文章浏览次数。

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 = models.TextField('正文')
    pub_date = models.DateTimeField('发布时间', null=True)
    create_date = models.DateTimeField('创建时间', auto_now_add=True)
    mod_date = models.DateTimeField('修改时间', auto_now=True)
    status = models.CharField('文章状态', max_length=1, choices=STATUS_CHOICES, default='p')
    views = models.PositiveIntegerField('浏览量', default=0)
    author = models.ForeignKey(User, verbose_name='作者', on_delete=models.CASCADE)

    category = models.ForeignKey('Category', verbose_name='分类', on_delete=models.CASCADE, blank=False, null=False)
    tags = models.ManyToManyField('Tag', verbose_name='标签集合', blank=True)

    def __str__(self):
        return self.title

我们现在就要看下这个模型能否满足我们的需求。

 

我们第1个要求就是要根据文章标题手动生成slug, 并把slug放到url里。slug最大的作用就是便于读者和搜索引擎直接从url中了解文章大概包含了什么内容,显然/article/1/django-blog-demo/比/article/1/包含更多信息。我们还要考虑到slug可能会随文章title的变化而改变导致后续网站上出现很多坏连接,所以我们希望slug只是在首次创建文章时生成,而不会随后续title的变化而改变。除此以外,我们还要解决中文标题无法生成slug的问题。最好的解决方案就是重写模型models的save方法, 代码如下所示。当id或slug为空时,利用unidecode对中文解码,利用slugify方法根据标题手动生成slug。

from django.db import models
from django.contrib.auth.models import User
from django.urls import reversefrom unidecode import unidecode
from django.template.defaultfilters import slugify
import datetime


class Article(models.Model):
    """文章模型"""
   
    title = models.CharField('标题', max_length=200, unique=True)
    slug = models.SlugField('slug', max_length=60, blank=True)
    ........ 

    def save(self, *args, **kwargs):
        if not self.id or not self.slug:
            # Newly created object, so set slug
            self.slug = slugify(unidecode(self.title))

        super().save(*args, **kwargs)

 

我们第2个需求是确保模型各个字段间的数据满足一定的逻辑关系。比如草稿文章(d)不应该有发布日期(pub_date)。当文章状态为发布(p), 而发布日期为空时,发布日期应该为当前时间。当一个模型的各个字段之间并不彼此独立的,而是存在一定的关联性时,我们可以在模型中添加自定义的clean方法来完成数据的清理与验证。

def clean(self):
    # Don't allow draft entries to have a pub_date.
    if self.status == 'd' and self.pub_date is not None:
        self.pub_date = None
        # raise ValidationError('草稿没有发布日期. 发布日期已清空。')
    if self.status == 'p' and self.pub_date is None:
        self.pub_date = datetime.datetime.now()

 

除此以外我们还要在模型中定义其它有用的方法,比如Django通用视图所需要的get_abosolute_url方法。我们还要定义是浏览次数自增1的viewed的方法和把文章状态由草案变成发布的published方法。一个完整的Article代码模型如下所示:

from django.db import models
from django.contrib.auth.models import User
from django.urls import reverse
from unidecode import unidecode
from django.template.defaultfilters import slugify
import datetime


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 = models.TextField('正文')
    pub_date = models.DateTimeField('发布时间', null=True)
    create_date = models.DateTimeField('创建时间', auto_now_add=True)
    mod_date = models.DateTimeField('修改时间', auto_now=True)
    status = models.CharField('文章状态', max_length=1, choices=STATUS_CHOICES, default='p')
    views = models.PositiveIntegerField('浏览量', default=0)
    author = models.ForeignKey(User, verbose_name='作者', on_delete=models.CASCADE)

    category = models.ForeignKey('Category', verbose_name='分类', on_delete=models.CASCADE, blank=False, null=False)
    tags = models.ManyToManyField('Tag', verbose_name='标签集合', blank=True)

    def __str__(self):
        return self.title

    def save(self, *args, **kwargs):
        if not self.id or not self.slug:
            # Newly created object, so set slug
            self.slug = slugify(unidecode(self.title))

        super().save(*args, **kwargs)

    def get_absolute_url(self):
        return reverse('blog:article_detail', args=[str(self.pk), self.slug])

    def clean(self):
        # Don't allow draft entries to have a pub_date.
        if self.status == 'd' and self.pub_date is not None:
            self.pub_date = None
            # raise ValidationError('草稿没有发布日期. 发布日期已清空。')
        if self.status == 'p' and self.pub_date is None:
            self.pub_date = datetime.datetime.now()

    def viewed(self):
        self.views += 1
        self.save(update_fields=['views'])

    def published(self):
        self.status = 'p'
        self.pub_date = datetime.datetime.now()
        self.save(update_fields=['status', 'pub_date'])

    class Meta:
        ordering = ['-pub_date']
        verbose_name = "文章"
        verbose_name_plural = verbose_name

Category和Tag模型如下所示。每个类别可能有母类别,指向的模型是自己。比如Python类包括Python基础和Django基础类。我们通过自定义的has_child方法来判断一个类别是否有子类别。你一定奇怪我们为什么不定义has_parent方法来判断一个类别是否有母类别呢? 因为我们可以通过category.parent_category是否为空来直接判断一个类别是否有母类别。在Tag模型中,我们定义了get_article_count方法来快速统计属于某个tag的文章总数。

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


class Tag(models.Model):
    """文章标签"""
    name = models.CharField('标签名', max_length=30, unique=True)
    slug = models.SlugField('slug', max_length=40)

    def __str__(self):
        return self.name

    def get_absolute_url(self):
        return reverse('blog:tag_detail', args=[self.slug])

    def get_article_count(self):
        return Article.objects.filter(tags__slug=self.slug).count()

    class Meta:
        ordering = ['name']
        verbose_name = "标签"
        verbose_name_plural = verbose_name

 

URLConf配置urls.py

每个path都对应一个视图,一个命名的url和我们本文刚开始介绍的一个功能性页面。本项目总体urls.py如下。实际上我们只需要传递文章的pk只即可实现文章的编辑和删除,丹我们还是在url里同时传递了文章的pk和slug, 提高了url的可读性, 便于搜索引擎检索。

from django.urls import path, re_path
from . import views

# namespace
app_name = 'blog'

urlpatterns = [


    # 所有文章列表 - 不需登录
    path('', views.ArticleListView.as_view(), name='article_list'),

    # 展示文章详情 - 登录/未登录均可
    re_path(r'^article/(?P\d+)/(?P[-\w]+)/$',
            views.ArticleDetailView.as_view(), name='article_detail'),

    # 草稿箱 - 需要登录
    path('draft/', views.ArticleDraftListView.as_view(), name='article_draft_list'),

    # 已发表文章列表(含编辑) - 需要登录
    path('admin/', views.PublishedArticleListView.as_view(), name='published_article_list'),


    # 更新文章- 需要登录
    re_path(r'^article/(?P\d+)/(?P[-\w]+)/update/$',
            views.ArticleUpdateView.as_view(), name='article_update'),
    # 创建文章 - 需要登录
    re_path(r'^article/create/$',
            views.ArticleCreateView.as_view(), name='article_create'),

    # 发表文章 - 需要登录
    re_path(r'^article/(?P\d+)/(?P[-\w]+)/publish/$',
            views.article_publish, name='article_publish'),

    # 删除文章 - 需要登录
    re_path(r'^article/(?P\d+)/(?P[-\w]+)/delete$',
            views.ArticleDeleteView.as_view(), name='article_delete'),

    # 展示类别列表
    re_path(r'^category/$',
            views.CategoryListView.as_view(), name='category_list'),

    # 展示类别详情
    re_path(r'^category/(?P[-\w]+)/$',
            views.CategoryDetailView.as_view(), name='category_detail'),

    # 展示Tag详情
    re_path(r'^tags/(?P[-\w]+)/$',
            views.TagDetailView.as_view(), name='tag_detail'),

    # 搜索文章
    re_path(r'^search/$', views.article_search, name='article_search'),

]

 

视图views.py

我们使用Django自带的通用视图ListView, DetailView, CreateView, UpdateView和DeleteView来展现文章列表和详情,并实现文章的增删查改。如果你不懂Django的通用视图,请阅读Django核心基础(3): View视图详解。一旦你使用通用视图,你就会爱上她。对于需要用户登录后才能访问的视图,我们直接使用了login_required装饰器。

 

本文仅介绍与article相关的视图。各个视图与我们本文初介绍的功能性页面对应关系如下。

  • 文章列表 - ArticleListView - 不需要login_required装饰器

  • 文章详情 - ArticleDetailView - 不需要login_required装饰器

  • 创建文章 - ArticleCreateView - 需要login_required装饰器

  • 修改文章 - ArticleUpdateView - 需要login_required装饰器

  • 删除文章 - ArticleDeleteView - 需要login_required装饰器

  • 查看已发布文章 - PublishedArticleListView - 需要login_required装饰器

  • 草稿箱 - ArticleDraftListView - 需要login_required装饰器

  • 发表文章 (由草稿变发布) - 需要login_required装饰器

     

from django.views.generic import DetailView, ListView
from django.views.generic.edit import CreateView, UpdateView, DeleteView
from .models import Article, Category, Tag
from django.http import HttpResponseRedirect
from django.shortcuts import render, get_object_or_404, redirect
from .forms import ArticleForm
from django.http import Http404
from django.core.paginator import Paginator

from django.contrib.auth.decorators import login_required
from django.utils.decorators import method_decorator
from django.urls import reverse, reverse_lazy


# Create your views here.
class ArticleListView(ListView):
    paginate_by = 3

    def get_queryset(self):
        return Article.objects.filter(status='p').order_by('-pub_date')


@method_decorator(login_required, name='dispatch')
class PublishedArticleListView(ListView):
    template_name = "blog/published_article_list.html"
    paginate_by = 3

    def get_queryset(self):
        return Article.objects.filter(author=self.request.user).
        filter(status='p').order_by('-pub_date')


@method_decorator(login_required, name='dispatch')
class ArticleDraftListView(ListView):
    template_name = "blog/article_draft_list.html"
    paginate_by = 3

    def get_queryset(self):
        return Article.objects.filter(author=self.request.user).
        filter(status='d').order_by('-pub_date')


class ArticleDetailView(DetailView):
    model = Article

    def get_object(self, queryset=None):
        obj = super().get_object(queryset=queryset)
        obj.viewed()
        return obj


@method_decorator(login_required, name='dispatch')
class ArticleCreateView(CreateView):
    model = Article
    form_class = ArticleForm
    template_name = 'blog/article_create_form.html'

    # Associate form.instance.user with self.request.user
    def form_valid(self, form):

        form.instance.author = self.request.user
        return super().form_valid(form)


@method_decorator(login_required, name='dispatch')
class ArticleUpdateView(UpdateView):
    model = Article
    form_class = ArticleForm
    template_name = 'blog/article_update_form.html'

    def get_object(self, queryset=None):
        obj = super().get_object(queryset=queryset)
        if obj.author != self.request.user:
            raise Http404()
        return obj


@method_decorator(login_required, name='dispatch')
class ArticleDeleteView(DeleteView):
    model = Article
    success_url = reverse_lazy('blog:article_list')

    def get_object(self, queryset=None):
        obj = super().get_object(queryset=queryset)
        if obj.author != self.request.user:
            raise Http404()
        return obj


@login_required()
def article_publish(request, pk, slug1):
    article = get_object_or_404(Article, pk=pk, author=request.user)
    article.published()
    return redirect(reverse("blog:article_detail", args=[str(pk), slug1]))

你注意到我们是如何实现登录用户只能查看,修改和删除自己的文章的了吗? 对的,就是你必需学会的get_queryset和get_object方法,详见如何使用Django通用视图的get_queryset, get_context_data和get_object等方法.

 

我们视图里使用了ArticleForm, 用于文章的创建和编辑。forms.py代码如下。

from django import forms
from .models import Article


class ArticleForm(forms.ModelForm):

    class Meta:
        model = Article
        exclude = ['author', 'views', 'slug', 'pub_date']
        widgets = {
            'title': forms.TextInput(attrs={'class': 'form-control'}),
            'body': forms.Textarea(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'}),
        }

 

模板templates

#blog/templates/blog/published_article_list.html(登录后查看自己发表的文章)

{% extends "blog/base.html" %}

{% block content %}

已发表文章

{# 注释: page_obj不要改。Article可以改成自己对象 #}
   {% csrf_token %}    
                                           
{% if page_obj %}                                                                                                         {% for article in page_obj %}                                                                                     {% endfor %}            
标题类别发布日期查看修改删除
           {{ article.title }}                        {{ article.category.name }}                        {{ article.pub_date | date:"Y-m-d" }}                                                                                                
{% else %} {# 注释: 这里可以换成自己的对象 #}    

没有文章。

{% endif %} {# 注释: 下面代码一点也不要动 #} {% if is_paginated %}    
       {% if page_obj.has_previous %}      
  • Previous
  •    {% else %}      
  • Previous
  •    {% endif %}    {% for i in paginator.page_range %}        {% if page_obj.number == i %}      
  • {{ i }} (current)
  •       {% else %}        
  • {{ i }}
  •       {% endif %}    {% endfor %}         {% if page_obj.has_next %}      
  • Next
  •    {% else %}      
  • Next
  •    {% endif %}    
{% endif %} {% endblock %}

效果如下。匿名用户看到界面差不多,唯一不同的是没有修改和删除的链接。草稿箱文章列表界面也差不多,唯一不同是没有发布日期。

Django实战专题: 专业博客开发(1)之内容管理后台开发_第3张图片

#blog/templates/blog/article_create_form.html(创建文章)

{% extends "blog/base.html" %}

{% block content %}


添加新文章

 {% csrf_token %}  {% for hidden_field in form.hidden_fields %}    {{ hidden_field }}  {% endfor %}  {% if form.non_field_errors %}      {% endif %}  {% for field in form.visible_fields %}  
       {{ field.label_tag }}        {{ field }}        {% if field.errors %}          {% for error in field.errors %}            
             {{ error }}            
         {% endfor %}        {% endif %}      {% if field.help_text %}        {{ field.help_text }}      {% endif %}    
 {% endfor %}  
   
       
 
{% endblock %}

效果如下:

Django实战专题: 专业博客开发(1)之内容管理后台开发_第4张图片

 

#blog/templates/blog/article_detail.html(文章详情)

{% extends "blog/base.html" %}

{% block content %}

类别:    {% if article.category.parent_category %} {{ article.category.parent_category.name }} /   {% endif %} {{ article.category }}

{{ article.title }}    {% if article.status == "d" %}    (草稿)    {% endif %}

{% if article.status == "p" %}

发布于{{ article.pub_date | date:"Y-m-d" }}      浏览{{ article.views }}次

{% endif %}

{{ article.body }}

标签:    {% for tag in article.tags.all %}    {{ tag.name }},    {% endfor %}

{% if article.author == request.user %}     {% if article.status == "d" %} 发布 |    {% endif %}编辑 | 删除 {% endif %} {% endblock %}

 

最终效果如下。请注意我们对文章的状态做了判断,不同的文章状态显示的内容是不同的。比如草稿文章标题上会多出草稿两个字。如果用户已登录,页面上会出现发布,编辑和删除文章的链接。

Django实战专题: 专业博客开发(1)之内容管理后台开发_第5张图片

小结

本教程介绍了如何利用Django开发一个专业博客,并利用Django自带的通用视图开发了博客管理后台。我们着重分析了Article模型中新增方法的作用以及我们为什么需要使用他们。事实上这个博客目前的功能还是非常初级的,比如没有评论和点赞功能,文本编辑器太简单,没有文章推荐功能,没有用户之间相互关注功能,也没有使用缓存技术,需要完善和升级的地方非常多。小编我将在后续专题中逐一讲解,前提是本文可以搜集20个以上的赞。

 

小编我写完所有代码只需要3个小时不到,然而完成本文却花了近4个小时。我那么努力,你却连个赞都不给吗? 

 

大江狗

2018.9.8

你可能感兴趣的:(Django,Django基础连载)