美文网首页大狗学python程序员Python 运维
Django学习笔记6:文章分类与日期归档

Django学习笔记6:文章分类与日期归档

作者: 2c99f2da2ca0 | 来源:发表于2017-02-26 17:06 被阅读204次

文章分类

在本篇教程中,大狗打算给我们的博客加入文章分类日期归档功能,同时,按照HTML5的相关标准对模板进行一些调整,初步的计划是把首页改成这个样子:

首页效果

在原有页头的基础上增加一个文章分类导航,在右边加入一个边栏(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)

现在,我们就可以在后台管理页面中添加文章分类了,大狗添了下面几项:

文章分类

然后在后台管理页面中为每篇文章指定了所属的分类。

页面模板与功能封装

接下来,我们就可以按照文章开头的效果图,在博客中加入新的功能了。但是在开工之前,大狗发现其实效果图中的很多功能,在多个页面中都会用到,比如页头(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得到当前分页对应的文章,放入上下文中。

尽管我们将页头封装到单独的模板文件,但是我们还需要在视图函数中将文章分类信息传入上下文,用于模板文件的内容填充。不知道有没有好的方法实现视图函数部分的封装。

我们先把开发服务器跑起来,看看效果。

首页

文章导航显示出来的,我们点击一个文章分类(比如「电影评论」)试试看:

电影评论

恩,Django的确把「电影评论」下的文章过滤了出来,显示在网页中,这个,很好很强大。那么,我们来选择另一个文章试试看,比如,「读书笔记」...

报错了

我擦...这特么什么鬼?“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>&nbsp;({{archive.articleCount}})
</div>
{% endfor %}

我们在这里依然利用URL名称构建日期归档的链接地址。与上面代码不同的是,我们在这里采用了“参数名称=参数值”的形式,以增强代码的可读性。

恩,我们在本篇教程中涉及到的修改,一致如此。让我们运行开发服务器,看一下效果:

日期归档列表

边栏中已经出现了发布文章的月份和每个月发布的文章数量。让我们点击"2017年1月":

日期归档列表错误

WTF?!这是神马情况?在边栏中已经显示2017年1月发布了6篇文章,怎么会“该页并不包含任何结果”呢?

让我们稍安勿躁,将页面向下滚动至这个位置,点击“Local vars”,查看一下程序运行到这个位置时的本地变量值:

本地变量

从图中可以看到,视图函数的确取得了文章记录,然而,请注意这里:

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'),
]

我们刷新一下页面,就可以看到效果了:

日期归档

篇幅所限,大狗无法在教程中包含所有的程序代码,完整的代码请访问该教程的GitHub源码库:

https://github.com/mingyeh/DjangoQuickTour

如果教程中有错误或不妥之处,请大家拨冗指出。如果喜欢我的教程,请用一秒钟点击下面的“喜欢”按钮,您的小小付出可以让这篇教程被更多人看到,谢谢!

[1]:https://en.wikipedia.org/wiki/Foreign_key "Foreign key""

相关文章

网友评论

    本文标题:Django学习笔记6:文章分类与日期归档

    本文链接:https://www.haomeiwen.com/subject/ccaowttx.html