跳到主要內容

[Django教學7]善用Django ModelForm快速開發CRUD應用程式教學

Photo by Daniel Fazio on Unsplash
在現今許多網站中,多數都會有使用到表單的時候,舉例來說,在註冊會員時,就是使用表單的方式來填寫基本資料,進而儲存會員資料至資料庫中。

Django網站框架,提供了許多表單的開發方式,其中最常使用的為ModelForm(資料模型表單),顧名思義,就是利用規劃資料庫時,所建立的資料模型類別(Model),來開發表單,能夠快速建置基本的CRUD(Create-Read-Update-Delete)表單應用程式。

本文將實際開發基本的記帳網站,來說明Django ModelForm的開發方式,其中的重點包含:
  • Django ModelForm建置
  • Django ModelForm初始化
  • Django ModelForm新增資料操作
  • Django Url Reference網址參考
  • Django ModelForm修改資料操作
  • Django ModelForm刪除資料操作

一、Django ModelForm建置

在開發Django網站的ModelForm(資料模型表單)前,需事先建立網站的資料模型(Model),其中的建立方式可以參考Django Migration(資料遷移)的重要觀念文章。

而本文所要建置的記帳網站資料模型(models.py),如下範例:
from django.db import models

class Expense(models.Model):
    name = models.CharField(max_length=255)  #花費項目
    price = models.IntegerField() #金額
Django ModelForm(資料模型表單)就是依據models.py中的資料模型類別,來建立網站表單。首先,在Django的應用程式(APP)下新增一個forms.py檔案,如下圖:
接著,在forms.py檔案中自定類別,繼承自ModelForm類別,並且在其中設定表單所要綁定的資料模型名稱,如下範例:
from django import forms
from .models import Expense


class ExpenseModelForm(forms.ModelForm):

    class Meta:
        model = Expense
而要指定在網站上所要顯示的表單欄位,可以透過fields屬性來進行設定,如下範例:
from django import forms
from .models import Expense


class ExpenseModelForm(forms.ModelForm):

    class Meta:
        model = Expense
        fields = ('name', 'price')
如果要顯示資料模型(Model)中的所有欄位,fields屬性的設定可以簡寫為__all__,如下範例:
from django import forms
from .models import Expense


class ExpenseModelForm(forms.ModelForm):

    class Meta:
        model = Expense
        fields = '__all__'
另外,Django ModelForm也提供了widgets屬性,用來客製化表單的顯示外觀,這邊套用Bootstrap的表單CSS類別為例:
from django import forms
from .models import Expense


class ExpenseModelForm(forms.ModelForm):

    class Meta:
        model = Expense
        fields = '__all__'
        widgets = {
            'name': forms.TextInput(attrs={'class': 'form-control'}),
            'price': forms.NumberInput(attrs={'class': 'form-control'})
        }
由於在資料模型(Model)中,都是定義英文的屬性(Attribute),所以表單欄位的標題也會顯示英文名稱,如果想要進行修改,可以透過labels屬性來設定,如下範例:
from django import forms
from .models import Expense


class ExpenseModelForm(forms.ModelForm):

    class Meta:
        model = Expense
        fields = '__all__'
        widgets = {
            'name': forms.TextInput(attrs={'class': 'form-control'}),
            'price': forms.NumberInput(attrs={'class': 'form-control'})
        }
        labels = {
            'name': '花費項目',
            'price': '金額'
        }

二、Django ModelForm初始化

一般情況下,瀏覽器請求網址的方法(method)有兩種,分別為GETPOST,當沒有特別指定請求的方法為POST時,預設皆為GET方法。所以想要在瀏覽器請求Django應用程式(APP)時,能夠顯示一個空白的表單讓使用者填寫,就可以在views.py中,透過以下的程式碼來達成:
from django.shortcuts import render
from .forms import ExpenseModelForm


def index(request):

    form = ExpenseModelForm()

    context = {
        'form': form
    }

    return render(request, 'expenses/index.html', context)
由於瀏覽器預設使用GET方法請求網址,所以在檢視函式(View Function)中,會執行初始化Django ModelForm表單,並且傳送至Django Template(樣板)中。

接下來在對應的index.html Django Template(樣板)中,即可透過以下的語法,顯示在forms.py中所設定的表單樣式:

{% extends 'base.html' %}

{% block content %}
<form action="" method="POST">
    {% csrf_token %}
    <div class="form-group">
        <div class="form-group col-md-6">
            
            {{ form }}

            <input class="btn btn-success" type="submit" value="儲存" />
        </div>
    </div>
</form>
{% endblock  %}

