Pertemuan 14 - Flask Fundamental

Pertemuan 14 - Flask Fundamental

Mengubah aplikasi terminal menjadi aplikasi web dengan Flask, template Jinja2, dan Bootstrap.

Tujuan Praktikum

Setelah menyelesaikan praktikum ini, mahasiswa mampu:

  1. Menjelaskan cara kerja aplikasi web (request dan response).
  2. Membuat aplikasi Flask dan menjalankan server pengembangan.
  3. Membuat routing dengan @app.route.
  4. Menampilkan data menggunakan template Jinja2 (render_template, {{ }}, {% for %}, {% if %}).
  5. Mempercantik tampilan dengan Bootstrap dan template induk (layout.html).

Persiapan

Kebutuhan praktikum:

text
- VS Code
- Project toko-elektronik
- Database + models + repository/produk_repository.py (dari pertemuan sebelumnya)
- Virtual environment aktif (venv)

Aktifkan venv, lalu pasang Flask:

bash
venv\Scripts\activate
pip install flask
pip freeze > requirements.txt
macOS / Linux: gunakan source venv/bin/activate.

Buat file app.py dan folder templates/ di project. Struktur project kini:

text
toko-elektronik/
├── database/toko.db
├── models/
├── repository/
│   └── produk_repository.py
├── templates/              <- baru (semua file HTML di sini)
│   ├── layout.html
│   ├── beranda.html
│   ├── produk.html
│   ├── detail.html
│   └── 404.html
└── app.py                  <- baru (menggantikan main.py)

Konsep Singkat

Dari Terminal ke Web

Selama Pertemuan 12–13, aplikasi kita berjalan di terminal dan menampilkan data dengan print(). Mulai sekarang, aplikasi yang sama akan tampil sebagai halaman web di browser.

Kabar terpentingnya: "mesin" aplikasi tidak dibongkar. Repository dan database dari pertemuan sebelumnya dipakai apa adanya. Yang berubah hanya cara menampilkan.

Cara Kerja Aplikasi Web

Aplikasi web bekerja dengan pola request–response:

Alur request-response
  1. Pengguna membuka alamat (misalnya /produk) di browser → mengirim request.
  2. Flask menerima request, menentukan fungsi mana yang menanganinya (routing).
  3. Fungsi mengambil data lewat repository (sama seperti di terminal).
  4. Data digabung dengan template HTML menjadi halaman utuh.
  5. Halaman dikirim kembali ke browser sebagai response.

Tiga Istilah Baru

Istilah Arti
Routing Menentukan fungsi mana yang menangani suatu alamat/URL
Template Berkas HTML yang bisa disisipi data Python
Jinja2 Bahasa template bawaan Flask ({{ }} dan {% %})

Langkah 1 - Aplikasi Flask Pertama

Buka app.py, tulis kode paling sederhana:

python
from flask import Flask

app = Flask(__name__)


@app.route("/")
def beranda():
    return "<h1>Halo, Toko Elektronik!</h1>"


if __name__ == "__main__":
    app.run(debug=True)

Jalankan:

bash
python app.py

Output di terminal:

text
 * Running on http://127.0.0.1:5000
 * Debugger is active!

Buka browser ke alamat http://127.0.0.1:5000. Akan tampil tulisan "Halo, Toko Elektronik!".

Penjelasan:

Bagian Arti
Flask(__name__) Membuat aplikasi Flask
@app.route("/") Menangani alamat / (halaman utama)
def beranda(): Fungsi yang dijalankan saat alamat itu dibuka
app.run(debug=True) Menjalankan server; debug=True menampilkan error & memuat ulang otomatis
Tips: dengan debug=True, setiap kali kode disimpan, server otomatis memuat ulang. Tidak perlu menjalankan ulang manual. Untuk menghentikan server, tekan Ctrl+C.

Langkah 2 - Menghubungkan Repository

Menampilkan teks biasa tidak menarik. Mari tampilkan data produk sungguhan dari database. Perbarui app.py:

python
from flask import Flask, render_template
from repository.produk_repository import ProdukRepository

app = Flask(__name__)
repo = ProdukRepository()          # repository yang SAMA dari Pertemuan 12


@app.route("/produk")
def daftar_produk():
    produk = repo.ambil_semua()
    return render_template("produk.html", produk=produk)


if __name__ == "__main__":
    app.run(debug=True)

