通過建立部落格學習Django-3
本篇文章主要對追夢人物的部落格《使用 django 開發一個個人部落格》進行總結
https://www.zmrenwu.com/courses/hellodjango-blog-tutorial/materials/70/
頁面側邊欄:使用自定義模板標籤
使用模板標籤的解決思路
- 在模板中寫入
{% show_recent_posts %}
,模板會渲染一個最新文章列表頁面 - 模板使用 for 模板標籤迴圈文章列表變數,從而展示文章
- 從資料庫獲取文章列表的操作在模板中通過自定義的
{% show_recent_posts %}
模板標籤進行
模板標籤目錄結構
- blog 應用下建立templatetags 包(Python Package = Directory +
__init__.py
- 在 templatetags 目錄下建立 blog_extras.py 檔案,存放自定義的模板標籤程式碼
blog\
__init__.py
admin.py
apps.py
migrations\
__init__.py
models.py
static\
templatetags\
__init__.py
blog_extras.py
tests.py
views.py
編寫模板標籤程式碼
最新文章模板標籤
# blog\templatetags\blog_extras.py ------------------------------------ from django import template from ..models import Post, Category, Tag register = template.Library() @register.inclusion_tag('blog/inclusions/_recent_posts.html', takes_context=True) def show_recent_posts(context, num=5): return { 'recent_post_list': Post.objects.all().order_by('-created_time')[:num], }
匯入 template 模組,例項化一個 template.Library
類,將函式 show_recent_posts
裝飾為 register.inclusion_tag
- 指函式
show_recent_posts
是自定義的型別為 inclusion_tag 的模板標籤 - inclusion_tag 模板標籤返回字典值作為模板變數,傳入 inclusion_tag 裝飾器第一個引數指定的模板
inclusion_tag 裝飾器的引數 takes_context
設定為 True
- 指在渲染 _recent_posts.html 模板時,不僅傳入
show_recent_posts
返回的模板變數 - 同時會傳入父模板(使用
{% show_recent_posts %}
模板標籤的模板)、上下文(渲染父模板的檢視函式、傳入父模板的模板變數、django 自己傳入的模板變數)
定義模板 _recent_posts.html 的內容
<!-- templates\blogs\inclusions\_recent_posts.html ------------------->
<div class="widget widget-recent-posts">
<h3 class="widget-title">最新文章</h3>
<ul>
{% for post in recent_post_list %}
<li>
<a href="{{ post.get_absolute_url }}">{{ post.title }}</a>
</li>
{% empty %}
暫無文章!
{% endfor %}
</ul>
</div>
迴圈由 show_recent_posts
傳遞的模板變數 recent_post_list
,與 index.html 中迴圈顯示文章列表相同
歸檔模板標籤
# blog\templatetags\blog_extras.py ------------------------------------
@register.inclusion_tag('blog/inclusions/_archives.html', takes_context=True)
def show_archives(context):
return {
'date_list': Post.objects.dates('created_time', 'month', order='DESC'),
}
渲染的模板 _archives.html 的內容
<!-- templates\blogs\inclusions\_archives.html ----------------------->
<div class="widget widget-archives">
<h3 class="widget-title">歸檔</h3>
<ul>
{% for date in date_list %}
<li>
<a href="#">{{ date.year }} 年 {{ date.month }} 月</a>
</li>
{% empty %}
暫無歸檔!
{% endfor %}
</ul>
</div>
分類模板標籤
# blog\templatetags\blog_extras.py ------------------------------------
@register.inclusion_tag('blog/inclusions/_categories.html', takes_context=True)
def show_categories(context):
return {
'category_list': Category.objects.all(),
}
<!-- templates\blogs\inclusions\_categories.html --------------------->
<div class="widget widget-category">
<h3 class="widget-title">分類</h3>
<ul>
{% for category in category_list %}
<li>
<a href="#">{{ category.name }} <span class="post-count">(13)</span></a>
</li>
{% empty %}
暫無分類!
{% endfor %}
</ul>
</div>
標籤雲模板標籤
# blog\templatetags\blog_extras.py ------------------------------------
@register.inclusion_tag('blog/inclusions/_tags.html', takes_context=True)
def show_tags(context):
return {
'tag_list': Tag.objects.all(),
}
<!-- templates\blogs\inclusions\_tags.html --------------------------->
<div class="widget widget-tag-cloud">
<h3 class="widget-title">標籤雲</h3>
<ul>
{% for tag in tag_list %}
<li>
<a href="#">{{ tag.name }}</a>
</li>
{% empty %}
暫無標籤!
{% endfor %}
</ul>
</div>
使用自定義的模板標籤
- 在
{% load static %}
下面匯入{% load blog_extras %}
<!-- templates/base.html --------------------------------------------->
{% load static %}
{% load blog_extras %}
<!DOCTYPE html>
<html>
...
</html>
- 將側邊欄各項都替換成對應的模板標籤
<!-- templates/base.html --------------------------------------------->
<aside class="col-md-4">
{% block toc %}
{% endblock toc %}
{% show_recent_posts %}
{% show_archives %}
{% show_categories %}
{% show_tags %}
<div class="rss">
<a href=""><span class="ion-social-rss-outline"></span> RSS 訂閱</a>
</div>
</aside>
https://www.zmrenwu.com/courses/hellodjango-blog-tutorial/materials/71/
分類、歸檔和標籤頁
歸檔頁面
- 設定歸檔檢視
- 主頁檢視函式中通過
Post.objects.all()
獲取全部文章 - 歸檔和分類檢視中使用
filter
來根據條件過濾
# blog/views.py -------------------------------------------------------
def archive(request, year, month):
post_list = Post.objects.filter(created_time__year=year,
created_time__month=month
).order_by('-created_time')
return render(request, 'blog/index.html', context={'post_list': post_list})
- 配置URL
# blog/urls.py --------------------------------------------------------
from django.urls import path
from . import views
app_name = 'blog'
urlpatterns = [
path('', views.index, name='index'),
path('posts/<int:pk>/', views.detail, name='detail'),
path('archives/<int:year>/<int:month>/', views.archive, name='archive'),
]
- 修改超連結的
href
屬性,讓使用者點選超連結後跳轉到文章歸檔頁面
<!-- templates\blogs\inclusions\_archives.html ----------------------->
...
{% for date in date_list %}
<li>
<a href="{% url 'blog:archive' date.year date.month %}">
{{ date.year }} 年 {{ date.month }} 月
</a>
</li>
{% endfor %}
...
分類頁面
- 寫好分類頁面的檢視函式
# blog/views.py -------------------------------------------------------
import markdown
from django.shortcuts import render, get_object_or_404
# 引入 Category 類
from .models import Post, Category
def category(request, pk):
# 記得在開始部分匯入 Category 類
cate = get_object_or_404(Category, pk=pk)
post_list = Post.objects.filter(category=cate).order_by('-created_time')
return render(request, 'blog/index.html', context={'post_list': post_list})
- URL 配置
# blog/urls.py --------------------------------------------------------
urlpatterns = [
path('archives/<int:year>/<int:month>/', views.archive, name='archive'),
path('categories/<int:pk>/', views.category, name='category'),
]
- 修改相應模板
<!-- inclusions/_categories.html ------------------------------------->
...
{% for category in category_list %}
<li>
<a href="{% url 'blog:category' category.pk %}">{{ category.name }}</a>
</li>
{% endfor %}
...
標籤頁面
- 寫好標籤頁面的檢視函式
# blog/views.py -------------------------------------------------------
from .models import Category, Post, Tag
def tag(request, pk):
# 記得在開始部分匯入 Tag 類
t = get_object_or_404(Tag, pk=pk)
post_list = Post.objects.filter(tags=t).order_by('-created_time')
return render(request, 'blog/index.html', context={'post_list': post_list})
- 配置 url
# blog/urls.py --------------------------------------------------------
from django.urls import path
from . import views
app_name = 'blog'
urlpatterns = [
...
path('categories/<int:pk>/', views.category, name='category'),
path('tags/<int:pk>/', views.tag, name='tag'),
]
- 修改 inclusions_tags.html 模板中的跳轉連結
<!-- inclusions/_tags.html ------------------------------------------->
...
{% for tag in tag_list %}
<li>
<a href="{% url 'blog:tag' tag.pk %}">{{ tag.name }}</a>
</li>
{% empty %}
暫無標籤!
{% endfor %}
...
https://www.zmrenwu.com/courses/hellodjango-blog-tutorial/materials/72/
交流的橋樑:評論功能
建立評論應用
- 通過專案根目錄建立一個新的應用
> pipenv run python manage.py startapp comments
- 在 settings.py 裡註冊這個應用
# blogproject/settings.py ---------------------------------------------
...
INSTALLED_APPS = [
...
'blog.apps.BlogConfig', # 註冊 blog 應用
'comments.apps.CommentsConfig', # 註冊 comments 應用
]
...
- 讓 blog 應用在 django 的 admin 後臺顯示中文名字
# comments/app.py -----------------------------------------------------
from django.apps import AppConfig
class CommentsConfig(AppConfig):
name = 'comments'
verbose_name = '評論'
設計評論的資料庫模型
- 為評論設計資料庫模型,和設計文章、分類、標籤的資料庫模型相同
# comments/models.py --------------------------------------------------
from django.db import models
from django.utils import timezone
class Comment(models.Model):
name = models.CharField('名字', max_length=50)
email = models.EmailField('郵箱')
url = models.URLField('網址', blank=True)
text = models.TextField('內容')
created_time = models.DateTimeField('建立時間', default=timezone.now)
post = models.ForeignKey('blog.Post', verbose_name='文章', on_delete=models.CASCADE)
class Meta:
verbose_name = '評論'
verbose_name_plural = verbose_name
def __str__(self):
return '{}: {}'.format(self.name, self.text[:20])
- 遷移資料庫,在專案根目錄下分別執行下面兩條命令
> pipenv run python manage.py makemigrations
> pipenv run python manage.py migrate
註冊評論模型到 admin
註冊到 django admin 後臺
# comments/admin.py ---------------------------------------------------
from django.contrib import admin
from .models import Comment
class CommentAdmin(admin.ModelAdmin):
list_display = ['name', 'email', 'url', 'post', 'created_time']
fields = ['name', 'email', 'url', 'text', 'post']
admin.site.register(Comment, CommentAdmin)
設計評論表單
編寫評論表單程式碼
# comments/forms.py ---------------------------------------------------
from django import forms
from .models import Comment
class CommentForm(forms.ModelForm):
class Meta:
model = Comment
fields = ['name', 'email', 'url', 'text']
展示評論表單
- 建立評論應用模板標籤的檔案結構:在 comments 資料夾下新建一個 templatetags 包,再建立 comments_extras.py 檔案用於存放模板標籤的程式碼
...
blog\
comments\
templatetags\
__init__.py
comments_extras.py
...
- 定義
inclusion_tag
型別的模板標籤,用於渲染評論表單
# comments\templatetags\comments_extras.py ----------------------------
from django import template
from ..forms import CommentForm
register = template.Library()
@register.inclusion_tag('comments/inclusions/_form.html', takes_context=True)
def show_comment_form(context, post, form=None):
if form is None:
form = CommentForm()
return {
'form': form,
'post': post,
}
- 在 templates/comments/inclusions 目錄下(沒有就新建)新建一個 _form.html 模板
<!-- templates/comments/inclusions/_form.html ------------------------>
<form action="{% url 'comments:comment' post.pk %}" method="post" class="comment-form">
{% csrf_token %}
<div class="row">
<div class="col-md-4">
<label for="{{ form.name.id_for_label }}">{{ form.name.label }}:</label>
{{ form.name }}
{{ form.name.errors }}
</div>
<div class="col-md-4">
<label for="{{ form.email.id_for_label }}">{{ form.email.label }}:</label>
{{ form.email }}
{{ form.email.errors }}
</div>
<div class="col-md-4">
<label for="{{ form.url.id_for_label }}">{{ form.url.label }}:</label>
{{ form.url }}
{{ form.url.errors }}
</div>
<div class="col-md-12">
<label for="{{ form.text.id_for_label }}">{{ form.text.label }}:</label>
{{ form.text }}
{{ form.text.errors }}
<button type="submit" class="comment-btn">發表</button>
</div>
</div> <!-- row -->
</form>
- 在 detail.html 中使用這個模板標籤渲染表單,注意在使用前記得先
{% load comments_extras %}
模組
<!-- templates/blog/inclusions/detail.html --------------------------->
{% extends 'base.html' %}
{% load comments_extras %}
...
<h3>發表評論</h3>
{% show_comment_form post %}
評論檢視函式
- 設定檢視函式
# comments/views.py ---------------------------------------------------
from blog.models import Post
from django.shortcuts import get_object_or_404, redirect, render
from django.views.decorators.http import require_POST
from .forms import CommentForm
@require_POST
def comment(request, post_pk):
# 先獲取被評論的文章,因為後面需要把評論和被評論的文章關聯起來。
# 這裡我們使用了 django 提供的一個快捷函式 get_object_or_404,
# 這個函式的作用是當獲取的文章(Post)存在時,則獲取;否則返回 404 頁面給使用者。
post = get_object_or_404(Post, pk=post_pk)
# django 將使用者提交的資料封裝在 request.POST 中,這是一個類字典物件。
# 我們利用這些資料構造了 CommentForm 的例項,這樣就生成了一個綁定了使用者提交資料的表單。
form = CommentForm(request.POST)
# 當呼叫 form.is_valid() 方法時,django 自動幫我們檢查表單的資料是否符合格式要求。
if form.is_valid():
# 檢查到資料是合法的,呼叫表單的 save 方法儲存資料到資料庫,
# commit=False 的作用是僅僅利用表單的資料生成 Comment 模型類的例項,但還不儲存評論資料到資料庫。
comment = form.save(commit=False)
# 將評論和被評論的文章關聯起來。
comment.post = post
# 最終將評論資料儲存進資料庫,呼叫模型例項的 save 方法
comment.save()
# 重定向到 post 的詳情頁,實際上當 redirect 函式接收一個模型的例項時,它會呼叫這個模型例項的 get_absolute_url 方法,
# 然後重定向到 get_absolute_url 方法返回的 URL。
return redirect(post)
# 檢查到資料不合法,我們渲染一個預覽頁面,用於展示表單的錯誤。
# 注意這裡被評論的文章 post 也傳給了模板,因為我們需要根據 post 來生成表單的提交地址。
context = {
'post': post,
'form': form,
}
return render(request, 'comments/preview.html', context=context)
- 如果使用者提交的資料不合法,將渲染一個 preview.html 頁面,來展示表單中的錯誤,以便使用者修改後重新提交
<!-- templates/comments/preview.html --------------------------------->
{% extends 'base.html' %}
{% load comments_extras %}
{% block main %}
{% show_comment_form post form %}
{% endblock main %}
繫結 URL
- 檢視函式需要和 URL 繫結,在 comment 應用中建一個 urls.py 檔案,寫上 URL 模式
# comment/urls.py -----------------------------------------------------
from django.urls import path
from . import views
app_name = 'comments' # 規定名稱空間
urlpatterns = [
path('comment/<int:post_pk>', views.comment, name='comment'),
]
- 在專案的 blogproject 目錄的 urls.py 裡包含 comments\urls.py 這個檔案
# blogproject/urls.py -------------------------------------------------
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path('admin/', admin.site.urls),
path('', include('blog.urls')),
path('', include('comments.urls')),
]
傳送評論訊息
django 預設做好了 messages 的相關配置,直接呼叫
- 兩個地方需要傳送訊息,第一個是當評論成功,即評論資料成功儲存到資料庫後,因此在 comment 檢視中加一句
# comments/views.py ---------------------------------------------------
from django.contrib import messages
if form.is_valid():
...
# 最終將評論資料儲存進資料庫,呼叫模型例項的 save 方法
comment.save()
messages.add_message(request, messages.SUCCESS, '評論發表成功!', extra_tags='success') # 使用 add_message 方法增加一條訊息
return redirect(post)
- 如果評論失敗,也傳送一條訊息
# comments/views.py ---------------------------------------------------
# 檢查到資料不合法,我們渲染一個預覽頁面,用於展示表單的錯誤。
# 注意這裡被評論的文章 post 也傳給了模板,因為我們需要根據 post 來生成表單的提交地址。
context = {
'post': post,
'form': form,
}
messages.add_message(request, messages.ERROR, '評論發表失敗!請修改表單中的錯誤後重新提交。', extra_tags='danger')
- 傳送的訊息被快取在 cookie 中,然後我們在模板中獲取顯示即可。顯示訊息比較好的地方是在導航條的下面,我們在模板 base.html 的導航條程式碼下增加如下程式碼
<!-- templates/base.html --------------------------------------------->
<header>
...
</header>
{% if messages %}
{% for message in messages %}
<div class="alert alert-{{ message.tags }} alert-dismissible" role="alert">
<button type="button" class="close" data-dismiss="alert" aria-label="Close"><span
aria-hidden="true">×</span></button>
{{ message }}
</div>
{% endfor %}
{% endif %}
- 當傳入的是 success 時,類名就為 alert-success,這時顯示的訊息背景顏色就是綠色,傳入的是 dangerous,則顯示的就是紅色
顯示評論內容
- 不改動已有的檢視函式的程式碼,評論資料我們也使用自定義的模板標籤來實現
# comments/templatetags/comments_extras.py ----------------------------
@register.inclusion_tag('comments/inclusions/_list.html', takes_context=True)
def show_comments(context, post):
comment_list = post.comment_set.all().order_by('-created_time')
comment_count = comment_list.count()
return {
'comment_count': comment_count,
'comment_list': comment_list,
}
- 設定模板 _list.html 程式碼
<!-- templates/comments/inclusions/_list.html ------------------------>
<h3>評論列表,共 <span>{{ comment_count }}</span> 條評論</h3>
<ul class="comment-list list-unstyled">
{% for comment in comment_list %}
<li class="comment-item">
<span class="nickname">{{ comment.name }}</span>
<time class="submit-date" datetime="{{ comment.created_time }}">{{ comment.created_time }}</time>
<div class="text">
{{ comment.text|linebreaks }} <!-- linebreaks 過濾器預先將換行符替換為 br HTML 標籤 -->
</div>
</li>
{% empty %}
暫無評論
{% endfor %}
</ul>
- 將 detail.html 中此前佔位用的評論模板替換為模板標籤渲染的內容
<!-- templates/blog/detail.html -------------------------------------->
<h3>發表評論</h3>
{% show_comment_form post %}
<div class="comment-list-panel">
{% show_comments post %}
</div>
- 訪問文章詳情頁,可以看到已經發表的評論列表了
https://www.zmrenwu.com/courses/hellodjango-blog-tutorial/materials/73/
優化部落格功能細節,提升使用體驗
在模型中指定排序
例如指定 Post 的排序方式
- 首先看到 Post 的程式碼
- 在
Post
模型的內部定義的Meta
類中,指定排序屬性ordering
# blog/models.py ------------------------------------------------------
class Post(models.Model):
...
created_time = models.DateTimeField()
...
class Meta:
verbose_name = '文章'
verbose_name_plural = verbose_name
ordering = ['-created_time']
- 評論的模型類(Comment)也可以新增這個屬性
完善跳轉連結
點選導航欄 Black & White 的 Logo回到首頁面,開啟 base.html,修改 Logo 處的超連結
<!-- templates/base.html --------------------------------------------->
<header id="site-header">
<div class="row">
<div class="logo">
<h1><a href="{% url 'blog:index' %}"><b>Black</b> & White</a></h1>
</div>
...
</div>
</header>
同理:
- 導航欄的首頁導航按鈕
- 文章標題下有分類、釋出時間、作者、評論量、閱讀量等資訊,可以設定點選分類跳轉到分類頁面
- 點選閱讀量就跳轉到文章詳情頁等
顯示正確的評論量
兩處地方顯示的評論量(首頁文章列表和文章詳情頁)
直接在模板中呼叫{{ post.comment_set.count }}
,將評論量替換成該模板變數可以正確顯示文章的評論數
跳轉評論區域
在評論區域增加一個錨點,兩處顯示評論量的地方超連結都指向這個錨點處,方便快速檢視評論內容
- 顯示評論的模板程式碼
<!-- templates/blog/detail.html --------------------------->
<section class="comment-area" id="comment-area">
<hr>
<h3>發表評論</h3>
...
</section>
- 已經給評論區域的標籤設定了 id,只需要在評論的連結後加上這個 id 的錨點即可
<!-- blog/index.html -------------------------------------->
<div class="entry-meta">
...
<span class="comments-link"><a href="{{ post.get_absolute_url }}#comment-area">{{ post.comment_set.count }} 評論</a></span>
<span class="views-count"><a href="#">588 閱讀</a></span>
</div>
<!-- blog/detail.html ------------------------------------->
<header class="entry-header">
<h1 class="entry-title">{{ post.title }}</h1>
...
<span class="comments-link"><a href="#comment-area">{{ post.comment_set.count }} 評論</a></span>
<span class="views-count"><a href="#">588 閱讀</a></span>
</div>
</header>