第 10 章    表單

▸ 建立 Django 表單類別

article/forms.py
from django import forms
from article.models import Article


class ArticleForm(forms.ModelForm):
    title = forms.CharField(label='標題', max_length=128)
    content = forms.CharField(label='內容', widget=forms.Textarea)

    class Meta:
        model = Article
        fields = ['title', 'content']

▸ 新增文章函式

article/views.py
...

def article(request):
    ...


def articleCreate(request):
    '''
    Create a new article instance
        1. If method is GET, render an empty form
        2. If method is POST,
           * validate the form and display error messages if the form is invalid
           * else, save it to the model and redirect to the article page
    '''
    # TODO: finish the code
    return render(request, 'article/article.html')

▸ 新增文章 URL mapping

article/urls.py:
...
urlpatterns = [
    path('', views.article, name='article'),
    path('articleCreate/', views.articleCreate, name='articleCreate'),
]

▸ 新增文章按鈕

article/templates/article/article.html
...
{% block content %}
<br>
<p><a class="btn" href="{% url 'article:articleCreate' %}">新增文章</a></p>
{% for article, comments in articles.items %}
...

▸ 按鈕樣式設定

main/static/main/css/main.css
...

ul#menu li {
  display: inline-block;
}

/* Button */
.btn {
  display: inline-block;
  color: black;
  font-size: 0.8em;
  padding: 0.5em 1em;
  text-decoration: none;
  border: thin solid gray;
  background: linear-gradient(#f7f7f7, #dedede);
}

▸ 新增文章函式處理 GET 請求

article/views.py
from django.shortcuts import render

from article.models import Article, Comment
from article.forms import ArticleForm


def article(request):
    ...
 

def articleCreate(request):
    ...
    # TODO: finish the code
    return render(request, 'article/article.html')
    template = 'article/articleCreate.html'
    if request.method == 'GET':
        return render(request, template, {'articleForm':ArticleForm()})

▸ 新增文章範本

article/templates/article/articleCreate.html
{% extends 'main/base.html' %}
{% load static %}
{% block css %}
<link rel="stylesheet" href="{% static 'article/css/article.css' %}">
{% endblock %}
{% block heading %}新增文章{% endblock %}
{% block content %}
<form method="post" action="{% url 'article:articleCreate' %}">
  {% csrf_token %}
  {{ articleForm.as_p }}
  <input type="submit" value="送出">
</form>
{% endblock %}

▸ 按鈕樣式設定

article/static/article/css/article.css
...

.commentTime {
  ...
}

input[type=text] {
  padding: 0.4em;
}

▸ 新增文章函式處理 POST 請求

article/views.py
...

def articleCreate(request):
    ...
    if request.method == 'GET':
        print(ArticleForm())
        return render(request, template, {'articleForm':ArticleForm()})

    # POST
    articleForm = ArticleForm(request.POST)
    if not articleForm.is_valid():
        return render(request, template, {'articleForm':articleForm})

    articleForm.save()
    return article(request)

▸ Django 的 Post/Redirect/Get 機制

article/views.py
from django.shortcuts import render, redirect

...

def articleCreate(request):
    ... 
    if not articleForm.is_valid():
        return render(request, template, {'articleForm':articleForm})

    articleForm.save() 
    return article(request)
    return redirect('article:article')

▸ 放棄編輯

article/templates/article/articleCreate.html
  ...
  <input class="btn" type="submit" value="送出">
  <a class="btn" href="{% url 'article:article' %}">放棄</a>
</form>  
...

▸ 按鈕字型設定

main/static/main/css/main.css
/* Button */
.btn {
  font-family: Arial;
  display: inline-block;
  ...
}

▸ 加入訊息

article/views.py
from django.shortcuts import render, redirect
from django.contrib import messages

...

def articleCreate(request):
    ...

    articleForm.save()
    messages.success(request, '文章已新增')
    return redirect('article:article')

▸ 在 base.html 顯示訊息

main/templates/main/base.html
...
<h2>部落格 -- {% block heading %}{% endblock %}</h2>
{% for message in messages %}
  <p class="{{ message.tags }}">{{ message }}</p>
{% endfor %}
{% block content %}{% endblock %}
...

▸ 為不同訊息型態加上 CSS 樣式

main/static/main/css/main.css
...

/* Button */
.btn {
  ...
}

/* Messages */
.success, .error {
  padding: 1em 0 1em 3em;
}

.success {
  background-color: #ddffdd;
  border: thin solid green;
}

.error {
  background-color: #fbd9d8;
  border: thin solid red;
}

▸ 閱讀文章函式

article/views.py
from django.shortcuts import render, redirect, get_object_or_404
...

def articleCreate(request):
    ...


def articleRead(request, articleId):
    '''
    Read an article
        1. Get the article instance; redirect to the 404 page if not found
        2. Render the articleRead template with the article instance and its
           associated comments
    '''
    article = get_object_or_404(Article, id=articleId)
    context = {
        'article': article,
        'comments': Comment.objects.filter(article=article)
    }
    return render(request, 'article/articleRead.html', context)

▸ 閱讀文章 URL mapping

article/urls.py
...
urlpatterns = [
    ...
    path('articleCreate/', views.articleCreate, name='articleCreate'),
    path('articleRead/<int:articleId>/', views.articleRead, name='articleRead'),
]

▸ 加入閱讀文章連結

article.html
...
{% for article, comments in articles.items %}
  <h3><a href="{% url 'article:articleRead' article.id %}">{{ article.title }}</a></h3>
  <p>發表時間:{{ article.pubDateTime|date:'Y-m-d H:i' }}</p>
...

▸ 閱讀文章範本

article/templates/article/articleRead.html
{% extends 'main/base.html' %}
{% load static %}
{% block css %}
<link rel="stylesheet" href="{% static 'article/css/article.css' %}">
{% endblock %}
{% block heading %}閱讀文章{% endblock %}
{% block content %}
<h3>{{ article.title }}</h3>
<p>發表時間:{{ article.pubDateTime|date:'Y-m-d H:i' }}</p>
<div class="articleContent">{{ article.content|linebreaks }}</div>
{% for comment in comments %}
  <div class="commentDiv">
    <span class="comment">{{ comment.content }}</span>
    <br>
    <span class="commentTime">{{ comment.pubDateTime|date:'Y-m-d H:i'}}</span>
  </div>
{% endfor %}
{% endblock %}

▸ 修改文章函式

article/views.py
...

def articleRead(request):
    ...


def articleUpdate(request, articleId):
    '''
    Update the article instance:
        1. Get the article to update; redirect to 404 if not found
        2. If method is GET, render a bound form
        3. If method is POST,
           * validate the form and render a bound form if the form is invalid
           * else, save it to the model and redirect to the articleRead page
    '''
    # TODO: finish the code
    return render(request, 'article/article.html')

▸ 修改文章 URL mapping

article/urls.py
urlpatterns = [
    ...
    path('articleRead/<int:articleId>/', views.articleRead, name='articleRead'),
    path('articleUpdate/<int:articleId>/', views.articleUpdate, name='articleUpdate'),
]

▸ 修改文章按鈕

article/templates/article/articleRead.html
...
<h3 class="inlineBlock">{{ article.title }}</h3>
<a class="btn inlineBlock" href="{% url 'article:articleUpdate' article.id %}">修改</a>
<p>發表時間:{{ article.pubDateTime|date:'Y-m-d H:i' }}</p>
...

▸ 修改按鈕樣式設定

main/static/main/css/main.css
...

.error {
  background-color: #fbd9d8;
  border: thin solid red;
}

/* Misc */
.inlineBlock {
  display: inline-block;
}

▸ 新增與修改功能共用同一範本

article/templates/article/articleCreateUpdate.html
...
{% block css %}
<link rel="stylesheet" href="{% static 'article/css/article.css' %}">
{% endblock %}
{% block heading %}
  {% if articleForm.instance.id %}
    修改文章
  {% else %}
    新增文章
  {% endif %}
{% endblock %}
{% block content %}
{% if articleForm.instance.id %}
  <form method="post" action="{% url 'article:articleUpdate' articleForm.instance.id %}">
{% else %}
  <form method="post" action="{% url 'article:articleCreate' %}">
{% endif %}
  {% csrf_token %}
  ...
{% endblock %}

▸ 更改新增文章所使用的範本

article/views.py
def articleCreate(request):
    ...
    template = 'article/articleCreateUpdate.html'
    ...

▸ 修改文章函式

article/views.py
...
def articleRead(request, articleId):
    ... 


def articleUpdate(request, articleId):
    '''
    Update the article instance:
        ...
    '''
    # TODO: finish the code
    return render(request, 'article/article.html')  
    article = get_object_or_404(Article, id=articleId)
    template = 'article/articleCreateUpdate.html'
    if request.method == 'GET':
        articleForm = ArticleForm(instance=article)
        return render(request, template, {'articleForm':articleForm})

    # POST
    articleForm = ArticleForm(request.POST, instance=article)
    if not articleForm.is_valid():
        return render(request, template, {'articleForm':articleForm})

    articleForm.save()
    messages.success(request, '文章已修改') 
    return redirect('article:articleRead', articleId=articleId)

▸ 刪除文章函式

article/views.py
...

def articleUpdate(request, articleId):
    ...


def articleDelete(request, articleId):
    '''
    Delete the article instance:
        1. Render the article page if the method is GET
        2. Get the article to delete; redirect to 404 if not found
    '''
    if request.method == 'GET':
        return redirect('article:article')

    # POST
    article = get_object_or_404(Article, id=articleId)
    article.delete()
    messages.success(request, '文章已刪除')  
    return redirect('article:article')

▸ 刪除文章 URL mapping

article/urls.py
urlpatterns = [
    ...
    path('articleUpdate/<int:articleId>/', views.articleUpdate, name='articleUpdate'),
    path('articleDelete/<int:articleId>/', views.articleDelete, name='articleDelete'),
]

▸ 刪除文章按鈕

article/templates/article/article.html
...
{% for article, comments in articles.items %}
  <h3 class="inlineBlock"><a href="{% url 'article:articleRead' article.id %}">{{ article.title }}</a></h3>
  <form class="inlineBlock" method="post" action="{% url 'article:articleDelete' article.id %}">
    {% csrf_token %}
    <input class="btn" type="submit" value="刪除">
  </form>
  <p>發表時間:{{ article.pubDateTime|date:'Y-m-d H:i' }}</p>
...

▸ 刪除資料再次確認

main/static/main/js/deleteConfirm.js
$(document).ready(function () {
  $(document).on('click', '.deleteConfirm', function() {
    return confirm("確定刪除?");
  });
});

▸ 在 base.html 中建立 Google jQuery 連結,並且新增 CSS 範本區塊

main/templates/main/base.html
...
{% block content %}{% endblock %}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js"></script>
{% block script %}{% endblock %}
</body>
</html>

▸ 在刪除按鈕加上 deleteConfirm 之 CSS 類別,並在最後加上 script 範本區塊

article/templates/article/article.html
...
{% block content %}
...

<form class="inlineBlock" method="post" ...>
  {% csrf_token %}
  <input class="btn deleteConfirm" type="submit" value="刪除">
</form>

...
{% endblock %}
{% block script %}
<script src="{% static 'main/js/deleteConfirm.js' %}"></script>
{% endblock %}

▸ 搜尋文章函式

article/views.py
from django.shortcuts import render, redirect, get_object_or_404
from django.contrib import messages
from django.db.models.query_utils import Q

...

def articleDelete(request, articleId):
    ... 


def articleSearch(request):
    '''
    Search for articles:
        1. Get the "searchTerm" from the HTML form
        2. Use "searchTerm" for filtering
    '''
    searchTerm = request.GET.get('searchTerm')
    articles = Article.objects.filter(Q(title__icontains=searchTerm) |
                                      Q(content__icontains=searchTerm))
    context = {'articles':articles} 
    return render(request, 'article/articleSearch.html', context)

▸ 搜尋文章 URL mapping

article/urls.py
urlpatterns = [
    ...
    path('articleDelete/<int:articleId>/', views.articleDelete, name='articleDelete'),
    path('articleSearch/', views.articleSearch, name='articleSearch'),
]

▸ 搜尋表單

article/templates/article/searchForm.html
<form class="inlineBlock" action="{% url 'article:articleSearch' %}">
  <input type="text" name="searchTerm">
  <input class="btn" type="submit" value="查詢">
</form>

▸ 在文章列表上方加上搜尋表單:

article/templates/article/article.html
...
{% block content %}
{% include 'article/searchForm.html' %}
<p class="inlineBlock"><a class="btn" href="{% url 'article:articleCreate' %}">新增文章</a></p>
<br><br>
<hr>

{% for article, comments in articles.items %}
...

▸ 查詢結果範本

article/templates/article/articleSearch.html
{% extends 'main/base.html' %}
{% load static %}
{% block heading %}查詢結果{% endblock %}
{% block css %}
<link rel="stylesheet" href="{% static 'article/css/article.css' %}">
{% endblock %}
{% block content %}
{% include 'article/searchForm.html' %}
<br><br><hr>

{% if not articles %}
  <p>查無資料</p>
{% else %}
  <table class="table table-striped table-hover">
    <tr><th>標題</th><th>發表時間</th></tr>
    {% for article in articles %}
    <tr>
      <td><a href="{% url 'article:articleRead' article.id %}">{{ article.title }}</a></td>
      <td>{{ article.pubDateTime|date:'Y-m-d H:i' }}</td>
    </tr>
    {% endfor %}
  </table>
{% endif %}
{% endblock %}

▸ 表格樣式

article/static/article/css/article.css
...

input[type=text] {
  padding: 0.4em;
}

/* Table */
.table {
  margin: 0 auto;
  width: 90%;
  border-collapse: collapse;
}

.table th {
  font-size: 1.1em;
  padding: 10px 0 10px 10px;
  text-align: left;
  background-color: #468cb1;
  border-bottom: 1px solid black
}

.table td {
  color: #222222;
  background-color: #e0e0e0;
  padding: 12px 0 12px 10px;
}

.table-striped tr:nth-child(even) td {
  background-color: #E1F9DC;
}

.table-hover tr:hover td {
  background-color: #cdeaf0;
}

▸ 顯示搜尋字串

article/views.py
...

def articleSearch(request):
    ...
    context = {'articles':articles, 'searchTerm':searchTerm} 
    return render(request, 'article/articleSearch.html', context)
article/templates/article/searchForm.html
<form class="inlineBlock" action="{% url 'article:articleSearch' %}">
  <input type="text" name="searchTerm" {% if searchTerm %}value="{{ searchTerm }}"{% endif %}>
  <input class="btn" type="submit" value="查詢">
</form>

▸ 本章完成專案:blog10.zip