Perhatikan baris produk = repo.ambil_semua(). Ini method yang persis sama dengan yang dipakai di terminal pada Pertemuan 12. Tidak ada yang diubah.

render_template("produk.html", produk=produk) berarti: ambil file produk.html, kirimkan data produk ke dalamnya, lalu hasilkan halaman jadi.

Langkah 3 - Template Induk (layout.html)

Agar semua halaman punya tampilan seragam (navbar, Bootstrap), kita buat satu template induk. Halaman lain cukup mewarisinya.

Buat templates/layout.html:

html
<!DOCTYPE html>
<html lang="id">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>{% block title %}Toko Elektronik{% endblock %}</title>
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body class="bg-light">

<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
    <div class="container">
        <a class="navbar-brand" href="/">Toko Elektronik</a>
        <div class="navbar-nav">
            <a class="nav-link" href="/">Beranda</a>
            <a class="nav-link" href="/produk">Produk</a>
        </div>
    </div>
</nav>

<div class="container my-4">
    {% block content %}{% endblock %}
</div>

</body>
</html>

Penjelasan:

  • Bootstrap dipanggil lewat satu baris <link> dari CDN. Cukup itu untuk mendapat tampilan rapi.
  • {% block title %} dan {% block content %} adalah lubang yang akan diisi oleh halaman turunan.
  • Navbar ini otomatis muncul di semua halaman yang mewarisi layout.html.

Langkah 4 - Menampilkan Data dengan Jinja2

Sekarang buat templates/produk.html yang mewarisi induk dan menampilkan tabel produk:

html
{% extends "layout.html" %}
{% block title %}Daftar Produk{% endblock %}

{% block content %}
<h1 class="mb-4">Daftar Produk</h1>

<table class="table table-striped table-hover bg-white shadow-sm">
    <thead class="table-dark">
        <tr>
            <th>ID</th>
            <th>Nama Produk</th>
            <th>Kategori</th>
            <th class="text-end">Harga</th>
            <th class="text-center">Stok</th>
            <th></th>
        </tr>
    </thead>
    <tbody>
        {% for p in produk %}
        <tr>
            <td>{{ p.id_produk }}</td>
            <td>{{ p.nama_produk }}</td>
            <td><span class="badge bg-secondary">{{ p.kategori }}</span></td>
            <td class="text-end">Rp {{ "{:,}".format(p.harga) }}</td>
            <td class="text-center">
                {% if p.stok < 10 %}
                    <span class="badge bg-danger">{{ p.stok }}</span>
                {% else %}
                    <span class="badge bg-success">{{ p.stok }}</span>
                {% endif %}
            </td>
            <td><a href="/produk/{{ p.id_produk }}" class="btn btn-sm btn-outline-primary">Detail</a></td>
        </tr>
        {% endfor %}
    </tbody>
</table>

<p class="text-muted">Total: {{ produk|length }} produk</p>
{% endblock %}

Inilah tiga alat utama Jinja2:

Sintaks Fungsi Contoh
{{ ... }} Menampilkan nilai {{ p.nama_produk }}
{% for ... %} Perulangan membuat satu baris tabel per produk
{% if ... %} Percabangan badge merah bila stok < 10

Perhatikan kaitannya dengan Python:

  • {% for p in produk %} sama seperti for p in produk:produk adalah data yang dikirim render_template.
  • {{ p.nama_produk }} mengakses atribut objek, persis seperti di Python.
  • {{ "{:,}".format(p.harga) }} memberi pemisah ribuan (Rp 8,500,000).

Jalankan python app.py, buka http://127.0.0.1:5000/produk:

Halaman daftar produk

Data yang sama dari terminal kini tampil sebagai tabel web yang rapi.

Langkah 5 - Halaman Beranda dan Detail

Beranda dengan Ringkasan

Tambahkan route beranda di app.py:

python
@app.route("/")
def beranda():
    daftar = repo.ambil_semua()
    total_nilai = sum(p.nilai_persediaan() for p in daftar)
    return render_template("beranda.html", jumlah=len(daftar), total_nilai=total_nilai)

Buat templates/beranda.html:

html
{% extends "layout.html" %}
{% block title %}Beranda - Toko Elektronik{% endblock %}

{% block content %}
<h1 class="mb-4">Selamat Datang</h1>

