店家的部分差不多完成了,接著我們來實作點餐部分。因為畫圖很麻煩(喂),我們直接上程式。首先建立 events
app:
python manage.py startapp events
把它加入設定:
# lunch/settings/base.py
INSTALLED_APPS = (
'events',
# ...
)
然後建立 models:
# events/models.py
from django.conf import settings
from django.core.urlresolvers import reverse
from django.db import models
from stores.models import MenuItem
class Event(models.Model):
store = models.ForeignKey('stores.Store', related_name='events')
def __str__(self):
return str(self.store)
def get_absolute_url(self):
return reverse('event_detail', kwargs={'pk': self.pk})
class Order(models.Model):
event = models.ForeignKey(Event, related_name='orders')
user = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='orders')
item = models.ForeignKey(MenuItem, related_name='orders')
notes = models.TextField(blank=True, default='')
class Meta:
unique_together = ('event', 'user',)
def __str__(self):
return '{item} of {user} for {event}'.format(
item=self.item, user=self.user, event=self.event
)
現在你應該可以看懂絕大部分的內容了(reverse
的部分先不管,我們之後再建這個 URL pattern)。但好像還是有個新東西——什麼是 class Meta
?
我們之前在討論 model form 時,就有用到 meta。這並不是 Python 的 metaclass,而是 Django 用來提示某個 class 應該擁有什麼屬性的工具。以 model form 而言,在 meta 中指定 model
屬性,就可以讓 Django 自動把該 model 中的某些欄位增加至 form 中,並且在 form 被 save 時自動產生對應的 model instance;model meta 則可以用來描述這個 model 的一些性質,以及跨欄位間的關聯。這裡我們指定了 unique_together
,代表 event
和 user
這兩個欄位的組合必須 unique——限制一個使用者只能在一次 event 中點一次餐。
把 models 對應的 tables 建出來:
python manage.py makemigrations events
python manage.py migrate events
順便把它們也加入 admin:
# events/admin.py
from django.contrib import admin
from .models import Event, Order
class OrderInline(admin.StackedInline):
model = Order
extra = 1
@admin.register(Event)
class EventAdmin(admin.ModelAdmin):
inlines = (OrderInline,)
@admin.register(Order)
class OrderAdmin(admin.ModelAdmin):
list_display = ('event', 'item', 'user',)
應該都不用解釋了。我們這裡用 StackedInline
替換了 stores
裡面用的 TabularInline
,不過它們只有外觀不同(一個是 div-based,一個是 table-based),用起來效果一樣。
接著是 views。我們想要達成以下的效果:
- 已登入的使用者可以在 store detail view 看到一個按鈕,按下去可以根據該店家建立新 event 讓大家來點餐。
- 建立完 event 後進入 event detail view。
- 所有人的點餐都會記錄在 event detail view 裡面。
- 已登入的使用者可以進入 event detail view 填 form 點餐。點完之後頁面會重新整理顯示最新狀態。
- 使用者也可以在同一頁面修改或刪除自己的 order。
所以我們需要為 event 與 order 建立 CRUD 頁面。當然,我們可以和前面一樣,建立需要的 model forms 與 views。不過你不覺得做的事情都重複了嗎?這樣一直寫一樣的東西就飽了!
所以 Django 提供了一個簡化 view 撰寫的工具:generic views。為了達到重用並保持擴充性,generic views 是用 class 配合 factory function 實作(有興趣的話可以看源碼),所以我們不再需要宣告 view functions,而是要繼承 Django 提供的 generic view classes。但這些類別裡面做的事情,其實還是和 view functions 相同。
常用的 generic views 有:
DetailView
:顯示一個 model instance 的內容。ListView
:顯示「數個」model instances(用 queryset 表示)的內容。支援 pagination。CreateView
:顯示一個 model form,接收 GET 與 POST 以建立 model instance。UpdateView
:和CreateView
類似,但是是用來更新 model instance(會指定 model form 的instance
參數)。DeleteView
:接收 POST 以刪除 model instance。也可以接收 GET,會顯示一個刪除用的 form(類似 admin 刪除時會出現的確認頁面)。TemplateView
:顯示某個特定的 template 內容。RedirectView
:回傳redirect
response。
這些 view classes 都是用 django.views.generic.View
與許多 mixins 組成,所以如果你有一天想做一些沒有內建功能的 view,也不見得就要自己從頭寫 function;說不定你可以重用一些 mixins 來達成目標!
再說下去也有點空泛,我們直接上例子。首先建立 model form:
# events/forms.py
from django import forms
from crispy_forms.helper import FormHelper
from crispy_forms.layout import Submit
from .models import Event
class EventForm(forms.ModelForm):
class Meta:
model = Event
fields = ('store',)
def __init__(self, *args, submit_title='Submit', **kwargs):
super().__init__(*args, **kwargs)
self.helper = FormHelper()
self.helper.add_input(Submit('submit', submit_title))
把 events/views.py
的內容換成這樣:
from django.views.generic import CreateView, DetailView
from .forms import EventForm
from .models import Event
class EventCreateView(CreateView):
form_class = EventForm
model = Event
class EventDetailView(DetailView):
model = Event
然後建立下面三個 templates:
{# events/templates/events/base.html #}
{% extends 'base.html' %}
{% block body %}
{{ block.super }}
<div class="container">{% block content %}{% endblock content %}</div>
{% endblock body %}
{# events/templates/events/event_form.html #}
{% extends 'events/base.html' %}
{% load crispy_forms_tags %}
{% block content %}
{% crispy form %}
{% endblock content %}
{# events/templates/events/event_detail.html #}
{% extends 'events/base.html' %}
{% block content %}
<h1>今天吃:{{ event }}。快點餐!</h1>
{% endblock content %}
最後是 URL patterns。建立 events/urls.py
from django.conf.urls import url
from . import views
urlpatterns = [
url(r'^new/$', views.EventCreateView.as_view(), name='event_create'),
url(r'^(?P<pk>\d+)/$', views.EventDetailView.as_view(),
name='event_detail'),
]
然後把它包含到 lunch/urls.py
:
urlpatterns = [
# ...
url(r'^event/', include('events.urls')),
# ...
]
完成!去 http://localhost:8000/event/new/ 新增一個 event。按下 Submit 後,你應該會直接被導向 event 的 detail view。
有跟上嗎?為什麼 views 可以這樣找到 templates,而且建立後還能自動重導向?這就是 generic views 的威力——對於簡單的應用而言,幾乎不用設定任何東西,就可以寫出你要的架構。我們明天會詳細解釋它們究竟做了什麼。