執行結果
範例程式碼中,在<form>標籤的地方,action為表單要送往的目標網址,預設為目前的網址。另一個method屬性就是表單資料送出時,所使用的請求方法。而其中的csrf_tokenDjango的表單安全驗證機制,防止有心人士透過表單,傳送破壞性的資料到網站來進行攻擊,是Django表單的安全防護機制。

除此之外,如果想要彈性的放置欄位標題及輸入框,來修改排版的方式,則可以在Template(樣板)中,透過form物件單獨存取label欄位標題及輸入框,如下範例:
{% extends 'base.html' %}

{% block content %}

    <form action="" method="POST">
        {% csrf_token %}
        <div class="form-row">
            <div class="form-group col-md-6">
                <label>{{ form.name.label }}</label>
                {{ form.name }}
            </div>
            <div class="form-group col-md-4">
                <label>{{ form.price.label }}</label>
                {{ form.price }}
            </div>
            <div class="form-group col-md-2">
                <input class="btn btn-success" style="margin-top: 30px;" type="submit" value="儲存" />
            </div>
        </div>
    </form>
{% endblock  %}
執行結果

三、Django ModelForm新增資料操作

在儲存送出表單資料時,Django會使用POST方法,將表單資料傳送至目前的網址,這時候在views.py中,就可以透過以下的程式碼,儲存表單資料至資料庫中:
from django.shortcuts import render, redirect
from .models import Expense
from .forms import ExpenseModelForm


def index(request):

    expenses = Expense.objects.all()  # 查詢所有資料

    form = ExpenseModelForm()

    if request.method == "POST":
        form = ExpenseModelForm(request.POST)
        if form.is_valid():
            form.save()
        return redirect("/expenses")

    context = {
        'expenses': expenses,
        'form': form
    }

    return render(request, 'expenses/index.html', context)
由於為了在畫面展示儲存資料後,能將資料顯示在表單的下方,所以在檢視函式(View Function)中增加所有資料的查詢。

在剛剛的index.html Template(樣板)中,指定了表單資料送出的請求方法(method)POST,所以在檢視函式(View Function)中,就會進入if區塊,利用請求方法所帶過來的表單資料,建立ModelForm物件(form),這時候form物件擁有了使用者在表單所填寫的資料,接著利用is_valid()方法進行驗證,如果通過,就可以直接儲存,最後,重新導向到目前的網址。

回到index.html Template(樣板),在表單下方增加顯示所有資料的查詢結果,以及修改和刪除按鈕,如下範例:
{% extends 'base.html' %}

{% block content %}

    <form action="" method="POST">
        {% csrf_token %}
        <div class="form-row">
            <div class="form-group col-md-6">
                <label>{{ form.name.label }}</label>
                {{ form.name }}
            </div>
            <div class="form-group col-md-4">
                <label>{{ form.price.label }}</label>
                {{ form.price }}
            </div>
            <div class="form-group col-md-2">
                <input type="submit" class="btn btn-success" style="margin-top:30px;" value="儲存">
            </div>
        </div>
    </form>

    <table class="table table-bordered table-striped">
        <thead class="thead-dark">
            <tr>
                <th>花費項目</th>
                <th>金額</th>
                <th></th>
            </tr>
        </thead>
        <tbody>
            {% for expense in expenses %}
                <tr>
                    <td>{{ expense.name }}</td>
                    <td>{{ expense.price }}</td>
                    <td>
                        <a href="#" class="btn btn-info">修改</a>
                        <a href="#" class="btn btn-danger">刪除</a>
                    </td>
                </tr>
            {% endfor %}
        </tbody>
    </table>
{% endblock  %}
執行結果

四、Django Url Reference網址參考

要修改資料時,一定要知道該筆資料的主鍵(PK),並且透過網址傳送至Django的檢視函式(View Function)中,這時候就需要在應用程式(APP)下的urls.py中增加網址設定,如下範例:
from django.urls import path
from . import views

urlpatterns = [
    path('', views.index, name='Index'),
    path('update/<str:pk>', views.update, name='Update')
]
在修改的網址的參數,首先需定義它的資料型態,接著在冒號後面,進行參數的命名。

網址設定完成後,回到index.html Template(樣板),在表單下方的顯示資料表格中,將修改按鈕的網址進行修改,如下範例:
<table class="table table-bordered table-striped">
    <thead class="thead-dark">
	<tr>
	    <th>花費項目</th>
	    <th>金額</th>
	    <th></th>
	</tr>
    </thead>
    <tbody>
        {% for expense in expenses %}
            <tr>
	        <td>{{ expense.name }}</td>
	        <td>{{ expense.price }}</td>
	        <td>
		    <a href="{% url 'Update' expense.id %}" class="btn btn-info">修改</a>
		    <a href="#" class="btn btn-danger">刪除</a>
	        </td>
	    </tr>
        {% endfor %}
    </tbody>
