วันเสาร์ที่ 1 กุมภาพันธ์ พ.ศ. 2557

Django Book Chapter 7 : Forms

    จาก Chapter 6 ที่ผ่านไปแล้วนั้นทำให้เราได้รู้ถึงวิธีการใช้ admin กันไปแล้วแต่ในบทนี้เราจะมาศึกษาเกี่ยวกับการใช้ form ใน Django พร้อมกับการนำไปใช้ใน Templates

1.พื้นฐานเกี่ยวกับ request ที่มีอยู่ใน views ของบทที่ 3
def hello(request):
    return HttpResponse("Hello world")
ซึ่ง request นั้นมี method พื้นฐานสำหรับการดึงค่าออกมาให้แล้วดังนี้
  • request.path ใช้สำหรับดึง url ออกมาโดยไม่รวม domain
  • request.get_host() ใช้สำหรับดึง domain ออกมา
  • request.get_full_path() ใช้สำหรับดึง url ออกมาเช่นเดียวกับ request.path แต่จะเพิ่ม query string เข้ามาหากว่ามี
  • request.is_secure() จะให้ค่าออกมาเป็น True กับ False โดยจะเป็น True เมื่อ request นี้เป็น https นอกนั้นเป็น False ทั้งหมด
ตัวอย่างการนำไปใช้
def current_url_view_good(request):
    return HttpResponse("Welcome to the page at %s" % request.path)
สิ่งที่จะ return ไปจะได้เป็น Welcome to the page at ตามด้วย Url ณ ปัจจุบันนั่นเอง

2.โดยนอกจาก method ในข้อแรกนั้น request ยังมี request.META อยู่อีกด้วยซึ่งเป็นข้อมูลแบบ Dictionary โดยมีตัวอย่าง Key ดังนี้
  • HTTP_USER_AGENT ซึ่งจะได้ผลลัพธ์เป็นข้อมูลเกี่ยวกับ Browser ที่เรียก request
  • REMOTE_ADDR จะให้ค่าออกมาเป็น IP Address ของ Client ที่เรียก request
ตัวอย่างการใช้งาน
def ua_display_good1(request):
    try:
        ua = request.META[’HTTP_USER_AGENT’]
    except KeyError:
        ua = ’unknown’
    return HttpResponse("Your browser is %s" % ua)
    เหตุผลที่ต้องใส่ try & except ก็เพราะว่าข้อมูลนี้เป็นแบบ Dictionary เวลาที่ดึงข้อมูลหากไม่มีก็จะเกิด Error KeyError ได้ดังนั้นเลยต้องใส่เงื่อนไขไว้เพื่อรับมือกับปัญหานี้

3.request.GET และ request.POST ทั้งสองอย่างนี้เป็น object ที่คล้ายๆกับ Dictionary ซึ่งทั้งสองนี้จะมีค่าได้โดยรับมาจาก form ผ่านทาง HTML แต่ GET นั้นจะแตกต่างกับ POST ที่ GET จะแสดงข้อมูลบน Url ด้วยแต่ POST จะไม่แสดงข้อมูลใดๆใน Url ด้วยเหตุนี้ทำให้ GET เหมาะกับการใช้ในการดึงข้อมูลออกมาแสดงผล แต่หากเป็นข้อมูลที่ผลต่อการเปลี่ยนใน Database หรือมีผลต่อคำนวณอื่นๆจะเหมาะกับการใช้ POST มากกว่า

4.ทดลองใช้ form ผ่าน Template โดยเริ่มต้นเข้าไปแก้ไขไฟล์ Views ใน books ดังนี้
from django.http import HttpResponse
from django.shortcuts import render
from books.models import Book

def search_form(request):
    return render(request, 'search_form.html')

def search(request):
    if 'q' in request.GET and request.GET['q']:
        q = request.GET['q']
        books = Book.objects.filter(title__icontains=q)
        return render(request, 'search_results.html', {'books': books, 'query': q})
    else:
        return HttpResponse('Please submit a search term.')
    ฟังก์ชั่น search นั้นจะดึงค่าของ q ออกมาจาก request.GET โดยถ้าไม่ใส่ค่าอะไรมาเลยก็จะไม่มี q ใน request แล้วแสดงผลออกทางหน้าจอว่า Please submit a search term. แทนแต่หากเราใส่ค่ามาก็จะแสดงรายชื่อหนังสือออกมาให้เราดูพร้อมบอกคำที่เราใส่ใน form ก่อนหน้านี้

