Phần 4: Template và Database

Mục lục

  1. Giới thiệu
  2. Cài đặt và chuẩn bị môi trường
  3. Xây dựng bộ khung cho một ứng dụng Flask
  4. Template và Database
  5. Xây dựng các tính năng chính của hệ thống

Tìm hiểu về Jinja template

Các bạn có thể truy cập vào đây để đọc tài liệu chi tiết của Jinja. Phần này mình sẽ giới thiệu qua một chút về template engine này, đủ để các bạn có thể đọc và hiểu source code của project.

Phần trước, mình đã giới thiệu tới các bạn hàm render_template() để render giao diện từ file HTML:

    return render_template('index.html')

Ngoài tên của template được đưa vào ở tham số thứ nhất (ở đây là index.html), hàm render_template() còn cho phép chúng ta đưa vào các tham số khác giúp template có thể hiển thị tùy biến dựa vào một điều kiện hoặc biến được đưa vào.

Ví dụ trong server.py mình có một list các server với tên như sau:

serverList = ['google.com', 'wordpress.com', 'quora.com', 'vnexpress.net', 'dantri.com.vn']

và mình muốn hiển thị danh sách này ra trang chủ dưới dạng một unordered-list, mỗi server một dòng thì làm thế nào?

Nếu như bài toán đặt ra đơn giản là hãy in ra console danh sách này dùng lệnh print, bạn sẽ biết ngay là sẽ phải dùng for loop:

for server in serverList:
    print server

Cũng tương tự như thế, Jinja2 cung cấp cho chúng ta một vòng lặp for để có thể chạy qua tất cả các giá trị trong một list và in ra. Cách dùng sẽ y hệt như dùng một vòng lặp for thông thường, tất nhiên là trông sẽ hơi loằng ngoằng một chút thôi. Vòng lặp for trong Jinja template sẽ có cú pháp như sau:


{% if em_yêu_thích_chó %}
    <p> Mua Chó </p>
{% elif em_yêu_thích_mèo %}
    <p> Mua Mèo </p>
{% elif em_yêu_thích_SH %}
    <h3> Mua SH </h3>
{% else %} <!-- chắc là thích ô tô -->
    <h1> Hết tiền </h1>
{% endif %}

Có thể ban đầu bạn sẽ cảm thấy hơi khó hiểu cú pháp này, mình cũng thế. Nhưng chỉ sau vài lần viết là sẽ quen. Chỉ cần lưu ý là không giống Python, Jinja yêu cầu bạn phải chỉ định điểm kết thúc của for loop bằng {% endfor %} hoặc câu điều kiện if dùng {% endif %}

Vậy file template HTML làm cách nào để có thể lấy được biến serverList như trong for loop ở phía trên? Chúng ta cho phép template nhận biến này bằng cách đưa tham số đó vào trong hàm render_template()

Trong server.py:

@app.route('/')
def index():
    serverList = ['google.com', 'wordpress.com', 'quora.com', 'vnexpress.net', 'dantri.com.vn']

    return render_template('index.html', serverList=serverList)

Bằng cách truyền tham số serverList vào hàm render_template() như trên, chúng ta có thể tùy ý sử dụng tham số này trong template.

Trong template chúng ta cũng có thể truy cập vào các object như request, session hay g của Flask. Mình sẽ nói qua về các object này trong những phần tới.

Template thực sự trở nên rất hữu ích và phát huy tối đa sức mạnh của nó khi được sử dụng trong Template Inheritance. Mình giới thiệu về lí thuyết này trong phần dưới.

Trong project mà chúng ta sẽ viết, trên thanh navigation bar, bạn sẽ muốn hiển thị đường link để Login khi user chưa đăng nhập và nếu user đã đăng nhập thì sẽ hiển thị Logout. Mình không biết đối với các ngôn ngữ khác thế nào, nhưng bạn có thể làm điều này rất dễ dàng với Jinja:


{% if not session.logged_in %}  
    <p class="navbar-text navbar-right login"> <a href="/login">
        Login (default: <strong>admin</strong> / <strong>admin</strong>)</a> 
    </p>
{% else %}  
    <p class="navbar-text navbar-left"> <a href="/addUser"> Add User </a> </p>
    <p class="navbar-text navbar-right logout"> <a href="/logout"> Logout </a> </p>
{% endif %}

