Tornado笔记——用Tornado搭建假单统计系统(二)
在上一篇博客里,我们用Tornado初步搭好了这个系统的架子,现在让我们开始往里填充一些内容。
五 Tornado基础介绍
首先让我们复习一下这个架子的后端代码:
# server/main.py
import sys
import platform
import os
import tornado.ioloop
import tornado.web
from tornado.web import url
from setting.globalsettings import getconfig,gettemplatepath
if platform.system() == 'Windows':
import asyncio
asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
class BaseHandler(tornado.web.RequestHandler):
def get_current_user(self):
return self.get_secure_cookie('currentuser')
class Index(BaseHandler):
def get(self):
indexpath = gettemplatepath('index.html')
self.render(indexpath)
def make_app():
routelist = [
(r"/",Index)
]
return tornado.web.Application(routelist,cookie_secret='12f6352#527')
if __name__ == '__main__':
app = make_app()
port = int(getconfig('PORT'))
app.listen(port)
mainserver = tornado.ioloop.IOLoop.current()
mainserver.start()
在上篇博客的结尾,我们用这堆代码将我们的网站跑了起来,现在让我们看一下这些代码都在干什么。
在代码的开头部分,我们判断了当前程序所运行的平台,如果当前程序运行在Windows平台下,则设置相应的事件循环策略。由于Tornado是基于协程和事件循环来作用,如果不设置这个循环策略,则程序无法运行。
随后我们基于Tornado的RequestHandler写了一个自己的BaseHandler,重写了它的get_current_user方法,通过名为currentuser的cookie来拿到当前登录的用户。
1 Tornado的RequestHandler
当Tornado作为Web框架时,它会提供RequestHandler作为接收HTTP请求的对象,这个对象提供了常用的http方法,如get和post。通过这个对象,我们可以获得客户端(浏览器)发送到服务器的各个请求参数,并将网页渲染回浏览器。
RequestHandler提供的常用方法如下:
- get:用于处理HTTP的Get请求,通常用于显示页面。
- post:用于接收表单数据。
- get_cookie:获得指定名称的cookie值,注意其返回值为bytes。
- set_cookie:设定指定名称的cookie值,默认期限为30天。
- 与以上两个方法对应的是get/set_secure_cookie,需要设定一个**以便对cookie加密。
- get_argument:获得指定名称的元素的值,用于接收表单中元素的值。如果不给默认值,而表单又没有传值的话,则会产生一个MissingArgumentError的错误。
- render:渲染指定html模板到浏览器,可向其传递参数作为模板中参数的值。
通常来说,我们对每一个RequestHandler要实现它的get方法,用于显示该页面;如果一个页面还要处理表单数据,则还要实现它的post方法以便处理表单数据。
所以,我们现在的Index就很好理解了:一个简单的get方法,渲染了index.html这个模板到浏览器。
2 Tornado的路由
接下来让我们看一下make_app函数,这个函数用于建立网站的路由,并依据路由列表来创建服务器。
在Tornado框架中,其通过Router类的实现对HTTP请求的路由,将不同的HTTP请求交给不同的RequestHandler去处理。为了实现这种路由关系,最简单的方式是直接使用Tornado提供的tornado.web.Application来创建一个Web应用,并将路由列表传递进去。
所谓的路由列表,就是指URL和RequestHandler的对应关系。一个URL对应一个RequestHandler。Tornado提供了三种匹配URL的方式:HostMatches,DefaultHostMatches和PathMatches,最常用的是PathMatches,即根据路径去匹配RequestHandler。
现在,我们在make_app里建立了一个名为routelist的列表,里面存了一个元组,表明当我们访问/这个路径,则将HTTP请求交给Index这个RequestHandler去处理。随着我们页面的增加,这个routelist会越来越长。
然后我们以其建立了一个Application返回出去,并设置了安全cookie的**。
3 主函数
主函数的部分相对简单。我们通过上面的make_app得到带有路由的Application,随后从配置文件中读出端口号,然后让Application监听这个端口,启动事件循环,整个页面就起来了。
现在我们对Tornado的运行原理有了一个大概的了解,可以开始往架子里写内容了。
六 前端大变脸以及创建用户组
我们的架子实在有些简陋,因此我们需要对它来一次变脸,并以此为基础风格建立我们的整个系统。
在这篇博客结束后,我们的系统应该是长这个样子:
在这个画面中,左侧边栏是三个做好的导航栏:主页,创建用户组和查看用户组,底下则是模板自带的标签,暂时保持不动;右边上侧导航栏右边为注册及登录,底下为之后要做的一些数据展示。
我们选用的模板为MonsterAdmin,大家可以去这里下载:https://themefisher.com/products/free-bootstrap-admin-dashboard-template/。
在下载好模板后,我们把它放到我们的工程目录里,目录结构如下:
其中,模板自带的css,js和scss目录扔进template目录,而assets放在template的上一层目录,这是因为我们要保持原模板的相对引用关系。images为我们新建的目录,用于之后存储一些自定义图片。
我们再将这几个模板目录配进nginx配置文件:
# nginx配置文件
...
server {
listen 9000;
server_name localhost;
#charset koi8-r;
#access_log logs/host.access.log main;
location / {
#root D:\\LeaveManage\\server;
proxy_pass http://localhost:8000;
#index index.html index.htm;
}
location /css/ {
#root html;
root D:\\LeaveManage\\server\\template;
#index index.html index.htm;
}
location /scss/ {
#root html;
root D:\\LeaveManage\\server\\template;
#index index.html index.htm;
}
location /assets/ {
#root html;
root D:\\LeaveManage\\server;
#index index.html index.htm;
}
location ~ \.(gif|jpg|png)$ {
#root html;
root D:\\LeaveManage\\server;
#index index.html index.htm;
}
location /js/ {
#root html;
root D:\\LeaveManage\\server\\template;
#index index.html index.htm;
}
...
}
...
(当然,大家也可以自己选喜欢的模板,再根据实际情况来配置nginx以及以下的操作)
在配置好nginx后,我们可以开始以此为基础开始建立我们的前端模板。我们随意选一个模板提供的html(404那个除外),改名为base_nav.html,以此作为我们的基础模板:
<!--base_nav.html-->
<!DOCTYPE html>
<html lang="en">
<head>
<!-- 保持原模板代码不动-->
<title>Tornado考勤系统</title>
<!-- 保持原模板代码不动-->
<![endif]-->
</head>
<body class="fix-header fix-sidebar card-no-border">
<!-- ============================================================== -->
<!-- Preloader - style you can find in spinners.css -->
<!-- ============================================================== -->
<div class="preloader">
<!-- 保持原模板代码不动-->
</div>
<!-- ============================================================== -->
<!-- Main wrapper - style you can find in pages.scss -->
<!-- ============================================================== -->
<div id="main-wrapper">
<!-- ============================================================== -->
<!-- Topbar header - style you can find in pages.scss -->
<!-- ============================================================== -->
{% block nav %}
<header class="topbar">
<nav class="navbar top-navbar navbar-toggleable-sm navbar-light">
<!-- ============================================================== -->
<!-- Logo -->
<!-- ============================================================== -->
<div class="navbar-header">
<a class="navbar-brand" href="/">
<!-- Logo icon -->
<b>
<!--You can put here icon as well // <i class="wi wi-sunset"></i> //-->
<!-- Dark Logo icon -->
<img src="../assets/images/logo-icon.png" alt="homepage" class="dark-logo" />
</b>
<!--End Logo icon -->
<!-- Logo text -->
<span>
<!-- dark Logo text -->
<img src="../assets/images/logo-text.png" alt="homepage" class="dark-logo" />
</span>
</a>
</div>
<!-- ============================================================== -->
<!-- End Logo -->
<!-- ============================================================== -->
<div class="navbar-collapse">
<!-- ============================================================== -->
<!-- toggle and nav items -->
<!-- ============================================================== -->
<ul class="navbar-nav mr-auto mt-md-0 ">
<!-- This is -->
<li class="nav-item"> <a class="nav-link nav-toggler hidden-md-up text-muted waves-effect waves-dark" href="javascript:void(0)"><i class="ti-menu"></i></a> </li>
<li class="nav-item hidden-sm-down">
<form class="app-search p-l-20">
<input type="text" class="form-control" placeholder="Search for..."> <a class="srh-btn"><i class="ti-search"></i></a>
</form>
</li>
</ul>
<!-- ============================================================== -->
<!-- User profile and search -->
<!-- ============================================================== -->
<ul class="navbar-nav my-lg-0">
{% if currentusergroup == 'Visitor' %}
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle text-muted waves-effect waves-dark" href="/register" aria-haspopup="true" aria-expanded="false">注册</a>|<a class="nav-link dropdown-toggle text-muted waves-effect waves-dark" href="/login" aria-haspopup="true" aria-expanded="false">登录</a>
</li>
{% else %}
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle text-muted waves-effect waves-dark" href="/personalinfo" aria-haspopup="true" aria-expanded="false">{{ escape(currentuser) }}</a>|<a class="nav-link dropdown-toggle text-muted waves-effect waves-dark" href="/logout" aria-haspopup="true" aria-expanded="false">登出</a>
</li>
{% end %}
</ul>
</div>
</nav>
</header>
<!-- ============================================================== -->
<!-- End Topbar header -->
<!-- ============================================================== -->
<!-- ============================================================== -->
<!-- Left Sidebar - style you can find in sidebar.scss -->
<!-- ============================================================== -->
<aside class="left-sidebar">
<!-- Sidebar scroll-->
<div class="scroll-sidebar">
<!-- Sidebar navigation-->
<nav class="sidebar-nav">
<ul id="sidebarnav">
<li>
<a href="/" class="waves-effect"><i class="fa fa-group m-r-10" aria-hidden="true"></i>主页</a>
</li>
<li>
<a href="/createusergroup" class="waves-effect"><i class="fa fa-group m-r-10" aria-hidden="true"></i>创建用户组</a>
</li>
<li>
<a href="/viewusergroup" class="waves-effect"><i class="fa fa-group m-r-10" aria-hidden="true"></i>查看用户组</a>
</li>
<li>
<a href="icon-fontawesome.html" class="waves-effect"><i class="fa fa-font m-r-10" aria-hidden="true"></i>Icons</a>
</li>
<li>
<a href="pages-blank.html" class="waves-effect"><i class="fa fa-columns m-r-10" aria-hidden="true"></i>Blank Page</a>
</li>
</ul>
</nav>
<!-- End Sidebar navigation -->
</div>
<!-- End Sidebar scroll-->
</aside>
{% end %}
<!-- ============================================================== -->
<!-- End Left Sidebar - style you can find in sidebar.scss -->
<!-- ============================================================== -->
<!-- ============================================================== -->
<!-- Page wrapper -->
<!-- ============================================================== -->
{% block content %}
{% end %}
<!-- ============================================================== -->
<!-- End Page wrapper -->
<!-- ============================================================== -->
</div>
<!-- ============================================================== -->
<!-- End Wrapper -->
<!-- ============================================================== -->
<!-- ============================================================== -->
<!-- All Jquery -->
<!-- ============================================================== -->
<!-- 保持原模板代码不动-->
</body>
</html>
在这个模板中,我们要关注的是两个block:nav和content。nav为左侧及上方导航栏,而content是右侧下半部分,为其他页面的具体内容。我们之后的所有页面都会继承这个模板来实现,以确保导航栏风格的统一。
我们可以看到,在前端模板中有{% if %},{% else %}和{% end %}的字样,这是因为,Tornado提供的前端模板可以支持一些逻辑判断,循环甚至是对象成员的访问,这使得我们可以通过后端来控制前端的内容显示,大大增加了灵活性。如在这里,我们会判断当前用户所属的用户组是否为Visitor来显示不同页面。
现在先让我们把index页面换掉。直接修改模板自带的index.html,使其继承base_nav.html,再重写content块,这里只列出关键代码:开头的{% extends "base_nav.html" %}表面我们要继承base_nav这个模板,而在底下的{% block content %}至{% end %}的包裹部分为重写的content块。如无特别说明,之后所有页面都只列出content块的内容。
<!--index.html-->
{% extends "base_nav.html" %}
...
<!-- ============================================================== -->
<!-- Main wrapper - style you can find in pages.scss -->
<!-- ============================================================== -->
<div id="main-wrapper">
<!-- ============================================================== -->
<!-- Topbar header - style you can find in pages.scss -->
<!-- ============================================================== -->
<!-- ============================================================== -->
<!-- End Left Sidebar - style you can find in sidebar.scss -->
<!-- ============================================================== -->
<!-- ============================================================== -->
<!-- Page wrapper -->
<!-- ============================================================== -->
{% block content %}
<div class="page-wrapper">
<!-- ============================================================== -->
<!-- Container fluid -->
<!-- ============================================================== -->
<div class="container-fluid">
<!-- ============================================================== -->
<!-- Bread crumb and right sidebar toggle -->
<!-- ============================================================== -->
<!-- ============================================================== -->
<!-- End Bread crumb and right sidebar toggle -->
<!-- ============================================================== -->
<!-- ============================================================== -->
<!-- Start Page Content -->
<!-- ============================================================== -->
<!-- Row -->
<div class="row">
<!-- Column -->
<div class="col-sm-6">
<div class="card">
<div class="card-block">
<h4 class="card-title">填写考勤</h4>
<div class="text-right">
<h2 class="font-light m-b-0">20天</h2>
<span class="text-muted">已出勤天数</span>
</div>
<span class="text-success">80%</span>
<div class="progress">
<div class="progress-bar bg-success" role="progressbar" style="width: 80%; height: 6px;" aria-valuenow="25" aria-valuemin="0" aria-valuemax="100"></div>
</div>
</div>
</div>
</div>
<!-- Column -->
<!-- Column -->
<div class="col-sm-6">
<div class="card">
<div class="card-block">
<h4 class="card-title">填写假单</h4>
<div class="text-right">
<h2 class="font-light m-b-0">10天</h2>
<span class="text-muted">假期余额</span>
</div>
<span class="text-info">30%</span>
<div class="progress">
<div class="progress-bar bg-info" role="progressbar" style="width: 30%; height: 6px;" aria-valuenow="25" aria-valuemin="0" aria-valuemax="100"></div>
</div>
</div>
</div>
</div>
<!-- Column -->
</div>
<!-- Row -->
<!-- Row -->
<div class="row">
<!-- column -->
<div class="col-sm-12">
<div class="card">
<div class="card-block">
<h4 class="card-title">Revenue Statistics</h4>
<div class="flot-chart">
<div class="flot-chart-content" id="flot-line-chart"></div>
</div>
</div>
</div>
</div>
<!-- column -->
</div>
<!-- Row --
<!-- Row -->
<!-- Row -->
<!-- ============================================================== -->
<!-- End PAge Content -->
<!-- ============================================================== -->
</div>
<!-- ============================================================== -->
<!-- End Container fluid -->
<!-- ============================================================== -->
<!-- ============================================================== -->
<!-- footer -->
<!-- ============================================================== -->
<footer class="footer text-center">
© 2020 Tornado考勤系统
</footer>
<!-- ============================================================== -->
<!-- End footer -->
<!-- ============================================================== -->
</div>
{% end %}
<!-- ============================================================== -->
<!-- End Page wrapper -->
<!-- ============================================================== -->
</div>
...
我们现在可以来实现我们的第一个功能了——建立用户组。在这个系统中,每个用户都会从属于一个用户组,而在后期会给每个用户组加上不同的权限,防止用户去操作那些没有权限的功能。因此,我们首先要提供一个建立用户组的功能。
我们在server里建立一个新的util包,并在其中再建立一个users包,再新建名为userutil.py文件。这个py文件将存放所有和用户相关的函数。创建用户组的函数createusergroup如下:
# server/util/users/userutil.py
from database.dbcore import session
from database.curd import insertdata,deletedata
from database.tblusergroup import UserGroup
from database.tbluser import User
from datetime import date,datetime
import hashlib
from sqlalchemy import and_
import re
def createusergroup(groupname):
usergroup = session.query(UserGroup).filter(UserGroup.groupname==groupname).first()
result = 'Fail'
if usergroup is None:
# 用户组不存在,创建
newusergroup = UserGroup(groupname=groupname,createdate=date.today())
result = insertdata(newusergroup)
return result
def getusergroup(username):
user = session.query(User).filter(User.username == username).first()
group = 'Visitor'
if type(user) is User:
group = user.usergroup
return group
这段函数很简单,通过我们之前建立的SQLAlchemy提供的session以groupname查询数据库中有没有这个usergroup存在,如果没有,则创建之。而getusergroup是查询一个指定用户属于哪个用户组,如果查不到,则算为游客组,在后续的权限控制中,游客组为权限最少的一组。
然后让我们回到main.py,去实现它的RequestHandler。这里我们需要分别实现它的get和post方法。
# server/main.py
# ...
class BaseHandler(tornado.web.RequestHandler):
def get_current_user(self):
currentuser = ''
bytes_user = self.get_secure_cookie('currentuser')
if type(bytes_user) is bytes:
currentuser = str(bytes_user,encoding='utf-8')
return currentuser
def render(self, template_name, **kwargs):
currentuser = self.get_current_user()
usergroup = getusergroup(currentuser)
super(BaseHandler, self).render(template_name,currentuser=currentuser,currentusergroup=usergroup,**kwargs)
# ...
class CreateUserGroup(BaseHandler):
def get(self):
createusergrouppath = gettemplatepath('createusergroup.html')
self.render(createusergrouppath)
def post(self):
groupname = self.get_argument('usergroupname')
result = 'Fail'
if groupname != '':
result = createusergroup(groupname)
resultpath = gettemplatepath('result.html')
if result == 'Success':
result = '成功创建用户组!'
else:
result = '创建用户组失败!'
self.render(resultpath,result=result)
# ...
def make_app():
routelist = [
# ...
(r"/createusergroup",CreateUserGroup),
# ...
]
# ...
这里我们又重写了BaseHandler的get_current_user方法,让它返回一个str而不是之前的bytes变量;我们还重写了render方法,因为在我们的导航模板中,右上角的元素由后端传入的用户组以及用户决定,因此我们让render直接写入currentuser和currentusergroup,以免每个页面都要传一次。
CreateUserGroup就是我们处理创建用户组功能的Handler。它的get和post方法都很简单:get直接渲染了我们的新页面createusergroup.html,而post方法中使用get_argument接收了usergroupnname表单元素的值,并调用刚才的createusergroup函数创建,然后将结果返回到result.html中。
createusergroup.html和result.html如下:
<!-- createusergroup.html-->
{% block content %}
<div class="page-wrapper">
<div class="container-fluid">
<div class="row page-titles">
<div class="col-md-6 col-8 align-self-center">
<h3 class="text-themecolor m-b-0 m-t-0">创建用户组</h3>
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="/">Home</a></li>
<li class="breadcrumb-item active">创建用户组</li>
</ol>
</div>
</div>
<div class="row">
<!-- Column -->
<div class="col-lg-8 col-xlg-9 col-md-7">
<div class="card">
<div class="card-block">
<form class="form-horizontal form-material" action="/createusergroup" method="post" >
<div class="form-group">
<label class="col-md-12">用户组名</label>
<div class="col-md-12">
<input type="text" class="form-control form-control-line" required=true name="usergroupname" id="usergroupname">
</div>
</div>
<div class="form-group">
<div class="col-sm-12">
<button type="submit" class="btn btn-success">创建</button>
</div>
</div>
</form>
</div>
</div>
</div>
<!-- Column -->
</div>
</div>
<footer class="footer text-center">
© 2020 Tornado考勤系统
</footer>
</div>
{% end %}
<!-- result.html-->
{% block content %}
<div class="page-wrapper">
<div class="container-fluid">
<div class="row page-titles">
<div class="col-md-6 col-8 align-self-center">
<h3 class="text-themecolor m-b-0 m-t-0">执行结果</h3>
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="javascript:void(0)">Home</a></li>
<li class="breadcrumb-item active">执行结果</li>
</ol>
</div>
</div>
<div class="row">
<div class="col-12">
<div class="card">
<div class="card-block">
{{ result }}
</div>
</div>
</div>
</div>
</div>
<footer class="footer text-center">
© 2020 Tornado考勤系统
</footer>
</div>
{% end %}
最后,我们在make_app函数中将CreateUserGroup加入路由列表,然后就可以来创建我们的用户组了:
好了,在这一篇博客中,我们对前端来了个大变脸,并写了我们在tornado中的第一个功能。在下一篇博客中,将继续介绍用户模块的其他功能,包括用户注册、用户登录、用户个人信息以及查看用户组等功能,希望大家继续关注~
上一篇: 基本调度函数定义与用法汇总