จากนั้นสร้าง Template ขึ้นมาชื่อ search_form.html เก็บไว้ใน Folder Templates ที่สร้างไว้ใน Chapter 3 และมีโค้ดภายในดังนี้
<html>
<head>
    <title>Search</title>
</head>
<body>
    <form action="/search/" method="get">
        <input type="text" name="q">
        <input type="submit" value="Search">
    </form>
</body>
</html>
    โดย action ใน form คือการบอกว่า form นี้เมื่อมีการ submit จะไปยัง Address ไหนหากไม่ใส่ค่าก็จะเป็นการ submit มายัง Address เดิมที่อยู่ ณ ปัจจุบัน ใน method ใช้ GET หรือ POST โดยการใช้นั้นอยู่ในข้อ 3 ที่ได้อธิบายไว้ก่อนหน้านี้ จากนั้นมี input 2 ตัวซึ่งเป็น input text เอาไว้รับตัวอักษรและค่าตัวนี้สามารถดึงค่าออกมาโดยมี name เป็น key และ input submit เป็นปุ่มกดเอาไว้ส่งค่าใน input text ไปยัง GET และ เปลี่ยน Url ไปตาม action

สร้าง Templates อีกตัวชื่อ search_results.html
<p>You searched for: <strong>{{ query }}</strong></p>

{% if books %}
    <p>Found {{ books|length }} book{{ books|pluralize }}.</p>
    <ul>
        {% for book in books %}
        <li>{{ book.title }}</li>
        {% endfor %}
    </ul>
{% else %}
    <p>No books matched your search criteria.</p>
{% endif %}
และเข้าไปแก้ไข Url ใน mysite โดยเพิ่มโค้ดเข้าไปดังนี้ from books import views
และเพิ่ม (r’^search-form/$’, views.search_form), กับ (r’^search/$’, views.search), เข้าไปใน urlpatterns

5.จาก form ในข้อ 4 หากเราไม่ใส่ค่าอะไรก็จะเด้งไปแสดงผลอีกหน้าว่าให้ใส่ค่าแต่เราต้องมานั่งกดย้อนกลับมาหน้าใส่ค่าใหม่ดังนั้นเราจะทำให้หน้าเว็บนี้กลับไปหน้าที่ให้ใส่ค่าใหม่เองและแสดงผล Please submit a search term. เอง โดยแก้ไข Templates search_form.html ดังนี้
<html>
<head>
    <title>Search</title>
</head>
<body>
    {% if error %}
        <p style="color: red;">Please submit a search term.</p>
    {% endif %}
    <form action="/search/" method="get">
        <input type="text" name="q">
        <input type="submit" value="Search">
    </form>
</body>
</html>
และแก้ไขฟังก์ชั่น search ใน views ของ books ดังนี้
def search(request):
    error = False
    if 'q' in request.GET:
        q = request.GET['q']
        if not q:
            error = True
        else:
            books = Book.objects.filter(title__icontains=q)
            return render(request, 'search_results.html', {'books': books, 'query': q})
    return render(request, 'search_form.html', {'error': error})
สึ่งที่แตกต่างจากตอนแรกก็คือหากเช็คแล้วว่า q ไม่ได้ใส่ค่าอะไรมานั้นจะเรียกหน้า search_form.html เองอีกครั้งเพียงแต่ครั้งนี้ error จะส่งค่า True ไปให้ทำหน้าเว็บนั้นแสดงผล Please submit a search term. ออกมาแล้วก็มี form ซึ่งอยู่ในหน้า search_form.html รอให้เรากรอกค่าใหม่ลงไป