session, cũng giống render_template() hay url_for(), sẽ phải được import nếu bạn muốn sử dụng:

from flask import session, ...

session là một object của Flask, cho phép chúng ta lưu thông tin của người dùng khi họ truy cập vào ứng dụng. session mã hóa cookies, cho phép người dùng có thể nhìn nhưng không thể chỉnh sửa cookies, trừ khi người dùng biết được khóa bảo mật được dùng để mã hóa.

Khóa bảo mật, giống như một tham số cấu hình à? Đúng vậy, bạn cần phải cấu hình tham số này (SECRET_KEY) để có thể sử dụng session. Vậy bạn đã biết để tham số này ở đâu chưa? Cũng đúng luôn, chúng ta sẽ để nó ở trong config.cfg và bạn nhớ là hãy viết in hoa nhé:

SECRET_KEY = 'Cực_kỳ_bảo_mật'

Một SECRET_KEY càng an toàn thì càng phải mang tính ngẫu nhiên. Bạn có thể sử dụng hàm os.urandom() của Python để làm việc này. Hàm os.urandom() sử dụng bộ sinh số ngẫu nhiên của chính hệ điều hành để tạo ra một chuỗi số ngẫu nhiên cho bạn:

>>> import os
>>> os.urandom(24)
'H<"\x7f\xfd\x9dz\xaa\xba\x92\n}p\x9eC\xb8fl\xdc_\xd9\x92v0'

Sau đó hãy copy & paste chuỗi này vào trong file config.cfg là được:

SECRET_KEY = 'H<"\x7f\xfd\x9dz\xaa\xba\x92\n}p\x9eC\xb8fl\xdc_\xd9\x92v0'


Template Inheritance

Đây được coi là tính năng mạnh nhất của Jinja. Template Inheritance cho phép bạn xây dựng một template chung, bao gồm tất cả những phần phổ biến nhất của ứng dụng và định nghĩa ra các block mà các template con có thể ghi đè lên.

Ví dụ, trong project này hay bất kì website nào khác, phần header và footer thường là những phần phổ biến nhất. Header và footer thường xuất hiện trong tất cả các URL. Chính vì thế rất hợp lí nếu cho 2 phần này vào một template “mẹ”, và các phần khác ở trong các template con.

Đầu tiên các bạn hãy xóa toàn bộ nội dung trong template index.html mà chúng ta đã viết ở phần trước, và copy chúng vào trong một file template mẹ, gọi là layout.html. Cây thư mục của chúng ta sẽ như sau:

server_monitoring\
    server.py
    config.cfg
    schema.sql
    static\
        bootstrap.min.css
        jquery.min.js
    templates\
        index.html
        layout.html

Đây là nội dung của file template mẹ layout.html:


<!doctype html>
<title>Server Monitoring</title>

<link rel="stylesheet" href="{{ url_for('static', filename='bootstrap.min.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">

<div class="container">
    <nav class="navbar navbar-default">
        <div class="container-fluid">
            <a class="navbar-brand" href="/">Server Monitoring</a>
            {% if not session.logged_in %}  <!-- if user not logged in, show Login link -->
                <p class="navbar-text navbar-right login"> <a href="/login">
                    Login (default: <strong>admin</strong> / <strong>admin</strong>)</a> 
                </p>
            {% else %}  <!-- otherwise, show logout link -->
                <p class="navbar-text navbar-left"> <a href="/addUser"> Add User </a> </p>
                <p class="navbar-text navbar-right logout"> <a href="/logout"> Logout </a> </p>
            {% endif %}
        </div>
    </nav>

    <!-- Sub template -->
    {% block body %} {% endblock %}
    
    <hr>
    <footer>
        <p class="text-muted text-center credit">Simple Server Monitoring System</p>
    </footer>
</div>

<script src="{{ url_for('static', filename='jquery.min.js') }}"></script>
<script src="{{ url_for('static', filename='main.js') }}"></script>

Bạn sẽ để ý thấy hầu hết nội dung trong layout.html sẽ không khác mấy so với index.html, ngoài việc load thêm các file .js.css, điểm rõ nhất đó là việc thêm vào dòng này:


{% block body %} {% endblock %}

Bằng việc “thừa kế” template layout.html bằng các template con, block này cho phép ghi đè nội dung từ các template con tại đây, tất cả các phần khác sẽ được giữ nguyên.

