Xây dựng hệ thống chat real time với aiohttp - Kết nối database
Trong bài hôm nay chúng ta sẽ tiếp tục với phần giao diện login. Bây giờ các bạn hãy giải nén file Login_v1.zip và copy thư mục images vào /project_root/media/, thư mục css, fonts, js và vendor vào thư mục static.
Trong thư mục login_V1 vừa giải nén còn một file index.html, bạn hãy mở file này bằng chương trình editor của bạn. Sau đó copy tất cả vào base.html, refresh lại trang login xem thử.
Để hiện css và js cho base.html, các bạn cần chỉnh sửa base.html như bên dưới
<!DOCTYPE html>
<html lang="en">
<head>
<title>{% block title %}{% endblock %} | Simplechat - Tự học Python</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<!--===============================================================================================-->
<link rel="icon" type="image/png" href="{{ url('static', filename='images/icons/favicon.ico') }}"/>
<!--===============================================================================================-->
<link rel="stylesheet" type="text/css" href="{{ url('static', filename='vendor/bootstrap/css/bootstrap.min.css') }}">
<!--===============================================================================================-->
<link rel="stylesheet" type="text/css" href="{{ url('static', filename='fonts/font-awesome-4.7.0/css/font-awesome.min.css') }}">
<!--===============================================================================================-->
<link rel="stylesheet" type="text/css" href="{{ url('static', filename='vendor/animate/animate.css') }}">
<!--===============================================================================================-->
<link rel="stylesheet" type="text/css" href="{{ url('static', filename='vendor/css-hamburgers/hamburgers.min.css') }}">
<!--===============================================================================================-->
<link rel="stylesheet" type="text/css" href="{{ url('static', filename='vendor/select2/select2.min.css') }}">
<!--===============================================================================================-->
<link rel="stylesheet" type="text/css" href="{{ url('static', filename='css/util.css') }}">
<link rel="stylesheet" type="text/css" href="{{ url('static', filename='css/main.css') }}">
<!--===============================================================================================-->
</head>
<body>
<div class="limiter">
<div class="container-login100">
<div class="wrap-login100">
<div class="login100-pic js-tilt" data-tilt>
<img src="{{ url('media', filename='images/img-01.png') }}" alt="IMG">
</div>
<form class="login100-form validate-form" action="#" method="post">
<span class="login100-form-title">
Member Login
</span>
<div class="wrap-input100 validate-input">
<input class="input100" type="text" name="username" placeholder="Username">
<span class="focus-input100"></span>
<span class="symbol-input100">
<i class="fa fa-user-circle-o" aria-hidden="true"></i>
</span>
</div>
<div class="wrap-input100 validate-input" data-validate = "Password is required">
<input class="input100" type="password" name="pass" placeholder="Password">
<span class="focus-input100"></span>
<span class="symbol-input100">
<i class="fa fa-lock" aria-hidden="true"></i>
</span>
</div>
<div class="container-login100-form-btn">
<button class="login100-form-btn">
Login
</button>
</div>
<div class="text-center p-t-12">
<span class="txt1">
Forgot
</span>
<a class="txt2" href="#">
Username / Password?
</a>
</div>
<div class="text-center p-t-136">
<a class="txt2" href="#">
Create your Account
<i class="fa fa-long-arrow-right m-l-5" aria-hidden="true"></i>
</a>
</div>
</form>
</div>
</div>
</div>
<!--===============================================================================================-->
<script src="{{ url('static', filename='vendor/jquery/jquery-3.2.1.min.js') }}"></script>
<!--===============================================================================================-->
<script src="{{ url('static', filename='vendor/bootstrap/js/popper.js') }}"></script>
<script src="{{ url('static', filename='vendor/bootstrap/js/bootstrap.min.js') }}"></script>
<!--===============================================================================================-->
<script src="{{ url('static', filename='vendor/select2/select2.min.js') }}"></script>
<!--===============================================================================================-->
<script src="{{ url('static', filename='vendor/tilt/tilt.jquery.min.js') }}"></script>
<script >
$('.js-tilt').tilt({
scale: 1.1
})
</script>
<!--===============================================================================================-->
<script src="{{ url('static', filename='js/main.js') }}"></script>
{% block footerjs %}
{% endblock %}
</body>
</html>
- {{ url('static', filename='/path/to/file/static') }}: Khi bạn dùng tag này, jinja2 sẽ nhận 2 tham số, tham số thứ nhất là static: Tên của url add_static mà bạn đã khai báo ở bài trước, tham số thứ hai là đường dẫn tới file static (.js, .css)
- {{ url('media', filename='/path/to/file/media') }}: Tương tự trên, bạn dùng tag này cho hình ảnh trong base.html. Tham số mediachính là đường dẫn add_static đến thư mục media đã khai báo ở bài trước
Sau khi bạn thay đổi hết cho các file js, css, images. Chúng ta hãy refresh lại http://0.0.0.0:8080 xem thử giao diện chúng ta đã có css chưa.
Hiện tại thì trang login của bạn đã hiển thị đầy đủ rồi, nhưng chúng ta cần đem phần form login sang block body_content trong login.html.
Bạn hãy tìm phần code này trong base.html và đem qua login.html
<div class="login100-pic js-tilt" data-tilt>
<img src="{{ url('media', filename='images/img-01.png') }}" alt="IMG">
</div>
<form class="login100-form validate-form" action="#" method="post">
<span class="login100-form-title">
Member Login
</span>
<div class="wrap-input100 validate-input">
<input class="input100" type="text" name="username" placeholder="Username">
<span class="focus-input100"></span>
<span class="symbol-input100">
<i class="fa fa-user-circle-o" aria-hidden="true"></i>
</span>
</div>
<div class="wrap-input100 validate-input" data-validate = "Password is required">
<input class="input100" type="password" name="pass" placeholder="Password">
<span class="focus-input100"></span>
<span class="symbol-input100">
<i class="fa fa-lock" aria-hidden="true"></i>
</span>
</div>
<div class="container-login100-form-btn">
<button class="login100-form-btn">
Login
</button>
</div>
<div class="text-center p-t-12">
<span class="txt1">
Forgot
</span>
<a class="txt2" href="#">
Username / Password?
</a>
</div>
<div class="text-center p-t-136">
<a class="txt2" href="#">
Create your Account
<i class="fa fa-long-arrow-right m-l-5" aria-hidden="true"></i>
</a>
</div>
</form>
Như vậy, code hiện tại của các bạn sẽ trong giống như sau:
base.html
<!DOCTYPE html>
<html lang="en">
<head>
<title>Login V1</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<!--===============================================================================================-->
<link rel="icon" type="image/png" href="{{ url('static', filename='images/icons/favicon.ico') }}"/>
<!--===============================================================================================-->
<link rel="stylesheet" type="text/css" href="{{ url('static', filename='vendor/bootstrap/css/bootstrap.min.css') }}">
<!--===============================================================================================-->
<link rel="stylesheet" type="text/css" href="{{ url('static', filename='fonts/font-awesome-4.7.0/css/font-awesome.min.css') }}">
<!--===============================================================================================-->
<link rel="stylesheet" type="text/css" href="{{ url('static', filename='vendor/animate/animate.css') }}">
<!--===============================================================================================-->
<link rel="stylesheet" type="text/css" href="{{ url('static', filename='vendor/css-hamburgers/hamburgers.min.css') }}">
<!--===============================================================================================-->
<link rel="stylesheet" type="text/css" href="{{ url('static', filename='vendor/select2/select2.min.css') }}">
<!--===============================================================================================-->
<link rel="stylesheet" type="text/css" href="{{ url('static', filename='css/util.css') }}">
<link rel="stylesheet" type="text/css" href="{{ url('static', filename='css/main.css') }}">
<!--===============================================================================================-->
</head>
<body>
<div class="limiter">
<div class="container-login100">
<div class="wrap-login100">
{% block body_content %}
{% endblock %}
</div>
</div>
</div>
<!--===============================================================================================-->
<script src="{{ url('static', filename='vendor/jquery/jquery-3.2.1.min.js') }}"></script>
<!--===============================================================================================-->
<script src="{{ url('static', filename='vendor/bootstrap/js/popper.js') }}"></script>
<script src="{{ url('static', filename='vendor/bootstrap/js/bootstrap.min.js') }}"></script>
<!--===============================================================================================-->
<script src="{{ url('static', filename='vendor/select2/select2.min.js') }}"></script>
<!--===============================================================================================-->
<script src="{{ url('static', filename='vendor/tilt/tilt.jquery.min.js') }}"></script>
<script >
$('.js-tilt').tilt({
scale: 1.1
})
</script>
<!--===============================================================================================-->
<script src="{{ url('static', filename='js/main.js') }}"></script>
</body>
</html>
login.html
{% extends "base.html" %}
{% block body_content %}
<div class="login100-pic js-tilt" data-tilt>
<img src="{{ url('media', filename='images/img-01.png') }}" alt="IMG">
</div>
<form class="login100-form validate-form" action="#" method="post">
<span class="login100-form-title">
Member Login
</span>
<div class="wrap-input100 validate-input">
<input class="input100" type="text" name="username" placeholder="Username">
<span class="focus-input100"></span>
<span class="symbol-input100">
<i class="fa fa-user-circle-o" aria-hidden="true"></i>
</span>
</div>
<div class="wrap-input100 validate-input" data-validate = "Password is required">
<input class="input100" type="password" name="pass" placeholder="Password">
<span class="focus-input100"></span>
<span class="symbol-input100">
<i class="fa fa-lock" aria-hidden="true"></i>
</span>
</div>
<div class="container-login100-form-btn">
<button class="login100-form-btn">
Login
</button>
</div>
<div class="text-center p-t-12">
<span class="txt1">
Forgot
</span>
<a class="txt2" href="#">
Username / Password?
</a>
</div>
<div class="text-center p-t-136">
<a class="txt2" href="#">
Create your Account
<i class="fa fa-long-arrow-right m-l-5" aria-hidden="true"></i>
</a>
</div>
</form>
{% endblock %}
Theo giao diện hiện tại chúng ta sẽ có hai phần cần làm
- Phần đăng nhập: Khi người dùng nhập username và password, nếu account hiện tại không có sẽ chuyển sang trang tạo account mới. Trường hợp account đã tạo rồi thì đăng nhập vào room và bắt đầu chat
- Tạo account mới: Chúng ta sẽ tạo một page mới. Tại trang tạo account mới này, mình sẽ yêu cầu người dùng nhập email, username, password để tạo account mới. Sau khi bấm Sign In thì sẽ tạo account mới và login người dùng và bắt đầu chat
Tạo trang createuser
Thêm 2 routes createuser vào file routes.py
from aiohttp import web
from chat.views import Login, CreateUser
routes = [
web.get('/', Login, name='homepage'),
web.get('/createuser', CreateUser, name='createuser'), #1
web.post('/createuser', CreateUser) #2
]
- Vì chúng ta cần tạo một route get cho createuser và một route post cho createuser nên chúng ta sẽ đặt tên là createuser để aiohttp phân biệt
- Cùng đường dẫn nhưng khác method get và post, chúng ta chỉ cần đặt tên một lần cho một trong hai method
Mở file chat/views.py và thêm đoạn code sau vào views.py
class CreateUser(web.View):
@aiohttp_jinja2.template('chat/create_user.html')
async def get(self):
return None
async def post(self): #1
return web.Response(text='you post')
- Ta cần add thêm async def post() vào class createroom để map vào routes.py
Code của chúng ta sẽ như thế này
from aiohttp import web
import aiohttp_jinja2
class Login(web.View):
@aiohttp_jinja2.template('chat/login.html')
async def get(self):
return None
class CreateUser(web.View):
@aiohttp_jinja2.template('chat/create_user.html')
async def get(self):
return None
async def post(self):
return web.Response(text='you post')
file login.html bạn tìm đoạn này
<div class="text-center p-t-136">
<a class="txt2">
Create new room
<i class="fa fa-long-arrow-right m-l-5" aria-hidden="true"></i>
</a>
</div>
và chỉnh thành như sau
<div class="text-center p-t-136">
<a class="txt2" href="{{ url('createuser') }}">
Create new account
<i class="fa fa-long-arrow-right m-l-5" aria-hidden="true"></i>
</a>
</div>
Ta tiếp tục copy file login.html sang thành file create_user.html và chỉnh sửa thành như sau:
{% extends "base.html" %}
{% block title %}Create new account{% endblock %}
{% block body_content %}
<div class="login100-pic js-tilt" data-tilt>
<img src="{{ url('media', filename='images/img-01.png') }}" alt="IMG">
</div>
<form class="login100-form validate-form" method="post" action="{{ url('createuser') }}">
<span class="login100-form-title">
Account Information
</span>
<div class="wrap-input100 validate-input">
<input class="input100" type="email" name="email" placeholder="Email">
<span class="focus-input100"></span>
<span class="symbol-input100">
<i class="fa fa-envelope" aria-hidden="true"></i>
</span>
</div>
<div class="wrap-input100 validate-input">
<input class="input100" type="text" name="username" placeholder="Username">
<span class="focus-input100"></span>
<span class="symbol-input100">
<i class="fa fa-user-circle-o" aria-hidden="true"></i>
</span>
</div>
<div class="wrap-input100 validate-input" data-validate = "Password is required">
<input class="input100" id="password" type="password" name="password" placeholder="Password">
<span class="focus-input100"></span>
<span class="symbol-input100">
<i class="fa fa-lock" aria-hidden="true"></i>
</span>
</div>
<div class="wrap-input100 validate-input" data-validate = "Password is required">
<input class="input100" id="confirm_password" type="password" name="confirm_password" placeholder="Confirm Password">
<span class="focus-input100"></span>
<span class="symbol-input100">
<i class="fa fa-lock" aria-hidden="true"></i>
</span>
</div>
<div class="container-login100-form-btn">
<button type="submit" class="login100-form-btn">
Sign In
</button>
</div>
<div class="text-center p-t-136">
<a class="txt2" href="{{ url('homepage') }}">
<i class="fa fa-long-arrow-left m-l-5" aria-hidden="true"></i>
Back to login
</a>
</div>
</form>
{% endblock %}
Tiếp đến ta cần chỉnh sửa lại javascript để có thể validate confirm password. Bạn hãy mở file static/js/main.js và thêm đoạn này vào trước })(jQuery);
var password = document.getElementById("password")
, confirm_password = document.getElementById("confirm_password");
function validatePassword(){
if(password.value != confirm_password.value) {
confirm_password.setCustomValidity("Passwords Don't Match");
} else {
confirm_password.setCustomValidity('');
}
}
if(confirm_password){
password.onchange = validatePassword;
confirm_password.onkeyup = validatePassword;
}
(function ($) {
"use strict";
/*==================================================================
[ Validate ]*/
var input = $('.validate-input .input100');
$('.validate-form').on('submit',function(){
var check = true;
for(var i=0; i<input.length; i++) {
if(validate(input[i]) == false){
showValidate(input[i]);
check=false;
}
}
return check;
});
$('.validate-form .input100').each(function(){
$(this).focus(function(){
hideValidate(this);
});
});
function validate (input) {
if($(input).attr('type') == 'email' || $(input).attr('name') == 'email') {
if($(input).val().trim().match(/^([a-zA-Z0-9_\-\.]+)@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.)|(([a-zA-Z0-9\-]+\.)+))([a-zA-Z]{1,5}|[0-9]{1,3})(\]?)$/) == null) {
return false;
}
}
else {
if($(input).val().trim() == ''){
return false;
}
}
}
function showValidate(input) {
var thisAlert = $(input).parent();
$(thisAlert).addClass('alert-validate');
}
function hideValidate(input) {
var thisAlert = $(input).parent();
$(thisAlert).removeClass('alert-validate');
}
var password = document.getElementById("password")
, confirm_password = document.getElementById("confirm_password");
function validatePassword(){
if(password.value != confirm_password.value) {
confirm_password.setCustomValidity("Passwords Don't Match");
} else {
confirm_password.setCustomValidity('');
}
}
if(confirm_password){
password.onchange = validatePassword;
confirm_password.onkeyup = validatePassword;
}
})(jQuery);
Bây giờ bạn hãy restart server và vào http://0.0.0.0:8080/createuser sau đó submit form thử. Nếu bạn ra được như hình dưới thì chúng ta tiếp tục bước tiếp theo
Kết nối database
Aiohttp hỗ trợ kết nối nhiều loại database như sqlite, mongo, mysql... vì đây là project nhỏ nên mình chọn sqlite cho gọn nhẹ
Để cài đặt sqlite với aiohttp, chúng ta cần cài đặt thêm package này
pip install aiosqlite
Trong thư mục chat, bạn tạo một file model.py
import aiosqlite #1
import settings #2
class InitDB(): #3
def __init__(self):
self.db_file = settings.DB_FILE #4
async def createdb(self):
async with aiosqlite.connect(self.db_file) as db: #5
await db.execute(
"create table if not exists users "
"("
"id integer primary key asc, "
"username varchar(50), password varchar(50),"
"email varchar(50)"
")"
)
class User(): #6
def __init__(self):
self.db_file = settings.DB_FILE
async def check_user(self, username): #7
async with aiosqlite.connect(self.db_file) as db:
cursor = await db.execute("select * from users where username = '{}'".format(username))
return await cursor.fetchone()
async def create_user(self, data): #8
user = await self.check_user(data.get('username'))
if not user and data.get('username'):
async with aiosqlite.connect(self.db_file) as db:
results = await db.execute("insert into users (username, password, email) "
"values (?, ?, ?)",
[data.get('username'), data.get('password'), data.get('email')])
await db.commit()
if results.lastrowid:
result = 'Create account success'
else:
result = "User exists"
return result
- Để sử dụng aiosqlite bạn cần import package aiosqlite
- settings chứa những thiết lập cho project chúng ta
- class InitDB dùng để khởi tạo các table cho database
- Gán giá trị sqlite cho self.db_file để sử dụng cho function trong class
- Đây là cách aiosqlite kết nối vào database
- class User chứa các function quản lý user
- function check_user nhận vào một param username và trả về thông tin nếu user này đã tồn tại, nếu không sẽ trả về None
- function create_user nhận vào một param là list, chúng ta sẽ kiểm tra xem user này đã tồn tại chưa, nếu đã tồn tại sẽ thông báo là "User exists", nếu chưa tồn tại thì sẽ tạo account cho user đó và thông báo "Create account success"
Tạo một file settings.py ngang hàng với server.py, mình sẽ đem phần bên dưới của server.py sang settings.py
PROJECT_APP_PATH = os.path.dirname(os.path.abspath(__file__))
TEMPLATE_PATH = os.path.join(PROJECT_APP_PATH, "templates")
STATIC_PATH = os.path.join(PROJECT_APP_PATH, "static")
MEDIA_PATH = os.path.join(PROJECT_APP_PATH, "media")
Sau khi đem qua, cả hai file sẽ như sau:
import os
PROJECT_APP_PATH = os.path.dirname(os.path.abspath(__file__))
TEMPLATE_PATH = os.path.join(PROJECT_APP_PATH, "templates")
STATIC_PATH = os.path.join(PROJECT_APP_PATH, "static")
MEDIA_PATH = os.path.join(PROJECT_APP_PATH, "media")
DB_FILE = "chat.db"
import jinja2
from aiohttp import web
import aiohttp_jinja2 as jtemplate
from routes import routes
import settings
from chat.model import InitDB #1
import asyncio
app = web.Application()
app.add_routes(routes)
app.router.add_static('/static', settings.STATIC_PATH, name='static')
app.router.add_static('/media', settings.MEDIA_PATH, name='media')
jtemplate.setup(app, loader=jinja2.FileSystemLoader(settings.TEMPLATE_PATH))
if __name__ == '__main__':
initdb = InitDB() #2
loop = asyncio.get_event_loop()
loop.run_until_complete(initdb.createdb())#3
web.run_app(app)
loop.close()
- Chúng ta gọi class InitDB để tiến hành tạo database
- Khởi tạo class InitDB()
- Vì aiohttp xây dựng trên asyncio nên tất cả phải chạy trong asyncio event.
Bây giờ, ta cần chỉnh lại file chat/views.py để có thể tạo user
import json
from aiohttp import web
import aiohttp_jinja2
from time import time
from .model import User #1
def redirect(request, router_name):
url = request.app.router[router_name].url()
raise web.HTTPFound(url)
def set_session(session, user_id, request):
session['user'] = str(user_id)
session['last_visit'] = time()
redirect(request, 'main')
def convert_json(message):
return json.dumps({'error': message})
class Login(web.View):
@aiohttp_jinja2.template('chat/login.html')
async def get(self):
return None
class CreateUser(web.View):
@aiohttp_jinja2.template('chat/create_user.html')
async def get(self):
return None
async def post(self):
data = await self.request.post() #2
user = User()
post = {'username': data.get('username'),
'password': data.get('password'),
'email': data.get('email')}
result = await user.create_user(data=post) #3
return web.Response(text=result) #4
- import class User() để tạo user
- Lấy giá trị dữ liệu từ post của form createuser
- Truyền list chưa thông tin user vào create_user
- Sau khi tạo user xong, function create_user sẽ trả về kết quả. Ta gán giá trị này vào result và hiển thị ra website thông qua web.Response()
Chúng ta sẽ chạy thử xem sao
Phần kết nối database đến đây là kết thúc. Chúc các bạn học lập trình Python thành công.
Bài kế tiếp chúng ta sẽ kết nối websocket và login user đã tạo vào room chat. Mọi ý kiến góp ý của các bạn mình rất hoan nghênh. Các bạn thắc mắc có thể hỏi mình bằng cách comment tại bài viết này hoặc trên group Python Community Viet Nam.
Nho
Anh ơi em đã sửa file base.html như anh rồi nhưng nó chỉ hiện thêm hình với thêm màu thôi ạ
Chứ không được hoành thiện như hình
thiết kế xây dựng
I'm impressed, I must say. Seldom do I encounter a blog that's both equally educative
and entertaining, and without a doubt, you've hit the nail on the head.
The problem is an issue that too few folks are speaking intelligently about.
Now i'm very happy that I found this in my search for something relating to this.
http://thietkenhadephcm.arwebo.com/
Good article! We will be linking to this particularly great content on our site.
Keep up the great writing.