6.จากข้อ 5 นั้นหากเราต้องการจะกำหนดเพิ่มว่า q ที่ส่งมานั้นต้องมีความยาวไม่เกิน 20 ตัวหากยาวเกินจะไม่ค้นหาให้ซึ่งส่วนที่ต้องแก้หลักๆก็ยังคงเป็น Templates และ Views เช่นเดิมดังนั้นเปิดไฟล์ Views.py ใน books แล้วแก้ไขตามนี้
def search(request):
    errors = []
    if 'q' in request.GET:
        q = request.GET['q']
        if not q:
            errors.append('Enter a search term.')
        elif len(q) > 20:
            errors.append('Please enter at most 20 characters.')
        else:
            books = Book.objects.filter(title__icontains=q)
            return render(request, 'search_results.html', {'books': books, 'query': q})
    return render(request, 'search_form.html', {'errors': errors})
แล้วทำการแก้ไข Templates search_form.html อีกรอบ
<html>
<head>
    <title>Search</title>
</head>
<body>
    {% if errors %}
        <ul>
            {% for error in errors %}
            <li>{{ error }}</li>
            {% endfor %}
        </ul>
    {% endif %}
    <form action="/search/" method="get">
        <input type="text" name="q">
        <input type="submit" value="Search">
    </form>
</body>
</html>
    โดยในฟังก์ชั่น search รอบนี้นั้นเปลี่ยน error ไปเป็น errors ซึ่งกก็คือการเปลี่ยนจากค่า True False ไปเป็น list ที่เก็บข้อมูลของ error ไว้แทนซึ่งหาก error เข้ากรณีใดก็ตามก็จะนำประโยคที่บอก error ใส่เข้าไปใน list นั่นเองหากเราไม่ใช่ list แบบนี้แต่ยังคงให้เป็น True False แบบเดิมก็ยังสามารถเช็ค error ได้แต่เราจะไม่สามารถระบุข้อความในการแสดง error ได้เลยนั่นเอง โดยวิธีเช็คว่าเกิน 20 ตัวอักษรหรือไม่ก็ใช้ len() ซึ่งเป็น API พื้นฐานของ Python ใช้ระบุความยาวของ object ได้ซึ่งในที่นี้จะได้ความยาวของตัวอักษร q ออกมา ส่วนใน Templates เมื่อเปลี่ยนเป็น list ของ error ดังนั้นเราจึงให้ Templates วนลูปเข้าถึงทุกตัวแปรที่อยู่ใน errors เพื่อปริ้นข้อความของ errors ออกมาให้หมด

7.ทดลองการสร้าง Contact form โดยศึกษาจากโค้ดดังต่อไปนี้
<html>
<head>
    <title>Contact us</title>
</head>
<body>
    <h1>Contact us</h1>

    {% if errors %}
        <ul>
            {% for error in errors %}
            <li>{{ error }}</li>
            {% endfor %}
        </ul>
    {% endif %}

    <form action="/contact/" method="post">
        <p>Subject: <input type="text" name="subject" value="{{ subject }}"></p>
        <p>Your e-mail (optional): <input type="text" name="email" value="{{ email }}"></p>
        <p>Message: <textarea name="message" rows="10" cols="50">**{{ message }}**</textarea></p>
        <input type="submit" value="Submit">
    </form>
