Django从零搭建个人博客 | 文章列表页查询筛选及分页

时间:2019-06-15
本文章向大家介绍Django从零搭建个人博客 | 文章列表页查询筛选及分页,主要包括Django从零搭建个人博客 | 文章列表页查询筛选及分页使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

原文章地址: EOSONES博客

在整个博客的搭建中,文章相关的功能是最关键的,比如文章相关数据模型的设计、不同分类下文章的筛选显示、以及对显示功能完善的分页功能。本文针对本博客的文章主要功能通过这几方面进行介绍,参考全部代码请到Github查看。

设计文章相关模型

1、功能分析

在数据库设计之前,我们首先要确定网站功能,结合本站,最主要的是我们的博文表,名字可以直接叫做 article,其中包含博文的标题、内容、发表时间、修改时间、分类、标签、阅读量、喜欢量、作者、关键词等。博文表直接关联的有分类表(一对多)、标签表(多对多)和文章关键词表 (多对多),分类表是隶属在导航栏下,到此我们可以确定出这些最基本的数据表,博客(Article)、分类(Category)、标签(Tag)与文章关键词 (Keyword)、导航(Bigcategory)。

2、编写 Storm 应用模型

首先打开项目根目录,创建 Storm APP

python manage.py startapp Storm

在 Myblog -> storm -> models.py 中首先设计导航表 (Bigcategory)与分类表(Category)。

from django.db import models
from django.conf import settings  #引入定义字段SEO设置(提前设置)与自定义User(参考管理用户登录与注册博文)
from django.shortcuts import reverse #查找URL
import re

# 网站导航菜单栏表
class BigCategory(models.Model):
    # 导航名称
    name = models.CharField('导航大分类', max_length=20)
    # 用作文章的访问路径,每篇文章有独一无二的标识
    slug = models.SlugField(unique=True) #此字符串字段可以建立唯一索引
    # 分类页描述
    description = models.TextField('描述', max_length=240, default=settings.SITE_DESCRIPTION,help_text='用来作为SEO中description,长度参考SEO标准')
    # 分类页Keywords
    keywords = models.TextField('关键字', max_length=240, default=settings.SITE_KEYWORDS,help_text='用来作为SEO中keywords,长度参考SEO标准')
    
    class Meta: #元信息
        # admin中显示的表名称
        verbose_name = '一级导航'
        verbose_name_plural = verbose_name #复数形式相同

    def __str__(self):
        return self.name

# 导航菜单分类下的下拉菜单分类
class Category(models.Model):
    # 分类名字
    name = models.CharField('文章分类', max_length=20)
    # 用作分类路径,独一无二
    slug = models.SlugField(unique=True)
    # 分类栏目页描述
    description = models.TextField('描述', max_length=240, default=settings.SITE_DESCRIPTION,help_text='用来作为SEO中description,长度参考SEO标准')
    # 导航菜单一对多二级菜单,django2.0后定义外键和一对一关系的时候需要加on_delete选项,此参数为了避免两个表里的数据不一致问题
    bigcategory = models.ForeignKey(BigCategory,related_name="Category", on_delete=models.CASCADE,verbose_name='大分类')

    class Meta:#元信息
        # admin中显示的表名称
        verbose_name = '二级导航'
        verbose_name_plural = verbose_name
        # 默认排序
        ordering = ['name']

    def __str__(self):
        return self.name

    #返回当前的url(一级分类+二级分类)
    def get_absolute_url(self):
        return reverse('blog:category', kwargs={'slug': self.slug, 'bigslug': self.bigcategory.slug}) #寻找路由为blog:category的url
    #返回当前二级分类下所有发表的文章列表
    def get_article_list(self):
        return Article.objects.filter(category=self)

标签(Tag)与关键字(Keyword)表的创建:

# 文章标签
class Tag(models.Model):
    name = models.CharField('文章标签', max_length=20)
    slug = models.SlugField(unique=True)
    description = models.TextField('描述', max_length=240, default=settings.SITE_DESCRIPTION,help_text='用来作为SEO中description,长度参考SEO标准')

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

    def __str__(self):
        return self.name

    def get_absolute_url(self):
        return reverse('blog:tag', kwargs={'tag': self.name})
    def get_article_list(self):
    #返回当前标签下所有发表的文章列表
    return Article.objects.filter(tags=self)

# 文章关键词,用来作为 SEO 中 keywords
class Keyword(models.Model):
    name = models.CharField('文章关键词', max_length=20)

    class Meta:
    verbose_name = '关键词'
    verbose_name_plural = verbose_name
    ordering = ['name']

    def __str__(self):
        return self.name

