在本講中,我們開始首頁功能的開發,在開發過程中,大家将會學習到Django中的通用視圖類、分頁對象paginator以及foreignKey外鍵的使用。
效果示範
整體功能
大家可先通過
網站示範位址浏覽一下首頁的效果。我們首頁呢,比較簡潔大方,讓人一目了然。我這樣設計的目的呢,是讓大家把精力放到學習django上面來,不必過度關注花哨的頁面效果。
我們把首頁拆解為4個小的業務子產品來開發,分别是:清單顯示、分頁功能、搜尋功能、分類功能。下面我們分别對這四個功能子產品進行開發講解。
開發思路
開發一個功能的基本思路是:先建立應用,然後分析功能涉及到哪些業務,進而分析出需要的資料庫字段,然後編寫模型,之後就是展示階段,通過url路由配置視圖函數,來将模型裡面的資料顯示出來。
ok,我們通過指令建立應用,命名為video。執行後,django将為我們建立video檔案夾。
python3 manage.py startapp video
下面的功能子產品開發都在該應用(video)下進行。
模組化型
此處,我們需要建立兩個模型,分别是分類表(classification)和視訊表(video)。他們是多對一的關系(一個分類對應多個視訊,一個視訊對應一個分類)。
首先編寫Classification表,在model.py下面,我們鍵入如下代碼。
字段有title(分類名稱)和status(是否啟用)
class Classification(models.Model):
list_display = ("title",)
title = models.CharField(max_length=100,blank=True, null=True)
status = models.BooleanField(default=True)
class Meta:
db_table = "v_classification"
字段說明
- title 分類名稱。資料類型是CharField,最大長度為max_length=100,允許為空null=True
- status 是否啟用。資料類型是BooleanField,預設為default=True
- db_table 表名
然後編寫Video模型,根據網站業務,我們設定了title(标題)、 desc(描述)、 classification(分類)、file(視訊檔案)、cover(封面)、status(釋出狀态)等字段。其中classification是一個ForeignKey外鍵字段,表示一個分類對應多個視訊,一個視訊對應一個分類(多對一)
class Video(models.Model):
STATUS_CHOICES = (
('0', '釋出中'),
('1', '未釋出'),
)
title = models.CharField(max_length=100,blank=True, null=True)
desc = models.CharField(max_length=255,blank=True, null=True)
classification = models.ForeignKey(Classification, on_delete=models.CASCADE, null=True)
file = models.FileField(max_length=255)
cover = models.ImageField(upload_to='cover/',blank=True, null=True)
status = models.CharField(max_length=1 ,choices=STATUS_CHOICES, blank=True, null=True)
create_time = models.DateTimeField(auto_now_add=True, blank=True, max_length=20)
- title 視訊标題。資料類型是charField,最大長度為max_length=100,允許為空null=True
- desc 視訊描述。資料類型是charField,最大長度為max_length=255,允許為空null=True
- file 視訊檔案位址。資料類型是fileField。其中存的是視訊檔案的位址,在之後的視訊管理中我們将會對視訊的上傳進行具體的講解。
- cover 視訊封面。資料類型是ImageField。存儲目錄為upload_to='cover/',允許為空null=True
- status 視訊狀态。是一個選擇狀态,用choices設定多選元祖。
- create_time 建立時間。資料類型是DateTimeField 。設定自動生成時間auto_now_add=True
ForeignKey 表明一種一對多的關聯關系。比如這裡我們的視訊和分類的關系,一個視訊隻能對應一個分類,而一個分類下可以有多個視訊。
更多關于ForeinkKey的說明,可以參看
ForeignKey官方介紹清單顯示
要想通路到首頁,必須先配置好路由。在video下建立urls.py檔案,寫入如下代碼
from django.urls import path
from . import views
app_name = 'video'
urlpatterns = [
path('index', views.IndexView.as_view(), name='index'),
]
一條path語句就代表一條路由資訊。這樣我們就可以在浏覽器輸入127.0.0.1:8000/video/index來通路首頁了。
顯示清單資料非常簡單,我們使用django中内置的視圖模版類ListView來顯示,首先在view.py中編寫IndexView類,用它來顯示清單資料。鍵入如下代碼
class IndexView(generic.ListView):
model = Video
template_name = 'video/index.html'
context_object_name = 'video_list'
此處,我們使用了django提供的通用視圖類ListView, ListView使用很簡單,隻需要我們簡單的配置幾行代碼,即可将資料庫裡面的資料渲染到前端。比如上述代碼中,我們配置了
- model = Video, 作用于Video模型
- template_name = 'video/index.html' ,告訴ListView要使用我們已經建立的模版檔案。
- context_object_name = 'video_list' ,上下文變量名,告訴ListView,在前端模版檔案中,可以使用該變量名來展現資料。
之後,我們在templates檔案夾下,建立video目錄,用來存放視訊相關的模闆檔案,首先我們建立首頁檔案index.html。并将剛才擷取到的資料顯示出來。
<div class="ui grid">
{% for item in video_list %}
<div class="four wide column">
<div class="ui card">
<a class="image">
{% thumbnail item.cover "300x200" crop="center" as im %}
<img class="ui image" src="{{ im.url }}">
{% empty %}
{% endthumbnail %}
<i class="large play icon v-play-icon"></i>
</a>
<div class="content">
<a class="header">{{ item.title }}</a>
<div class="meta">
<span class="date">釋出于{{ item.create_time|time_since}}</span>
</div>
<div class="description">
{{ item.view_count}}次觀看
</div>
</div>
</div>
</div>
{% empty %}
<h3>暫無資料</h3>
{% endfor %}
</div>
通過for循環,将video_list渲染到前端。這裡我們使用到了django中的
内置标簽,比如for語句、empty語句。這些都是django中非常常用的語句。在之後的教程中我們會經常遇到。
另外,還使用了thumbnail标簽來顯示圖檔,
thumbnail是一個很常用的python庫,常常被用來做圖檔顯示。
顯示結果如下
分類功能
在寫分類功能之前,我們先學習一個回調函數 get_context_data() 這是ListView視圖類中的一個函數,在 get_context_data() 函數中,可以傳一些額外内容到模闆。是以我們可以使用該函數來傳遞分類資料。
要使用它,很簡單。
隻需要在IndexView類下面,追加get_context_data()的實作即可。
class IndexView(generic.ListView):
model = Video
template_name = 'video/index.html'
context_object_name = 'video_list'
def get_context_data(self, *, object_list=None, **kwargs):
context = super(IndexView, self).get_context_data(**kwargs)
classification_list = Classification.objects.filter(status=True).values()
context['classification_list'] = classification_list
return context
在上述代碼中,我們将分類資料通過Classification.objects.filter(status=True).values()從資料庫裡面過濾出來,然後賦給classification_list,最後放到context字典裡面。
在前端模闆(templates/video/index.html)中,就可以通過classification_list來取資料。添加代碼
<div class="classification">
<a class="ui red label" href="">全部</a>
{% for item in classification_list %}
<a class="ui label" href="">{{ item.title }}</a>
{% endfor %}
</div>
顯示效果如下
當然現在隻是實作了分類展示效果,我們還需要繼續實作點選效果,即點選不同的分類,顯示不同的視訊清單。
我們先給每個分類按鈕加上href連結
<div class="classification">
<a class="ui red label" href="{% url 'home' %}">全部</a>
{% for item in classification_list %}
<a class="ui label" href="?c={{ item.id }}">{{ item.title }}</a>
{% endfor %}
</div>
通過添加?c={{ item.id }} 這裡用c代表分類的id,點選後,會傳到視圖類中,在視圖類中,我們使用 get_queryset() 函數,将get資料取出來。通過self.request.GET.get("c", None) 賦給c,判斷c是否為None,如果為None,就響應全部,如果有值,就通過get_object_or_404(Classification, pk=self.c)先擷取目前類,然後classification.video_set擷取外鍵資料。
def get_queryset(self):
self.c = self.request.GET.get("c", None)
if self.c:
classification = get_object_or_404(Classification, pk=self.c)
return classification.video_set.all().order_by('-create_time')
else:
return Video.objects.filter(status=0).order_by('-create_time')
更多關于ForeignKey的使用方法,可參考 這裡
分頁功能
在Django中,有現成的分頁解決方案,我們開發者省了不少事情。如果是簡單的分頁,隻需要配置一下paginate_by即可實作。
class IndexView(generic.ListView):
model = Video
template_name = 'video/index.html'
context_object_name = 'video_list'
paginate_by = 12
c = None
- painate_by = 12每頁顯示12條
這樣每頁的分頁資料就能正确的顯示出來來,現在來完善底部的頁碼條。
頁碼清單需要視圖類和模闆共同來完成,我們先來寫視圖類。在前面我們已經寫過get_context_data了,該函數的主要功能就是傳遞額外的資料給模闆。這裡,我們就利用get_context_data來傳遞頁碼資料。
我們先定義一個工具函數,叫get_page_list。 在項目根目錄下,建立一個檔案helpers.py該檔案當作一個全局的工具類,用來存放各種工具函數。把get_page_list放到helpers.py裡面 該函數用來生産頁碼清單,不但這裡可以使用,以後在其他地方也可以調用該函數。
def get_page_list(paginator, page):
page_list = []
if paginator.num_pages > 10:
if page.number <= 5:
start_page = 1
elif page.number > paginator.num_pages - 5:
start_page = paginator.num_pages - 9
else:
start_page = page.number - 5
for i in range(start_page, start_page + 10):
page_list.append(i)
else:
for i in range(1, paginator.num_pages + 1):
page_list.append(i)
return page_list
分頁邏輯:
if 頁數>=10:
目前頁<=5時,起始頁為1
目前頁>(總頁數-5)時,起始頁為(總頁數-9)
其他情況 起始頁為(目前頁-5)
舉例:
假設一共16頁
情況1: 目前頁==5 則頁碼清單為[1,2,3,4,5,6,7,8,9,10]
情況2: 目前頁==8 則頁碼清單為[3,4,5,6,7,8,9,10,11,12]
情況3: 目前頁==15 則頁碼清單為[7,8,9,10,11,12,13,14,15,16]
當然你看到這個邏輯會有點亂,建議大家讀着代碼,多試驗幾遍。
當拿到頁碼清單,我們繼續改寫get_context_data()函數。 将擷取到的classification_list追加到context字典中。
def get_context_data(self, *, object_list=None, **kwargs):
context = super(IndexView, self).get_context_data(**kwargs)
paginator = context.get('paginator')
page = context.get('page_obj')
page_list = get_page_list(paginator, page)
classification_list = Classification.objects.filter(status=True).values()
context['c'] = self.c
context['classification_list'] = classification_list
context['page_list'] = page_list
return context
你或許對 paginator = context.get('paginator') page = context.get('page_obj')這兩行代碼感到陌生,我們隻需要知道context.get('page_obj')傳回的是目前頁碼,context.get('paginator')傳回的是分頁對象,就夠了。更加詳細的介紹,可參考 官方 。
當資料傳遞給模闆之後,模闆就負責顯示出來就行了。
因為分頁功能比較常用,是以需要把它單獨拿出來封裝到一個單獨的檔案中,我們建立templates/base/page_nav.html檔案。然後在index.html裡面我們将該檔案include進來。
{% include "base/page_nav.html" %}
打開page_nav.html,寫入代碼
{% if is_paginated %}
<div class="video-page">
<div class="ui circular labels">
{% if page_obj.has_previous %}
<a class="ui circular label" href="?page={{ page_obj.previous_page_number }}{% if c %}&c={{c}}{% endif %}{% if q %}&q={{q}}{% endif %}"><</a>
{% endif %}
{% for i in page_list %}
{% if page_obj.number == i %}
<a class="ui red circular label">{{ i }}</a>
{% else %}
<a class="ui circular label" href="?page={{ i }}{% if c %}&c={{c}}{% endif %}{% if q %}&q={{q}}{% endif %}">{{ i }}</a>
{% endif %}
{% endfor %}
{% if page_obj.has_next %}
<a class="ui circular label" href="?page={{ page_obj.next_page_number }}{% if c %}&c={{c}}{% endif %}{% if q %}&q={{q}}{% endif %}">></a>
{% endif %}
</div>
</div>
{% endif %}
上面代碼中,我們用到了page_obj對象的幾個屬性:has_previous、previous_page_number、next_page_number。通過這幾個屬性,即可實作複雜的頁碼顯示效果。其中我們還這href裡面加了
{% if c %}&c={{c}}
代表分類的id。
搜尋功能
要實作搜尋,我們需要一個搜尋框
因為搜尋框是很多頁面都需要的,是以我們把代碼寫到templates/base/header.html檔案裡面。
<div class="ui small icon input v-video-search">
<input class="prompt" value="{{ q }}" type="text" placeholder="搜尋視訊" id="v-search">
<i id="search" class="search icon" style="cursor:pointer;"></i>
</div>
點選搜尋或回車的代碼寫在了static/js/header.js裡面。
我們還需要配置一下路由,添加一行搜尋的路由。
app_name = 'video'
urlpatterns = [
path('index', views.IndexView.as_view(), name='index'),
path('search/', views.SearchListView.as_view(), name='search'),
]
搜尋路由指向的視圖類為SearchListView
下面我們來寫SearchListView的代碼
class SearchListView(generic.ListView):
model = Video
template_name = 'video/search.html'
context_object_name = 'video_list'
paginate_by = 8
q = ''
def get_queryset(self):
self.q = self.request.GET.get("q","")
return Video.objects.filter(title__contains=self.q).filter(status=0)
def get_context_data(self, *, object_list=None, **kwargs):
context = super(SearchListView, self).get_context_data(**kwargs)
paginator = context.get('paginator')
page = context.get('page_obj')
page_list = get_page_list(paginator, page)
context['page_list'] = page_list
context['q'] = self.q
return context
關鍵代碼就是Video.objects.filter(title__contains=self.q).filter(status=0)
title__contains是包含的意思,表示查詢title包含q的記錄。利用filter将資料過濾出來。這裡寫了兩層過濾,第一層過濾搜尋關鍵詞,第二層過濾status已釋出的視訊。
另外,這裡也用到了get_context_data來存放額外的資料,包括分頁資料、q關鍵詞。
配置模闆檔案是templates/video/search.html
是以模闆代碼寫在search.html裡面
<div class="ui unstackable items">
{% for item in video_list %}
<div class="item">
<div class="ui tiny image">
{% thumbnail item.cover "300x200" crop="center" as im %}
<img class="ui image" src="{{ im.url }}">
{% empty %}
{% endthumbnail %}
</div>
<div class="middle aligned content">
<a class="header" href="{% url 'video:detail' item.pk %}">{{ item.title }}</a>
</div>
</div>
{% empty %}
<h3>暫無資料</h3>
{% endfor %}
</div>
{% include "base/page_nav.html" %}
搜尋功能效果