Flask Tutorial Part 2: Static files and database setup with Flask

Tiếp tục với bài tech blog lần trước, lần này ta sẽ tiếp tục tìm hiểu về framework Flask. Bài lần này sẽ đề cập đến 3 vấn đề sau:

  • Sử dụng các file tĩnh trong Flask
  • Tạo Model và quản lý DB thông qua sqlalchemy migrations

Sử dụng các file tĩnh trong Flask

ở bài viết trước, ta đã tạo một trang cùng hai template đơn giản, lần này ta sẽ sử dụng layout của Bootstrap 3, ta tải các file js và css tại địa chỉ: http://getbootstrap.com/.

Để sử dụng các file static trong Flask rất đơn giản, ta chỉ cần tạo một thư mục static bên trong thư mục app và đưa những file static vào đó. Ví dụ như sau:

.
├── css
│   ├── bootstrap.css
│   ├── bootstrap.css.map
│   ├── bootstrap.min.css
│   ├── bootstrap-theme.css
│   ├── bootstrap-theme.css.map
│   ├── bootstrap-theme.min.css
│   └── style.css
├── favicon.ico
├── fonts
│   ├── glyphicons-halflings-regular.eot
│   ├── glyphicons-halflings-regular.svg
│   ├── glyphicons-halflings-regular.ttf
│   └── glyphicons-halflings-regular.woff
└── js
    ├── bootstrap.js
    └── bootstrap.min.js

Sau khi đã đặt các file vào thư mục static, để có thể sử dụng được các file này, ta sửa dụng tag sau trong template (cụ thể ở trong file base sau đây):

url_for('static', filename='style.css')

Ta sẽ tạo một file base để chứa các phần chung của trang web, ví dụ như navi bar, header, footer, các file khác sẽ kế thừa từ file base template này:

<html>
  <head>
    {% if title %}
    <title>{{title}} - Ny Books</title>
    {% else %}
    <title>My Books</title>
    {% endif %}
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <meta name="author" content="Framgia CodeContest Team">
    <link rel="shortcut icon" href="{{ url_for('static', filename='favicon.ico') }}" type="image/x-icon"/>
    <!-- Latest compiled and minified CSS -->
    <link rel="stylesheet" href="{{ url_for('static', filename='css/bootstrap.min.css') }}">
    <!-- Custom CSS -->
    <link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
    <!-- Latest compiled and minified JavaScript -->
    <script src="//code.jquery.com/jquery-1.11.0.min.js"></script>
    <script src="{{ url_for('static', filename='js/bootstrap.min.js') }}"></script>
    {% block css %}{% endblock %}
    {% block js %}{% endblock %}
  </head>
  <body>
    <!-- Fixed navbar -->
    <div class="navbar navbar-default navbar-fixed-top" role="navigation">
      <div class="container">
        <div class="navbar-header">
          <a class="navbar-brand" href="{{ url_for('index') }}">My Books</a>
        </div>
        <div class="navbar-collapse collapse">
          <ul class="nav navbar-nav">
            <li class="active"><a href="{{ url_for('index') }}">Home</a></li>
          </ul>
          <ul class="nav navbar-nav navbar-right">
            <li class="dropdown">
              <a href="#" class="dropdown-toggle" data-toggle="dropdown">Guest<span class="caret"></span></a>
              <ul class="dropdown-menu" role="menu">
                <li><a href="#">Profile</a></li>
                <li class="divider"></li>
                <li><a href="#">Log out</a></li>
              </ul>
            </li>
          </ul>
        </div><!--/.nav-collapse -->
      </div>
    </div><!-- Fixed navbar -->
    <div class="main-container">
    {% with messages = get_flashed_messages() %}
    {% if messages %}
    {% for message in messages %}
    <div class="alert alert-warning alert-dismissible" role="alert">
    <button type="button" class="close" data-dismiss="alert"><span aria-hidden="true">&times;</span><span class="sr-only">Close</span></button>
    {{ message }}
    </div>
    {% endfor %}
    {% endif %}
    {% endwith %}
    {% block content %}{% endblock %}
    </div>
  </body>
</html>

Trong file base ta có sử dụng một số file css và js của bootstrap và

{% extends "base.html" %}
{% block content %}
<h1>Hi, Guest!</h1>
{% endblock %}

Như ta thấy, trong file index ta đã kế thừa base template.html. thử server ta có kết qủa như sau: bootstrap

Tạo Model và quản lý DB thông qua sqlalchemy migrations

Tiếp theo ta sẽ cấu hình cơ sở dữ liệu. Đầu tiên, ta cần cài thêm hai package, đó là sqlalchemy và sqlalchemy migrations. (Update: hãy sử dụng alembic để migrate DB tốt hơn)

Sqlalchemy là một ORM backend cho python cũng giống như doctrine cho PHP, giúp chúng ta có thể truy vấn cơ sở dữ liệu mà ko cần phải viết các query bằng mySQL. Ta cần thêm 2 package + với python-mySQL để có thể tương tác với mySQL từ python + package flask-sqlalchemy giúp ta setup nhanh chóng, trong file requirement.txt như sau:

sqlalchemy==0.7.9
flask-sqlalchemy==1.0
sqlalchemy-migrate==0.9.1
MySQL-python==1.2.5

chạy lại pip install -r requirements.txt để cài đặt (nếu có thông báo lỗi ko tìm thấy Python.h, bạn cần cài thêm sudo apt-get install python-dev)

Ta cần tạo thêm một file config để chứa các cấu hình của server. File này thực chất là một file python thông thường, có chứa các hằng số ta sẽ import khi khởi tạo app. File config.py ở thư mục base có nội dung như sau:

import os
basedir = os.path.abspath(os.path.dirname(__file__))

SQLALCHEMY_DATABASE_URI = 'mysql://book_admin:[email protected]/my_book'
SQLALCHEMY_MIGRATE_REPO = os.path.join(basedir, 'db_repository')

CSRF_ENABLED = True
SECRET_KEY = '&amp;%^%&amp;12o)!JHGHVJBK671i2lkj;^*&amp;:'

Mình sẽ giải thích qua về các biến trong file config:

  • SQLALCHEMY_DATABASE_URI: là thông tin và đăng nhập của server mySQL: username, password, host, database. Ta có thể tạo các thông tin trên bằng cách đăng nhập vào root và chạy các lệnh sau:
CREATE USER 'book_admin'@'localhost' IDENTIFIED BY 'book_admin';
CREATE DATABASE my_book CHARACTER SET utf8;
GRANT ALL ON my_book.* to [email protected];
FLUSH PRIVILEGES;
  • SQLALCHEMY_MIGRATE_REPO: là thư mục chứa các file migration và các file chạy migration mà sqlalchemy-migrate sẽ sử dụng
  • CSRF_ENABLED: ta bật csrf cho form và thiết lập secret key dùng để sign cho cookie để có thể sử dụng các config này ta cần import trong app/init.py như sau:
import os
from flask import Flask
from flask.ext.sqlalchemy import SQLAlchemy

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

db = SQLAlchemy(app)

from app import views, models

thông qua biến db, ta có thể truy vấn đến cơ sở dữ liệu.

Để hỗ trợ cho việc tạo DB từ model của app, ta sử dụng script db_create.py sau:

#!venv/bin/python
from migrate.versioning import api
from config import SQLALCHEMY_DATABASE_URI
from config import SQLALCHEMY_MIGRATE_REPO
from app import db
import os.path
db.create_all()
if not os.path.exists(SQLALCHEMY_MIGRATE_REPO):
    api.create(SQLALCHEMY_MIGRATE_REPO, 'database repository')
    api.version_control(SQLALCHEMY_DATABASE_URI, SQLALCHEMY_MIGRATE_REPO)
else:
    api.version_control(SQLALCHEMY_DATABASE_URI, SQLALCHEMY_MIGRATE_REPO, api.version(SQLALCHEMY_MIGRATE_REPO))

Tiếp theo ta sẽ tạo một model User đơn giản cho app gồm có các thuộc tính:

  • username
  • password
  • name
  • email
  • role

Ta viết file app/models.py cùng với class User như sau:

from app import db
from werkzeug.security import generate_password_hash, check_password_hash

ROLE_USER = 0
ROLE_ADMIN = 1

class User(db.Model):
    __tablename__ = 'users'
    id = db.Column(db.Integer, primary_key = True)
    username = db.Column(db.String(30), unique = True, nullable = False)
    password = db.Column(db.String(100))
    name = db.Column(db.String(50))
    email = db.Column(db.String(45))
    role = db.Column(db.SmallInteger, default = ROLE_USER)

    def __init__(self, username, email, password):
        self.username = username
        self.name = username
        self.email = email.lower()
        self.set_password(password)

    def set_password(self, password):
        self.password = generate_password_hash(password)

    def check_password(self, password):
        return check_password_hash(self.password, password)

    def __repr__(self):
        return '' % (self.name)

Ta khai báo mỗi trường của bảng user là một đối tượng Column của SQLAlchemy. Ta có thể truyền thêm các điều kiện cho Column tương tự như các điều kiện trong MySQL, ví dụ như NOT NULL, UNIQUE, DEFAULT. Bạn có thể đọc thêm về SQLAlchemy Column tại đây: http://docs.sqlalchemy.org/en/rel_0_9/core/types.html

Tất nhiên là password trong DB ko thể lưu dưới dạng plain text, mà sẽ được hash rồi sau đó mới lưu lại. Ta sử dụng helper generate_password_hashcheck_password_hash của werkzeug.security để làm việc này. Ta chạy lệnh sau để create DB và check:

(venv)[[email protected] ~/flask-intro (master)]$ ./db_create.py
(venv)[[email protected] ~/flask-intro (master)]$ mysql -ubook_admin -pbook_admin -Dmy_book

mysql>; desc users;
+----------+--------------+------+-----+---------+----------------+
| Field    | Type         | Null | Key | Default | Extra          |
+----------+--------------+------+-----+---------+----------------+
| id       | int(11)      | NO   | PRI | NULL    | auto_increment |
| username | varchar(30)  | NO   | UNI | NULL    |                |
| password | varchar(100) | YES  |     | NULL    |                |
| name     | varchar(50)  | YES  |     | NULL    |                |
| email    | varchar(45)  | YES  |     | NULL    |                |
| role     | smallint(6)  | YES  |     | NULL    |                |
+----------+--------------+------+-----+---------+----------------+
6 rows in set (0.00 sec)

mysql>

Ngoài ra ta còn thấy có bảng migrate_version:

mysql> select * from migrate_version;
+---------------------+--------------------------------------+---------+
| repository_id       | repository_path                      | version |
+---------------------+--------------------------------------+---------+
| database repository | /home/vigo/flask-intro/db_repository |       0 |
+---------------------+--------------------------------------+---------+
1 row in set (0.00 sec)

mysql>

Đây là bảng dùng để quản lý version của DB cũng như đường dẫn của db_repo. Hiện tại DB đang ở version 0.

Final

Vậy là ta đã thiết lập xong DB và các file static của DB, trong phần sau ta sẽ sử dụng plugin login manager và WTF Form để xác thực user.

Bài tiếp: User Authentication and Form in Flask

Reference