iT邦幫忙

2024 iThome 鐵人賽

DAY 16
0
Python

Python 錦囊密技系列 第 16

【Python錦囊㊙️技16】Django網頁程式完整範例

  • 分享至 

  • xImage
  •  

前言

接續前幾篇的討論,本篇的任務是以Django實作一個完整功能的網頁應用程式,以MVT(Model、View、Template)架構開發問卷調查系統,內容如下:

  1. 模型(Model)。
  2. 視圖(View)。
  3. 模板(Template)。
  4. 路由(Routing)。
  5. 身分驗證及權限控管(Authentication and Authorization)。
  6. 交易控易(Transaction control)。

功能含前/後台,後台負責設定問卷及所屬問題及答案選項,前台提供使用者投票。後台功能較多,包括:

  1. CRUD:包括問卷及所屬問題及答案選項。
  2. 投票統計。
  3. 身分驗證及權限控管。

接下來,我們就遵循MVT(Model、View、Template)順序進行專案開發。

建立專案

建立新專案。步驟如下:

  1. 建立專案:
django-admin startproject mysite
  1. 切換至專案目錄mysite,並啟動server。
cd mysite
  1. 建立app:以下列指令建立polls及frontend app。
python manage.py startapp polls
python manage.py startapp frontend
  1. 將app納入專案設定檔:修改mysite\mysite\settings.py,加入polls.apps.PollsConfig、 "frontend.apps.FrontendConfig"。
