Ứng dụng Python

Xây dựng hệ thống chat real time với aiohttp - Kết nối database

Đăng bởi - Ngày 29-04-2018

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.

resources folder

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ử.

new login

Để 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. login with css

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 %}

giao diện create_user.html

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
]
  1. 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
  2. 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')
  1. 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

create new account post

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
  1. Để sử dụng aiosqlite bạn cần import package aiosqlite
  2. settings chứa những thiết lập cho project chúng ta
  3. class InitDB dùng để khởi tạo các table cho database
  4. Gán giá trị sqlite cho self.db_file để sử dụng cho function trong class
  5. Đây là cách aiosqlite kết nối vào database
  6. class User chứa các function quản lý user
  7. 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
  8. 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()
  1. Chúng ta gọi class InitDB để tiến hành tạo database
  2. Khởi tạo class InitDB()
  3. 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
  1. import class User() để tạo user
  2. Lấy giá trị dữ liệu từ post của form createuser
  3. Truyền list chưa thông tin user vào create_user
  4. 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

test create account

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.

Các thẻ
Bài viết liên quan
3 nhận xét
  1. Trả lời

    Nho

    22 Tháng 11, 2018

    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

  2. Trả lời

    thiết kế xây dựng

    25 Tháng 3, 2019

    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.

  3. Trả lời

    http://thietkenhadephcm.arwebo.com/

    10 Tháng 12, 2019

    Good article! We will be linking to this particularly great content on our site.
    Keep up the great writing.

Nhận xét mới

bắt buộc

yu.kusanagi
Từ Anh Vũ
Hồ Chí Minh, Việt Nam

Xin chào, tôi tên Từ Anh Vũ và là 1 free lancer developer và ngôn ngữ code yêu thích của tôi là Python và PHP. Công việc chủ yếu là viết các module cho magento, magento2, wordpress, django, flask và các framework khác
Nếu bạn muốn trao đổi với tôi hoặc muốn thuê tôi làm việc cho dự án của bạn, hãy liên hệ với tôi

ĐĂNG KÝ NHẬN BÀI MỚI

Tweets gần đây
Tác giả
Feeds
RSS / Atom
-->

Đăng ký nhận bài viết mới tại hocpython.com?

Hãy đăng ký nhận bài viết mới tại hocpython.com để:

  • Không bỏ lỡ các bài tutorials mới tại hocpython.com!
  • Cập nhật các công nghệ mới trong python!

Chỉ cần điền email và họ tên của bạn và nhấn Đăng ký nhận tin!