在整个博客的搭建中,文章相关的功能是最关键的,好比文章相关数据模型的设计、不一样分类下文章的筛选显示、以及对显示功能完善的分页功能。本文针对本博客的文章主要功能经过这几方面进行介绍,参考所有代码请到Github查看。html
在数据库设计以前,咱们首先要肯定网站功能,结合本站,最主要的是咱们的博文表,名字能够直接叫作 article,其中包含博文的标题、内容、发表时间、修改时间、分类、标签、阅读量、喜欢量、做者、关键词等。博文表直接关联的有分类表(一对多)、标签表(多对多)和文章关键词表 (多对多),分类表是隶属在导航栏下,到此咱们能够肯定出这些最基本的数据表,博客(Article)、分类(Category)、标签(Tag)与文章关键词 (Keyword)、导航(Bigcategory)。前端
首先打开项目根目录,建立 Storm APPpython
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)表的建立:django
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功能,前端引用模板语言能够达到一样效果并使用更自由。markdown
在此以前先配置urlapp
#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
经过视图类处理后的文章数据 articleList 在前端中用Django的模板语言能够直接引用,前端模板根据需求进行自定义。
{% for article in articleList %} {{article.category.name}} {{article.title}} ... {{article.create_date | date:"Y-m-j"}}< {{article.loves}} {% endfor %}
分页传来的数据中,除了咱们自定义的 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">«</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">»</span> </a> </li> </ul> </nav> </div>
<small>参考:追梦任务 | Django Pagination分页功能 </small>