接續前幾篇的討論,本篇的任務是以Django實作一個完整功能的網頁應用程式,以MVT(Model、View、Template)架構開發問卷調查系統,內容如下:
功能含前/後台,後台負責設定問卷及所屬問題及答案選項,前台提供使用者投票。後台功能較多,包括:
接下來,我們就遵循MVT(Model、View、Template)順序進行專案開發。
建立新專案。步驟如下:
django-admin startproject mysite
cd mysite
python manage.py startapp polls
python manage.py startapp frontend
INSTALLED_APPS = [
"polls.apps.PollsConfig",
"frontend.apps.FrontendConfig",
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) # 答案選項說明
# 問卷調查
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)表示資料表的關聯:
python manage.py makemigrations polls
python manage.py migrate
視圖主要是接收使用者的請求(Request)、處理商業邏輯、存取資料庫、指定模板及所需資料(Model),要負責的工作很多,如果較複雜,可另建類別(OOP)開發。
@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})
@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)
在app目錄下建立templates資料夾,新增網頁,例如16\mysite\polls\templates\index.html,內容如下:
Django及Flask都使用【Jinja2】套件,它功能非常強大,包括網頁母版套用(extends)、內嵌Python程式碼、格式化(Formatting)、過濾(Filters)...等功能,也可以單獨拿來作為報表引擎(Report engine)。
筆者開發整個系統完全未使用到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 %}
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"),
]
from django.urls import path
from . import views
urlpatterns = [
path("", views.home, name="home"),
path("vote/<int:poll_id>", views.vote, name="vote"),
]
一般框架都會提供內建的身份驗證(Authentication)及權限控管(Authorization)機制,Django也不例外,步驟如下:
python manage.py createsuperuser
python manage.py runserver
執行結果:除了管理使用者(Users)及所屬群組(Groups)外,還包括Permission,可以進一步設定Model的新增/更正/刪除/瀏覽權限。
若要管理其他資料表,可修改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)
執行結果:已經可以管理所有資料表。
身份驗證(Authentication)機制:須修改16\mysite\mysite\settings.py。
TEMPLATES = [
{
"BACKEND": "django.template.backends.django.DjangoTemplates",
"DIRS": [BASE_DIR / "templates"],
"APP_DIRS": True,
LOGIN_URL='/accounts/login/'
# Redirect to home URL after login, Default redirects to /accounts/profile/
LOGIN_REDIRECT_URL='/'
<form method="post">
{% csrf_token %}
{{ form }}
<button type="submit">登入</button>
</form>
@login_required()
def index(request):
# 取得所有問卷資料,並依發行日期排序
poll_list = Poll.objects.order_by("-pub_date")
context = {"poll_list": poll_list}
return render(request, "index.html", context)
Django預設就提供下列功能,完整作法可參閱【User authentication in Django】。
交易控易(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資料夾,進行測試。
python manage.py runserver
後台:http://localhost:8000/poll
管理後台:http://localhost:8000/admin,系統管理者帳密為admin/1234。
前台:http://localhost:8000/
python manage.py makemigrations
python manage.py migrate
以上就是Django ORM的操作方式,完全遵照OOP精神開發資料庫應用程式,程序雖然繁複,但可以體會到Django提供的功能非常完備,是資料庫網頁應用程式開發很棒的學習資源,同時,它運作也非常穩定,筆者曾經開發電商購物平台,含金流,只花了2個月,生產力非常高。
GitHub提供的範例功能是一個具體而微的系統,包括CRUD、統計報表、安全控管,但是還有一些功能美中不足:
Django功能豐富,筆者只是希望藉以說明OOP、ORM及MVC等設計模式,無意涵蓋Django所有功能,由於篇幅已經過大而且程式碼有點多,就此打住,還請見諒。
本系列的程式碼會統一放在GitHub,本篇的程式放在src/16資料夾,歡迎讀者下載測試,如有錯誤或疏漏,請不吝指正。