文章分类
在本篇教程中,大狗打算给我们的博客加入文章分类和日期归档功能,同时,按照HTML5的相关标准对模板进行一些调整,初步的计划是把首页改成这个样子:
![](https://img.haomeiwen.com/i3824779/98929e3339f48ebc.jpg)
在原有页头的基础上增加一个文章分类导航,在右边加入一个边栏(sidebar),可以按照文章的发布时间进行过滤。
如果需要增加文章分类导航,首先需要在文章中加入「分章分类」这个属性(我们前面没有考虑这个,寒...),我们需要打开blog目录下的models.py文件,在其中加入文章分类(category)这个类:
class Category(models.Model):
name = models.CharField(max_length = 20, verbose_name = '分类名称')
def __unicode__(self):
return self.name
def __str__(self):
return self.__unicode__()
class Meta:
verbose_name = '分类'
verbose_name_plural = '分类'
简单起见,我们只使用一级分类,文章分类中仅有分类名称一个属性。
我们还需要在文章(article)类中增加一个属性,用来标识文章所属的分类:
category = models.ForeignKey(Category, null = True, on_delete = models.SET_NULL)
models.ForeignKey表示该属性需要与另外一个类相关联(在数据库中表现为「外键」),null用来设定该属性是否可以为空,on_delete用来设定当删除相关对象的处理方式,有以下几种设定:
- CASCADE: 级联删除,即删除文章分类时,同时删除该分类下所有文章
- PROTECT:删除文章分类时,如果该分类下还有文章,则禁止删除
- SET_NULL:删除文章分类时,将相关文章的分类属性设置为空(NULL)
- SET_DEFAUL:删除文章分类时,将相关文章的分类属性设置为默认值(需要设定文章属性的默认值)
- SET:删除文章分类时,将相关文章的分类属性设置为指定值
- DO_NOTHING:删除文章分类时,神马也不做
我们在这里选择SET_NULL,即当我们从后台删除了一个文章分类,将该分类下的所有文章的分类属性设置为空值。
关于数据库外键的相关知识,请参见 [维基百科: Foreign Key][1]
因为我们在models.py中添加类新的类,并对原有的Article类进行了改动,所以我们需要将这些变动更新到数据库中。首先在命令行中运行:
./manage.py makemigrations
接下来,将改动合并至数据库:
./manage.py migrate
然后,在blog目录下的admin.py文件中添加下面的代码行,将文章分类挂接到后台管理:
admin.site.register(Category)
现在,我们就可以在后台管理页面中添加文章分类了,大狗添了下面几项:
![](https://img.haomeiwen.com/i3824779/1c050816c2c52a39.jpg)
然后在后台管理页面中为每篇文章指定了所属的分类。
页面模板与功能封装
接下来,我们就可以按照文章开头的效果图,在博客中加入新的功能了。但是在开工之前,大狗发现其实效果图中的很多功能,在多个页面中都会用到,比如页头(header),页脚(footer),边栏(sidebar)等。我们在每个页面中重写这些功能么(copy and paste)?那样就弱爆了,因为Django已经内置了页面模板的功能。
Django的页面模板可以包含各个模板文件(也就是templates目录中的html文件)中的公共部分(例如页头、页脚和边栏等),并利用占位符(placeholder)为模板文件中需要自定义的部分预留空间。
在页面模板和模板文件中,还可以引用其他模板文件中的内容,实现模板文件的功能封装。
我们还是来看具体的例子吧,下面就是大狗为我们的博客编写的页面模板:
{% load static %}
<!DOCTYPE html>
<htmL>
<head>
<title>{% block title %}欢迎光临「大狗汪叨叨」{% endblock %}</title>
<meta charset="utf-8">
<link rel="stylesheet" type="text/css" href="{% static 'blog/css/style.css' %}">
</head>
<body>
<header id="headerContainer">
{% include "blog/header.html" %}
</header>
<section id="contentContainer">
{% block content %}
{% endblock %}
</section>
<section id="sideContainer">
{% include "blog/sidebar.html" %}
</section>
<footer id="footerContainer">
{% include "blog/footer.html" %}
</footer>
</body>
</html>
我们可以看到,页面模板的格式和语法,和模板文件并没有什么大的差别,只是多了一些新的元素(element):
- block: 占位符,为模板文件中的自定义部分预留空间
- include:引用其他模板文件中的内容(其实这个在模板文件中也可以使用)
我们在页面模板中利用block元素添加了两个占位符,一个是title,用于指定网页的标题,另一个是content,用于加载网页的主要内容(如果是首页、文章分类页面或者文章存档页面,应该是文件列表;如果是文章页面的话,则应该是文章内容)。我们为title这个占位符指定了默认值,如果在模板文件中没有进行自定义,就会沿用页面模板中指定的默认值,即“欢迎光临「大狗汪叨叨」”。
大狗将页头、边栏和页脚的内容分别单独存储至header.html,sidebar.html和footer.html,并利用incude元素将它们分别链接至页面模板。
我们先在blog目录下的模板目录下分别新建上述三个文件,并将上面的代码以base.html保存在模板目录中。
我们可以在header.html中添加博客的页头部分:
{% load static %}
<div id="headerLogoSection">
![]({% static 'blog/images/logo.jpeg' %})
</div>
<div id="headerTitleSection">
<span class="siteTitle">大狗汪叨叨</span>
</div>
<div id="headerNavSection">
</div>
在footer.html中添加博客的页脚部分:
<hr class="footerLine" />
<span class="articleBody">Copyrights 大狗 All Rights Reserved. </span>
sidebar.html暂时保持空白,我们将在后面在其中添加内容。
下面我们来看看怎样以页面模板为基础创建模板文件,我们先从index.html开始。首先,我们需要在文件的开始,添加对页面模板的引用:
{% extends "blog/base.html" %}
因为我们在页面模板中已经添加了页头、边栏和页脚,我们在这个模板文件中只需要添加占位符对应的部分就可以了。我们可以仅保留文章列表的部分。修改后的模板文件应类似于:
{% extends "blog/base.html" %}
{% load static %}
{% block content %}
{% for article in articles.object_list %}
<article>
<header>
<h2 class="articleTitle">{{article.title}}</h2>
<p class="articleAuthor">by: {{article.author}} {{article.timestamp}}</p>
</header>
<span class="articleBody">{{article.body|slice:'200'}}...</span>
<br/>
<footer>
<a href="article/{{article.id}}">查看全文</a>
</footer>
</article>
{% endfor %}
<nav id="contentPagerSection">
{% if all_articles.has_previous %}
<div style="left:200px;position:absolute;width:60px;margin:30px;">
<a href="{{all_articles.previous_page_number}}"><< 前滚翻</a>
</div>
{% endif %}
{% if all_articles.has_next %}
<div style="left:340px;position:absolute;width:60px;margin:30px">
<a href="{{all_articles.next_page_number}}">后滚翻 >></a>
</div>
{% endif %}
</nav>
{% endblock %}
我们还可以按照同样的方法,修改文章模板文件:
{% extends "blog/base.html" %}
{% load static %}
{% block title %}{{article.title}}{% endblock %}
{% block content %}
<h2 class="articleTitle">{{article.title}}</h2>
<p class="articleAuthor">by: {{article.author}} {{article.timestamp}}</p>
<span class="articleBody">{{article.body|linebreaks}}</span>
{% endblock %}
我们在文章模板文件中重写(override)了title占位符中的内容,将其替换为文章的标题。
文章分类导航
下面我们要为博客增加文章分类导航功能,我们还是从URL配置文件开始:
from django.conf.urls import url
from . import views
urlpatterns = [
url(r'^$', views.index, name = 'index'),
url(r'^(?P<pageNo>[\d]+)',views.indexInPages, name = 'indexInPages'),
url(r'^category/(?P<categoryID>[\d]+)/(?P<pageNo>[\d]+)', views.categoryInPages, name = 'categoryInPages'),
url(r'^category/(?P<categoryID>[\d]+)', views.category, name = 'category'),
url(r'^article/(?P<articleID>[\d]+)', views.article, name = 'article'),
]
我们的设想是,当URL以“category”起始的时候,利用URL中附加的文章分类ID,过滤出属于该分类的文章,以列表的形式显示在页面中。当然,还有一个对应的分页版本。
首先,我们在页头中增加文章分类导航,因为我们已经将页头封装到header.html中,所以我们只需要在header.html模板文件的headerNavSection元素中加入如下代码:
{% for category in categories %}
<span class="categoryItem">
<a class="categoryLink" href="category/{{category.id}}">{{category.name}}</a>
</span>
{% endfor %}
接下来我们需要在views.py中添加相关的视图函数:
def categoryInPages(request, categoryID, pageNo):
template = loader.get_template('blog/index.html')
cat = get_object_or_404(Category, id = categoryID)
categories = Category.objects.all();
articlesInCat = Article.objects.filter(category = categoryID).order_by('-timestamp')
p = Paginator(articlesInCat, 5)
articlesInPage = p.page(pageNo)
context = {
'cat' : cat,
'articles': articlesInPage,
'categories' : categories,
}
return HttpResponse(template.render(context, request))
def category(request, categoryID):
return categoryInPages(request, categoryID, 1)
我们在这里,仍旧使用首页的模板文件index.html,利用ORM的filter功能过滤出传入的categoryID对应的文章分类下的文章,利用分页组件Paginator得到当前分页对应的文章,放入上下文中。
尽管我们将页头封装到单独的模板文件,但是我们还需要在视图函数中将文章分类信息传入上下文,用于模板文件的内容填充。不知道有没有好的方法实现视图函数部分的封装。
我们先把开发服务器跑起来,看看效果。
![](https://img.haomeiwen.com/i3824779/0be8ea53082b742b.jpg)
文章导航显示出来的,我们点击一个文章分类(比如「电影评论」)试试看:
![](https://img.haomeiwen.com/i3824779/e1ade740dc7ae971.jpg)
恩,Django的确把「电影评论」下的文章过滤了出来,显示在网页中,这个,很好很强大。那么,我们来选择另一个文章试试看,比如,「读书笔记」...
![](https://img.haomeiwen.com/i3824779/7ee253f7bea8cfb1.jpg)
我擦...这特么什么鬼?“category/category/3”是几个意思啊?
貌似“<a class="categoryLink" href="category/{{category.id}}">{{category.name}}</a>”只是在当前的URL后面附加“catetory/XXX”,所以在首页上点击是妥妥的,但如果在文章分类列表中点击,就杯具了。
我们可以将其改成绝对路径的形式:
<a class="categoryLink" href="/blog/category/{{category.id}}">{{category.name}}</a>
但是,不要这么做!原因是blog模块应该是可以灵活挂接的,如果在网站中将blog模块挂接为"DogBlog",该绝对路径就会失效,所以,正确的方法应该是使用URL名称构建网页链接地址!使用URL名称构建网页链接地址!使用URL名称构建网页链接地址!重要的事情说三遍,这个问题,大家一定要牢牢记住。
利用URL构建网页链接地址的语法是:
{% url 'URL名称' 参数值1 参数值2 参数值3... %}
其中的URL名称需要与URL配置文件中的“name”属性保持一致。如果参数较多,为保证代码可读性,也可以采用下面的形式:
{% url 'URL名称' 参数名称1=参数值1 参数名称2=参数值2 参数名称3=参数值3... %}
所以,我们应该把header.html模板文件中的链接改成:
<span class="categoryItem">
<a class="categoryLink" href="{% url 'category' category.id %}">{{category.name}}</a>
</span>
我们顺手再加一个首页链接,修改后的header.html应该是这个样子:
{% load static %}
<div id="headerLogoSection">
![]({% static 'blog/images/logo.jpeg' %})
</div>
<div id="headerTitleSection">
<span class="siteTitle">大狗汪叨叨</span>
</div>
<div id="headerNavSection">
<span class="categoryItem">
<a class="categoryLink" href="{% url 'index' %}">首页</a>
</span>
{% for category in categories %}
<span class="categoryItem">
<a class="categoryLink" href="{% url 'category' category.id %}">{{category.name}}</a>
</span>
{% endfor %}
</div>
我们还需要对index.html模板文件进行一些小的改动。
首先,如果是文章分类列表,我们在页面中加入一个标题:
<header>
{% if cat != nil %}
<h2 class="pageTitle">{{cat.name}}</h2>
{% endif %}
</header>
接下来,将模板文件中的文章链接改成URL名称的形式:
<a href="{% url 'article' article.id %}">查看全文</a>
我们还需要修改一下分页部分,在首页和文章分类列表页面将链接指向不同的视图:
{% if articles.has_previous %}
<div style="left:200px;position:relative;width:60px;height:15px;">
{% if categoryID != nil %}
<a href="{% url 'categoryInPages' categoryID=cat.id pageNo=articles.previous_page_number %}"><< 前滚翻</a>
{% else %}
<a href="{% url 'indexInPages' pageNo=articles.previous_page_number %}"><< 前滚翻</a>
{% endif %}
</div>
{% endif %}
{% if articles.has_next %}
<div style="left:340px;top:-15px;position:relative;width:60px;height:15px;">
{% if categoryID != nil %}
<a href="{% url 'categoryInPages' categoryID=cat.id pageNo=articles.next_page_number %}">后滚翻 >></a>
{% else %}
<a href="{% url 'indexInPages' pageNo=articles.next_page_number %}">后滚翻 >></a>
{% endif %}
</div>
{% endif %}
日期归档
我们现在给农科增加按日期归档的功能。我们的设想是在URL中附加年和月,例如利用下面的URL:
http://127.0.0.1:8000/blog/2017/01
可以查看在2017年1月发布的文章。
我们还是从URL配置文件开始:
from django.conf.urls import url
from . import views
urlpatterns = [
url(r'^$', views.index, name = 'index'),
url(r'^(?P<pageNo>[\d]+)',views.indexInPages, name = 'indexInPages'),
url(r'^category/(?P<categoryID>[\d]+)/(?P<pageNo>[\d]+)', views.categoryInPages, name = 'categoryInPages'),
url(r'^category/(?P<categoryID>[\d]+)', views.category, name = 'category'),
url(r'^(?P<archiveYear>[\d]{4})/(?P<archiveMonth>[\d]{2})/(?P<pageNo>[\d]+)', views.archiveInPages, name = 'archiveInPages'),
url(r'^(?P<archiveYear>[\d]{4})/(?P<archiveMonth>[\d]{2})', views.archive, name = 'archive'),
url(r'^article/(?P<articleID>[\d]+)', views.article, name = 'article'),
]
接下来,在views.py中增加相关的视图函数。在编写日期归档的视图函数的时候,大狗纠结了很久要如何表达“在XXXX年XX月发布的文章”这个查询条件,曾经想过声明一个日期范围,即大于该月的第一天并小于下个月的第一天。后来查了下文档,发现ORM中其实已经包含了对应的函数,我们只需要这样:
allArticles = Article.objects.all().filter(timestamp__year=archiveYear).filter(timestamp__month=archiveMonth).order_by('-timestamp')
是不是非常简单呢?函数的其他部分,和文章分类列表的视图函数类似,这里就不赘述了。
我们还需要在sidebar.html模板文件中显示日期归档的链接,为用户提供页面导航。大狗的设想是,只显示发布过文章的月份,比如,如果2016年10月份有发布的文章,那么“2016年10月”就会出现在边栏的列表中,否则,就不会出现在边栏的列表中。同时,在列表中的每个月份后面,还要显示每个月发布的文章数量。
大狗新建了一个模型类,用于表示日期归档列表中的每一个项目:
class archiveItem:
displayText = ''
year = ''
month = ''
articleCount = 0
除了利用ORM中提供的函数取得数据之外,Django中也可以通过SQL语句,直接从数据库中读取数据,这里大狗就采用这种方法,从数据库中查询有文章发布的月份和每个月发布文章的数量。大狗在views.py中添加了如下的方法:
def getArchiveItems():
cursor = connection.cursor()
# Please update this SQL script when migrating to other db
sql = '''select distinct strftime('%Y年%m月',timestamp) postMonth,count(*) articleCount
from blog_article
group by postMonth
order by postMonth desc;'''
cursor.execute(sql)
resultList = []
yearMonthRegEx = re.compile(r'(?P<year>[\d]{4})\D+(?P<month>[\d]{2})\D+')
dataRows = cursor.fetchall()
for row in dataRows:
resultItem = archiveItem()
resultItem.displayText = row[0]
resultItem.articleCount = row[1]
yearMonthMatchObject = re.match(yearMonthRegEx, row[0])
if yearMonthMatchObject is not None:
resultItem.year = yearMonthMatchObject.group('year')
resultItem.month = yearMonthMatchObject.group('month')
resultList.append(resultItem)
return resultList
我们可以通过connection.cursor()获取一个数据库游标,利用数据库游标的execute方法,可以执行SQL语句,调用数据库游标的fetchall方法,就可以获得查询结果,并通过位置索引获取每个字段的值。
就像大狗在注释中写的,因为SQL语句是针对特定数据库引擎的(当然,我们可以按照Ansi92标准编写SQL语句,但是不是所有功能都可以使用Ansi92 SQL表示的),所以,如果我们需要将博客部署至其他数据库引擎,请务必确认其中的SQL语句仍适用。
因为博客中所有的页面都需要呈现日期归档列表,所以,我们需要在视图函数中调用上面的方法,并以键值“archives”将返回值放入上下文。
接下来,我们就要修改sidebar.html模板文件,在其中显示日期归档列表:
{% for archive in archives %}
<div style="width:100%">
<a class="archiveLink" href="{% url 'archive' archiveYear=archive.year archiveMonth=archive.month %}">{{archive.displayText}}</a> ({{archive.articleCount}})
</div>
{% endfor %}
我们在这里依然利用URL名称构建日期归档的链接地址。与上面代码不同的是,我们在这里采用了“参数名称=参数值”的形式,以增强代码的可读性。
恩,我们在本篇教程中涉及到的修改,一致如此。让我们运行开发服务器,看一下效果:
![](https://img.haomeiwen.com/i3824779/cc65c56326fe6b57.jpg)
边栏中已经出现了发布文章的月份和每个月发布的文章数量。让我们点击"2017年1月":
![](https://img.haomeiwen.com/i3824779/6bff6d35724ca489.jpg)
WTF?!这是神马情况?在边栏中已经显示2017年1月发布了6篇文章,怎么会“该页并不包含任何结果”呢?
让我们稍安勿躁,将页面向下滚动至这个位置,点击“Local vars”,查看一下程序运行到这个位置时的本地变量值:
![](https://img.haomeiwen.com/i3824779/ec867e342d6885fe.jpg)
从图中可以看到,视图函数的确取得了文章记录,然而,请注意这里:
pageNo u'2017'
在日期归档视图函数中,并未涉及“pageNo”参数,而且,这个参数也不应该是“2017”.
让我们再看一下URL配置文件:
from django.conf.urls import url
from . import views
urlpatterns = [
url(r'^$', views.index, name = 'index'),
url(r'^(?P<pageNo>[\d]+)',views.indexInPages, name = 'indexInPages'),
url(r'^category/(?P<categoryID>[\d]+)/(?P<pageNo>[\d]+)', views.categoryInPages, name = 'categoryInPages'),
url(r'^category/(?P<categoryID>[\d]+)', views.category, name = 'category'),
url(r'^(?P<archiveYear>[\d]{4})/(?P<archiveMonth>[\d]{2})/(?P<pageNo>[\d]+)', views.archiveInPages, name = 'archiveInPages'),
url(r'^(?P<archiveYear>[\d]{4})/(?P<archiveMonth>[\d]{2})', views.archive, name = 'archive'),
url(r'^article/(?P<articleID>[\d]+)', views.article, name = 'article'),
]
第二项配置是首页的分页视图函数,其中包含了"pageNo"参数。原来页面报错的原因是,Django将我们的请求映射到了首页的分页视图函数,所以年份“2017”被识别成了页码“2017”。解决的办法也非常的简单,因为Django的URL配置是依照从前之后的次序匹配的,我们只需要把日期归档相关的配置提前,就可以了:
from django.conf.urls import url
from . import views
urlpatterns = [
url(r'^$', views.index, name = 'index'),
url(r'^(?P<archiveYear>[\d]{4})/(?P<archiveMonth>[\d]{2})/(?P<pageNo>[\d]+)', views.archiveInPages, name = 'archiveInPages'),
url(r'^(?P<archiveYear>[\d]{4})/(?P<archiveMonth>[\d]{2})', views.archive, name = 'archive'),
url(r'^(?P<pageNo>[\d]+)',views.indexInPages, name = 'indexInPages'),
url(r'^category/(?P<categoryID>[\d]+)/(?P<pageNo>[\d]+)', views.categoryInPages, name = 'categoryInPages'),
url(r'^category/(?P<categoryID>[\d]+)', views.category, name = 'category'),
url(r'^article/(?P<articleID>[\d]+)', views.article, name = 'article'),
]
我们刷新一下页面,就可以看到效果了:
![](https://img.haomeiwen.com/i3824779/993b4272ac8320cd.jpg)
篇幅所限,大狗无法在教程中包含所有的程序代码,完整的代码请访问该教程的GitHub源码库:
https://github.com/mingyeh/DjangoQuickTour
如果教程中有错误或不妥之处,请大家拨冗指出。如果喜欢我的教程,请用一秒钟点击下面的“喜欢”按钮,您的小小付出可以让这篇教程被更多人看到,谢谢!
[1]:https://en.wikipedia.org/wiki/Foreign_key "Foreign key""
网友评论