Menjadi Developer Web dengan Python dan Flask Bagian III: Menampilkan dan Memproses Form

Bagus Aji Santoso 2 Januari 2018

 Menjadi Developer Web dengan Python dan Flask Bagian III: Menampilkan dan Memproses Form

Artikel ini merupakan terjemahan The Flask Mega-Tutorial Part III: Web Forms dari seri The Flask Mega-Tutorial karya Miguel Grinberg yang akan dirilis secara gratis bab per bab sampai bulan Mei 2018. Dukung penulis aslinya dengan membeli buku/video tutorial lengkapnya di sini.

Daftar Isi:

  1. Menjadi Developer Web dengan Python dan Flask: Hello World
  2. Menjadi Developer Web dengan Python dan Flask: Template
  3. Menjadi Developer Web dengan Python dan Flask: Menampilkan dan Memproses Form (Artikel ini)

Di Bab 2 kita membuat template sederhana untuk halaman home dan menggunakan fake object sebagai placeholder hal-hal yang kita belum selesai kita buat seperti user atau blog post. Di bab ini kita akan membahas satu kekurangan yang belum kita miliki di aplikasi ini yaitu bagaimana menerima data dari user melalui web form.

Web form adalah salah satu komponen paling mendasar dari aplikasi web apapun. Kita akan menggunakan form untuk menerima data dari user berupa blog post dan untuk melakukan login ke dalam aplikasi.

Membuat web form di aplikasi ini akan kita lakukan dengan ekstensi Flask-WTF yang merupakan sebuah wrapper untuk package WTForm yang lebih memudahkan. Ekstensi merupakan bagian yang sangat penting dari ekosistem Flask karena mereka memberikan solusi untuk permasalahan umum yang tidak langsung disediakan oleh Flask.

Ekstensi Flask hanyalah package Python biasa yang dapat dipasang dengan pip. Kita dapat memasang ekstensi di atas dengan perintah:

(venv) $ pip install flask-wtf

Sebelum melanjutkan bab ini pastikan aplikasi pembaca sudah memiliki aplikasi microblog sesuai dengan langkah terakhir di bab sebelumnya dan sudah berjalan tanpa ada error.

Link GitHub untuk bab ini adalah: Browse, Zip, Diff.

Configuration

Sejauh ini aplikasi yang kita buat sangat sederhana dan oleh karena itu penulis belum pusing memikirkan konfigurasinya. Tapi, untuk aplikasi lainnya kita akan menemukan bahwa Flask (dan kemungkinan ekstensi Flask lain) menawarkan kebebasan untuk melakukan sesuatu. Karena kita tentu akan memilih salah satu cara, maka kita akan mengirimkan cara yang dipilih melalui configuration variables.

Ada beberapa format aplikasi yang dapat dipakai untuk membuat opsi konfigurasi tersebut. Solusi paling sederhana ialah dengan membuat variabel sebagai key di app.config yang akan menggunakan gaya dictionary untuk bekerja dengan variabel. Contoh, kita bisa membuat isi konfigurasi menjadi seperti berikut ini:

app = Flask(__name__)
app.config['SECRET_KEY'] = 'you-will-never-guess'
# ... add more variables here as needed

Meskipun sintaks diatas sudah cukup untuk memenuhi kebutuhan kita, namun penulis ingin memaksakan prinsip separation of concern yang kurang lebih artinya membagi program sesuai dengan tujuannya. Dengan begitu kita akan memisahkan seluruh konfigurasi di file yang berbeda.

Cara yang paling penulis sukai untuk memisahkannya adalah dengan menggunakan sebuah kelas. Kita akan membuat kelas konfigurasi di modul Python yang berbeda. Di bawah ini pembaca dapat melihat kelas konfigurasi baru untuk aplikasi kita yang disimpan di dalam modul config.py di direktori utama:

import os

class Config(object):
    SECRET_KEY = os.environ.get('SECRET_KEY') or 'you-will-never-guess'

Sangat sederhana bukan? Konfigurasi yang ingin kita tambahkan ditulis sebagai class variable di dalam kelasConfig. Apabila nanti aplikasi akan membutuhkan item konfigurasi tambahan, mereka dapat ditulis sebagi class variable yang baru.