博客(Article)表的创建:

from mdeditor.fields import MDTextField #admin markdown编辑器插件
import markdown #导入markdown
# 文章
class Article(models.Model):
    # 文章默认缩略图
    IMG_LINK = '/static/images/article/default.jpg'
    # 文章信息(作者一对多注册用户,这样用户也可以有发文权限)
    author = models.ForeignKey(settings.AUTH_USER_MODEL,on_delete=models.CASCADE, verbose_name='作者')
    title = models.CharField(max_length=150, verbose_name='文章标题')
    summary = models.TextField('文章摘要', max_length=230, default='文章摘要等同于网页description内容,请务必填写...')
    # 文章内容(普通字段models.TextField(verbose_name='文章内容'))
    body = MDTextField(verbose_name='文章内容')
    #图片链接
    img_link = models.CharField('图片地址', default=IMG_LINK, max_length=255)
    #自动添加创建时间
    create_date = models.DateTimeField(verbose_name='创建时间', auto_now_add=True)
    #自动添加修改时间
    update_date = models.DateTimeField(verbose_name='修改时间', auto_now=True)
    #浏览点赞整数字段
    views = models.IntegerField('阅览量', default=0)
    loves = models.IntegerField('喜爱量', default=0)
    # 文章唯一标识符
    slug = models.SlugField(unique=True)
    #分类一对多文章 #related_name反向查询
    category = models.ForeignKey(Category,on_delete=models.CASCADE, verbose_name='文章分类')
    #标签多对多文章
    tags = models.ManyToManyField(Tag, verbose_name='标签')
    #文章关键词多对多文章
    keywords = models.ManyToManyField(Keyword, verbose_name='文章关键词',help_text='文章关键词,用来作为SEO中keywords,最好使用长尾词,3-4个足够')

    class Meta:
        verbose_name = '博文'
        verbose_name_plural = verbose_name
        ordering = ['-create_date']

    def __str__(self):
        return self.title[:20]
    #返回当前文章的url
    def get_absolute_url(self):
        return reverse('blog:article', kwargs={'slug': self.slug})
    #将内容markdown
    def body_to_markdown(self):
        return markdown.markdown(self.body, extensions=[
        # 包含 缩写、表格等常用扩展
        'markdown.extensions.extra',
        # 语法高亮扩展
        'markdown.extensions.codehilite',
        # 自动生成目录扩展
        'markdown.extensions.toc',
    ])

    #点赞+1方法
    def update_loves(self):
        self.loves += 1
        self.save(update_fields=['loves']) #更新字段

    #浏览+1方法
    def update_views(self):
        self.views += 1
        self.save(update_fields=['views']) #更新字段

    #前篇方法:当前小于文章并倒序排列的第一个
    def get_pre(self):
        return Article.objects.filter(id__lt=self.id).order_by('-id').first()
    #后篇方法:当前大于文章并正序排列的第一个
    def get_next(self):
        return Article.objects.filter(id__gt=self.id).order_by('id').first()

其中模型中定义的一些方便给前端传递数据的方法,可以使用Django的自定义templatetags功能,前端引用模板语言可以达到同样效果并使用更自由。

查询文章与分页视图

在此之前先配置url

#Myblog/urls.py

from django.conf.urls import re_path,include

urlpatterns = [
    ...
    # storm博客应用
    re_path(r'^',include('Storm.urls', namespace='blog')), 
    ...
]
#Myblog/Storm/urls.py

from django.urls import path
from django.conf.urls import re_path
from Storm import views

app_name='Storm'

urlpatterns = [
   ...
    #一级二级菜单分类文章列表
    #django 2.x中用re_path兼容1.x中的url中的方法(如正则表达式)
    re_path(r'category/(?P<bigslug>.*?)/(?P<slug>.*?)/',views.CtegoryView.as_view(),name='category'),#?分隔实际的URL和参数,?p数据库里面唯一索引 & URL中指定的参数间的分隔符
    re_path(r'category/(?P<bigslug>.*?)/',views.CtegoryView.as_view(),name='category'),
    # 标签搜索文章列表
    re_path(r'tags/(?P<tagslug>.*?)/', views.CtegoryView.as_view(),name='tag'),
    ...
]

网站前端功能中,可以进行筛选文章列表显示的途径有:通过一级导航、二级分类、标签以及自定义一级导航下的最新与最热筛选,我们通过url传参进行视图分别的处理。
一般的,视图函数从数据库中获取文章列表数据:

def index(request):
    # ...

def archives(request, year, month):
    # ...