INSTALLED_APPS = [
    "polls.apps.PollsConfig",
    "frontend.apps.FrontendConfig",

建立模型(Model)

  1. 建立後台模型:之前【【Python錦囊㊙️技13】OOP 實作(3) -- 資料庫ORM】資料表設計過於簡略,我們希望建立前/後台,可以允多個問卷、多人填寫,為便於說明,限定問卷題目都是單選題。修改16\mysite\polls\models.py:
from django.db import models

# 問卷
class Poll(models.Model):
    name = models.CharField(max_length=50) # 問卷名稱
    description = models.TextField()       # 問卷摘要
    pub_date = models.DateTimeField("date published") # 問卷發行日期
    
# 問題
class Question(models.Model):
    poll = models.ForeignKey(Poll, on_delete=models.CASCADE)
    seq_no = models.IntegerField(default=1) # 序號
    question_text = models.CharField(max_length=200) # 問題說明
    is_optional = models.BooleanField(default=False) # 是否選擇性填寫

# 答案選項
class Choice(models.Model):
    question = models.ForeignKey(Question, on_delete=models.CASCADE)
    seq_no = models.IntegerField(default=1) # 序號
    choice_text = models.CharField(max_length=200) # 答案選項說明
  1. 建立前台模型:儲存調查結果。
# 問卷調查
class Vote(models.Model):
    poll = models.ForeignKey(Poll, on_delete=models.CASCADE) # 填寫問卷
    user_id = models.CharField(max_length=20)            # 使用者代碼
    fill_date = models.DateTimeField("date filled") # 填寫日期
     
# 問卷調查結果
class Vote_Result(models.Model):
    vote = models.ForeignKey(Vote, on_delete=models.CASCADE) # 填寫使用者及問卷
    question = models.ForeignKey(Question, on_delete=models.CASCADE)   # 填寫問題
    choice = models.ForeignKey(Choice, on_delete=models.CASCADE)       # 填寫答案

以實體關聯圖(Entity Relationship Diagram, ERD)表示資料表的關聯:
https://ithelp.ithome.com.tw/upload/images/20240930/20001976D17Js5pbX7.png

同步資料庫(Migration)

  1. 建立Migration plan。
python manage.py makemigrations polls
  1. 同步:生成資料表。
python manage.py migrate
  1. 執行結果:
    https://ithelp.ithome.com.tw/upload/images/20240928/20001976OIr5oVqIJA.png

建立視圖(View)

視圖主要是接收使用者的請求(Request)、處理商業邏輯、存取資料庫、指定模板及所需資料(Model),要負責的工作很多,如果較複雜,可另建類別(OOP)開發。

  1. 建立後台視圖:功能眾多,不適合全部列出,以下只是部分內容,其中@login_required decorator表示須先登入才有權執行視圖,後續會詳細說明。程式名稱為16\mysite\polls\views.py。
@login_required()
def index(request):
    # 取得所有問卷資料,並依發行日期排序
    poll_list = Poll.objects.order_by("-pub_date") 
    context = {"poll_list": poll_list}
    return render(request, "index.html", context)

@login_required()
def poll_detail(request, poll_id):
    # 取得特定問卷資料及所有問題
    poll = get_object_or_404(Poll, pk=poll_id)
    return render(request, "poll_detail.html", {"poll": poll})

@login_required()
def question_detail(request, question_id):
    # 取得特定問題及所有答案選項
    question = get_object_or_404(Question, pk=question_id)
    return render(request, "question_detail.html", {"question": question})
  1. 建立前台視圖:提供使用者投票,將表單輸入內容存入Vote及Vote_Result資料表,部分內容如下。注意,視圖函數名稱不可與其他App重複,否則會造成錯亂,程式名稱為16\mysite\frontend\views.py。
@login_required()
def vote(request, poll_id):
    if request.method == "GET": # 顯示輸入表單
        poll = get_object_or_404(Poll, pk=poll_id)
        return render(request, "vote.html", {"poll": poll})
    elif request.method == "POST": # 將表單輸入內容存入資料表
        poll = Poll.objects.get(id=poll_id)
        with transaction.atomic(): # 交易
            vote = Vote(poll=poll, user_id=request.user.username, 
                        fill_date=datetime.now().strftime("%Y-%m-%d %H:%M:%S"))
            vote.save()  # 存入Vote資料表      
            question_list = poll.question_set.all().order_by("seq_no")
            for question in question_list:
                choice_no = request.POST.get("q_"+str(question.id), "")
                if choice_no.isdigit():
                    vote_result = Vote_Result(question=question, choice_no=int(choice_no), 
                                            vote=vote) 
                    vote_result.save()  # 存入Vote_Result資料表                                
        return home(request)

建立模板(Template)

在app目錄下建立templates資料夾,新增網頁,例如16\mysite\polls\templates\index.html,內容如下:

  1. Django及Flask都使用【Jinja2】套件,它功能非常強大,包括網頁母版套用(extends)、內嵌Python程式碼、格式化(Formatting)、過濾(Filters)...等功能,也可以單獨拿來作為報表引擎(Report engine)。

  2. 筆者開發整個系統完全未使用到Javascript,Jinja2真的很方便。下面程式示範套用網頁母版、嵌入區塊內容、使用if判斷有無資料、迴圈...等。

{% extends "base.html" %} <!-- 套用網頁母版base.html -->
{% block content %} <!-- 嵌入內容 -->
    <h1>問卷資料</h1>
    <a href="/poll/poll/insert">新增問卷</a>
    {% if poll_list %} <!-- 判斷有無資料 -->
        <table class="table table-striped table-bordered table-hover">
          <tr>
            <th>問卷名稱</th>
            <th>摘要</th>
            <th>發行日期</th>
            <th>投票統計</th>
            <th colspan="2">修改/刪除</th>
         </tr>
      {% for poll in poll_list %} <!-- 迴圈 -->
          <tr>
            <td><a href="/poll/{{ poll.id }}/">{{ poll.name }}</a></td>
            <td>{{ poll.description }}</td>
            <td>{{ poll.pub_date|date:"Y-m-d" }}</td>
            <td><a href="/poll/summary/{{ poll.id }}">統計</a></td>
            <td><a href="/poll/{{ poll.id }}">修改</a></td>
            <td><a href="/poll/poll/delete/{{ poll.id }}" 
                onclick="return confirm('確定要刪除嗎?');">刪除</a></td>
          </tr>
        {% endfor %}
        </table>
    {% else %}
        <p>No polls are available.</p>
    {% endif %}
{% endblock %}

建立路由(Routing)

  1. 建立後台路由:指定每一個網址(URL)對應的視圖,程式名稱為16\mysite\polls\urls.py,內容如下:
from django.urls import path
from . import views

urlpatterns = [
    # 問卷
    path("", views.index, name="index"),
    path("<int:poll_id>/", views.poll_detail, name="poll_detail"),
    path("poll/insert/", views.poll_insert, name="poll_insert"),
    path("poll/update/", views.poll_update, name="poll_update"),
    path("poll/delete/<int:poll_id>/", views.poll_delete, name="poll_delete"),
    path("summary/<int:poll_id>/", views.vote_summary, name="vote_summary"),

    # 問題
    path("question/<int:question_id>/", views.question_detail, name="question_detail"),
    path("question/insert/<int:poll_id>/", views.question_insert, name="question_insert"),
    path("question/update/", views.question_update, name="question_update"),
    path("question/delete/<int:question_id>/", views.question_delete, name="question_delete"),

    # 答案選項
    path("choice/<int:choice_id>/", views.choice_detail, name="choice_detail"),
    path("choice/insert/<int:question_id>/", views.choice_insert, name="choice_insert"),
    path("choice/update/", views.choice_update, name="choice_update"),
    path("choice/delete/<int:choice_id>/", views.choice_delete, name="choice_delete"),
]
  1. 建立前台視圖:讓使用者指定投票項目,並進行投票,程式名稱為16\mysite\frontend\urls.py。
from django.urls import path
from . import views

urlpatterns = [
    path("", views.home, name="home"),
    path("vote/<int:poll_id>", views.vote, name="vote"),
]

身分驗證及權限控管(Authentication and Authorization)

一般框架都會提供內建的身份驗證(Authentication)及權限控管(Authorization)機制,Django也不例外,步驟如下:

  1. 先建立系統管理者,指定帳號、Email及密碼。
python manage.py createsuperuser
  1. 建立其他使用者:以下列指令啟動Server,並瀏覽管理網頁(http://localhost:8000/admin),即可建立使用者(Users)及群組(Groups)。
python manage.py runserver
  1. 執行結果:除了管理使用者(Users)及所屬群組(Groups)外,還包括Permission,可以進一步設定Model的新增/更正/刪除/瀏覽權限。
    https://ithelp.ithome.com.tw/upload/images/20240929/200019766zCWPS4wBT.png

  2. 若要管理其他資料表,可修改16\mysite\polls\admin.py,註冊要納入管理的資料表。

from django.contrib import admin
from .models import *

admin.site.register(Poll)
admin.site.register(Question)
admin.site.register(Choice)
admin.site.register(Vote)
admin.site.register(Vote_Result)
  1. 執行結果:已經可以管理所有資料表。
    https://ithelp.ithome.com.tw/upload/images/20240929/200019764dw92nZ6Ga.png

  2. 身份驗證(Authentication)機制:須修改16\mysite\mysite\settings.py。

  • 指定登入、註冊、登出,修改密碼...等網頁模板(templates)的目錄:加入【BASE_DIR / "templates"】。
TEMPLATES = [
    {
        "BACKEND": "django.template.backends.django.DjangoTemplates",
        "DIRS": [BASE_DIR / "templates"],
        "APP_DIRS": True,
  • 指定登入網頁及返回網頁的URL:在檔案尾巴加入LOGIN_URL、LOGIN_REDIRECT_URL。
LOGIN_URL='/accounts/login/'
# Redirect to home URL after login, Default redirects to /accounts/profile/
LOGIN_REDIRECT_URL='/'
  • 撰寫登入網頁:在專案目錄下建立templates資料夾,再往下建立registration資料夾,新增16\mysite\templates\registration\login.html網頁,最簡單內容如下,可使用HTML/CSS修飾:
<form method="post">
  {% csrf_token %}
  {{ form }}
  <button type="submit">登入</button>
</form>
  1. 在需要身份驗證的視圖函數宣告@login_required(),表示執行視圖前須先登入,若未登入會自動切換至登入網頁。
@login_required()
def index(request):
    # 取得所有問卷資料,並依發行日期排序
    poll_list = Poll.objects.order_by("-pub_date") 
    context = {"poll_list": poll_list}
    return render(request, "index.html", context)
  1. 測試:請開啟新的無痕視窗,瀏覽 http://localhost:8000/poll/ ,會出現登入頁面。
    https://ithelp.ithome.com.tw/upload/images/20240929/20001976xpSlVq2AHr.png

Django預設就提供下列功能,完整作法可參閱【User authentication in Django】

  1. User Registration
  2. User Login and Logout
  3. Password Management
  4. User Permissions and Groups
  5. Customizing the Authentication System

交易控易(Transaction control)

交易控易(Transaction control)是撰寫資料庫程式非常重要的一環,我們可以將多筆新增/更正/刪除綁在同一個交易(Transaction),中間過程若有任何錯誤,會自動復原(Rollback),不會寫入部份資料,造成難以復原的狀況。Django使用很簡單,只要將所有異動包在【with transaction.atomic】指令內即可,完整程式請參考16\mysite\frontend\views.py。

with transaction.atomic(): # 交易
    vote = Vote(poll=poll, user_id=request.user.username, 
                fill_date=datetime.now().strftime("%Y-%m-%d %H:%M:%S"))
    vote.save()  # 存入Vote資料表      
    question_list = poll.question_set.all().order_by("seq_no")
    for question in question_list:
        choice_no = request.POST.get("q_"+str(question.id), "")
        if choice_no.isdigit():
            vote_result = Vote_Result(question=question, choice_no=int(choice_no), 
                                    vote=vote) 
            vote_result.save()  # 存入Vote_Result資料表                                

測試

整個專案至此開發完畢,讀者可以自GitHub複製src\16\mysite資料夾,進行測試。

  1. 執行以下指令啟動Server:
python manage.py runserver
  1. 在瀏覽器輸入URL即可。
後台:http://localhost:8000/poll
管理後台:http://localhost:8000/admin,系統管理者帳密為admin/1234。
前台:http://localhost:8000/
  1. 也可以刪除db.sqlite3,清除執行以下指令重建資料庫。
python manage.py makemigrations
python manage.py migrate

結語

以上就是Django ORM的操作方式,完全遵照OOP精神開發資料庫應用程式,程序雖然繁複,但可以體會到Django提供的功能非常完備,是資料庫網頁應用程式開發很棒的學習資源,同時,它運作也非常穩定,筆者曾經開發電商購物平台,含金流,只花了2個月,生產力非常高。

GitHub提供的範例功能是一個具體而微的系統,包括CRUD、統計報表、安全控管,但是還有一些功能美中不足:

  1. 資料檢查(Data validation):包括輸入資料商業邏輯的檢查,例如每個人只能投一票。
  2. 畫面的美化:可以至網路或電商找尋設計好的模板套用,另外,選單也待補充。
  3. 例外處理、工作日誌:應該在每個視圖內加上try/except。
  4. 統計圖及報表功能。
  5. Web API。

Django功能豐富,筆者只是希望藉以說明OOP、ORM及MVC等設計模式,無意涵蓋Django所有功能,由於篇幅已經過大而且程式碼有點多,就此打住,還請見諒。

本系列的程式碼會統一放在GitHub,本篇的程式放在src/16資料夾,歡迎讀者下載測試,如有錯誤或疏漏,請不吝指正。


上一篇
【Python錦囊㊙️技15】淺談網頁開發架構 (MVC、MVT、MVVM、MVP)
下一篇
【Python錦囊㊙️技17】單元測試(Unit Testing)入門
系列文
Python 錦囊密技30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言