Variabel konfigurasi SECRET_KEY merupakan bagian paling penting dari sebuah aplikasi Flask. Flask dan beberapa ekstensinya menggunakan isi dari variabel secret key sebagai cryptographic key untuk membuat signature atau token. Ekstensi Flask-WTF menggunakan secret key untuk menjaga web form dari serangan bernama Cross-Site Request Forgery atau CSRF (dibaca "seasurf"). Sesuai namanya, isi secret key seharusnya rahasia karena tingkat keamanan sebuah token atau signature ini bergantung pada orang-orang yang mengatahui konten suatu aplikasi (developer/perusahaan/maintainer).

Isi secret key dapat berupa dua nilai. Nilai yang pertama akan mengambil isi dari environment variables (disebelah kiri or), apabila tidak tersedia maka isi secret key akan diambil dari string yang di hardcoded-kan. Pola ini akan diulang beberapa kali untuk variabel konfigurasi yang lain. String yang di hardcoded masih memiliki kekurangan karena bisa dilihat orang lain tanpa sengaja (terunggah ke repositori publik semacam GitHub misalnya) oleh karena itu kita akan mengambil dari environment variable yang ada di server production sehingga yang tahu hanya yang memiliki akses ke server tersebut, orang lain yang hanya bisa melihat source code aplikasi tidak akan tahu.

Karena sudah memiliki sebuah file konfigurasi, sekarang kita beritahu Flask untuk membaca dan mengaplikasikan file tersebut. Hal ini dapat dilakukan setelah pembuatan application instance di dalam method app.config.from_object(Config):

from flask import Flask
from config import Config

app = Flask(__name__)
app.config.from_object(Config)

from app import routes

Cara impor kelas Config mungkin terlihat membingungkan, namun jika pembaca memperhatikan bagaimana kelas Flask ("F" besar) di impor kelas package flask ("f" kecil) maka apa yang kita lakukan dengan kelas Config sebetulnya sama. Bagian "config" kecil merupakan nama modul Python config.py dan bagian Config jelas sekali merupakan nama Kelas di dalam modul tadi.

Seperti yang sudah dibahas di atas, isi konfigurasi dapat diakses menggunakan sintaks dictionary dari app.config. Disini pembaca dapat melihat bagaimana penulis dapat memeriksa isi dari secret key:

>>> from microblog import app
>>> app.config['SECRET_KEY']
'you-will-never-guess'

User Login Form

Ekstensi Flask-WTF menggunakan kelas Python untuk merepresentasi web forms. Sebuah kelas form menentukan field dari sebuah form sebagai variabel kelas.

Mari kita buat modul baru bernama app/forms.py untuk membuat kelas-kelas yang kita butuhkan. Mari kita mulai dengan membuat sebuah user login form yang nantinya akan meminta user memasukkan sebuah username, sebuah password beserta check box "remember me" dan tentu saja sebuah tombol submit:

from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, BooleanField, SubmitField
from wtforms.validators import DataRequired

class LoginForm(FlaskForm):
    username = StringField('Username', validators=[DataRequired()])
    password = PasswordField('Password', validators=[DataRequired()])
    remember_me = BooleanField('Remember Me')
    submit = SubmitField('Sign In')

Sebagian besar ekstensi Flask menggunakan aturan penulisan flask_<name> saat melakukan impor modul utama. Misalnya pada contoh di atas, semua simbol Flask-WTF ada di modul flask_wtf. Modul tersebut merupakan kelas utama yang diimpor di file app/forms.py.

Empat kelas lain yang kita impor dari package WTForms (StringField, PasswordField, BooleanField, SubmitField dan DataRequired) merepresentrasikan isi form yang ingin dibuat. Setiap isi form diwakilkan oleh sebuah objek berupa variabel kelas di dalam LoginForm. Setiap isi form ini diberikan sebuah deskripsi atau label sebagai argumen pertama.