Vậy thừa kế thế nào? Hiện tại chúng ta đã xóa toàn bộ nội dung của index.html, bước tiếp theo chúng ta sẽ biến index.html thành một template con của layout.html, thừa hưởng toàn bộ phần header và footer của layout.html và chèn phần thông tin server và trạng thái vào giữa.


{% extends "layout.html" %}
{% block body %}

{% endblock %}

Chuỗi loằng ngoằng ở dòng đầu tiên {% extends "layout.html" %} chỉ định rằng template này là một template con của layout.html, toàn bộ nội dung sau đó bên trong {% block body %} {% endblock %} sẽ được ghi đè lên layout.html. Lưu ý là body chính là tên của block sẽ bao gồm nội dung sẽ được ghi đè từ template con, tên này phải giống nhau từ cả template mẹ và con.

Dưới đây là toàn bộ nội dung của index.html


{% extends "layout.html" %}
{% block body %}

    {% if session.logged_in %}
        <form action="" method=post>
            <dl>
                <strong>Server Name: </strong>
                <input type="text" size=20 name=name class="input">
                <input type="submit" value="Add" class="btn btn-primary btn-sm">
            </dl>
        </form>
    {% endif %}

    <div id="content">
        <table class="table table-striped">
                <tr>
                    <th class="header">SERVER</th>
                    <th class="header">STATUS</th>  
                    <th class="header">ACTION</th>          
                </tr>

            {% for server in serverList %}      
                <tr> 
                    <td class="server-name">  </td> 
                    {% if server.status == 0 %}
                        <td class="online"> Online </td>
                    {% else %}
                        <td class="offline"> Offline </td>
                    {% endif %}

                    <td>
                        <form action="/removeServer/" method="post">
                            <!-- Only allow logged in user to remove server -->
                            {% if session.logged_in %}
                                <input type="submit" value="Remove" class="btn btn-danger btn-sm">
                            {% else %}
                                <input type="submit" value="Remove" class="btn btn-danger btn-sm" disabled>
                            {% endif %}
                        </form>
                    </td>
                </tr>   
            {% endfor %}
        </table>
    </div>
{% endblock %}

index.html sẽ kiểm tra nếu user đã đăng nhập hay chưa. Nếu đúng thì sẽ hiển thị một form cho phép user thêm user để theo dõi, nếu không sẽ ẩn form này đi. Sau đó sẽ hiển thị danh sách toàn bộ server đang có trong database, nút Remove để xóa server sẽ bị ẩn đi nếu user chưa đăng nhập.

Kết nối tới database

Như trong phần trước, chúng ta đã tiến hành phân tích xem database cần có những gì và kết quả là đã có một file schema.sql được tạo ra ngay trong thư mục server_monitoring. Phần này chúng ta sẽ tiếp tục tìm hiểu xem làm thế nào để import file này vào database, và kết nối tới database như thế nào.

Đầu tiên hãy tạo một thư mục tên là db trong server_monitoring để chứa database của hệ thống. Cây thư mục của chúng ta lúc này sẽ như sau:

server_monitoring\
    server.py
    config.cfg
    schema.sql
    db\
    static\
        bootstrap.min.css
        jquery.min.js
    templates\
        index.html
        layout.html

Mỗi lần user truy cập vào hệ thống của chúng ta, Flask sẽ phải tự động mở một kết nối tới database và hiển thị danh sách server cùng trạng thái của chúng. Để xử lí vấn đề này, chúng ta sẽ viết một hàm kết nối tới database, và sử dụng hàm này trong một object rất đặc biệt cho phép tự động mở một kết nối tới database và truy vấn: object g. Trong server.py:

def connect_db():
    return sqlite3.connect(app.config['DATABASE'])

Mỗi khi hàm connect_db() được gọi, chúng ta sẽ dùng hàm connect() được cung cấp bởi thư viện sqlite3 với một tham số được truyền vào là đường dẫn tới file database được cấu hình trong tham số DATABASE. Có 2 điều cần lưu ý ở đây:

  • Bạn sẽ phải import thư viện sqlite3 vào server.py

      import sqlite3
    
  • Dễ dàng nhận thấy DATABASE ở đây là một tham số cấu hình, tham số này có giá trị là đường dẫn tới file .db. Bạn hãy update vào file config.cfg như sau:

      DATABASE = "db/servers.db"
    