<div class="row">
    <div class="col-md-6">
        <div class="card text-bg-primary mb-3">
            <div class="card-body">
                <h5 class="card-title">Jumlah Produk</h5>
                <p class="card-text fs-2">{{ jumlah }}</p>
            </div>
        </div>
    </div>
    <div class="col-md-6">
        <div class="card text-bg-success mb-3">
            <div class="card-body">
                <h5 class="card-title">Nilai Persediaan</h5>
                <p class="card-text fs-2">Rp {{ "{:,}".format(total_nilai) }}</p>
            </div>
        </div>
    </div>
</div>

<a href="/produk" class="btn btn-primary">Lihat Daftar Produk</a>
{% endblock %}

Beranda kini menampilkan Jumlah Produk = 4 dan Nilai Persediaan = Rp 132.500.000 (memakai method nilai_persediaan() dari Pertemuan 11).

Halaman beranda

Halaman Detail (Route dengan Parameter)

Route dapat menerima nilai dari URL. Tambahkan di app.py:

python
@app.route("/produk/<int:id_produk>")
def detail_produk(id_produk):
    p = repo.ambil_by_id(id_produk)
    if p is None:
        return render_template("404.html", pesan=f"Produk id {id_produk} tidak ditemukan"), 404
    return render_template("detail.html", p=p)

<int:id_produk> berarti bagian URL itu adalah angka yang dikirim ke fungsi. Jadi /produk/1 memanggil detail_produk(1).

Buat templates/detail.html:

html
{% extends "layout.html" %}
{% block title %}{{ p.nama_produk }}{% endblock %}

{% block content %}
<div class="card shadow-sm">
    <div class="card-body">
        <h2 class="card-title">{{ p.nama_produk }}</h2>
        <p class="text-muted">{{ p.kategori }}</p>
        <hr>
        <p><strong>Harga:</strong> Rp {{ "{:,}".format(p.harga) }}</p>
        <p><strong>Stok:</strong> {{ p.stok }}</p>
        <p><strong>Nilai Persediaan:</strong> Rp {{ "{:,}".format(p.nilai_persediaan()) }}</p>
        <a href="/produk" class="btn btn-secondary">Kembali</a>
    </div>
</div>
{% endblock %}

Buat juga templates/404.html untuk produk yang tidak ditemukan:

html
{% extends "layout.html" %}
{% block content %}
<div class="alert alert-warning">
    <h4>Tidak Ditemukan</h4>
    <p>{{ pesan }}</p>
    <a href="/produk" class="btn btn-primary">Kembali ke Daftar Produk</a>
</div>
{% endblock %}

Uji: buka /produk/1 (tampil detail Laptop), lalu /produk/999 (tampil pesan tidak ditemukan).

Halaman detail produk

Langkah 6 - app.py Lengkap

Berikut app.py utuh setelah semua route digabung:

python
from flask import Flask, render_template
from repository.produk_repository import ProdukRepository

app = Flask(__name__)
repo = ProdukRepository()


@app.route("/")
def beranda():
    daftar = repo.ambil_semua()
    total_nilai = sum(p.nilai_persediaan() for p in daftar)
    return render_template("beranda.html", jumlah=len(daftar), total_nilai=total_nilai)


@app.route("/produk")
def daftar_produk():
    produk = repo.ambil_semua()
    return render_template("produk.html", produk=produk)


@app.route("/produk/<int:id_produk>")
def detail_produk(id_produk):
    p = repo.ambil_by_id(id_produk)
    if p is None:
        return render_template("404.html", pesan=f"Produk id {id_produk} tidak ditemukan"), 404
    return render_template("detail.html", p=p)


if __name__ == "__main__":
    app.run(debug=True)
Perhatikan: di seluruh app.py tidak ada satu pun perintah SQL. Semua urusan database tetap ditangani repo. Inilah bukti nyata manfaat pemisahan lapisan dari Pertemuan 12 — kita mengganti tampilan dari terminal ke web tanpa menyentuh repository sedikit pun.

Referensi Fungsi Penting

Flask

Perintah Fungsi
Flask(__name__) Membuat aplikasi
@app.route("/alamat") Menentukan alamat/URL
@app.route("/produk/<int:id>") Alamat dengan parameter angka
render_template("file.html", data=data) Menggabungkan template dengan data
app.run(debug=True) Menjalankan server pengembangan
return ..., 404 Mengirim response dengan kode status