Argumen tambahan, validators, memungkinakn kita untuk memberikan fitur validasi tambahan. Validator DataRequired akan memeriksa apabila ada kolom yang belum diisi saat form dikirim. Ada banyak validator lain yang tersedia, beberapa diantaranya akan kita pakai di form lain.

Form Templates

Langkah berikutnya ialah untuk menambahkan form tadi ke template HTML sehingga form tersebut dapat ditampilkan di halaman web. Berita baiknya, kolom-kolom yang ada di kelas LoginForm sudah tahu bagaimana ia harus tampil sebagai HTML. Di bawah ini kita bisa melihat login template yang kita simpan dengan nama app/templates/login.html:

{% extends "base.html" %}

{% block content %}
    <h1>Sign In</h1>
    <form action="" method="post" name="login">
        {{ form.hidden_tag() }}
        <p>
            {{ form.username.label }}<br>
            {{ form.username(size=32) }}
        </p>
        <p>
            {{ form.password.label }}<br>
            {{ form.password(size=32) }}
        </p>
        <p>{{ form.remember_me() }} {{ form.remember_me.label }}</p>
        <p>{{ form.submit() }}</p>
    </form>
{% endblock %}

Kita kembali menggunakan template base.html seperti yang kita gunakan di Bab 2 melalui perintah extends. Kita akan selalu menggunakan template tersebut untuk memastikan layout yang konsisten dengan navigation bar disemua halaman.

Template ini akan menggunakan sebuah objek yang dibuat dari kelas LoginForm bernama form. Objek form akan dikirim dari fungsi view login yang belum kita buat.

Perintah form.hidden_tag() akan membuat hidden field yang di dalamnya ada sebuah token untuk menjaga form ini dari CSRF attack. Kita tidak perlu tahu detail bagaimana penjagaan yang dilakukan, yang perlu kita lakukan hanyalah menambahkan perintah tadi dan memiliki sebuah variabel SECRET_KEY di konfigurasi Flask.

Jika sebelumnya pembaca sudah pernah membuat form HTML, pembaca mungkin kaget karena tidak ada kode HTML yang kita tuliskan. Hal ini karena field dari objek form sudah tahu bagaimana untuk berubah menjadi kode HTML. Yang perlu kita lakukan adalah untuk menambahkan {{ form.<field_name>.label }} untuk menampilkan label dan {{ form.<field_name>() }} untuk menampilkan input-nya.

Form Views

Sebelum kita bisa melihat bagaimana form ini tampil di browser, kita perlu membuat view function terlebih dahulu.

Sekarang mari kita tulis sebuah view function yang akan dipasang ke URL /login yang akan membuat sebuah form dan mengirimkannya ke template untuk di tampilkan. View function ini juga akan di tambahkan ke app/routes.py seperlu view function sebelumnya:

    from flask import render_template
    from app import app
    from app.forms import LoginForm

    # ...

    @app.route('/login')
    def login():
        form = LoginForm()
        return render_template('login.html', title='Sign In', form=form)

Yang kita lakukan di atas adalah mengimpor kelas LoginForm dari forms.py, membuat objek dari kelas tersebut dan mengirimnya ke template. Sintaks form=form mungkin terlihat aneh, tapi maksud dari perintah tersebut ialah mengirimkan objek form (bagian sebelah kanan) yang kita buat di baris atasnya ke template dengan nama form (bagian sebelah kiri).

Untuk memudahkan kita mengakses login form, tambahkan sebuah link ke navigation bar di base template:

<div>
    Microblog:
    <a href="/index">Home</a>
    <a href="/login">Login</a>
</div>

Sammpai di sini kita bisa menjalankan aplikasi dan emlihat form tampil di web browser. Jalankan aplikasinya lalu buka http://localhost:5000/ di address bar browser dan klik link "Login" untuk melihat form login yang sudah kita buat. Keren kan?

Login Form

Receiving Form Data

Saat tombol submit di klik, browser akan menampilkan pesan "Method Not Allowed". Hal ini karena view function belum selesai ditulis. Dia sudah bisa menampilkan form di halaman web tapi belum bisa menangani data yang dikirim oleh user. Disinilah Flask-WTF memudahkan pekerjaan kita. Berikut ini view function yang sudah diperbarui untuk membaca data yang dikirim user:

from flask import render_template, flash, redirect

@app.route('/login', methods=['GET', 'POST'])
def login():
    form = LoginForm()
    if form.validate_on_submit():
        flash('Login requested for user {}, remember_me={}'.format(
            form.username.data, form.remember_me.data))
        return redirect('/index')
    return render_template('login.html', title='Sign In', form=form)

Hal baru yang kita tambahkan adalah argument methods di route decorator. Argumen ini memberitahu Flask untuk menerima request GET dan POST, menimpa konfigurasi awal yang hanya menerima GET.

Saat browser memanggil request GET, method form.validate_on_submit() akan mengembalikan nilai False sehingga view function akan langsung me-render template yang ada di baris terakhir. Namun, apabila browser mengirim request POST, maka method tersebut akan mengembalikan nilai True dan melanjutkan proses pembacaan form. Perlu diingat, apabila salah satu kolom form gagal melalui validasi, maka fungsi tersebut juga akan mengembalikan nilai False sehingga form akan ditampilkan lagi ke user seperti saat pemanggilan request GET. Nanti kita akan menambahkan pesan keterangan saat validasi gagal dilakukan.

Apabila form.validate_on_submit() mengembalikan nilai True, view function login kita memanggil dua fungsi lainnya yang diimpor dari Flask. Fungsi flash() berguna untuk menampilkan sebuah pesan ke user. Banyak aplikasi menggunakan teknik ini untuk memberitahu pengguna apakah aksi yang mereka lakukan berhasil atau gagal. Dalam kasus ini, kita menggunakan teknik flash() sebagai solusi sementara untuk memberitahu user jika dia berhasil login karena kita belum punya sistem user.

Fungsi yang kedua adalah redirect(). Fungsi ini akan membawa user ke halaman yang diberikan sebagai argumennya, dalam kasus ini user akan di bawah ke URL /index.

Saat kita memanggil fungsi flash(), Flask sudah menyimpan isi pesan yang akan ditampilkan tapi pesan tersebut tidak secara otomatis tampil. Mari kita tambahkan pesan ini ke template sehingga semua template dapat menampilkan pesan flash di manapun.

<html>
    <head>
        {% if title %}
        <title>{{ title }} - microblog</title>
        {% else %}
        <title>microblog</title>
        {% endif %}
    </head>
    <body>
        <div>
            Microblog:
            <a href="/index">Home</a>
            <a href="/login">Login</a>
        </div>
        <hr>
        {% with messages = get_flashed_messages() %}
        {% if messages %}
        <ul>
            {% for message in messages %}
            <li>{{ message }}</li>
            {% endfor %}
        </ul>
        {% endif %}
        {% endwith %}
        {% block content %}{% endblock %}
    </body>
</html>

Perintah with akan menyimpan method get_flashed_messages() ke variabel messages. Method ini didapat dari Flask dan akan menampilkan semua pesan yang ditambah oleh pemanggilan flash(). Perintah if di bawahnya akan memeriksa apakah variabel messages memiliki isi, jika tidak, elemen <ul> akan menampilkan setiap pesan sebagai item <li>. Tampilannya belum terlihat bagus karena kita baru akan membahas bagaimana melakukan styling nanti.

Setelah diminta sekali melalui pemanggilan fungsi get_flashed_messages, maka pesan tersebut akan dihapus sehingga hanya tampil sekali setelah ditambahkan dari fungsi flash().

Improving Field Validation

Validator yang dipasang ke kolom form akan mencegah data diterima oleh aplikasi. Cara aplikasi kita menangani error saat form dikirim saat salah satu kolom tidak diisi ialah dengan menampilkan ulang form tersebut sehingga user bisa mengisi data-data yang kurang.

Jika pembaca mencoba mengirimkan data yang tidak valid maka pembaca pasti menemukan bahwa mekanisme di atas sudah bekerja dengan baik. Namun, kemungkinan ada user yang tidak mengetahui bahwa ada data yang tidak sesuai sehingga selanjutnya kita akan meningkatkan user experience dengan menambahkan pesan error yang memberitahu user di sebelah setiap kolom yang gagal.