</table>
在修改按鈕的網址中,利用Djangourl樣板語法,指定網址的名稱及該筆資料的主鍵,這樣Django就會依據此名稱,在應用程式(APP)urls.py中,產生相對應的連結,並且由對應的檢視函式(View Function)來進行請求的處理。

五、Django ModelForm修改資料操作

修改按鈕的網址參考設定完成後,在views.py中,新增一個update檢視函式(View Function),並且利用剛剛在urls.py中所命名的參數名稱,取得表單資料的主鍵(id),這時候就能夠透過get()方法取得該筆表單資料。

接著指定Django ModelForminstance關鍵字參數(Keyword Argument)為取得的表單資料物件,來初始化表單,傳送至Template(樣板)中,如下範例:
def update(request, pk):

    expense = Expense.objects.get(id=pk)

    form = ExpenseModelForm(instance=expense)

    context = {
        'form': form
    }

    return render(request, 'expenses/update.html', context)
最後,建立修改畫面的Django Template(樣板),如下圖:
同樣利用剛剛所介紹的表單語法來建立Django ModelForm表單,如下範例:

{% extends 'base.html' %}

{% block content %}

    <form action="" method="POST">
        {% csrf_token %}
        <div class="form-group">
            <div class="form-group col-md-6">

            {{ form }}

            <input class="btn btn-success" type="submit" value="更新" />
            </div>
        </div>
    </form>
{% endblock  %}

假設這時候點選index.html Template(樣板)中,雞腿飯的修改按鈕,就可以看到Django所產生的網址及執行結果:
可以看到成功的透過網址傳遞該筆資料的主鍵至檢視函式(View Function),並且在表單顯示資料庫中的資料。

接下來,在點擊更新按鈕時,同樣利用POST請求方法,將修改的資料傳送至目前的網址,所以在相對的 update檢視函式(View Function)中,判斷如果為POST方法,就把取得的修改資料及資料庫中的該筆資料傳入Django ModelForm中,來建立form物件,如果修改的表單資料驗證通過,則進行修改的動作,如下範例:
def update(request, pk):

    expense = Expense.objects.get(id=pk)

    form = ExpenseModelForm(instance=expense)

    if request.method == 'POST':
        form = ExpenseModelForm(request.POST, instance=expense)
        if form.is_valid():
            form.save()
        return redirect('/expenses')

    context = {
        'form': form
    }

    return render(request, 'expenses/update.html', context)

六、Django ModelForm刪除資料操作

首先,開啟urls.py檔案,新增刪除按鈕的網址,如下範例:
from django.urls import path
from . import views

urlpatterns = [
    path('', views.index, name='Index'),
    path('update/<str:pk>', views.update, name='Update'),
    path('delete/<str:pk>', views.delete, name='Delete')
]
接著,開啟index.html Template(樣板),設定刪除按鈕的網址,如下範例:
<table class="table table-bordered table-striped">
    <thead class="thead-dark">
	<tr>
	    <th>花費項目</th>
	    <th>金額</th>
	    <th></th>
	</tr>
    </thead>
    <tbody>
        {% for expense in expenses %}
            <tr>
	        <td>{{ expense.name }}</td>
	        <td>{{ expense.price }}</td>
	        <td>
		    <a href="{% url 'Update' expense.id %}" class="btn btn-info">修改</a>
		    <a href="{% url 'Delete' expense.id %}" class="btn btn-danger">刪除</a>
	        </td>
	    </tr>
        {% endfor %}
    </tbody>
</table>
2020/03/28補充說明
完成設定後,在views.py中,建立刪除的檢視函式(View Function),同樣先利用get()方法取得該筆資料後,傳遞至刪除的Template(樣板),讓使用者確認是否要進行刪除的動作,如下範例:
def delete(request, pk):

    expense = Expense.objects.get(id=pk)

    context = {
        'expense': expense
    }

    return render(request, 'expenses/delete.html', context)
Django應用程式(APP)下,新增刪除的Template(樣板),如下圖:
delete.html Template(樣板)中,首先,利用檢視函式(View Function)所傳過來的expense物件,顯示花費項目的名稱。接著,將確定及取消按鈕放置在表單中,讓「確定按鈕能夠使用POST方法進行請求,如下範例:

{% extends 'base.html' %}

{% block content %}

<h3>是否確定刪除{{ expense.name }}?</h3>

<form action="" method="POST">
    {% csrf_token %}
    <input class="btn btn-success" type="submit" value="確定" />
    <a class="btn btn-warning" href="{% url &#39;Index&#39; %}">取消</a>
</form>

{% endblock  %}
範例中的「取消」按鈕,由於它的型態(type)不是送出(submit),所以會使用GET請求方法回到首頁。

