欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Tornado笔记——用Tornado搭建假单统计系统(二)

程序员文章站 2022-04-30 16:35:05
...

在上一篇博客里,我们用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的运行原理有了一个大概的了解,可以开始往架子里写内容了。

六 前端大变脸以及创建用户组

我们的架子实在有些简陋,因此我们需要对它来一次变脸,并以此为基础风格建立我们的整个系统。

在这篇博客结束后,我们的系统应该是长这个样子:

Tornado笔记——用Tornado搭建假单统计系统(二)

在这个画面中,左侧边栏是三个做好的导航栏:主页,创建用户组和查看用户组,底下则是模板自带的标签,暂时保持不动;右边上侧导航栏右边为注册及登录,底下为之后要做的一些数据展示。

我们选用的模板为MonsterAdmin,大家可以去这里下载:https://themefisher.com/products/free-bootstrap-admin-dashboard-template/

在下载好模板后,我们把它放到我们的工程目录里,目录结构如下:

Tornado笔记——用Tornado搭建假单统计系统(二)

 其中,模板自带的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笔记——用Tornado搭建假单统计系统(二)

Tornado笔记——用Tornado搭建假单统计系统(二) 

 好了,在这一篇博客中,我们对前端来了个大变脸,并写了我们在tornado中的第一个功能。在下一篇博客中,将继续介绍用户模块的其他功能,包括用户注册、用户登录、用户个人信息以及查看用户组等功能,希望大家继续关注~