def category(request, pk):
    # ...

在Django中专门提供了各种功能的处理类来使我们快捷的处理数据,其中ListView视图帮我们内部做这些查询等操作,只需将 model 指定为 Article,告诉 Django 我要获取的模型是 Article。template_name 指定这个视图渲染的模板。context_object_name 指定获取的模型列表数据保存的变量名。这个变量会被传递给模板。 paginate_by 通过指定属性即可开启分页功能。

from django.shortcuts import render,get_object_or_404
from Storm import models
#从数据库中获取某个模型列表数据基类ListView
from django.views.generic import ListView
#Django自带的分页模块
from django.core.paginator import Paginator
#分类查找文章列表视图类
class CtegoryView(ListView):
    model=models.Article
    template_name = 'articleList.html' 
    context_object_name = 'articleList' 
    paginate_by = 8 

由于针对不同url进行文章筛选的方式不同,所以我们通过覆写了父类的 get_queryset 方法获取定制文章列表数据,通过覆写def get_context_data方法来获取定制的分页效果,其中调用了自定义方法 pagination_data 获得显示分页导航条需要的数据。

#分类查询文章与视图类
class CtegoryView(ListView):
    model=models.Article
    template_name = 'articleList.html' 
    context_object_name = 'articleList' 
    paginate_by = 8 #指定 paginate_by 属性来开启分页功能
    #覆写了父类的 get_queryset 方法获取定制数据
    #类视图中,从 URL 捕获的命名组参数值保存在实例的 kwargs 属性(是一个字典)里,非命名组参数值保存在实例的 args 属性(是一个列表)里
    def get_queryset(self):
        #get_queryset方法获得全部文章列表
        queryset = super(CtegoryView, self).get_queryset()

        # 导航菜单
        big_slug = self.kwargs.get('bigslug', '')

        # 二级菜单
        slug = self.kwargs.get('slug', '')

        # 标签
        tag_slug = self.kwargs.get('tagslug', '')

        if big_slug:
        big = get_object_or_404(models.BigCategory, slug=big_slug)
        queryset = queryset.filter(category__bigcategory=big)
        if slug:
            if slug=='newest':
                queryset = queryset.filter(category__bigcategory=big).order_by('-create_date')
            elif slug=='hottest':
                queryset = queryset.filter(category__bigcategory=big).order_by('-loves')
            else :
                slu = get_object_or_404(models.Category, slug=slug)
                queryset = queryset.filter(category=slu)
        if tag_slug:
            tlu = get_object_or_404(models.Tag, slug=tag_slug)
            queryset = queryset.filter(tags=tlu)
        return queryset


    #在视图函数中将模板变量传递给模板是通过给 render 函数的 context 参数传递一个字典实现的
    def get_context_data(self, **kwargs):
        # 首先获得父类生成的传递给模板的字典。
        context = super().get_context_data(**kwargs)
        paginator = context.get('paginator')
        page = context.get('page_obj')
        is_paginated = context.get('is_paginated')
        # 调用自己写的 pagination_data 方法获得显示分页导航条需要的数据,见下方。
        pagination_data = self.pagination_data(paginator, page, is_paginated)
        # 将分页导航条的模板变量更新到 context 中,注意 pagination_data 方法返回的也是一个字典。
        context.update(pagination_data)
    return context

    def pagination_data(self, paginator, page, is_paginated):
        if not is_paginated:# 如果没有分页,则无需显示分页导航条,不用任何分页导航条的数据,因此返回一个空的字典
            return {}
        # 当前页左边连续的页码号,初始值为空
        left = []
        # 当前页右边连续的页码号,初始值为空
        right = []
        # 标示第 1 页页码后是否需要显示省略号
        left_has_more = False
        # 标示最后一页页码前是否需要显示省略号
        right_has_more = False
        # 标示是否需要显示第 1 页的页码号。
        first = False
        # 标示是否需要显示最后一页的页码号
        last = False

        # 获得用户当前请求的页码号
        page_number = page.number
        # 获得分页后的总页数
        total_pages = paginator.num_pages
        # 获得整个分页页码列表,比如分了四页,那么就是 [1, 2, 3, 4]
        page_range = paginator.page_range
        #请求的是第一页的数据
        if page_number == 1:
            #获取了当前页码后连续两个页码
            right = page_range[page_number:(page_number + 2) if (page_number + 2) < paginator.num_pages else paginator.num_pages]
            # 如果最右边的页码号比最后一页的页码号减去 1 还要小,
            # 说明最右边的页码号和最后一页的页码号之间还有其它页码,因此需要显示省略号,通过 right_has_more 来指示。
            if right[-1] < total_pages - 1:
                right_has_more = True
            # 如果最右边的页码号比最后一页的页码号小,说明当前页右边的连续页码号中不包含最后一页的页码
            # 所以需要显示最后一页的页码号,通过 last 来指示
            if right[-1] < total_pages:
                last = True

        # 如果用户请求的是最后一页的数据,
        elif page_number == total_pages:
            #获取了当前页码前连续两个页码
            left = page_range[(page_number - 3) if (page_number - 3) > 0 else 0:page_number - 1]
            # 如果最左边的页码号比第 2 页页码号还大,
            # 说明最左边的页码号和第 1 页的页码号之间还有其它页码,因此需要显示省略号,通过 left_has_more 来指示。
            if left[0] > 2:
                left_has_more = True
            # 如果最左边的页码号比第 1 页的页码号大,说明当前页左边的连续页码号中不包含第一页的页码,
            # 所以需要显示第一页的页码号,通过 first 来指示
            if left[0] > 1:
                first = True
        else:
            # 用户请求的既不是最后一页,也不是第 1 页,则需要获取当前页左右两边的连续页码号,
            # 这里只获取了当前页码前后连续两个页码,你可以更改这个数字以获取更多页码。
            left = page_range[(page_number - 3) if (page_number - 3) > 0 else 0:page_number - 1]
            right = page_range[page_number:(page_number + 2) if (page_number + 2) < paginator.num_pages else paginator.num_pages]
            # 是否需要显示最后一页和最后一页前的省略号
            if right[-1] < total_pages - 1:
                right_has_more = True
            if right[-1] < total_pages:
                last = True

            # 是否需要显示第 1 页和第 1 页后的省略号
            if left[0] > 2:
                left_has_more = True
            if left[0] > 1:
                first = True

        data = {
            'left': left,
            'right': right,
            'left_has_more': left_has_more,
            'right_has_more': right_has_more,
            'first': first,
            'last': last,
        }
        return data

