[TOC]
Django的MVC模式/MTV模式
Django緊緊地遵循MVC模式和敬,可以稱得上是一種MVC框架朝捆。 以下是Django中M炕贵、V和C各自的含義:
- M:數(shù)據(jù)存取部分护糖,由django數(shù)據(jù)庫(kù)層處理晶疼;
- V:選擇顯示哪些數(shù)據(jù)要顯示以及怎樣顯示的部分,由視圖和模板處理吧恃;
- C:根據(jù)用戶輸入委派視圖的部分臊泰,由Django框架根據(jù)URLconf設(shè)置,對(duì)給定URL調(diào)用適當(dāng)?shù)腜ython函數(shù)蚜枢。
由于C由框架自行處理缸逃,而Django里更關(guān)注的是模型(Model)、模板(Template)和視圖(Views)厂抽,Django也被稱為MTV框架需频。在MTV開發(fā)模式中:
- M:代表模型(Model),即數(shù)據(jù)存取層筷凤。該層處理與數(shù)據(jù)相關(guān)的所有事務(wù):如何存取昭殉、如何驗(yàn)證有效性、包含哪些行為以及數(shù)據(jù)之間的關(guān)系等藐守;
- T:代表模板(Template)挪丢,即表現(xiàn)層。該層處理與表現(xiàn)相關(guān)的決定:如何在頁(yè)面或其他類型文檔中進(jìn)行顯示卢厂。
- V:代表視圖(View)乾蓬,即業(yè)務(wù)邏輯層。該層包含存取模型及調(diào)取恰當(dāng)模板的相關(guān)邏輯慎恒。你可以把它看作模型與模板之間的橋梁任内。
(以上摘自《The Django Book》)
完整的開發(fā)過(guò)程
1. 創(chuàng)建開發(fā)環(huán)境
mkvirtualenv myblog # 創(chuàng)建虛擬環(huán)境
pip install django # 安裝最新版的django-1.10.6
2. 創(chuàng)建項(xiàng)目和app
django-admin.py startproject myblog
django-admin.py startapp blog # 一個(gè)項(xiàng)目可以包含多個(gè)app
當(dāng)前目錄結(jié)構(gòu)如下圖所示
目錄結(jié)構(gòu)介紹
- blog: app的目錄
- migrations: 包含對(duì)模型定義與修改的遷移記錄;
- admin.py: 對(duì) Django 站點(diǎn)管理的定制融柬;
- apps.py: 包含對(duì) App 的配置死嗦;
- models.py: 應(yīng)包含定義的模型;
- tests.py: 包含單元測(cè)試粒氧;
- views.py: 應(yīng)包含各種視圖越除。
- myblog: 整個(gè)項(xiàng)目的配置目錄
- settings.py: 項(xiàng)目的配置文件
- urls.py: 總的urls配置文件
- **wsgi.py **: 部署服務(wù)器時(shí)用到的文件
新定義的app加到settings.py中的INSTALL_APPS中
編輯setting文件,如下圖
3. 連接數(shù)據(jù)庫(kù)
暫時(shí)先用默認(rèn)的sqlite3數(shù)據(jù)庫(kù),不需要特別的配置摘盆。以后需要再改成別的數(shù)據(jù)庫(kù)翼雀。
4. 建立模型
- 文章模型
class Article(models.Model):
STATUS = (
('0', '發(fā)布'),
('1', '草稿')
)
title = models.CharField(max_length=64, unique=True, verbose_name='標(biāo)題')
abstract = models.TextField(verbose_name='摘要', max_length=54, blank=True, null=True, help_text="可選項(xiàng),若為空格則摘取正文前54個(gè)字符")
body = models.TextField(verbose_name='內(nèi)容')
# on_delete 當(dāng)指向的表被刪除時(shí)骡澈,將該項(xiàng)設(shè)為空
category = models.ForeignKey('Category', verbose_name='分類', null=True, on_delete=models.SET_NULL)
tags = models.ManyToManyField('Tag', verbose_name='標(biāo)簽', blank=True)
url = models.CharField(max_length=255, verbose_name='鏈接', unique=True)
status = models.CharField(default='0', max_length=1, choices=STATUS, verbose_name='文章狀態(tài)')
created_time = models.DateTimeField(auto_now_add=True, verbose_name='創(chuàng)建時(shí)間')
last_modified_time = models.DateTimeField(auto_now=True, verbose_name='最近修改時(shí)間')
class Meta:
# Meta 包含一系列選項(xiàng)锅纺,這里的ordering表示排序, - 表示逆序
# 即當(dāng)從數(shù)據(jù)庫(kù)中取出文章時(shí)掷空,以文章最后修改時(shí)間逆向排序
verbose_name = '文章(Article)'
verbose_name_plural = verbose_name
ordering = ['-last_modified_time']
def __str__(self):
return self.title
- 分類模型
class Category(models.Model):
name = models.CharField(max_length=20, verbose_name='類名')
created_time = models.DateTimeField(auto_now_add=True, verbose_name='創(chuàng)建時(shí)間')
last_modified_time = models.DateTimeField(auto_now=True, verbose_name='最近修改時(shí)間')
class Meta:
verbose_name = '分類(Category)'
verbose_name_plural = verbose_name
ordering = ['-created_time']
def __str__(self):
return self.name
- 標(biāo)簽?zāi)P?/li>
class Tag(models.Model):
name = models.CharField(max_length=20, verbose_name='標(biāo)簽名')
created_time = models.DateTimeField(auto_now_add=True, verbose_name='創(chuàng)建時(shí)間')
last_modified_time = models.DateTimeField(auto_now=True, verbose_name='最近修改時(shí)間')
class Meta:
verbose_name = '標(biāo)簽(Tag)'
verbose_name_plural = verbose_name
ordering = ['-created_time']
def __str__(self):
return self.name
- 評(píng)論模型
class Comment(models.Model):
user_name = models.CharField(max_length=64, verbose_name='評(píng)論者名字')
content = models.TextField(verbose_name='評(píng)論內(nèi)容')
created_time = models.DateTimeField(auto_now_add=True, verbose_name='評(píng)論時(shí)間')
article = models.ForeignKey('Article', verbose_name='評(píng)論所屬文章', on_delete=models.CASCADE)
def __str__(self):
return self.content[:20]
模型只是利用 Django 提供的 ORM 完成對(duì)實(shí)際表結(jié)構(gòu)的映射肋殴,因此在完成模型定義后,我們需要將其真正同步到實(shí)際的數(shù)據(jù)庫(kù)中去坦弟。新版本的 Django 中护锤,該操作需要分成兩步,
- 生成 migrations:
python manage.py makemigrations blog
- 進(jìn)行數(shù)據(jù)庫(kù)同步
python manage.py migrate
可以用python manage.py shell
進(jìn)入django shell對(duì)數(shù)據(jù)庫(kù)進(jìn)行操作
如下圖所示
5. 設(shè)置后臺(tái)管理
from django.contrib import admin
from .models import Article, Category, BlogComment, Tag
# Register your models here.
admin.site.register([Article, Category, BlogComment, Tag])
6. 設(shè)置分頁(yè)
Django提供了一些類來(lái)幫助你管理分頁(yè)的數(shù)據(jù)酿傍,這些類位于django/core/paginator.py中烙懦。
分頁(yè)需要使用到的的 API ,Django 官方文檔對(duì)此有十分詳細(xì)的介紹赤炒,請(qǐng)參考django文檔中分頁(yè)教程氯析。
盡管可以把分頁(yè)邏輯直接寫在視圖內(nèi),但是為了通用性莺褒,我們使用一點(diǎn)點(diǎn) Django 更加高級(jí)的技巧——模板標(biāo)簽(TemplateTags)掩缓。
模板標(biāo)簽介紹
為了使用模板標(biāo)簽,Django 要求我們先建立一個(gè) templatetags 文件夾遵岩,并在里面加上 init.py文件以指示 python 這是一個(gè)模塊(python 把含有該問(wèn)價(jià)的文件夾當(dāng)做一個(gè)模塊你辣,具體請(qǐng)參考任何一個(gè)關(guān)于 python 模塊的教程)。并且 templatetags 文件夾和你的 model.py尘执,views.py 文件是同級(jí)的舍哄,也就是說(shuō)你的目錄結(jié)構(gòu)看起來(lái)應(yīng)該是這樣:
app/
__init__.py
models.py
templatetags/
__init__.py
app_extras.py
views.py
分頁(yè)代碼
首先來(lái)回顧一下 Django 的模板系統(tǒng)是如何工作的,回想一下視圖函數(shù)的工作流程誊锭,視圖函數(shù)接收一個(gè) Http 請(qǐng)求表悬,經(jīng)過(guò)一系列處理,通常情況下其會(huì)渲染某個(gè)模板文件丧靡,把模板文件中的一些用 {{ }} 包裹的變量替換成從該視圖函數(shù)中相應(yīng)變量的值签孔。事實(shí)上在此過(guò)程中 Django 悄悄幫我們做了一些事情,它把視圖函數(shù)中的變量的值封裝在了一個(gè) Context (一般翻譯成上下文)對(duì)象中窘行,只要模板文件中的變量在 Context 中有對(duì)應(yīng)的值饥追,它就會(huì)被相應(yīng)的值替換。
因此罐盔,我們的程序可以這樣做:首先把取到的文章列表(官方術(shù)語(yǔ)是一個(gè) queryset)分頁(yè)但绕,用戶請(qǐng)求第幾頁(yè),我們就把第幾頁(yè)的文章列表傳遞給模板文件;另外還要根據(jù)上面的需求傳遞頁(yè)碼值給模板文件捏顺,這樣只要把模板文件中的變量替換成我們傳遞過(guò)去的值六孵,那么就達(dá)到本文開篇處那樣的分頁(yè)顯示效果了。
paginate.py
from django import template
from django.core.paginator import Paginator, PageNotAnInteger, EmptyPage
// 這是分頁(yè)功能涉及的一些類和異常幅骄,官方文檔對(duì)此有詳細(xì)介紹劫窒。當(dāng)然從命名也可以直接看出它們的用途:Paginator(分頁(yè)),PageNotAnInteger(頁(yè)碼不是一個(gè)整數(shù)異常)拆座,EmptyPage(空的頁(yè)碼號(hào)異常)
register = template.Library()
// 這是定義模板標(biāo)簽要用到的
@register.simple_tag(takes_context=True)
// 這個(gè)裝飾器表明這個(gè)函數(shù)是一個(gè)模板標(biāo)簽主巍,takes_context = True 表示接收上下文對(duì)象,就是前面所說(shuō)的封裝了各種變量的 Context 對(duì)象挪凑。
def paginate(context, object_list, page_count):
//context是Context 對(duì)象孕索,object_list是你要分頁(yè)的對(duì)象,page_count表示每頁(yè)的數(shù)量
left = 3 # 當(dāng)前頁(yè)碼左邊顯示幾個(gè)頁(yè)碼號(hào) -1躏碳,比如3就顯示2個(gè)
right = 3 # 當(dāng)前頁(yè)碼右邊顯示幾個(gè)頁(yè)碼號(hào) -1
paginator = Paginator(object_list, page_count) # 通過(guò)object_list分頁(yè)對(duì)象
page = context['request'].GET.get('page') # 從 Http 請(qǐng)求中獲取用戶請(qǐng)求的頁(yè)碼號(hào)
try:
object_list = paginator.page(page) # 根據(jù)頁(yè)碼號(hào)獲取第幾頁(yè)的數(shù)據(jù)
context['current_page'] = int(page) # 把當(dāng)前頁(yè)封裝進(jìn)context(上下文)中
pages = get_left(context['current_page'], left, paginator.num_pages) + get_right(context['current_page'], right, paginator.num_pages)
// 調(diào)用了兩個(gè)輔助函數(shù)搞旭,根據(jù)當(dāng)前頁(yè)得到了左右的頁(yè)碼號(hào),比如設(shè)置成獲取左右兩邊2個(gè)頁(yè)碼號(hào)菇绵,那么假如當(dāng)前頁(yè)是5肄渗,則 pages = [3,4,5,6,7],當(dāng)然一些細(xì)節(jié)需要處理,比如如果當(dāng)前頁(yè)是2咬最,那么獲取的是pages = [1,2,3,4]
except PageNotAnInteger:
// 異常處理翎嫡,如果用戶傳遞的page值不是整數(shù),則把第一頁(yè)的值返回給他
object_list = paginator.page(1)
context['current_page'] = 1 # 當(dāng)前頁(yè)是1
pages = get_right(context['current_page'], right, paginator.num_pages)
except EmptyPage:
// 如果用戶傳遞的 page 值是一個(gè)空值丹诀,那么把最后一頁(yè)的值返回給他
object_list = paginator.page(paginator.num_pages)
context['current_page'] = paginator.num_pages # 當(dāng)前頁(yè)是最后一頁(yè)钝的,num_pages的值是總分頁(yè)數(shù)
pages = get_left(context['current_page'], left, paginator.num_pages)
context['article_list'] = object_list # 把獲取到的分頁(yè)的數(shù)據(jù)封裝到上下文中
context['pages'] = pages # 把頁(yè)碼號(hào)列表封裝進(jìn)去
context['last_page'] = paginator.num_pages # 最后一頁(yè)的頁(yè)碼號(hào)
context['first_page'] = 1 # 第一頁(yè)的頁(yè)碼號(hào)為1
try:
// 獲取 pages 列表第一個(gè)值和最后一個(gè)值,主要用于在是否該插入省略號(hào)的判斷铆遭,在模板文件中將會(huì)體會(huì)到它的用處硝桩。注意這里可能產(chǎn)生異常,因?yàn)閜ages可能是一個(gè)空列表枚荣,比如本身只有一個(gè)分頁(yè)碗脊,那么pages就為空,因?yàn)槲覀冇肋h(yuǎn)不會(huì)獲取頁(yè)碼為1的頁(yè)碼號(hào)(至少有1頁(yè)橄妆,1的頁(yè)碼號(hào)已經(jīng)固定寫在模板文件中)
context['pages_first'] = pages[0]
context['pages_last'] = pages[-1] + 1
// +1的原因是為了方便判斷衙伶,在模板文件中將會(huì)體會(huì)到其作用。
except IndexError:
context['pages_first'] = 1 # 發(fā)生異常說(shuō)明只有1頁(yè)
context['pages_last'] = 2 # 1 + 1 后的值
return '' # 必須加這個(gè)害碾,否則首頁(yè)會(huì)顯示個(gè)None
def get_left(current_page, left, num_pages):
"""
輔助函數(shù)矢劲,獲取當(dāng)前頁(yè)碼的值得左邊兩個(gè)頁(yè)碼值,要注意一些細(xì)節(jié)慌随,比如不夠兩個(gè)那么最左取到2芬沉,為了方便處理躺同,包含當(dāng)前頁(yè)碼值,比如當(dāng)前頁(yè)碼值為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):
"""
輔助函數(shù)蹋艺,獲取當(dāng)前頁(yè)碼的值得右邊兩個(gè)頁(yè)碼值,要注意一些細(xì)節(jié)黄刚,比如不夠兩個(gè)那么最右取到最大頁(yè)碼值捎谨。不包含當(dāng)前頁(yè)碼值。比如當(dāng)前頁(yè)碼值為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 %} # 判斷是否還有上一頁(yè)涛救,有的話要顯示一個(gè)上一頁(yè)按鈕
<a class="previous-page" href="?page={{ article_list.previous_page_number }}">
<span class="icon-previous"></span>上一頁(yè)
</a>
{% endif %}
# 頁(yè)碼號(hào)為1永遠(yuǎn)顯示
{% if first_page == current_page %} # 當(dāng)前頁(yè)就是第一頁(yè)
<span class="first-page current">1</span>
{% else %} # 否則的話,第一頁(yè)是可以點(diǎn)擊的埋同,點(diǎn)擊后通過(guò)?page=1的形式把頁(yè)碼號(hào)傳遞給視圖函數(shù)
<a href="?page=1" class="first-page">1</a>
{% endif %}
{% if pages_first > 2 %} # 2以前的頁(yè)碼號(hào)要被顯示成省略號(hào)了
<span>...</span>
{% endif %}
{% for page in pages %} # 通過(guò)for循環(huán)把pages中的值顯示出來(lái)
{% if page == current_page %} # 是否當(dāng)前頁(yè)州叠,按鈕會(huì)顯示不同的樣式
<span class="current">{{ page }}</span>
{% else %}
<a href="?page={{ page }}">{{ page }}</a>
{% endif %}
{% endfor %}
# pages最后一個(gè)值+1的值小于最大頁(yè)碼號(hào)棵红,說(shuō)明有頁(yè)碼號(hào)需要被省略號(hào)替換
{% if pages_last < last_page %}
<span>...</span>
{% endif %}
# 永遠(yuǎn)顯示最后一頁(yè)的頁(yè)碼號(hào)凶赁,如果只有一頁(yè)則前面已經(jīng)顯示了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 %}
# 還有下一頁(yè),則顯示一個(gè)下一頁(yè)按鈕
{% if article_list.has_next %}
<a class="next-page" href="?page={{ article_list.next_page_number }}">
下一頁(yè)<span class="icon-next"></span>
</a>
{% endif %}
</div>
至此逆甜,整個(gè)分頁(yè)功能就完成了虱肄。