今天是从头开始做一个在线聊天网站系类的第三部分,调整项目结构,增强功能。
随着我们项目功能越来越多,把所有的逻辑代码都写在一个文件里已经不太合适了,下面就通过 flask 的工厂模式,把项目代码拆分开。
首先来看下拆分后的项目结构:
main 中主要存放后台逻辑代码。 static 中存放 js,css 以及用到的图片等。 templates 中存放 HTML 模板。 models.py 中是数据库模型。 config.py 中是一些公共的配置信息。 manage.py 中是项目的启动信息。
下面我们分别来看看各个模块对应的代码
在 config.py 中,填入代码:
import os
import redis
basedir = os.path.abspath(os.path.dirname(__file__))
class Config:
SECRET_KEY = 'hardtohard'
SQLALCHEMY_DATABASE_URI = 'sqlite:///' + os.path.join(basedir, 'chat.sqlite3')
@staticmethod
def init_app(app):
pass
class DevelopmentConfig(Config):
pass
class TestingConfig(Config):
pass
class ProductionConfig(Config):
pass
config = {
'development': DevelopmentConfig,
'testing': TestingConfig,
'production': ProductionConfig,
'default': DevelopmentConfig
}
在 app/__init__.py 中填入代码:
from flask import Flask
from flask_login import LoginManager
from flask_sqlalchemy import SQLAlchemy
from flask_bootstrap import Bootstrap
from flask_socketio import SocketIO
from config import config
login_manager = LoginManager()
login_manager.session_protection = 'strong'
login_manager.login_view = 'main.login'
db = SQLAlchemy()
bootstrap = Bootstrap()
socketio = SocketIO()
def create_app(config_name):
app = Flask(__name__)
app.config.from_object(config[config_name])
config[config_name].init_app(app)
socketio.init_app(app)
login_manager.init_app(app)
db.init_app(app)
bootstrap.init_app(app)
# 注册蓝本
from .main import main as main_blueprint
app.register_blueprint(main_blueprint)
return app
create_app 函数就是程序的工厂函数,它接受一个配置名的参数。
蓝本和程序类似,也可以定义路由。不同的是,在蓝本中定义的路由处于休眠状态,直到蓝本注册到程序上后,路由才真正成为程序的一部分。
在 main/__init__.py 中创建蓝本
from flask import Blueprint
main = Blueprint('main', __name__)
from . import views, forms
通过实例化一个 Blueprint 类对象可以创建蓝本。这个构造函数有两个必须指定的参数: 蓝本的名字和蓝本所在的包或模块。和程序一样,大多数情况下第二个参数使用 Python 的 __name__ 变量即可。
对于视图函数,需要导入相关的包,同时由于使用了蓝本,原来用来装饰路由的 app.route 都要修改为 main.route,url_for 函数也需要增加 main 作用域,修改后的部分代码如下:
from flask import render_template, redirect, url_for, request
from flask_login import login_required, login_user, logout_user, current_user
from . import main
from .. import db
from .forms import LoginForm
from ..models import User
from config import config
import time
import json
from ..socket_conn import socket_send
pool = redis.ConnectionPool(host='redis-12143.c8.us-east-1-3.ec2.cloud.redislabs.com', port=12143,
decode_responses=True, password='pkAWNdYWfbLLfNOfxTJinm9SO16eSJFx')
r = redis.Redis(connection_pool=pool)
@main.route('/login', methods=['GET', 'POST'])
def login():
form = LoginForm()
if form.validate_on_submit():
user = User.query.filter_by(username=form.username.data).first()
if user:
login_user(user)
return redirect(url_for('main.index'))
return render_template('login.html', form=form)
@main.route('/createroom', methods=["GET", 'POST'])
@login_required
def create_room():
rname = request.form.get('chatroomname', '')
if r.exists("chat-" + rname) is False:
r.zadd("chat-" + rname, current_user.username, 1)
return redirect(url_for('main.chat', rname=rname))
else:
return redirect(url_for('main.chat_room_list'))
在 models.py 的同级目录下创建 socket_conn.py 文件,添加代码如下:
from . import socketio
from flask_socketio import emit
@socketio.on('request_for_response', namespace='/testnamespace')
def socket_send(data, user):
emit("response", {"code": '200', "msg": data, "username": user}, broadcast=True, namespace='/testnamespace')
该函数供视图函数调用,广播 socket 消息。
将原来的表单代码和数据库模型代码分别拷贝到这两个文件中 forms.py
from wtforms import StringField, PasswordField, BooleanField, SubmitField
from wtforms.validators import DataRequired
from flask_wtf import FlaskForm
class LoginForm(FlaskForm):
username = StringField('Username', validators=[DataRequired(), ])
password = PasswordField('Password', validators=[DataRequired()])
remember_me = BooleanField('Keep me logged in')
submit = SubmitField('Log in')
models.py
from . import db
from flask_login import UserMixin
from flask import request
import hashlib
class User(UserMixin, db.Model):
__tablename__ = 'users'
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(64), unique=True, index=True)
password = db.Column(db.String(64))
avatar_hash = db.Column(db.String(32))
def gravatar(self, name=None, size=100, default='identicon', rating='g'):
if request.is_secure:
url = 'https://secure.gravatar.com/avatar'
else:
url = 'http://www.gravatar.com/avatar'
if name is not None:
email = name + "@hihichat.com"
else:
email = self.username + "@hihichat.com"
myhash = self.avatar_hash or hashlib.md5(email.encode('utf-8')).hexdigest()
return '{url}/{hash}?s={size}&d={default}&r={rating}'.format(url=url, hash=myhash, size=size,
default=default, rating=rating)
这里把生成头像的函数整合优化了。
把 HTML 模板里的 url_for() 函数都增加 main.,再放置到 templates 下面即可。
顶级文件夹中的 manage.py 文件用于启动程序。
import os
from app import create_app, socketio
app = create_app(os.getenv('FLASK_CONFIG') or 'default')
if __name__ == '__main__':
socketio.run(app, debug=True)
还是使用 socketio.run 的方式启动应用。
至此,代码拆分完毕。
以前我们都是使用浏览器 URL 直接新增用户的,即函数 adduser,现在我们做一个简单的页面,来规范这个操作。
定义表单
class CreateUserForm(FlaskForm):
username = StringField('Username', validators=[DataRequired()])
password = PasswordField('Password', validators=[DataRequired(), EqualTo('password2',
message='Password must match.')])
password2 = PasswordField('Confirm password', validators=[DataRequired()])
submit = SubmitField('Create User')
def validate_username(self, field):
if User.query.filter_by(username=field.data).first():
raise ValidationError('Username already in use.')
定义了一个函数,来校验用户名是否重复。
修改原来的视图函数 adduser
@main.route('/adduser', methods=['GET', 'POST'])
@login_required
def adduser():
form = CreateUserForm()
if form.validate_on_submit():
user = User(username=form.username.data, password=form.password.data)
db.session.add(user)
db.session.commit()
return redirect(url_for('main.index'))
return render_template('adduser.html', form=form)
还要再修改下 User 模型,因为当前保存的是明文密码,修改成使用 hash 存储。
@property
def password(self):
raise AttributeError('password is not a readable attribute')
@password.setter
def password(self, password):
self.password_hash = generate_password_hash(password)
def verify_password(self, password):
return check_password_hash(self.password_hash, password)
分别设置密码的只读权限,以及 hash 计算和验证功能。
接下来编写 HTML 模板
{% extends "bootstrap/base.html" %}
{% import "bootstrap/wtf.html" as wtf %}
{% block title %}Flasky{% endblock %}
{% block navbar %}
<div class="navbar navbar-inverse" role="navigation">
<div class="container">
<div class="navbar-header">
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="/">Flasky</a>
</div>
<div class="navbar-collapse collapse">
<ul class="nav navbar-nav">
<li><a href="/">Home</a></li>
</ul>
<ul class="nav navbar-nav navbar-right">
{% if current_user.is_authenticated %}
<li><a href="{{ url_for('main.logout') }}">Logout</a></li>
{% else %}
<li><a href="{{ url_for('main.login') }}">Login</a></li>
{% endif %}
</ul>
</div>
</div>
</div> {% endblock %}
{% block content %}
<div class="container">
<div class="page-header">
<h1>Hello, New 一个 User 吧!</h1>
</div>
{{ wtf.quick_form(form) }}
</div>
{% endblock %}
至此,一个简单的新增用户功能就好了。当然,我们还可以增加删除用户,重置密码等功能,这些的具体实现,都可以在文末的连接中找到哦,就不再赘述了。
我们其实并不希望所有人都能够创建聊天室,那么就要做一个简单的控制功能。 首先定义一个 permission 表,用来存储创建聊天室等权限,再定义一个用户和权限的关联关系表
class Permission(db.Model):
id = db.Column(db.Integer, primary_key=True)
permission_name = db.Column(db.String(64), unique=True, index=True)
class RelUserPermission(db.Model):
id = db.Column(db.Integer, primary_key=True)
user_id = db.Column(db.Integer)
permission_id = db.Column(db.Integer)
然后我们还需要一个增加权限的表,以及一个用户列表页面 在 forms.py 中添加
class EditUserForm(FlaskForm):
permission = SelectMultipleField('Permission', coerce=int)
submit = SubmitField('Submit')
def __init__(self, user, *args, **kwargs):
super(EditUserForm, self).__init__(*args, **kwargs)
self.permission.choices = [(per.id, per.permission_name)
for per in Permission.query.order_by(Permission.permission_name).all()]
self.user = user
定义了一个初始化函数,会获取到 Permission 表中的 name,id 等信息
接下来编写视图函数
@main.route('/listuser/', methods=['GET', 'POST'])
@login_required
def listuser():
user_list = User.query.all()
return render_template('listuser.html', user_list=user_list)
@main.route('/addper/', methods=['GET', 'POST'])
@login_required
def addper():
form = CreatePerForm()
if form.validate_on_submit():
per = Permission(permission_name=form.permissionname.data)
db.session.add(per)
db.session.commit()
return redirect(url_for('main.index'))
return render_template('addper.html', form=form)
@main.route('/edituser/<int:id>/', methods=['GET', 'POST'])
@login_required
def edituser(id):
user = User.query.filter_by(id=id).first()
form = EditUserForm(user=user)
if form.validate_on_submit():
for p in form.permission.data:
rup = RelUserPermission(user_id=id, permission_id=p)
db.session.add(rup)
db.session.commit()
return redirect(url_for('main.index'))
return render_template('edituser.html', form=form)
三个函数,分别是展示用户列表,增加权限,以及为用户添加权限。
然后再修改下 chat_room_list 函数,使得没有权限的用户不能展示创建聊天室的表单。
@main.route('/roomlist/', methods=["GET", 'POST'])
@login_required
def chat_room_list():
roomlist_tmp = r.keys(pattern='chat-*')
roomlist = []
can_create = False
create_room_id = Permission.query.filter_by(permission_name='createroom').first().id
rel_user_id = RelUserPermission.query.filter_by(user_id=current_user.id).first()
rel_permission = RelUserPermission.query.filter_by(user_id=current_user.id).first()
if rel_permission and rel_user_id and create_room_id:
rel_permission_id = rel_permission.permission_id
if rel_permission_id == create_room_id:
can_create = True
for i in roomlist_tmp:
i_str = str(i)
istr_list = i_str.split('-', 1)
roomlist.append(istr_list[1])
return render_template('chatroomlist.html', roomlist=roomlist, can_create=can_create)
这里主要是判断用户是否拥有 createroom 权限,其实还有一种更加简便,但是稍微有些绕的鉴权方式,可以在文末的链接中找到,大家也可以尝试下。
最后处理 HTML 表单
对于聊天室列表页面:
{% if can_create %}
<form action="{{ url_for('main.create_room') }}" method="POST" class="comment-form">
<div class="form-group comment-form-author">
<label for="chatroomname">Chat Room Name <span class="required">*</span></label>
<input class="form-control" id="chatroomname" name="chatroomname" type="text" value="" size="30" aria-required='true' />
</div>
<div class="form-group comment-form-comment">
<label for="description">Chat Room Description <span class="required">*</span></label>
<textarea class="form-control" id="description" name="description" cols="45" rows="6"></textarea>
</div>
<button name="submit" type="submit" id="submit" class="btn btn-primary" value="Submit Comment">Create Room</button>
</form>
{% endif %}
对于用户列表页面:
{% extends "bootstrap/base.html" %}
{% import "bootstrap/wtf.html" as wtf %}
{% block title %}Flasky{% endblock %}
{% block navbar %}
<div class="navbar navbar-inverse" role="navigation">
<div class="container">
<div class="navbar-header">
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="/">Flasky</a>
</div>
<div class="navbar-collapse collapse">
<ul class="nav navbar-nav">
<li><a href="/">Home</a></li>
</ul>
<ul class="nav navbar-nav navbar-right">
{% if current_user.is_authenticated %}
<li><a href="{{ url_for('main.logout') }}">Logout</a></li>
{% else %}
<li><a href="{{ url_for('main.login') }}">Login</a></li>
{% endif %}
</ul>
</div>
</div>
</div> {% endblock %}
{% block content %}
<div class="container">
<div class="page-header">
<h1>Hello, 这里是所有的用户哦!</h1>
</div>
{% for user in user_list %}
<a href="{{ url_for('main.edityouser', id=user.id) }}" class="btn btn-default" role="button">{{ user.username }}</a>
{% endfor %}
</div>
{% endblock %}
这里为了方便起见,当点击用户时,就会跳转至编辑用户权限的页面。
现在,没有权限的用户,就不能看到创建聊天室的表单喽!
当前只增加了创建聊天室的权限,我们同样还可以创建是否有权限加入某个聊天室的权限,大家自己可以先实现下哦。
当前的登陆,只要用户名是正确的,不会验证密码,直接登陆成功,现在来处理下密码校验功能。其实也简单,我们在 User 模型中新增了一个函数 verify_password,只要登陆的时候,调用该函数来验证密码即可。
@main.route('/login/', methods=['GET', 'POST'])
def login():
form = LoginForm()
if form.validate_on_submit():
user = User.query.filter_by(username=form.username.data).first()
if user is not None and user.verify_password(form.password.data):
login_user(user)
return redirect(url_for('main.index'))
return render_template('login.html', form=form)
ok,密码错误的你,是没法再登陆了。
@main.route('/roomlist/', methods=["GET", 'POST'])
def chat_room_list():
roomlist_tmp = r.keys(pattern='chat-*')
roomlist = []
can_create = False
create_room = Permission.query.filter_by(permission_name='createroom').first()
if current_user.is_authenticated: # 判断用户是否登陆
rel_user_id = RelUserPermission.query.filter_by(user_id=current_user.id).first()
rel_permission = RelUserPermission.query.filter_by(user_id=current_user.id).first()
if rel_permission and rel_user_id and create_room:
rel_permission_id = rel_permission.permission_id
create_room_id = create_room.id
if rel_permission_id == create_room_id:
can_create = True
for i in roomlist_tmp:
i_str = str(i)
istr_list = i_str.split('-', 1)
roomlist.append(istr_list[1])
return render_template('chatroomlist.html', roomlist=roomlist, can_create=can_create)
<ul class="nav navbar-nav">
<li><a href="/">Home</a></li>
<li><a href="{{ url_for('main.chat_room_list') }}">Room List</a></li>
</ul>
@main.route('/chat/', methods=['GET', 'POST'])
def chat():
rname = request.args.get('rname', "")
ulist = r.zrange("chat-" + rname, 0, -1)
messages = r.zrange("msg-" + rname, 0, -1, withscores=True)
msg_list = []
for i in messages:
msg_list.append([json.loads(i[0]), time.strftime("%Y/%m/%d %p%H:%M:%S", time.localtime(i[1]))])
if current_user.is_authenticated:
return render_template('chat.html', rname=rname, user_list=ulist, msg_list=msg_list)
else:
email = "youke" + "@hihichat.com"
hash = hashlib.md5(email.encode('utf-8')).hexdigest()
gravatar_url = 'http://www.gravatar.com/avatar/' + hash + '?s=40&d=identicon&r=g'
return render_template('chat.html', rname=rname, user_list=ulist,
msg_list=msg_list, g=gravatar_url)
@main.route('/api/sendchat/<info>', methods=['GET', 'POST'])
def send_chat(info):
if current_user.is_authenticated:
rname = request.form.get("rname", "")
body = {"username": current_user.username, "msg": info}
r.zadd("msg-" + rname, json.dumps(body), time.time())
socket_send(info, current_user.username)
return info
else:
return info
当前对于未登陆的用户(游客),直接回复游客发送的消息。
今天的分享就到这里了,在下次的分享中,我们会尝试增加自己训练的聊天机器人到系统中,这样就能让没有登陆的用户,也能愉快的耍起来了。
所有的代码,都已经上传到 GitHub 上了,喜欢的小伙伴还请给个 star 啊,感谢!
https://github.com/zhouwei713/online-chat