本教程内容已过期,更新版教程请访问: django 博客开发入门教程。css
摘要:前两期教程咱们实现了博客的 Model 部分,以及 Blog 的首页视图 IndexView,详情页面 DetailView,以及分类页面 CategoryView,前两期教程连接请戳:html
本周咱们将继续完善咱们的我的博客,来实现分页和代码高亮的功能。github
提示:在阅读教程的过程当中,若有任何问题请访问咱们项目的 GithHub 或评论留言以获取帮助,本教程的相关代码已所有上传在 Github。若是你对咱们的教程或者项目有任何改进建议,请您随时告知咱们。更多交流请加入咱们的邮件列表 django_study@groups.163.com 和关注咱们在 Github 上的项目。数据库
本文首发于编程派微信公众号:编程派(微信号:codingpy)是一个专一Python编程的公众号,天天更新有关Python的国外教程和优质书籍等精选干货,欢迎关注。django
咱们的数据库中会有愈来愈多的文章,把它们所有用一个列表显示在首页好像不太合适,若是显示必定数量的文章,好比8篇,这就须要用到分页功能。
Django提供了一些类来帮助你管理分页的数据 -- 也就是说,数据被分在不一样页面中,并带有“上一页/下一页”标签。这些类位于django/core/paginator.py
中。编程
文章过多,为了提升用户体验,一次只展现部分文章,为用户提供一个分页功能,就像下面这样:segmentfault
比较完善的分页效果,应该是这样的:微信
用户在哪一页,则当前页号高亮以提示用户所在位置,好比上图显示用户正处在第二页。
当用户所处的位置还有上一页时,显示上一页按钮;当还有下一页时,显示下一页按钮,不然不显示。
当分页较多时,老是显示当前页及其前几页和后几页的页码(教程中使用的是两页),其余页码用省略号代替。
老是显示第一页和最后一页的页码。
根据上面的需求,咱们开始编写相应代码。
关于分页须要使用到的的 API ,Django 官方文档对此有十分详细的介绍,它还给出了一个完整示例,读懂它的代码后仿照它便可实现基本的分页功能。请参考官方文档对于分页的示例,若是你不习惯英文的话,也能够参照网友的翻译版本Django 中文文档:分页。下面就根据官方的示例来实现咱们的需求。
尽管能够把分页逻辑直接写在视图内,可是为了通用性,咱们使用一点点 Django 更加高级的技巧——模板标签(TemplateTags)。分页功能的实现有不少第三方 APP 能够直接使用,可是为了学习 Django 的知识,因此咱们本身实现一个。这些第三方 APP 基本都是使用的模板标签,所以这多是一种比较好的实践。
为了使用模板标签,Django 要求咱们先创建一个 templatetags 文件夹,并在里面加上 __init__.py
文件以指示 python 这是一个模块(python 把含有该问价的文件夹当作一个模块,具体请参考任何一个关于 python 模块的教程)。而且 templatetags 文件夹和你的 model.py,views.py 文件是同级的,也就是说你的目录结构看起来应该是这样:
polls/ __init__.py models.py templatetags/ __init__.py poll_extras.py views.py
(这个目录结构引自官方文档,关于详细的模板标签的介绍请参考官方文档:custom template tags,不必定所有读懂,但仍是推荐花几十分钟扫一遍明白其大体说了什么)。
在 templatetags 目录下创建一个 paginate_tags .py 文件,准备工做作完,结合 Django 的模板系统,咱们来看看该如何编写咱们的程序。
首先来回顾一下 Django 的模板系统是如何工做的,回想一下视图函数的工做流程,视图函数接收一个 Http 请求,通过一系列处理,一般状况下其会渲染某个模板文件,把模板文件中的一些用 {{ }} 包裹的变量替换成从该视图函数中相应变量的值。事实上在此过程当中 Django 悄悄帮咱们作了一些事情,它把视图函数中的变量的值封装在了一个 Context (通常翻译成上下文)对象中,只要模板文件中的变量在 Context 中有对应的值,它就会被相应的值替换。所以,咱们的程序能够这样作:首先把取到的文章列表(官方术语是一个 queryset)分页,用户请求第几页,咱们就把第几页的文章列表传递给模板文件;另外还要根据上面的需求传递页码值给模板文件,这样只要把模板文件中的变量替换成咱们传递过去的值,那么就达到本文开篇处那样的分页显示效果了。
开始编写咱们的代码了,惯例依然是先看代码,而后咱们再逐行解释:
paginate_tags.py from django import template from django.core.paginator import Paginator, PageNotAnInteger, EmptyPage register = template.Library() @register.simple_tag(takes_context=True) def paginate(context, object_list, page_count): left = 3 right = 3 paginator = Paginator(object_list, page_count) page = context['request'].GET.get('page') try: object_list = paginator.page(page) context['current_page'] = int(page) pages = get_left(context['current_page'], left, paginator.num_pages) + get_right(context['current_page'], right, paginator.num_pages) except PageNotAnInteger: object_list = paginator.page(1) context['current_page'] = 1 pages = get_right(context['current_page'], right, paginator.num_pages) except EmptyPage: object_list = paginator.page(paginator.num_pages) context['current_page'] = paginator.num_pages pages = get_left(context['current_page'], left, paginator.num_pages) context['article_list'] = object_list context['pages'] = pages context['last_page'] = paginator.num_pages context['first_page'] = 1 try: context['pages_first'] = pages[0] context['pages_last'] = pages[-1] + 1 except IndexError: context['pages_first'] = 1 context['pages_last'] = 2 return '' # 必须加这个,不然首页会显示个None def get_left(current_page, left, num_pages): if current_page == 1: return [] elif current_page == num_pages: l = [i - 1 for i in range(current_page, current_page - left, -1) if i - 1 > 1] l.sort() return l l = [i for i in range(current_page, current_page - left, -1) if i > 1] l.sort() return l def get_right(current_page, right, num_pages): if current_page == num_pages: return [] return [i + 1 for i in range(current_page, current_page + right - 1) if i < num_pages - 1]
首先让咱们来看看整个分页程序的执行过程,模板标签本质上来讲就是一个 python 函数而已,只是该函数能够被用在 Django 的模板系统里面。函数就是接受参数,返回一个值。例如咱们这里定义的 def paginate(context, object_list, page_count):
分页函数,它接收了这么一些参数,通过各类处理,最终返回了 None 。
逐行解释:
paginate.py from django import template from django.core.paginator import Paginator, PageNotAnInteger, EmptyPage # 这是分页功能涉及的一些类和异常,官方文档对此有详细介绍。固然从命名也能够直接看出它们的用途:Paginator(分页),PageNotAnInteger(页码不是一个整数异常),EmptyPage(空的页码号异常) register = template.Library() # 这是定义模板标签要用到的 @register.simple_tag(takes_context=True) # 这个装饰器代表这个函数是一个模板标签,takes_context = True 表示接收上下文对象,就是前面所说的封装了各类变量的 Context 对象。 def paginate(context, object_list, page_count): # context是Context 对象,object_list是你要分页的对象,page_count表示每页的数量 left = 3 # 当前页码左边显示几个页码号 -1,好比3就显示2个 right = 3 # 当前页码右边显示几个页码号 -1 paginator = Paginator(object_list, page_count) # 经过object_list分页对象 page = context['request'].GET.get('page') # 从 Http 请求中获取用户请求的页码号 try: object_list = paginator.page(page) # 根据页码号获取第几页的数据 context['current_page'] = int(page) # 把当前页封装进context(上下文)中 pages = get_left(context['current_page'], left, paginator.num_pages) + get_right(context['current_page'], right, paginator.num_pages) # 调用了两个辅助函数,根据当前页获得了左右的页码号,好比设置成获取左右两边2个页码号,那么假如当前页是5,则 pages = [3,4,5,6,7],固然一些细节须要处理,好比若是当前页是2,那么获取的是pages = [1,2,3,4] except PageNotAnInteger: # 异常处理,若是用户传递的page值不是整数,则把第一页的值返回给他 object_list = paginator.page(1) context['current_page'] = 1 # 当前页是1 pages = get_right(context['current_page'], right, paginator.num_pages) except EmptyPage: # 若是用户传递的 page 值是一个空值,那么把最后一页的值返回给他 object_list = paginator.page(paginator.num_pages) context['current_page'] = paginator.num_pages # 当前页是最后一页,num_pages的值是总分页数 pages = get_left(context['current_page'], left, paginator.num_pages) context['article_list'] = object_list # 把获取到的分页的数据封装到上下文中 context['pages'] = pages # 把页码号列表封装进去 context['last_page'] = paginator.num_pages # 最后一页的页码号 context['first_page'] = 1 # 第一页的页码号为1 try: # 获取 pages 列表第一个值和最后一个值,主要用于在是否该插入省略号的判断,在模板文件中将会体会到它的用处。注意这里可能产生异常,由于pages多是一个空列表,好比自己只有一个分页,那么pages就为空,由于咱们永远不会获取页码为1的页码号(至少有1页,1的页码号已经固定写在模板文件中) context['pages_first'] = pages[0] context['pages_last'] = pages[-1] + 1 # +1的缘由是为了方便判断,在模板文件中将会体会到其做用。 except IndexError: context['pages_first'] = 1 # 发生异常说明只有1页 context['pages_last'] = 2 # 1 + 1 后的值 return '' # 必须加这个,不然首页会显示个None def get_left(current_page, left, num_pages): """ 辅助函数,获取当前页码的值得左边两个页码值,要注意一些细节,好比不够两个那么最左取到2,为了方便处理,包含当前页码值,好比当前页码值为5,那么pages = [3,4,5] """ if current_page == 1: return [] elif current_page == num_pages: l = [i - 1 for i in range(current_page, current_page - left, -1) if i - 1 > 1] l.sort() return l l = [i for i in range(current_page, current_page - left, -1) if i > 1] l.sort() return l def get_right(current_page, right, num_pages): """ 辅助函数,获取当前页码的值得右边两个页码值,要注意一些细节,好比不够两个那么最右取到最大页码值。不包含当前页码值。好比当前页码值为5,那么pages = [6,7] """ if current_page == num_pages: return [] return [i + 1 for i in range(current_page, current_page + right - 1) if i < num_pages - 1]
把须要变量值都添加到上下文了,看看咱们的模板文件该怎么写:
templates/blog/pagination.html <div id="pagenavi" class="noselect"> {% if article_list.has_previous %} # 判断是否还有上一页,有的话要显示一个上一页按钮 <a class="previous-page" href="?page={{ article_list.previous_page_number }}"> <span class="icon-previous"></span>上一页 </a> {% endif %} # 页码号为1永远显示 {% if first_page == current_page %} # 当前页就是第一页 <span class="first-page current">1</span> {% else %} # 不然的话,第一页是能够点击的,点击后经过?page=1的形式把页码号传递给视图函数 <a href="?page=1" class="first-page">1</a> {% endif %} {% if pages_first > 2 %} # 2之前的页码号要被显示成省略号了 <span>...</span> {% endif %} {% for page in pages %} # 经过for循环把pages中的值显示出来 {% if page == current_page %} # 是否当前页,按钮会显示不一样的样式 <span class="current">{{ page }}</span> {% else %} <a href="?page={{ page }}">{{ page }}</a> {% endif %} {% endfor %} # pages最后一个值+1的值小于最大页码号,说明有页码号须要被省略号替换 {% if pages_last < last_page %} <span>...</span> {% endif %} # 永远显示最后一页的页码号,若是只有一页则前面已经显示了1就不用再显示了 {% if last_page != 1 %} {% if last_page == current_page %} <span class="current">{{ last_page }}</span> {% else %} <a href="?page={{ last_page }}">{{ last_page }}</a> {% endif %} {% endif %} # 还有下一页,则显示一个下一页按钮 {% if article_list.has_next %} <a class="next-page" href="?page={{ article_list.next_page_number }}"> 下一页<span class="icon-next"></span> </a> {% endif %} </div>
至此代码部分编写完了,看看如何使用这个模板标签吧,好比咱们要在首页对文章列表进行分页:
templates/blog/index.html {% load paginate_tags %} # 首先必须经过load模板标签载入分页标签 {% paginate article_list 7 %} 把文章列表传给paginate函数,每页分7个,context上下文则自动被传入,无需显示指定 {% for article in article_list %} display the article information {% endfor %} {% include 'blog/pagination.html' %} # 这里用到一个 include 技巧,把pagination的模板代码写在单独的pagination.html文件中,这样哪里须要用到哪里就 include 进来就行,提升代码的复用性。
至此,整个分页功能就完成了,看看效果:
咱们的博客文章是支持 markdown 语法标记的(使用的是 markdown2 第三方 app),markdown 比较经常使用的两个特性是 fetch code 和语法高亮。因为咱们目前没有对博客文章的 markdown 标记作任何拓展,所以要标记一段代码,咱们必须在每行代码前缩进 4 个空格,这很不方便。而 fetch code 可让咱们在写文章时只按照下面的输入就能够标记一段代码,相比每行缩进四个空格要方便不少:
``` def test_function(): print('fectch code like this!') ```
下面来拓展它,很简单,把用 markdown 标记的语句拓展一下,在 Views.py 中找到 IndexView,其中有一句代码的做用是来 markdown 咱们的博客文章的:
for article in article_list: article.body = markdown2.markdown(article.body, )
将 markdown 函数拓展一下,传入以下参数便可:
for article in article_list: article.body = markdown2.markdown(article.body, extras=['fenced-code-blocks'], )
这样,每次要输入一段代码时,按照上面的语法输入就能够了,好比我输入下面的代码段:
``` # 注意这个符号是半角下波浪符号,即数字1左边的那个键对应的符号 class ArticleDetailView(DetailView): model = Article template_name = "blog/detail.html" context_object_name = "article" pk_url_kwarg = 'article_id' def get_object(self, queryset=None): obj = super(ArticleDetailView, self).get_object() obj.body = markdown2.markdown(obj.body, extras=['fenced-code-blocks'], ) return obj ```
来看看效果:
此外别忘了把其余作了 markdown 标记的地方也作相应拓展,目前咱们一共有三处:IndexView,DetailView,CategoryView。
如今输入代码方便了,可是美中不足的是代码只有一种颜色,咱们想要代码高亮,须要使用到 Pygments 包。先安装它:pip install pygments,安装好后别忘了添加到 settings.py 中:
settings.py INSTALLED_APPS = [ 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', 'blog', 'markdown2', 'pygments', # 添加进来 ]
pygments 的工做原理是把代码切分红一个个单词,而后为这些单词添加 css 样式,不一样的词应用不一样的样式,这样就实现了代码颜色的区分,即高亮了语法,所以咱们要引入一些 css 样式文件。在咱们的 GitHub 项目的 DjangoBlog/blog/static/blog/css 目录下有相应的文件,拷贝下来添加到你的项目相同目录下就能够了。以后再模板中引入样式文件:
templates/base.html <head> <meta charset="UTF-8"> <title>Myblog</title> ... <link rel="stylesheet" href="{% static 'blog/css/pygments/github.css' %}"> 引入上面的样式文件,固然里面有不少样式文件,喜欢哪一个引哪一个,好比我引的是github风格的语法高亮 ... </head>
再次输入代码块看看:
```python # 注:这里必定要指定相应语言,不然没法高亮代码 class ArticleDetailView(DetailView): model = Article template_name = "blog/detail.html" context_object_name = "article" pk_url_kwarg = 'article_id' def get_object(self, queryset=None): obj = super(ArticleDetailView, self).get_object() obj.body = markdown2.markdown(obj.body, extras=['fenced-code-blocks'], ) return obj ```
看看效果:
这里比较麻烦的是必须指定代码对应的语言,有人说 pygments 能够自动识别语言的,可是我目前的测试来看彷佛没有效果。目前没有找到设置方法,若有知道的朋友请告知。
整个完整的 Blog 项目代码请访问咱们的 GitHub 组织仓库获取。
声明:本教程只是演示如何实现分页和 markdown 语法高亮功能,在细节上处理上还有不少须要斟酌的地方,若是您有更好的实现方式或者实践经验,恳请传授咱们。若是您对本教程有任何不清晰的地方或者其余意见和建议,请及时经过邮件列表或者 GitHub Issue 或者评论留言反馈给咱们。您的反馈和建议是咱们持续改善本教程的最佳方式。
我的博客功能逐步完善,接下来的教程咱们将继续实现我的博客常带的功能:标签云和文章归档,敬请期待下一期教程。若是你还有其余想实现的功能,也请告诉咱们,咱们会在教程中陆续实现。
django学习小组是一个促进 django 新手互相学习、互相帮助的组织。
小组在一边学习 django 的同时将一块儿完成几个项目,包括:
一个简单的 django 博客,用于发布小组每周的学习和开发文档;
django中国社区,为国内的 django 开发者们提供一个长期维护的 django 社区;
上面所说的这个社区相似于 segmentfault 和 stackoverflow ,但更加专一(只专一于 django 开发的问题)。
目前小组正在完成第一个项目,本文便是该项目第三周的相关文档。
更多的信息请关注咱们的 github 组织,本教程项目的相关源代码也已上传到 github 上。
同时,你也能够加入咱们的邮件列表 django_study@groups.163.com ,随时关注咱们的动态。咱们会将每周的详细开发文档和代码经过邮件列表发出。
若有任何建议,欢迎提 Issue,欢迎 fork,pr,固然也别忘了 star 哦!