</body>
</html>
และมีฟังก์ชั่น contact ดังนี้
def contact(request):
    errors = []
    if request.method == 'POST':
        if not request.POST.get('subject', ''):
            errors.append('Enter a subject.')
        if not request.POST.get('message', ''):
            errors.append('Enter a message.')
        if request.POST.get('email') and '@' not in request.POST['email']:
            errors.append('Enter a valid e-mail address.')
        if not errors:
           send_mail(
               request.POST['subject'],
               request.POST['message'],
               request.POST.get('email', 'noreply@example.com'),
        ['siteowner@example.com'],)
        return HttpResponseRedirect('/contact/thanks/’)
    return render(request, 'contact_form.html', {
        'errors': errors,
        'subject': request.POST.get('subject', ''),
        'message': request.POST.get('message', ''),
        'email': request.POST.get('email', ''),})
    โดยจะมี Form รับ Input แบบ POST มาส่งให้ทางฟังก์ชั่นใช้ซึ่งต้องกรอกค่าให้ครบทุกช่องหากขาดช่องใดช่องหนึ่งจะเกิด error แล้วโหลดหน้า contact ใหม่อีกครั้งแต่หากไม่มี error ก็จะเรียกฟังก์ชั่นส่งเมลไป ในส่วนของ Template นั้น <textarea name="message" rows="10" cols="50"> โดย input ตัวนี้นั้นเป็นกล่องที่ใส่ข้อความโดยสามารถกำหนดขนาดได้เองจาก rows และ cols

8.นอกจาก Django จะสามารถใช้งาน Form ในแบบทั่วๆไปแล้วนั้น Django เองก็สามารถใช้ Form ในรูปแบบของ Class ได้อีกด้วยดังนั้นเริ่มต้นด้วยการสร้างไฟล์ forms.py ใน โฟลเดอร์ books และมีโค้ด ดังนี้
from django import forms

class ContactForm(forms.Form):
    subject = forms.CharField()
    email = forms.EmailField(required=False)
    message = forms.CharField()
    โดยใน Code นี้จะเป็นการเอา API forms จาก Django มาใช้ซึ่งมีมาให้อยู่แล้ว และสร้าง Class ContactForm โดยสืบทอดมาจาก Class Form ใน forms และมีตัวแปรรับค่า 3 ตัวคือ subject, email, message โดย email จะมี required=False ซึ่งเป็นการระบุว่าตัวแปรนี้ไม่จำเป็นต้องใส่มาให้ก็ได้เพราะโดยปกติแล้วนั้นใน Class forms ของ Django จะตั้งค่าพื้นฐานให้ทุกตัวแปรต้องใส่ค่ามาให้ถึงจะไม่เกิด error ขึ้นเมื่อใช้งานเพื่อรับข้อมูลจากหน้าเว็บซึ่ง required=False มีไว้เพื่อให้สามารถตั้งค่าได้ตามต้องการ

9.ทดสอบ ContactForm() ใน forms.py ผ่าน shell ของ Django


    จากการ print ค่าของ ContactForm() ออกมาจะเห็นว่าจะได้ออกมาเป็น Code HTML ในรูปของตารางนั่นเองซึ่งโดยพื้นฐานแล้ว Django จะใช้รูปแบบของตารางเป็นค่าเริ่มต้น แต่ก็มีรูปแบบอื่นให้เลือกใช้อีกเช่น <ul>, <p>


    จากนั้นจากใน Class ที่เราสร้างไว้และได้กล่าวถึง required=False เราจะมาทดสอบกันว่ามันมีผลอย่างไรบ้าง


    แบบแรก (In [6]) เราสร้าง Object ContactForm โดยกำหนดค่าให้ครบทุกตัวแล้วตรวจสอบโดยใช้ method is_valid() ซึ่งได้ผลออกมาว่าเป็น True แสดงว่า Form เมื่อรับค่านี้ทำงานได้ปกติไม่มีปัญหา จากนั้นในแบบที่สอง (In [8]) ทดลองตัด email ออกไปแล้วทดสอบด้วน is_valid() เหมือนเดิมก็ยังได้ True เพราะ required=False ที่เรากำหนดไว้ดังนั้นเมื่อไม่มีค่าอะไร Form นี้ก็ยังคงไม่มีปัญหาใดๆเกิดขึ้น ในแบบที่สาม (In [10]) รอบนี้เหลือไว้เพียง subject ตัวเดียวแล้วทดสอบโดย is_valid() เช่นเดิมผลที่ได้จะเป็น False ก็เพราะว่าเราไม่ได้ใส่ค่า message เข้าไปด้วยสังเกตุได้ว่าจากใน In [12] เราใช้ f.errors จะได้เหตุผลที่เกิด error ขึ้นซึ่งผลที่ได้คือ {'message': [u'This field is required.']} และเหตุผลก็เพราะใน Class ContactForm ตัวแปร message ของเราไม่ได้ใส่ required=False ไว้นั่นเอง

10.นำ ContactForm() ที่เราสร้างไว้มาใช้ใน contact(request) โดยแก้ไขฟังก์ชั่น contact(request) และ Template contact_form.html ดังนี้
from books.forms import ContactForm
def contact(request):
    if request.method == 'POST':
        form = ContactForm(request.POST)
        if form.is_valid():
            cd = form.cleaned_data
            send_mail(cd['subject'], cd['message'], cd.get('email', 'noreply@example.com'), ['siteowner@example.com'],)
            return HttpResponseRedirect('/contact/thanks/')
    else:
        form = ContactForm()
    return render(request, 'contact_form.html', {'form': form})
Template contact_form.html แก้ไขดังนี้
<html>
<head>
    <title>Contact us</title>
</head>
<body>
    <h1>Contact us</h1>
    {% if form.errors %}
        <p style="color: red;">
            Please correct the error{{ form.errors|pluralize }} below.
        </p>
    {% endif %}
    <form action="" method="post">
        {% csrf_token %}
        <table>
            {{ form.as_table }}
        </table>
        <input type="submit" value="Submit">
    </form>
</body>
</html>
    จาก Templates ที่แก้ไขนั้นเราจะนำ form ที่สร้างขึ้นจาก ContactForm() มาใส่ในช่วง {{ form.as_table }} และหากมี error ก็จะดึงค่าจาก form.errors มาแสดงผลเช่นเดียวกันจากตรงนี้หน้า Contact ของเราจะทำงานได้เหมือนก่อนหน้านี้แต่ว่าส่วนของ message นั้นจะกลายเป็น input text ธรรมดา และ จำนวนโค้ดที่ใช้นั้นจะสั้นลงอย่างมากถือเป็นข้อดีในการใช้ Class ContactForm เข้าช่วย นอกจากนี้ในฟังก์ชั่น contact ใน views ของเราเองก็ไม่ต้องนั่งเขียนส่วนที่ใช้ตรวจสอบ error ของ form ที่ส่งมาเองอีกด้วย

11.จากข้อ 10 message นั้นถูกเปลี่ยนไปเป็น input text ธรรมดาแต่หากเราต้องการให้เป็น input textarea แบบเดิมก็สามารถทำได้โดยเข้าไปแก้ไข Class ContactForm ดังนี้
class ContactForm(forms.Form):
    subject = forms.CharField(max_length=100)
    email = forms.EmailField(required=False)
    message = forms.CharField(widget=forms.Textarea)
    โดยการเพิ่ม widget=forms.Textarea เข้าไปใน message ทำให้เมื่อเรา render templates ขึ้นมาใช้ message ก็กลับไปเป็น input textarea เช่นเดิม

12.นอกจากเราจะกำหนด input แบบที่เราต้องการได้แล้วเราก็ยังสามารถกำหนดจำนวนตัวอักษรสูงสุดและกำหนด Label ของ input ได้อีกเช่นกันโดยวิธีการคือการเพิ่ม max_length=100 เข้าไปใน subject และเพิ่ม label='Your e-mail address' เข้าไปใน email ของ Class ContactForm จะได้รูปแบบดังนี้
class ContactForm(forms.Form):
    subject = forms.CharField(max_length=100)
    email = forms.EmailField(required=False, label='Your e-mail address')
    message = forms.CharField(widget=forms.Textarea)
13.นอกจากจะกำหนด label, input, ความยาวตัวอักษรสูงสุด ได้เองเรายังสามารถใส่ค่าเริ่มให้ form ของเราได้อีกด้วยโดยแก้ไขในฟังก์ชั่น contact ดังนี้
def contact(request):
    if request.method == 'POST':
        form = ContactForm(request.POST)
        if form.is_valid():
            cd = form.cleaned_data
            send_mail(cd['subject'], cd['message'], cd.get('email', 'noreply@example.com'), ['siteowner@example.com'],)
            return HttpResponseRedirect('/contact/thanks/')
    else:
        form = ContactForm(initial={'subject': 'I love your site!'})
    return render(request, 'contact_form.html', {'form': form})
    จาก initial={'subject': 'I love your site!'} คือการกำหนดค่าเริ่มต้นให้กับ subject นั่นเองหากเราอยากกำหนดให้ email และ message ก็ได้เช่นกันโดยใส่ค่าเข้าไปใน initial นั่นเอง

14.การใช้ Class ContactForm ก็ช่วยให้เราลดโค้ดในฟังก์ชั่น contact ลงไปได้แต่เราไม่สามารถกำหนดเงื่อนไขในเช็คตัวแปรใดๆได้เอง ทาง Django จะเป็นตัวจัดการให้แต่หากเราต้องการเพิ่มเงื่อนไขเองก็สามารถทำได้เช่นกันโดยการเพิ่มฟังก์ชั่นนี้เข้าไปใน Class ContactForm
def clean_message(self):
    message = self.cleaned_data[’message’]
    num_words = len(message.split())
    if num_words < 4:
        raise forms.ValidationError("Not enough words!")
    return message
    ซึ่ง Django จะเรียกฟังก์ชั่น clean_message เองเมื่อตอนที่เช็คว่า message นั้นถูกต้องหรือไม่ โดยหลักการก็คือให้เราตั้งชื่อฟังก์ชั่นว่า clean_ ตามด้วยชื่อตัวแปรที่ต้องการตรวจสอบ Django ก็จะรู้เองว่าฟังก์ชั่นนี้มีไว้เพื่อเช็คตัวแปรไหน โดยฟังก์ชั่นที่เขียนเพิ่มมานี้จะทำการเช็คว่า message ที่ส่งมานั้นมีจำนวนคำน้อยกว่า 4 ตัวหรือไม่ หากน้อยกว่า 4 คำก็จะ error แล้วส่ง exception ไปว่า Not enough words! เพื่อไปแสดงผลในหน้าเว็บนั่นเอง

15.จากข้อ 10 ที่เรานำ class ContactForm มาใช้ครั้งแรกนั้นเราให้ Django สร้างรูปแบบของ Form เองทั้งหมดแต่หากเราต้องการจัดรูปแบบเองอย่างละเอียด เราก็สามารถทำได้เช่นกันโดยการแก้ไข Templates ดังตัวอย่าง
<form action="" method="post">
    {% csrf_token %}
    <div class="field">
        {{ form.subject.errors }}
        <label for="id_subject">Subject:</label>
        {{ form.subject }}
    </div>
    <div class="field">
        {{ form.email.errors }}
        <label for="id_email">Your e-mail address:</label>
        {{ form.email }}
    </div>
    <div class="field">
        {{ form.message.errors }}
        <label for="id_message">Message:</label>
        {{ form.message }}
    </div>
    <input type="submit" value="Submit">
</form>
    จากตัวอย่างจะเห็นว่าเราสามารถเข้าถึง form ของแต่ละตัวตัวแปรได้โดยตรงเลยเช่น {{ form.subject }} หรือถ้าหากต้องการดู error ของตัวไหนก็ทำได้เช่น {{ form.subject.errors }} ดังนั้นเมื่อเราเข้าถึงตัว form และ error แบบนี้ได้นั้นเราก็สามารถจัดรูปแบบของหน้าเว็บเองได้เช่นกันไม่จำเป็นต้องใช้ตามรูปแบบที่ Django จัดมาให้

    จาก Chapter 7 นี้เราได้เรียนรู้เกี่ยวกับการใช้งาน Form ใน Django กันไปแล้วโดยใช้ Form ทั้งในรูปแบบของ Class หรือสร้างไว้ใน Template เองก็ได้ซึ่งก็แล้วแต่ความถนัดและความชอบของแต่ละคนว่าใช้จะใช้แบบไหนดีเพราะถ้าใช้ใน Class เราไม่ต้องกังวลเรื่องการเช็คว่าค่าที่ส่งมาใน Form จะถูกต้องไหมเพราะ Django เองก็จะตรวจสอบให้ในเบื้องต้น และ เรายังสามารถกำหนดเงื่อนไขเพิ่มเองได้อีกด้วย แต่การจัดรูปแบบใน Template ก็จะยุ่งยากกว่าแบบไม่ใช่ Class ส่วนรูปแบบที่ไม่ใช่ Class ก็มีข้อเสียคือเราต้องจัดการตรวจเช็คทุกอย่างจาก Form เองอีกทั้งโค้ดที่ต้องเขียนนั้นยังมีความยาวมากกว่าอีกด้วย
Read more ...