Validator form sebetulnya sudah membuatkan pesan error saat terjadi, sehingga kita tinggal menampilkan isi pesan tersebut.

Berikut ini template login yang telah di tambahkan pesan validasi dikolom username dan password:

{% extends "base.html" %}

{% block content %}
    <h1>Sign In</h1>
    <form action="" method="post" name="login">
        {{ form.hidden_tag() }}
        <p>
            {{ form.username.label }}<br>
            {{ form.username(size=32) }}<br>
            {% for error in form.username.errors %}
            <span style="color: red;">[{{ error }}]</span>
            {% endfor %}
        </p>
        <p>
            {{ form.password.label }}<br>
            {{ form.password(size=32) }}<br>
            {% for error in form.password.errors %}
            <span style="color: red;">[{{ error }}]</span>
            {% endfor %}
        </p>
        <p>{{ form.remember_me() }} {{ form.remember_me.label }}</p>
        <p>{{ form.submit() }}</p>
    </form>
{% endblock %}

Perusahan yang penulis lakukan hanya menambah perulangan for di bawah kolom username dan password yang akan menampilkan pesan error dari validator berwarna merah. Kolom yang menggunakan validator akan memiliki pesan error diproperti form.<field_name>.errors. Pesan error ini berupa list karena setiap kolom dapat memiliki beberapa validator sehingga dapat memiliki beberapa pesan error juga.

Jika sekarang pembaca mencoba mengirim form dengan kolom username atau password kosong, maka akan ada pesan error berwarna merah yang tampil.

Form validation

Generating Links

Secara umum login form sudah selesai. Sebelum kita menutup bab ini, mari kita bahas bagaimana menambahkan tautan di template dan redirect. Sejauh ini kita sudah melihat bagaimana tautan ditulis. Contoh, navigation bar yang ada di base template kita memiliki tautan sebagai berikut:

    <div>
        Microblog:
        <a href="/index">Home</a>
        <a href="/login">Login</a>
    </div>

View function login juga menuliskan tautan didalam fungsi redirect:

@app.route('/login', methods=['GET', 'POST'])
def login():
    form = LoginForm()
    if form.validate_on_submit():
        # ...
        return redirect('/index')
    # ...

Satu masalah yang dapat muncul jika menuliskan tautan secara langsung di template atau source file yang lain adalah apabila suatu hari nanti nama tautan ini diganti, maka kita harus mencari dan mengganti tautan-tautan lain yang ada dibanyak file di aplikasi tersebut.

Oleh karena itu untuk mempermudah, Flask memberikan sebuah fungsi bernama url_for() yang akan meuliskan URL dari daftar yang tersedia. Misal, url_for('login') akan menuliskan /login, dan url_for('index') akan mengembalikan /index. Argumen di dalam url_for() adalah nama endpoint yang merupakan nama view function yang kita buat.

Pembaca mungkin bertanya mengapa menggunakan nama fungsi lebih baik ketimbang alamat URL-nya langsung. Faktanya URL kemungkinan akan lebih sering berubah dibanding nama fungsi. Alasan kedua yang akan kita pelajari nanti, beberapa URL dapat memiliki komponen dinamis sehingga menuliskan URL tersebut secara manual akan memerlukan operasi kontakenansi yang sarat kesalahan. url_for() juga dapat membuat URL yang kompleks seperti itu.

Jadi mulai dari sekarang, kita akan menggunakan url_for() setiap kali kita perlu menuliskan URL. Kode navigation bar di base template ubah menjadi:

    <div>
        Microblog:
        <a href="{{ url_for('index') }}">Home</a>
        <a href="{{ url_for('login') }}">Login</a>
    </div>

Dan berikut ini view function login() yang sudah diperbaiki:

from flask import render_template, flash, redirect, url_for

# ...

@app.route('/login', methods=['GET', 'POST'])
def login():
    form = LoginForm()
    if form.validate_on_submit():
        # ...
        return redirect(url_for('index'))
    # ...