Jinja2 (di dalam template)

Sintaks Fungsi
{{ nilai }} Menampilkan nilai
{% for x in daftar %} ... {% endfor %} Perulangan
{% if syarat %} ... {% else %} ... {% endif %} Percabangan
{% extends "layout.html" %} Mewarisi template induk
{% block nama %} ... {% endblock %} Menandai bagian yang bisa diisi
`{{ daftar\ length }}` Filter (di sini: menghitung jumlah)
{{ "{:,}".format(angka) }} Format ribuan

Padanan Terminal → Web

Terminal (Pertemuan 12) Web (Pertemuan 14)
print(p) {{ p }} di template
for p in repo.ambil_semua(): print(p) {% for p in produk %} di template
Menu angka (input()) Alamat URL & tautan
repo.ambil_semua() repo.ambil_semua() (sama)

Pengujian

Pastikan hasil praktikum sesuai berikut:

  1. python app.py berjalan dan server aktif di http://127.0.0.1:5000.
  2. Halaman beranda menampilkan Jumlah Produk = 4 dan Nilai Persediaan = Rp 132.500.000.
  3. Halaman /produk menampilkan tabel berisi 4 produk dengan navbar Bootstrap di atas.
  4. Produk dengan stok kurang dari 10 ditandai badge merah, selebihnya hijau.
  5. Halaman /produk/1 menampilkan detail Laptop; /produk/999 menampilkan pesan "tidak ditemukan".
  6. Navbar muncul di semua halaman (bukti layout.html diwarisi).

Troubleshooting

Masalah Penyebab Solusi
TemplateNotFound: produk.html File HTML bukan di folder templates/ Pastikan semua template ada di folder templates
Halaman menampilkan {{ p.nama_produk }} mentah File dibuka langsung, bukan lewat Flask Akses lewat http://127.0.0.1:5000, bukan klik file HTML
ModuleNotFoundError: No module named 'repository' app.py tidak di folder utama project Letakkan app.py di root project
Tampilan polos tanpa Bootstrap Tidak ada internet / link CDN salah Periksa koneksi dan baris <link> Bootstrap
Address already in use Server lain masih jalan di port 5000 Tekan Ctrl+C di terminal lama, atau ubah port: app.run(port=5001)
Perubahan tidak muncul Lupa menyimpan / server tidak reload Simpan file; pastikan debug=True
Halaman 404 dari Flask (bukan milik kita) Alamat salah ketik Cek @app.route dan URL di browser

Tugas Praktikum

Kembangkan aplikasi web:

  1. Tambahkan halaman /produk/kategori/<nama> yang menampilkan produk pada satu kategori saja (mis. /produk/kategori/Elektronik). Tambahkan method pendukung di repository bila perlu.
  2. Pada halaman daftar produk, tambahkan baris total di bawah tabel yang menampilkan jumlah keseluruhan nilai persediaan.
  3. Tambahkan menu "Pelanggan" di navbar dan halaman /pelanggan yang menampilkan daftar pelanggan (pakai PelangganRepository bila sudah dibuat di tugas Pertemuan 12, atau buat sederhana).
  4. Ubah tampilan beranda: tambahkan satu kartu (card) lagi yang menampilkan jumlah produk dengan stok menipis (< 10).
  5. Ganti warna atau tema navbar, dan tambahkan footer berisi nama serta NIM Anda di layout.html (agar muncul di semua halaman).

Kumpulkan seluruh file .py dan .html beserta tangkapan layar tiap halaman di browser (format PDF).

Kesimpulan

Pada pertemuan ini aplikasi penjualan berpindah dari terminal ke web menggunakan Flask. Mahasiswa mempelajari pola request–response, membuat routing dengan @app.route, menampilkan data dengan template Jinja2 ({{ }}, {% for %}, {% if %}), serta merapikan tampilan dengan Bootstrap dan template induk layout.html. Hal terpenting: seluruh repository dan database dari pertemuan sebelumnya dipakai tanpa perubahan — yang berganti hanya lapisan tampilan. Inilah bukti nyata mengapa kode disusun berlapis sejak awal. Pada pertemuan berikutnya, aplikasi web ini akan dilengkapi kemampuan menambah, mengubah, dan menghapus data melalui form (Flask CRUD), sehingga menjadi aplikasi penjualan yang benar-benar interaktif.