设计模板

1、获取文章

通过视图类处理后的文章数据 articleList 在前端中用Django的模板语言可以直接引用,前端模板根据需求进行自定义。

{% for article in articleList %}
{{article.category.name}}
{{article.title}} 
...
{{article.create_date | date:"Y-m-j"}}<
{{article.loves}}
{% endfor %}

2、获取分页

分页传来的数据中,除了我们自定义的 data 数据,还自带了paginator:Paginator 的实例,page_obj:当前请求页面分页对象,is_paginated:是否开启分页,其中page_obj具有当前页属性page_obj.number、判断是否含有上一页:page_obj.has_previous,是否含有下一页:page_obj.has_next。注意我们在这里用了Bootstrap的分页模板,需要在开头引入相关文件。

{% if is_paginated %}
<div class="PageList">
    <nav aria-label="Page navigation">
        <ul class="pagination pagination-sm">

            <li class="{% if not page_obj.has_previous %} disabled {% endif %}">
                <a href="{% if page_obj.has_previous %} ?page={{ page_obj.previous_page_number }} {% endif %}" aria-label="Previous">
                    <span aria-hidden="true">&laquo;</span>
                </a>
            </li>

            {% if first %}
            <li>
                <a href="?page=1">1</a>
            </li>
            {% endif %}
            {% if left %}
            {% if left_has_more %}
            <li>
                <span>...</span>
            </li>
            {% endif %}
            {% for i in left %}
            <li>
                <a href="?page={{ i }}">{{ i }}</a>
            </li>
            {% endfor %}
            {% endif %}
            <li class="active"><a href="?page={{ page_obj.number }}">{{ page_obj.number }}</a></li>
            {% if right %}
            {% for i in right %}
            <li>
                <a href="?page={{ i }}">{{ i }}</a>
            </li>
            {% endfor %}
            {% if right_has_more %}
            <li>
                <span>...</span>
            </li>
            {% endif %}
            {% endif %}
            {% if last %}
            <li>
                <a href="?page={{ paginator.num_pages }}">{{ paginator.num_pages }}</a>
            </li>
            {% endif %}
            <li class="{% if not page_obj.has_next %} disabled {% endif %}">
                <a href="{% if page_obj.has_next %} ?page={{ page_obj.next_page_number }} {% endif %}" aria-label="Next">
                    <span aria-hidden="true">&raquo;</span>
                </a>
            </li>
        </ul>
    </nav>
</div>

参考:追梦任务 | Django Pagination分页功能

原文地址:https://www.cnblogs.com/crime/p/11027968.html