假設想要刪除雞腿飯這筆花費項目,點擊「刪除」按鈕後,則可以看到如下圖的執行結果:
最後,回到刪除的檢視函式(View Function),如果請求的方法是POST,也就是按下了「確定」按鈕,則呼叫delete()方法進行刪除的動作,完成後回到首頁,如下範例:
def delete(request, pk):

    expense = Expense.objects.get(id=pk)

    if request.method == "POST":
        expense.delete()
        return redirect('/expenses')

    context = {
        'expense': expense
    }

    return render(request, 'expenses/delete.html', context)

七、小結

以上就是利用Django ModelForm來快速的建立一個CRUD的應用程式,希望透過本文的實作,可以瞭解Django ModelForm的使用方式。如果在實作的過程中有碰到任何問題,歡迎留言提問,我將盡力為各位解答。

如果您喜歡我的文章,請幫我按五下Like(使用GoogleFacebook帳號免費註冊),支持我創作教學文章,回饋由LikeCoin基金會出資,完全不會花到錢,感謝大家。


GitHub網址:https://github.com/mikeku1116/django-urexpenses

有想要看的教學內容嗎?歡迎利用以下的Google表單讓我知道,將有機會成為教學文章,分享給大家😊

你可能有興趣的文章








留言

  1. 回覆
    1. 已補充在文章最後,GitHub網址為https://github.com/mikeku1116/django-urexpenses

      刪除
  2. if form.is_valid: 少了( )

    :)

    回覆刪除
    回覆
    1. pika您好:
      已完成修改,感謝您的提醒 :)

      刪除
  3. 您好

    在第一次執行解果的地方 按鈕沒有顯示 儲存 index.html 的value那邊可能要補一下

    Thank you~

    回覆刪除
  4. MIKE大您好,我照著文中在實作UPDATE部分時有遇到以下問題

    NoReverseMatch at /expenses/
    Reverse for 'Update' with arguments '(1,)' not found. 1 pattern(s) tried: ['expenses/update/<str:pk>$']

    想說是不是update.html中的問題,或是哪邊我沒有顧慮到?

    回覆刪除
    回覆
    1. 您好,檢查一下urls.py檔案中是否有加上以下的網址:

      urlpatterns = [
       path('', views.index, name='Index'),
       path('update/<str:pk>', views.update, name='Update')
      ]

      特別注意「/」為半型,以及views.py檔案中的update檢視函式(view function)是否有加上參數,如下範例:

      def update(request, pk):
       ...

      希望有解決您的問題 :)

      刪除
    2. <>
      urlpatterns = [
      path('', views.index, name='Index'),
      path('update/<str:pk>', views.update, name='Update'),
      path('update/<str:pk>', views.delete, name='Delete')
      ]

      <>
      def update(request, pk):
      expense = Expense.objects.get(id=pk)
      form = ExpenseModelForm(instance=expense)
      context = {
      'form': form
      }
      return render(request, 'expenses/update.html', context)

      但這樣還是沒辦法成功..

      想問M大,update.html中的

      {{ form }}

      語法就是這樣嗎? 還是您有省略步驟?

      刪除
    3. XIAO您好,您的urlpatterns有兩個update的path,如下:

      path('update/<str:pk>', views.update, name='Update'),
      path('update/<str:pk>', views.delete, name='Delete')

      第二個path應該是delete/<str:pk>

      刪除
    4. 剛剛有修改了
      但目前還是顯示
      NoReverseMatch at /expenses/
      Reverse for 'Update' with arguments '(1,)' not found. 1 pattern(s) tried: ['expenses/update/<str:pk>$']

      它是回報
      return render(request, 'expenses/index.html', context)
      處有誤

      刪除
    5. 我猜想會不會是我的相關內容中 Expense 和 expenses有錯用?

      刪除
    6. @XIAO 我的實作發現這邊的 <str:pk> 大於、小於 用的是全形的,需要改成半形。
      -->
      其他的部分,按照M大的步驟,齊全了就出現了。

      刪除
    7. 感謝Jiang的回覆,因為部落格編碼的關係,為了正確顯示,才使用了全形,再麻煩XIAO檢查一下,謝謝 :)

      刪除
    8. 謝謝@Jiang和M的回覆,這樣一來就可以了!

      刪除
  5. 作者已經移除這則留言。

    回覆刪除
  6. 您好

    我參考你的update功能,練習寫了update的功能,但是在submit送出時,回去驗證model欄位資料時,就報錯了;試了很久還是想不懂為什麼會報錯說我找的資料欄位不存在(DoesNotExist;matching query does not exist)

    謝謝

    回覆刪除
  7. Mike您好

    非常謝謝您的教學,若我form想要進階一點用到foreingKey,

    那我該朝甚麼關鍵字下手呢?

    回覆刪除

張貼留言