Tiếp theo, có vài cách để import file schema.sql vào SQLite3 database. Cách thông thường nhất là cài đặt SQLite3 và dùng lệnh import từ shell của nó.

sqlite3 db/servers.db < schema.sql

Tuy nhiên cách này như chúng ta thấy, cần SQLite3 được cài đặt trước trên hệ thống, cùng với việc gõ lệnh với đường dẫn thư mục sẽ dễ dàng khiến chúng ta mắc lỗi. Cách tốt hơn là chúng ta sẽ viết hàm để kết nối tới database và import file schema.sql vào database khi hàm đó được gọi.

Việc này cần hỗ trợ bởi hàm closing() của thư viện contextlib, hãy import nó vào trong server.py:

from contextlib import closing

Tiếp theo chúng ta sẽ viết một hàm tên là init_db(), có nhiệm vụ import toàn bộ nội dung của schema.sql vào SQLite database:

def init_db()
    with closing(connect_db()) as db:
        with app.open_resource('schema.sql', mode='r') as f:
            db.cursor().executescript(f.read())

        db.commit()

Hàm closing() kết hợp với with cho phép chúng ta giữ kết nối liên tục tới database. Hàm open_resource() cung cấp bởi Flask đọc nội dung của schema.sql. closing() trả về một cursor cho phép chúng ta thực hiện việc thực thi nội dung của file schema.sql từng dòng một và ghi vào database.

Trước khi bật server, bạn hãy gọi hàm đó để init database trong Python shell:

>>> from server import init_db
>>> init_db()

Hàm này khi được gọi sẽ tạo ra file servers.db trong thư mục db như đã được cấu hình trong DATABASE.

Vấn đề tiếp theo của chúng ta, là làm cách nào để xử lí việc kết nối tới database một cách hiệu quả. Là thế nào? Tức là với mỗi truy vấn của user vào hệ thống, một kết nối tới database sẽ được tạo, và kết nối này sẽ phải được tự động ngắt đi khi user thoát khỏi hệ thống. Flask cho phép chúng ta xử lí vấn đề này bằng 3 decorator: before_request, after_requestteardown_request

Trong server.py, ngay sau hàm init_db()connect_db(), bạn hãy thêm code sau:

...
def connect_db():
...
def init_db():
...
@app.before_request
def before_request():
    g.db = connect_db()

@app.teardown_request
def teardown_request(exception):
    db = getattr(g, 'db', None)
    if db is not None:
        db.close()

Hàm before_request() sẽ được gọi trước khi user truy vấn vào hệ thống. Hàm after_request() được gọi sau khi truy vấn, với 1 tham số truyền vào là response sẽ trả về cho user. Nhưng do hoạt động của hàm này không ổn định, nên thay vào đó hàm teardown_request() sẽ được dùng.

Như đề cập ở phía trên, mỗi kết nối tới database cho một truy vấn của người dùng sẽ được lưu vào một object đặc biệt của Flask tên là g. Object này lưu trữ thông tin cho 1 truy vấn, như kết nối tới database chả hạn. g cho phép chúng ta thực hiện các thao tác truy vấn tới db và trả về kết quả tương ứng với mỗi user khi họ những tác vụ khác nhau trên hệ thống: user A muốn xem thông tin server, user B muốn xem toàn bộ user đang có, …

Để sử dụng g, bạn cũng phải import từ Flask:

from flask import g, ...

Tóm tắt

Trong phần này, chúng ta đã tìm hiểu thêm về Jinja Template Engine đi kèm với Flask. Cách sử dụng các câu điều kiện, loop trong template như thế nào. Chúng ta đã học cách thiết kế template một cách hiệu quả sử dụng tính năng rất mạnh mẽ của Jinja là Template Inheritance, trong đó các phần giao diện chung nhất của ứng dụng sẽ được cho vào một template mẹ, và các template con sẽ ghi đè lên một cách tùy ý trong các phần khác của giao diện.

Chúng ta cũng tìm hiểu khá nhiều về cách init database, kết nối tới database ra sao và việc xử lí những kết nối này một cách hiệu quả.

Phần sau sẽ chủ yếu là code vì lúc này bạn đã có đủ kiến thức cơ bản về Flask rồi, chúng ta sẽ viết những tính năng chính của hệ thống như đã đề cập ở Phần 1.

Phần cuối: Xây dựng các tính năng chính của hệ thống


« Homepage