---
# System prepended metadata

title: Django 網頁應用程式框架入門
tags: [IT]

---

---
tags: IT
---

[![hackmd-github-sync-badge](https://hackmd.io/JyNUDT0kT-2IBVN7zR2jTw/badge)](https://hackmd.io/JyNUDT0kT-2IBVN7zR2jTw)

<style>
.emp {color: red}
</style>
# Django 網頁應用程式框架入門
![](https://i.imgur.com/EkU8twm.png)
:::success
筆者以稍具 Python 程式開發、網頁程式設計經驗者為對象，撰寫此入門教學，若有錯誤疏漏之處還請多多指教。
框架(Framework)的作用是將程式的架構建好，等開發者填入需要的邏輯進去。 框架會負責在合適的時機呼叫開發者寫的程式，而不是每個人自行設計程式架構與函數，不說明很難理解。 使用相同框架的開發的程式，可以比喻為遵從相同的SOP設計。
:::


# 簡介
Django (發音：jang-goh) 可以說是 Python 最著名的網頁框架(Web Framework)，目標是讓複雜、以資料庫驅動的網站開發起來簡單。它注重元件的可重用性和 DRY 法則（Don't Repeat Yourself）。軟體架構採用了MVT(Model–View–Template)框架，Django 的 View 和 Template 都對應到 MVC 架構的 View，都是關於資料顯示的部分。
![](https://i.imgur.com/rjU9ibX.png)

# 環境建置
在 windows 10 作業系統下以系統管理員身分開啟命令提示字元`cmd` 之後，請依照下列指令，建立專案目錄 D:\pyTest 並設定 env 虛擬環境，然後安裝 Django 套件。
```shell=
d:
mkdir pyTest                             # 建立專案目錄
cd pyTest
D:\pyTest>py -m venv env                 # 建立 env 虛擬環境(子目錄)
D:\pyTest>env\Scripts\activate           # 啟動虛擬環境
(env) D:\pyTest>py -m pip install -U pip # 升級 pip（可省略）

(env) D:\pyTest>pip install pylint
(env) D:\pyTest>pip install django       # 在虛擬環境中安裝套件 django
(env) D:\pyTest>py -m django --version   # 測試 django 套件是否正確安裝
3.2.3
```
:::info
- 此後的程式碼將省略路徑前的虛擬環境的名稱標示 <span class="emp">(env)</span>
- 也可用此指令顯示 Django 目前的版本號：`django-admin --version`
:::

# 建立專案
使用 django-admin 來建立第一個 Django 專案 : myweb
```shell=
D:\pyTest>django-admin startproject myweb
```

執行 django-admin 建立 myweb 專案之後，會產生一個 myweb 專案目錄，內有 manage.py 檔案與一個同名的 myweb 套件目錄。
```python
D:\pyTest>explorer myweb

- [D:\pyTest\myweb]        # 專案的容器(根目錄)
    - manage.py            # 管理專案的命令列工具
    -[myweb]               # 管理專案的套件目錄(與專案同名)
        - __init__.py      # 一個空文件，代表這個目錄是一個套件
        - settings.py      # 本專案的環境設定檔
        - urls.py          # 本專案的路由設定檔
        - asgi.py
        - wsgi.py
```
![](https://openhome.cc/Gossip/CodeData/PythonTutorial/images/AppModelPy3-1.JPG)
:::danger
django-admin 的實際路徑在 env\Scripts\django-admin.exe，相同目錄下還有個 django-admin.py 檔案可將之刪除，因為自從 Django 3.1 之後就以 .exe 取代 .py 了，因此若您參考網路上某些教學用到的指令是 django-admin.py 請拿掉 .py。
:::

## 啟動網頁伺服器進行測試
:::info
此步驟旨在介紹網頁伺服器啟動後的訊息說明，了解之後可省略
:::
以下指令可啟動內建的簡易網頁伺服器，以測試Django功能是否正常。
```shell=
D:\pyTest>cd myweb                        # 進入 myweb 專案目錄
D:\pyTest\myweb>py manage.py runserver    # 啟動 web server
```

執行上述指令後會產生以下回應
```
Watching for file changes with StatReloader
Performing system checks...

System check identified no issues (0 silenced).

You have 18 unapplied migration(s). Your project may not work properly until you apply the migrations for app(s): admin, auth, contenttypes, sessions.
Run 'python manage.py migrate' to apply them.
May 27, 2021 - 00:59:57
Django version 3.2.3, using settings 'myweb.settings'
Starting development server at http://127.0.0.1:8000/
Quit the server with CTRL-BREAK.
```
以瀏覽器開啟 http://127.0.0.1:8000/ 將出現以下畫面，可按 Ctrl + C 終止執行。

![](https://i.imgur.com/xAVffIP.png)

manage.py 是 Django 提供管理專案用的命令列工具，利用它可以執行很多工作，例如同步資料庫、建立 app 等等，下達 -h 參數可得知更多的使用方式，例如：
- `py manage.py -h`
- `py manage.py runserver -h`

:::warning
- 使用 `py manage.py runserver` 這種啟動 Web 服務的方式，僅用於開發專案，千萬不要在正式上線的場合中使用。
- 預設使用的 port 為 8000，若要改變 port 號可參考範例：
`py manage.py runserver 8080`
- 此測試執行後，會在專案根目錄下產生一個 db.sqlite3 的資料庫檔案，但因為尚未產生其中的資料表，因此訊息中會出現 18 個未處理資料庫遷移警告，在此請忽略，後續說明資料庫時會加以解釋。
- 因為環境設定檔中預設的資料庫是 sqlite3，所以會在專案根目錄下產生 db.sqlite3。
:::

:::info
:question: \_\_pycache__目錄是作什麼的呢？
Ans：Python 在首次執行程式碼時，會先將原始碼在記憶體中轉成 PyCodeObject ，執行完畢後會寫回 \_\_pycache__目錄，未來只要程式碼沒有變動，Python 直接可讀取該目錄的 *.pyc 檔案，可減少重新編譯的時間。
:::
> 參考：[Python 基礎 - pyc 是什麼](https://ithelp.ithome.com.tw/articles/10185442)


# 建立應用程式 application (app)
application 可視作為本專案建立的套件，通常會有多個 app 進行搭配，負責不同的功能。
```shell=
D:\pyTest\myweb>py manage.py startapp myapp
```
startapp 會按照你的命名在專案目錄下建立應用程式所屬的套件目錄
```shell
D:\pyTest\myweb>explorer myapp

- [D:\pyTest\myweb\myapp] # 應用程式套件目錄
    - __init__.py         # 空文件，代表這個目錄是一個套件。
    - admin.py            #
    - apps.py             #
    - models.py           #
    - tests.py            #
    - views.py            #
    -[migrations]         # 
        - __init__.py     # 空文件，代表這個目錄是一個套件。
```

# 建立專案所需之靜態檔案目錄
```shell=
D:\pyTest\myweb>md templates     # 放置 html 檔案
D:\pyTest\myweb>md static        # 放置各種靜態檔案
D:\pyTest\myweb>md static\images # 圖片檔
D:\pyTest\myweb>md static\css    # css檔
D:\pyTest\myweb>md static\js     # javascript檔
```
```shell
D:\pyTest\myweb>explorer .

- [D:\pyTest\myweb]              # 專案目錄
    -[myapp]                     # 應用程式目錄
    -[myweb]                     # 專案管理目錄
    -[static]                    # 網頁靜態檔案目錄
        -[images]                # 圖片檔
        -[css]                   # css檔
        -[js]                    # javascript檔
    -[templates]                 # 網頁樣板檔案目錄
    - manage.py                  # 管理專案的命令列工具
```

# 建立 vscode 的專案設定檔
:::info
本步驟旨在介紹使用 vscode 開發 django 程式的專案設定，設定完成之後可省略。
:::
因為使用了 Python 虛擬環境，因此必須建立 vscode 的專案設定檔，告知python.exe 的路徑變更到虛擬環境下了，其步驟如下。
1. 開啟 vscode
```shell=
D:\pyTest\myweb>code .
```
2. 點選 vscode 左下角的 Python 3.x.x 64-bit
3. 上方選擇直譯器視窗，請選擇位於虛擬環境路徑的python.exe（ex:`D:\pyTest\env\Scripts\python.exe`）
4. 專案目錄下會多出一個 .vscode\settings.json 檔案，該檔案為 vscode 的專案設定檔，內容參考如下：
```json=1
{
    "python.pythonPath": "D:\\pyTest\\env\\Scripts\\python.exe",
}
```
另建議加入以下設定：
```json=3
    "editor.formatOnType": true,
    "editor.mouseWheelZoom": true,
    "editor.renderIndentGuides": true,
    "window.zoomLevel": 1.5,                // 視窗縮放
    "files.autoSave": "onFocusChange",      // 失焦自動儲存
    "files.encoding": "utf8",               // 設定預設編碼
    "files.trimTrailingWhitespace": true,   // 儲存的時候，會自動過濾多餘的空格
```

## 設定 powershell 的指令執行安全性政策
為了減少頻繁在命令提示字元與vscode的切換，我們可以善用 vscode 的終端機功能(terminal)。
vscode 預設的終端機是 powershell，在預設的指令執行安全性政策為 Restricted，我們必須將之修改為 RemoteSigned，才能使用本機所撰寫的腳本檔案，且不需要簽署就可以執行，但是從遠端下載的腳本就必須經由可信任的發行者簽署後才可執行，設定步驟參考如下：
- 以系統管理員身份執行 powershell
- 輸入以下指令，之後按 A 進行執行原則的變更
```shell=
Set-ExecutionPolicy RemoteSigned
```
![](https://i.imgur.com/hDAhlxX.png)

- 返回 vscode 後，按下 Ctrl+\` 就會出現 powershell 啟動虛擬環境的畫面

![](https://i.imgur.com/2vCafPR.png)


# 編輯專案環境設定檔：myweb/settings.py
專案環境設定位於管理專案的套件目錄下的 settings.py，請以 vscode 開啟本專案目錄，再透過檔案總管開啟它。

![](https://i.imgur.com/yNYyL75.png)

>:question: vscode提示要安裝的 pylance 是什麼呢？
>
> 請參考：[VS Code 无法实现 “转到定义 “？](https://zhuanlan.zhihu.com/p/344118024/)

## 設定應用程式
Django 已預設將常用的 app 設定為 INSTALLED_APPS 例如：auth（認證授權管理）、admin （管理者後台）... 等等，可依需求自行增減設定專案會用到的應用程式。
請參考第 40 行，將應用程式 myapp 設定進去。
```python=33
INSTALLED_APPS = [
    'django.contrib.admin',        # 管理者後台
    'django.contrib.auth',         # 認證授權管理
    'django.contrib.contenttypes', # 內容類型管理
    'django.contrib.sessions',     # session 管理
    'django.contrib.messages',     # 訊息管理
    'django.contrib.staticfiles',  # 靜態檔案管理
    'myapp',
]
```

## 設定樣板目錄的路徑
請參考第 58 行，設定 DIRS 這個樣板目錄路徑
```python=55
TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [BASE_DIR / "templates"],
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
            ],
        },
    },
]
```
> 參考：[Overriding templates](https://docs.djangoproject.com/en/3.2/howto/overriding-templates/)

## 設定語系和時間
請參考下列行數，設定語系和時間
```python=107
LANGUAGE_CODE = 'zh-Hant'

TIME_ZONE = 'Asia/Taipei'
```

## 設定靜態檔案的路徑
請設定 STATICFILES_DIRS 這個 list 以指定靜態檔案的路徑。
```python=121
STATIC_URL = '/static/'

STATICFILES_DIRS = [
    BASE_DIR / "static",
]
```

## 環境設定完成後，再一次進行測試
:::info
此步驟旨在介紹專案設定後，網頁伺服器啟動後的訊息有何不同，了解之後可省略。
:::
更新完上述設定之後，請再啟動一次 web server。
```shell=
D:\pyTest\myweb>py manage.py runserver    # 啟動 web server
```
以瀏覽器開啟 http://127.0.0.1:8000/ 會發現頁面變成中文了。
![](https://i.imgur.com/R5hRuCV.png)

> 開發環境與正式環境對於靜態檔案的設定不同，請參考：https://chenuin.blogspot.com/2018/10/django-static-filescss-javascript-images.html

# 網址的對應與委派

## 設定網址路由：myweb/urls.py
管理專案的套件目錄下的 urls.py，內有 urlpatterns 這個 list 負責網址與view(應用程式中函數)的對應，請呼叫 vscode 來編輯它。
```python
D:\pyTest\myweb>code .
```
![](https://i.imgur.com/26vprIZ.png)

新增如下圖的第 18 行以下之程式碼
```python=16
from django.contrib import admin
from django.urls import path
from myapp import views

urlpatterns = [
    path('admin/', admin.site.urls),
    path('', views.home),
    path('hi/<username>/', views.hiname),      # 傳遞字串參數 username
    path('age/<int:year>/', views.age),        # 傳遞數值參數 year
    path('hello/', views.hello_view),
    # path(r'^admin/', admin.site.urls),
    # path(r'^$', sayhello),
]
```
> 更多的網址路由用法可參考：
> - [Django 2.0 以 path 函式設定 urlpatterns](http://blog.e-happy.com.tw/django2-0-%E4%BB%A5-path-%E5%87%BD%E5%BC%8F%E8%A8%AD%E5%AE%9A-urlpatterns/)
> - [path & re_path vs. url](https://ithelp.ithome.com.tw/articles/10201230)
> - [URL dispatcher](https://docs.djangoproject.com/zh-hans/3.2/topics/http/urls/)

## 設定應用程式中函數的對應
以 vscode 編輯 myapp\views.py，定義 4 個會被 urls.py import 的函數，新增如下圖的第 1 行以下之程式碼
```python=1
from django.shortcuts import render
from django.http import HttpResponse
from datetime import datetime

# Create your views here.   
def home(request):
    return HttpResponse("Home page")
    
def hiname(request, username):
    return HttpResponse("Hi " + username)
    
def age(request, year):
    return HttpResponse("Age: " + str(year))
    
def hello_view(request):
    fourSeason = range(1, 5)
    p1={"name":"Amy","phone":"0912-345678","age":20}
    p2={"name":"Jack","phone":"0937-123456","age":25}
    p3={"name":"Nacy","phone":"0958-654321","age":17}
    persons=[p1,p2,p3]
    return render(request, 'hello_django.html', {
        'title':"樣板使用",
        'data':"Hello Django!",
        'seasons':fourSeason,
        'persons':persons,
        'now':datetime.now()
    } )
```
> 更多定義 view 的用法可參考：[Writing views](https://docs.djangoproject.com/zh-hans/3.2/topics/http/views/)

## 於樣板目錄下建立網頁
1. 下載靜態圖片到static\images 目錄下
    https://train.csie.ntu.edu.tw/images/courses/py-django.png

2. 在 static\css 目錄下，建立 style.css 樣式檔，內容如下：
```css=
.info {color:red;}
```
3. 在 templates 樣板目錄下新增一個 hello_django.html，並利用 Emmet 縮寫語法輸入：`html:5` ，快速完成下列網頁內容
```htmlembedded=
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>{{title}}</title>
    {% load static %}
    <link href="{% static 'css/style.css'%}" rel="stylesheet">
</head>
<body>
    <h1>{{data}}</h1>
    <h2>季統計</h2>
    {% if seasons %}
        <ul>
        {% for i in seasons %}
            <li>{{i}}</li>
        {% endfor %}
        </ul>
    {% else %}
        <p>沒有資料</p>
    {% endif %}
    <h2>人員清單</h2>
    {% if persons %}
    <table border="1">
    {% for person in persons %}
        <tr>
            <td>{{person.name}}</td>
            <td>{{person.phone}}</td>
            <td>{{person.age}}</td>
        </tr>
    {% endfor %}
    </table>
    {% else %}
        <p>沒有資料</p>
    {% endif %}
    <img src="{% static 'images/py-django.png' %}" alt="python/django">
    <p>現在時刻：<span class="info">{{now}}</span></p>
</body>
</html>
```
Template 內的 `{{變數}}` 會透過 views.py 將變數傳送過來，`{% 指令 %}` 是 Template 專用語言，專為呈現顯示結果所進行的判斷/迴圈/邏輯等處理。
![](https://i.imgur.com/qXCA19g.png)

:::warning
Template 雖然有自己的語法，可以顯示變數、有 if/for/filter 等指令，但主要是用來顯示由 View 中取得的資料，如果取得資料後還要進行複雜的處理，應設法在 View 先處理完再傳送給 Template，而不要在 Template 中進行複雜的程式邏輯。
:::

> 更多定義 Template 的用法可參考：
> - [模板](https://docs.djangoproject.com/zh-hans/3.2/topics/templates/)
> - [Django 模板语言](https://docs.djangoproject.com/zh-hans/3.2/ref/templates/language/)

## 啟動並瀏覽網頁
請於命令提示字元中輸入以下指令
```shell=
D:\pyTest\myweb>py manage.py runserver    # 啟動 web server
```
以瀏覽器開啟 http://127.0.0.1:8000 將出現以下畫面。
![](https://i.imgur.com/hYtMQro.png)

瀏覽器URL改為 http://127.0.0.1:8000/hi/peter 將出現以下畫面。
![](https://i.imgur.com/P7WcoR2.png)

瀏覽器URL改為 http://127.0.0.1:8000/age/22 將出現以下畫面。
![](https://i.imgur.com/CftUp7p.png)

瀏覽器URL改為 http://127.0.0.1:8000/hello 將出現以下畫面。
![](https://i.imgur.com/9j2Nqzo.png)

瀏覽器URL改為 http://127.0.0.1:8000/admin 將出現以下畫面。
![](https://i.imgur.com/HqPyITQ.png)

<!--
![](https://i.imgur.com/hBv2e95.jpg)
-->

# Django 存取資料庫
Django 框架透過 ORM(Object Relational Mapping) 技術，將資料庫、資料表及欄位等，抽象化為程式設計中的類別(Class)和屬性(Attribute)，實現利用物件的概念，操作資料庫，因此必須對會使用到資料庫的應用程式進行建立資料模型的動作。

## 建立內建 APP 所需之資料表
位於 myweb/settings.py 的內建 APP(admin / auth / contenttypes / sessions) ，運作時所需之資料表(預設資料庫：sqlite3)，可透過以下指令產生：
```shell=
D:\pyTest\myweb>py manage.py migrate
```
![](https://i.imgur.com/FTWyuah.png)


使用 [DB Browser for SQLite](http://sqlitebrowser.org/) 檢視專案根目錄的 db.sqlite3，可發現多了11個資料表與15個索引。
![](https://i.imgur.com/6oCQda7.png)

## 異動資料模型 models.py
資料模型預設是以應用程式作為對應的單位，因此請透過檔案總管以 vscode 開啟 myapp\models.py。

![](https://i.imgur.com/i0bn8DS.png)


**資料表名稱：student**
| 欄位名稱 | 資料形態 | 資料長度 | 預設值 | 可否空值 |
| -------- | ----- | ------ | ----- | ------- |
| cName    | 字串   |  20    |       | 不可    |
| cSex     | 字串   |   2    | 'M'   | 不可    |
| cBirthday| 日期   |        |       | 不可    |
| cEmail   | Email | 100    | ''    | 可      |
| cPhone   | 字串   |  50    | ''   | 可       |
| cAddr    | 字串   | 255    | ''   | 可       |
| last_modified | 日期時間 |     | now   | 不可     |
| created  | 日期時間   |     | now   | 不可     |

根據上表定義，在 models.py 輸入以下程式碼，類別代表資料表，變數代表欄位定義
```python=1
from django.db import models
from django.utils import timezone

# Create your models here.
class student(models.Model):
    SEX_CHOICES = [
        ('M', '男'),
        ('F', '女'),
    ]
    cName = models.CharField('姓名',max_length=20, null=False)
    cSex = models.CharField('性別',max_length=1, choices=SEX_CHOICES, default='', null=False)
    cBirthday = models.DateField('生日',null=False)
    cEmail = models.EmailField('Email',max_length=100, blank=True, default='')
    cPhone = models.CharField('手機',max_length=50, blank=True, default='')
    cAddr = models.CharField('地址',max_length=255, blank=True, default='')
    last_modified = models.DateTimeField('最后修改日期', auto_now = True)
    created = models.DateTimeField('保存日期',default = timezone.now)

    def __str__(self):
        return self.cName
```
> 延伸參考資料：
> - 資料表欄位變數的詳盡說明：[模型字段参考](https://docs.djangoproject.com/zh-hans/3.2/ref/models/fields/)
> - [Model 中的 null 和 blank 設定與差異](https://whhnote.blogspot.com/2010/12/django-modelnullblank.html)
> - [django 的 model 中的 auto_now_add 和 auto_now](https://agvszwk.github.io/2019/05/11/django%E7%9A%84model-auto-now-add%E5%92%8Cauto-now/)

## 將模型的異動產生 migrations 遷移檔
透過 makemigrations 命令，Django 會檢測到我們對資料模型的修改，並把這些修改的部分，儲存成一次遷移(migration)。
```shell=
D:\pyTest\myweb>py manage.py makemigrations myapp
```
![](https://i.imgur.com/Yuu2qiU.png)

執行上述指令後，會在應用程式 myapp 下增加 migrations 目錄如下圖。
![](https://i.imgur.com/B6fSomw.png)
首次的遷移會被儲存在 `myapp/migrations/0001_initial.py` 裡

## 檢視指定的 migration 會產生的 SQL 敘述
:::info
此步驟旨在理解 Django 的 ORM 機制，了解之後可省略
:::
我們可以透過以下指令看到此遷移之後會產生的 SQL 敘述為何？
```shell=
D:\pyTest\myweb>py manage.py sqlmigrate myapp 0001
```
![](https://i.imgur.com/aOp8AJp.png)

將上述 SQL 敘述整理為較好閱讀的格式如下：
```sql=
BEGIN;
--
-- Create model student
--
CREATE TABLE "myapp_student" (
	"id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, 
	"cName" varchar(20) NOT NULL, 
	"cSex" varchar(1) NOT NULL, 
	"cBirthday" date NOT NULL, 
	"cEmail" varchar(100) NOT NULL, 
	"cPhone" varchar(50) NOT NULL, 
	"cAddr" varchar(255) NOT NULL, 
	"last_modified" datetime NOT NULL, 
	"created" datetime NOT NULL
);

COMMIT;
```
:::info
這個 sqlmigrate 命令並沒有真正被執行，只是讓你看看 Django 會產生什麼樣的 SQL 敘述。
:::

## 以 migrate 指令將遷移套用於資料庫
migrate 命令會將還沒執行過的遷移(migrations)，執行實際的資料庫異動。
```shell=
D:\pyTest\myweb>py manage.py migrate myapp
```
![](https://i.imgur.com/NBO9PsA.png)

若使用 [DB Browser for SQLite](http://sqlitebrowser.org/) 檢視專案根目錄的 db.sqlite3，可發現多了1個 myapp_student 的資料表，且自動多了一個記錄流水編號的 id 主鍵欄位。

![](https://i.imgur.com/w4x5tiS.png)

# API 測試
完成上述 ORM 設定後，Django 會自動產生可用於資料表 CRUD 的 API。
| 語法 | 說明 |
| -------- | -------- |
|資料表.objects.all()| 取得所有資料 |
|資料表.objects.order_by('欄位名稱')| - 取得所有資料並依指定欄位遞增排序 |
|資料表.objects.order_by('-欄位名稱')| - 取得所有資料並依指定欄位遞減排序 |
|資料表.objects.get(查詢條件)| 取得一筆資料 |

## 以互動模式測試 API
請以下述指令進入 Python 的互動模式進行測試
```shell=
D:\pyTest\myweb>py manage.py shell
```

進入後，可輸入以下>>>之後的指令，測試資料庫 API
```python
>>> from myapp.models import student
>>> from django.utils import timezone
>>> student.objects.all()
<QuerySet []>
>>> s1 = student(cName="Kevin", cSex="M", cBirthday='1995-05-23', cEmail="kevin@student.ncut.edu.tw", cPhone="0937-123456", cAddr="臺中市和平街13號", last_modified=timezone.now(),created=timezone.now())
>>> s1.save()
>>> s1.id
1
>>> s1.cBirthday
'1995-05-23'
>>> s1.cName
'Kevin'
>>> s1.cName = "John"
>>> s1.cName
'John'
>>> student.objects.order_by('id')
<QuerySet [<student: Kevin>]>
>>> s1.save()
>>> student.objects.order_by('-id')
<QuerySet [<student: John>]>
>>>student.objects.get(cName="John")
<student: John>
>>> student.objects.filter(cName="John")
<QuerySet [<student: John>]>
>>> s1.delete()
(1, {'myapp.student': 1})
```

## 以樣板測試 API
### 1. 新增 urls.py
新增 myweb\urls.py 以下第 26 與 27 行程式碼
```python=16
from django.contrib import admin
from django.urls import path
from myapp import views

urlpatterns = [
    path('admin/', admin.site.urls),
    path('', views.home),
    path('hi/<username>/', views.hiname),      # 傳遞字串參數 username
    path('age/<int:year>/', views.age),        # 傳遞數值參數 year
    path('hello/', views.hello_view),
    path('getName/<username>/', views.getOneByName), # 傳遞字串參數 username
    path('getAll/', views.getAll),
    # path(r'^admin/', admin.site.urls),
    # path(r'^$', sayhello),
]
```

### 2. 新增 views.py
新增 myapp\views.py 以下第 4-6 行程式碼
```python=4
from myapp.models import student
from django.http import Http404
# from django.shortcuts import get_object_or_404, get_list_or_404 # 快捷函数
```
還有以下第 31 行開始之程式碼
```python=31
def getOneByName(request, username):
    title = "顯示一筆資料"
    # unit = get_object_or_404(student, cName=username)
    try:
        unit = student.objects.get(cName=username)
    except student.DoesNotExist:
        raise Http404("查無此學生")
    except:
        raise Http404("讀取錯誤")
    return render(request, 'listone.html', locals() )

def getAll(request):
    title = "顯示全部資料"
    # students = get_list_or_404(student)
    try:
        students = student.objects.all()
    except student.DoesNotExist:
        raise Http404("查無學生資料")
    except:
        raise Http404("讀取錯誤")
    return render(request, 'listall.html', locals() )
```
:::info
render()的第3個參數 locals()，可將所有區域變數轉為字典
:::

### 3. 新增樣板
3.1 新增 templates\listone.html
```htmlembedded=1
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>{{title}}</title>
</head>
<body>
    {% if unit %}
        <ul>
            <li>編號：{{unit.id}}</li>
            <li>姓名：{{unit.cName}}</li>
            <li>性別：{{unit.cSex}}</li>
            <li>生日：{{unit.cBirthday}}</li>
            <li>Email：{{unit.cEmail}}</li>
            <li>手機：{{unit.cPhone}}</li>
            <li>地址：{{unit.cAddr}}</li>
        </ul>
    {% else %}
        <p>沒有資料</p>
    {% endif %}
</body>
</html>
```
3.2 再新增 templates\listall.html
```htmlembedded=1
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>{{title}}</title>
</head>
<body>
    {% if students %}
    <table border="1" cellpadding="0" cellspacing="0">
        <tr>
            <th>編號</th>
            <th>姓名</th>
            <th>性別</th>
            <th>生日</th>
            <th>Email</th>
            <th>手機</th>
            <th>地址</th>
        </tr>
    {% for student in students %}
        <tr>
            <td>{{student.id}}</td>
            <td>{{student.cName}}</td>
            <td>{{student.cSex}}</td>
            <td>{{student.cBirthday}}</td>
            <td>{{student.cEmail}}</td>
            <td>{{student.cPhone}}</td>
            <td>{{student.cAddr}}</td>
        </tr>
    {% endfor %}
    </table>
    {% else %}
        <p>沒有資料</p>
    {% endif %}
</body>
</html>
```

### 啟動伺服器測試 API
請輸入以下指令啟動開發伺服器
```shell=
D:\pyTest\myweb>py manage.py runserver
```
打開瀏覽器，輸入網址：`http://127.0.0.1:8000/getName/Janet/`，出現畫面如下：

![](https://i.imgur.com/BOoBWsJ.png)

打開瀏覽器，輸入網址：`http://127.0.0.1:8000/getAll/`，出現畫面如下：

![](https://i.imgur.com/ySVf5dX.png)


:::info
ORM 所能應用的範圍其實相當狹窄，很多問題其實都要以普通 SQL 解決，想正確理解 ORM 的使用時機，請參考：[SQL 三部曲：你不需要 ORM](https://tecky.io/en/blog/SQL%E4%B8%89%E9%83%A8%E6%9B%B2:%E4%BD%A0%E4%B8%8D%E9%9C%80%E8%A6%81ORM/)
:::

# admin 後台管理
Django 會自動建立管理的後台，具體來說就是透過 admin 應用程式管理 db.sqlite3 內的使用者、群組與對應模型的資料表。

## 建立管理員帳號
請輸入以下指令進行管理員帳號的建立，並依序輸入
1. 使用者名稱：預設會帶出目前登入的使用者名稱。
2. 電子信箱。
3. 密碼：輸入至少8碼包含文數字的密碼2次，若不符合系統會問你是否略過此項驗證。
```shell=
D:\pyTest\myweb>py manage.py createsuperuser
```
![](https://i.imgur.com/U8d57eI.png)

## 啟動伺服器並打開 admin 頁面
請輸入以下指令啟動開發伺服器
```shell=
D:\pyTest\myweb>py manage.py runserver
```
打開瀏覽器，輸入網址：`http://127.0.0.1:8000/admin/`，再輸入上一步驟建立的管理員帳密

![](https://i.imgur.com/b9cTdHF.png)

登入後看到的管理頁面

![](https://i.imgur.com/UIL5Gqs.png)

:::warning
再次重申，使用`python manage.py runserver` 來運行網頁伺服器的方式，僅適用於開發與測試之用，正式上線的服務請改用可穩定且持續的伺服器，例如 apache, Nginx,... 等。
:::

## 向 admin 頁面註冊自訂的應用程式模型
以 vscode 開啟 myapp\admin.py，填入以下第 3 行之後的程式碼。
```python=3
# Register your models here.
from myapp.models import student

admin.site.register(student)
```
然後回到剛剛開啟的 admin 頁面，按下 F5 重新載入頁面，會看到增加了的應用程式模型。

![](https://i.imgur.com/9hpA0az.png)

## 新增與變更資料
按下 Students 的 +新增

![](https://i.imgur.com/IdoQuC0.png)


儲存後，會跳到可異動資料的畫面，請試試修改與刪除一筆資料試試。

![](https://i.imgur.com/3avTvCw.png)

## admin 頁面顯示多個欄位
若要在 admin 的後台，一次看到多個欄位並設定欄位的過濾、搜尋與排序，則必須在 mapp\admin.py 中定義 ModelAdmin 類別，再以 list_display 變數定義想要顯示的欄位，然後透過 register 方法註冊。

以 vscode 開啟 myapp\admin.py，填入以下第 3 行之後的程式碼。
```python=3
# Register your models here.
from myapp.models import student

# admin.site.register(student) # 最簡易的顯示

class studentAdmin(admin.ModelAdmin):
    list_display=('id','cName','cSex','cBirthday','cEmail','cPhone',)
    list_filter=('cSex',)
    search_fields=('cName','cEmail','cPhone',)
    ordering=('id','cName',)

admin.site.register(student, studentAdmin)
```
然後回到剛剛開啟的 admin 頁面，按下 F5 重新載入頁面，會看到 admin 頁面顯示了多個欄位、搜尋框以及過濾器。

![](https://i.imgur.com/2InO6lg.png)

# 模板繼承
通常一個網站都會規劃相同的顏色、主頁標題、主頁結尾作為母版(或稱骨架)，然後讓所有的頁面繼承這個母版，就會有同樣的風格，未來只要修改母版就能快速更換網站風格。

## 母版設計
首先設計一個簡單的網頁與樣式表作為模板繼承的參考
1. 畫面

> ![](https://i.imgur.com/NoKYWmo.png)

2. base.html
```htmlembedded=
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <link rel="stylesheet" href="style.css">
  <title>網頁母版</title>
</head>
<body>
  <header>
    <h1>主頁標題</h1>
    <p>副標題</p>
  </header>
  <main>
  <h1>段落標題</h1>
  <p>段落內文</p>
  <article>
    <h2>文章1標題</h2>
    <p>文章1內文</p>
  </article>
  <article>
    <h2>文章2標題</h2>
    <p>文章2內文</p>
  </article>
  </main>
  <footer>
    <h2>主頁結尾</h2>
  </footer>
</body>
</html>
```
3. style.css
```css=
header {
  padding: 60px;
  text-align: center;
  background: #1abc9c;
  color: white;
  font-size: 30px;
  display: flex;
  justify-content: space-between;
}

footer {
  padding: 20px;
  text-align: center;
  color: white;
  background-color: indigo;
}

main {
  margin: 0 auto;
  padding: 18px;
}

.info {color:red;}
```

## 靜態檔案與樣板的放置
1. 將上述 style.css 放到 static/css/ 目錄下，原有的 style.css 檔案請覆蓋。
2. 將上述 base.html 放到 templates 目錄下，並修改為下列內容

- templates/base.html
```htmlembedded=
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  {% load static %}
  <link href="{% static 'css/style.css'%}" rel="stylesheet">
  <title>{% block title %}{% endblock %}</title>
</head>
<body>
  <header>
    <h1>主頁標題</h1>
    <p>副標題</p>
  </header>
  <main>
    {% block main %}{% endblock %}
  </main>
  <footer>
    <h2>主頁結尾</h2>
  </footer>
</body>
</html>
```
請留意以上7-9與17行，除了載入樣式表外，我們挖了 2 個區塊：title、main作為子網頁繼承之用。

3. 建立子網頁 templates/index.html
> 日後子網頁只要描述以下三大區塊即可
- 使用 extends 繼承母版 base.html
- 使用 block title 填寫子網頁標題
- 使用 block main 填寫子網頁內容
```htmlembedded=
{% extends 'base.html'%}

{% block title %}{{pageTitle}}{% endblock %}

{% block main %}
    <h1>{{mainTitle}}</h1>
    <p>{{mainContent}}</p>
    {% if artitles %}
        {% for i in artitles %}
        <article>
            <h2>{{i.aTitle}}</h2>
            <p>{{i.aContent}}</p>
        </article>
        {% endfor %}
    {% else %}
        <p>沒有資料</p>
    {% endif %}
{% endblock %}
```

## 調整路由(urls.py)與視圖(views.py)
1. 調整 myweb/urls.py

將第 22 行 的 views.home 更改為 views.main
```python=20
urlpatterns = [
    path('admin/', admin.site.urls),
    path('', views.main),
    path('hi/<username>/', views.hiname),      # 傳遞字串參數 username
    path('age/<int:year>/', views.age),        # 傳遞數值參數 year
    path('hello/', views.hello_view),
    path('getName/<username>/', views.getOneByName), # 傳遞字串參數 username
    path('getAll/', views.getAll),
    # path(r'^admin/', admin.site.urls),
    # path(r'^$', sayhello),
]
```
2. 調整 myapp/views.py

增加第 53 行的 main 函數
```python=53
def main(request):
    pageTitle="子網頁繼承"
    mainTitle="段落標題"
    mainContent="段落內文"
    artitle1={"aTitle":"文章標題","aContent":"文章1內文"}
    artitle2={"aTitle":"文章標題","aContent":"文章2內文"}
    artitles=[artitle1, artitle2]
    return render(request, 'index.html', locals())
```
:::info
render()的第3個參數 locals()，可將所有區域變數轉為字典
:::

## 瀏覽測試
更新完上述設定之後，請再啟動一次 web server。
```shell=
D:\pyTest\myweb>py manage.py runserver    # 啟動 web server
```
以瀏覽器開啟 http://127.0.0.1:8000/ 會發現頁面已變成我們要的版型

![](https://i.imgur.com/081l5pk.png)

# 表單處理
## 單純傳送欄位變數的表單
新增一個內含表單的子網頁 login.html ，留意表單的傳送處為`/login/`
1. templates/login.html
```htmlembedded=
{% extends 'base.html'%}

{% block title %}使用者登入{% endblock %}

{% block main %}
    <h1>使用者登入</h1>
    <form action="/login/" method="post">
        {% csrf_token %}
        <label for="uName">名稱：</label>
        <input id="uName" type="text" name="uName">
        <label for="uPass">密碼：</label>
        <input id="uPass" type="text" name="uPass">
        <input type="submit" value="送出">
    </form>
{% endblock %}
```
![](https://i.imgur.com/ScbYZZP.png)

2. myweb/urls.py

增加第 28 行的 `views.login` 的敘述
```python=20
urlpatterns = [
    path('admin/', admin.site.urls),
    path('', views.main),
    path('hi/<username>/', views.hiname),      # 傳遞字串參數 username
    path('age/<int:year>/', views.age),        # 傳遞數值參數 year
    path('hello/', views.hello_view),
    path('getName/<username>/', views.getOneByName), # 傳遞字串參數 username
    path('getAll/', views.getAll),
    path('login/', views.login),
    # path(r'^admin/', admin.site.urls),
    # path(r'^$', sayhello),
]
```
3. myapp/views.py

- 修改第 1 行敘述，多了 redirect
- 增加第 6 行敘述
```python=1
from django.shortcuts import render, redirect
from django.http import HttpResponse
from datetime import datetime
from myapp.models import student
from django.http import Http404
from django.contrib import auth
```
增加第 63 行開始的敘述，多了 login（） 方法
- 69 - 73 行，直接判斷帳密有效性
- 75 - 81 行，以 Django 內建的管理者帳密判斷有效性
```python=63
def login(request):
    if request.method == 'GET':
        return render(request, 'login.html')
    elif request.method == 'POST':
        uName = request.POST.get('uName') # login.html 傳來的變數
        uPass = request.POST.get('uPass') # login.html 傳來的變數
        # 直接判斷帳密有效性
        # if uName=='Peter' and uPass=='591026':
        #     return HttpResponse("已登入")
        # else:
        #     return redirect('/login/')
        
        # 以 Django 內建的管理者帳密判斷有效性
        user = auth.authenticate(username=uName, password=uPass)
        if user is not None:
            auth.login(request, user)
            return HttpResponse("已登入")
        else:
            return redirect('/login/')
```
:::info
[使用 Django 的验证系统](https://docs.djangoproject.com/zh-hans/3.2/topics/auth/default/)
:::
## 與資料模型有關的表單

# 參考網頁
- [Django官網文件](https://docs.djangoproject.com/zh-hans/3.2/intro/)
- [Django 網站框架 (Python)](https://developer.mozilla.org/zh-TW/docs/Learn/Server-side/Django)
- [Django 基本教學 - 從無到有 Django-Beginners-Guide](https://github.com/twtrubiks/django-tutorial)
- [From Django 1.11 to Django 2.1 系列](https://ithelp.ithome.com.tw/users/20111829/ironman/1804)
- [Django 教學第一篇 - 專案與環境設定](https://blog.chairco.me/posts/2016/06/Django-Tutorial-01.html#Django-Tutorial-01)
- [Django 教學第二篇 - 建立第一個 APP (上)](https://blog.chairco.me/posts/2016/07/Django-Tutorial-02-1.html)
- [Django 教學第二篇 - 建立第一個 APP (下)](https://blog.chairco.me/posts/2016/07/Django-Tutorial-02-2.html)
- [Django 教學第三篇 - 用 CBV 來做程式開發](https://blog.chairco.me/posts/2016/07/Django-Tutorial-03.html)
- [Django 教學第三篇 - 用 CBV 做程式開發 (續)](https://blog.chairco.me/posts/2016/08/Django-Tutorial-03-2.html)
- [專欄文章：Python Tutorial](https://openhome.cc/Gossip/CodeData/PythonTutorial/)
- [Django 筆記 - Django Template Language (1)](https://chentsungyu.github.io/2020/02/20/Python/[Python]%20Django%E7%AD%86%E8%A8%98%20-%20Django%20Template%20Language(1)/)
- [善用 Django ModelForm 快速開發 CRUD 應用程式教學](https://www.learncodewithmike.com/2020/03/django-modelform.html)
- [Django 連接現有 MySQL 資料庫](https://chentsungyu.github.io/2020/02/13/Python/%5BPython%5D%20Django%E9%80%A3%E6%8E%A5%E7%8F%BE%E6%9C%89MySQL%E8%B3%87%E6%96%99%E5%BA%AB/)
- [Python 專案開發入門的十堂課（影片）](http://kaiching.org/pydoing/py-lesson/index.html)
- [Python Django tutorial 2021 - full course | Learn Django 3](https://www.youtube.com/watch?v=tLq20htu3ss)
- [Django Tutorial in Visual Studio Code](https://code.visualstudio.com/docs/python/tutorial-django)
- [Python Django Tutorial (2021) | Full Course for Beginners](https://www.youtube.com/watch?v=Q_OgdrsWmGM)