在上一篇博客中,我们实现了填写假单以及查看假单的功能。在这篇博客中,我们将实现审批假单的功能,以及构建用户权限系统,防止用户执行没有权限的功能。
十五 审批假单
审批假单的界面如下所示:
在这个页面,用户可以看到他所有下级提交的假期申请,并对其进行操作批准或拒绝。
打开util/users/timesheetutil.py,实现util函数:
# util/users/timesheetutil.py
def changevacationapplystate(vacationId,state,approveuser):
vacation = session.query(Vacation).filter(Vacation.id == vacationId).first()
if type(vacation) is Vacation:
if vacation.state == 'WaitForApprove':
vacation.state = state
vacation.applydate = datetime.date.today()
vacation.approveuser = approveuser
result = insertdata(vacation)
return result
return 'Fail'
这个函数没啥说的,就是改变Vacation表里的state状态,更新批准日期和批准人。
回到timesheet_app.py文件,开始实现页面的后端代码:
# timesheet_app.py
class ApproveVacationApply(BaseHandler):
def get(self):
username = ''
bytes_user = self.get_secure_cookie('currentuser')
if type(bytes_user) is bytes:
username = str(bytes_user, encoding='utf-8')
vacationlist = []
employees = session.query(User).filter(User.supervisor == username)
for employee in employees:
tmp_vacationlist = viewvacationapply(employee.username)
vacationlist += tmp_vacationlist
approvevacationapplypath = gettemplatepath('approvevacationapply.html')
self.render(approvevacationapplypath, vacationlist=vacationlist)
def post(self):
vacationId = self.get_argument('vacationId')
operationId = vacationId + '_operation'
operation = self.get_argument(operationId)
username = ''
bytes_user = self.get_secure_cookie('currentuser')
if type(bytes_user) is bytes:
username = str(bytes_user, encoding='utf-8')
result = changevacationapplystate(vacationId,operation,username)
if result == 'Success':
self.redirect('/approvevacationapply')
else:
resultpath = gettemplatepath('result.html')
self.render(resultpath,result=result)
然后,我们建立前端页面文件approvevacationapply.html:
<!--approvevacationapply.html-->
<!--...-->
{% block content %}
<div class="page-wrapper">
<!-- ============================================================== -->
<!-- Container fluid -->
<!-- ============================================================== -->
<div class="container-fluid">
<!-- ============================================================== -->
<!-- Bread crumb and right sidebar toggle -->
<!-- ============================================================== -->
<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>
<!-- ============================================================== -->
<!-- End Bread crumb and right sidebar toggle -->
<!-- ============================================================== -->
<!-- ============================================================== -->
<!-- Start Page Content -->
<!-- ============================================================== -->
<!-- Row -->
<div class="row">
<!-- Column -->
<div class="col-lg-8 col-xlg-9 col-md-7">
{% for vacation in vacationlist %}
<div class="card">
<div class="card-block">
<div class="form-group">
<label class="col-md-12">假单ID: {{ vacation['id'] }}</label>
<label class="col-md-12">请假人: {{ vacation['username'] }}</label>
<label class="col-md-12">假别: {{ escape(vacation['category']) }}</label>
<label class="col-md-12">
{{ vacation['startdate'] }} {{ vacation['starttime'] }} - {{ vacation['enddate'] }} {{ vacation['endtime'] }}
</label>
<label class="col-md-12">
总计 {{ escape(vacation['timesum']) }} 天
</label>
<label class="col-md-12" id="{{ vacation['id'] }}_vacationstate">
状态: {{ escape(vacation['state']) }}
</label>
<form id="{{ vacation['id'] }}" method="post" action="/approvevacationapply">
<label class="col-md-12">
<input type="hidden" id="vacationId" name="vacationId" value="{{ vacation['id'] }}"/>
<select class="form-control form-control-line" name="{{ vacation['id'] }}_operation" id="{{ vacation['id'] }}_operation" >
<option>Approved</option>
<option>Rejected</option>
</select>
<button id="{{ vacation['id'] }}_operationButton" type="submit" class="btn btn-success">操作</button>
</label>
</form>
</div>
</div>
</div>
<script>
var vacationstateId = "{{ vacation['id'] }}" + "_vacationstate";
var operationButtonId = "{{ vacation['id'] }}" + "_operationButton";
var vacationstate = document.getElementById(vacationstateId).innerHTML;
console.log(vacationstate);
var operationButton = document.getElementById(operationButtonId);
if (vacationstate.indexOf("WaitForApprove") == -1)
{
operationButton.disabled = "disabled";
}
else
{
operationButton.disabled = "";
}
</script>
{% end %}
</div>
<!-- Column -->
</div>
<!-- Row -->
<!-- ============================================================== -->
<!-- End PAge Content -->
<!-- ============================================================== -->
</div>
<!-- ============================================================== -->
<!-- End Container fluid -->
<!-- ============================================================== -->
<!-- ============================================================== -->
<!-- footer -->
<!-- ============================================================== -->
<footer class="footer text-center">
© 2020 Tornado考勤系统
</footer>
<!-- ============================================================== -->
<!-- End footer -->
<!-- ============================================================== -->
</div>
{% end %}
<!--...-->
这里需要注意的是那小段js代码,即如果假单的状态不是WaitForApprove的话,就将submit按钮置灰,防止继续修改。
然后在main.py中加上路由:
# main.py
routelist = [
# ...
(r"/approvevacationapply",ApproveVacationApply),
# ...
]
然后自行在导航栏中加入链接,这里就不再赘述了。
这样,我们就实现了假单系统的整个功能:提交假单、查看假单和审批假单。
下面让我们来看整个系统的最后一部分:用户权限系统。我们的用户权限系统是基于用户组来实现的,即每个用户组具有执行一系列功能的权限。如果用户所属的用户组没有某个功能的权限,在打开页面时就会提示当前用户没有权限。
十六 权限系统
权限系统分三部分:设置权限、查看权限和权限卡控。
设置权限页面如下:
可以为不同的用户组一次性设置各个功能的权限。
查看权限页面如下:
这个页面会以图片形式来显示用户组可以执行哪些功能,当然,大家也可以自行寻找好看的图。。。。。。
1 设置权限
让我们回忆一下权限系统的数据库部分:在这个系列的第一篇,我们就为权限系统设计了GroupPrivilege表:
这个表将存储每个用户组可执行的功能列表。
我们需要一个地方来储存我们所有的功能,因此,我们打开setting/globalsettings.py,建立一个全局变量来储存我们所有功能的列表ALLFUNCTIONLIST:
# setting/globalsettings.py
# ...
ALLFUNCTIONLIST = []
然后,我们修改main.py中的make_app函数,在其中填充ALLFUNCTIONLIST的内容:
# server/main.py
# ..
from setting.globalsettings import ALLFUNCTIONLIST
# ...
def make_app():
# ...
for funcs in routelist:
ALLFUNCTIONLIST.append(funcs[1].__name__)
# ...
这里我们用__name__属性来获得每个RequestHandler的名称,即我们使用RequestHandler的名称来表示功能名。
然后,我们打开users/userutil.py,实现setgroupprivilege和getgroupprivilege函数,前者用于给用户组设置权限,后者用于列出所有用户组的权限。先来看setgroupprivilege函数:
# users/userutil.py
def setgroupprivilege(privilegemap):
# {'Index_Root':'on','Index_Normal':'off'}
group_func_map = {}
result = ''
groups = session.query(UserGroup)
for group in groups:
group_func_map[group.groupname] = ''
for funcname in privilegemap:
func = funcname.split('_')[0]
group = funcname.split('_')[1]
if privilegemap[funcname] == 'on':
if group_func_map[group] != '':
group_func_map[group] += ','
group_func_map[group] += func
for group in groups:
privilege = session.query(GroupPrivilege).filter(GroupPrivilege.groupname == group.groupname).first()
if privilege is None:
newPrivlege = GroupPrivilege(groupname=group.groupname,funclist=group_func_map[group.groupname])
result = insertdata(newPrivlege)
else:
privilege.funclist = group_func_map[group.groupname]
result = insertdata(privilege)
return result
privilegemap是通过表单获得的一个字典,其形式为{'Index_Root':'on','Index_Normal':'off',...}。Key的构成方式是功能_用户组,用于表示用户组是否对这个功能有权限,value为on或off,对应前端页面checkbox的值。group_func_map是最终权限字典,我们要将这个字典的值存入GroupPrivilege表中,因此其形式为{'Root':'Index,Register,...','Normal':'Index,Login,...'},即key为用户组,value为用户组可执行的功能列表。函数中的前两个for循环就是在构造这个group_func_map字典,而最后一个for循环则是将这个字典存入GroupPrivilege表中。
下面来看getgroupprivilege函数:
# users/userutil.py
# ...
from setting.globalsettings import ALLFUNCTIONLIST
def getgroupprivilege():
group_func_map = {}
groups = session.query(UserGroup)
for func in ALLFUNCTIONLIST:
for group in groups:
funcname = func + '_' + group.groupname
group_func_map[funcname] = 'off'
for group in groups:
privilege = session.query(GroupPrivilege).filter(GroupPrivilege.groupname == group.groupname).first()
if type(privilege) is GroupPrivilege:
group_funcs = privilege.funclist.split(',')
for func in ALLFUNCTIONLIST:
if func in group_funcs:
funcname = func + '_' + group.groupname
group_func_map[funcname] = 'on'
return group_func_map
这个函数和setgroupprivilege干的事情相反,是通过GroupPrivilege表中得到group_func_map,即用户组对每个功能的权限。所以,这里的group_func_map形式为{'Index_Root':'on','Index_Normal':'off',...},返回后可供前端显示。
回到user_app.py,实现ModifyGroupPrivilege的后端函数:
# user_app.py
class ModifyGroupPrivilege(BaseHandler):
def get(self):
result = {}
groups = session.query(UserGroup)
for group in groups:
result[group.groupname] = ''
privilegepath = gettemplatepath('modifygroupprivilege.html')
groupprivilege = getgroupprivilege()
self.render(privilegepath,groups = result,funclist=ALLFUNCTIONLIST,groupprivilege=groupprivilege)
def post(self):
privilegevalue = {}
groups = session.query(UserGroup)
for func in ALLFUNCTIONLIST:
for group in groups:
privilegename = func + '_' + group.groupname
privilegevalue[privilegename] = self.get_argument(privilegename,'off')
result = setgroupprivilege(privilegevalue)
resultpath = gettemplatepath('result.html')
if result == 'Success':
self.redirect('/modifygroupprivilege')
else:
result = '操作失败!'
self.render(resultpath, result=result)
# main.py
def make_app():
routelist = [
# ...
(r"/modifygroupprivilege",ModifyGroupPrivilege),
# ...
]
在get方法里,我们会得到所有的用户组,包括Root和Visitor,并且通过刚才的getgroupprivilege得到用户组的权限。而在post方法里,我们会通过表单构造上面提到的privilegemap字典,并把它丢进setgroupprivilege函数来为用户组添加权限。
我们在template目录中建立modifygroupprivilege.html,实现前端页面:
<!--modifygroupprivilege.html-->
{% block content %}
<div class="page-wrapper">
<!-- ============================================================== -->
<!-- Container fluid -->
<!-- ============================================================== -->
<div class="container-fluid">
<!-- ============================================================== -->
<!-- Bread crumb and right sidebar toggle -->
<!-- ============================================================== -->
<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>
<!-- ============================================================== -->
<!-- End Bread crumb and right sidebar toggle -->
<!-- ============================================================== -->
<!-- ============================================================== -->
<!-- Start Page Content -->
<!-- ============================================================== -->
<div class="row">
<!-- column -->
<div class="col-sm-12">
<div class="card">
<div class="card-block">
<h4 class="card-title">权限</h4>
<div class="table-responsive">
<form class="form-horizontal form-material" method="post" action="/modifygroupprivilege" >
<table class="table">
<thead>
<tr>
<th>功能</th>
{% for group in groups %}
<th>{{ group }}</th>
{% end %}
</tr>
</thead>
<tbody>
{% for func in funclist %}
<tr>
<td>{{ func }}</td>
{% for group in groups %}
{% set func_name = func+'_'+group %}
{% if groupprivilege[func_name] == "on" %}
<td><input type="checkbox" class="checkbox checkbox-circle" name="{{ func }}_{{ group }}" id="{{ func }}_{{ group }}" checked=true></input></td>
{% else %}
<td><input type="checkbox" class="checkbox checkbox-circle" name="{{ func }}_{{ group }}" id="{{ func }}_{{ group }}" ></input></td>
{% end %}
{% end %}
</tr>
{% end %}
</tbody>
</table>
<button type="submit" class="btn btn-success">修改</button>
</form>
</div>
</div>
</div>
</div>
</div>
<!-- ============================================================== -->
<!-- End PAge Content -->
<!-- ============================================================== -->
</div>
<!-- ============================================================== -->
<!-- End Container fluid -->
<!-- ============================================================== -->
<!-- ============================================================== -->
<!-- footer -->
<!-- ============================================================== -->
<footer class="footer text-center">
© 2020 Tornado考勤系统
</footer>
<!-- ============================================================== -->
<!-- End footer -->
<!-- ============================================================== -->
</div>
{% end %}
这个文件需要注意的一点是这句话:{% set func_name = func+'_'+group %}。它的目的是在前端设置一个局部变量。由于我们groupprivilege的key值是由两个字典的key值拼成的,所以这里需要定义一个临时变量拼接一下,否则直接写如下的语法会报错:
{% if groupprivilege[{{ func }}+'_'+{{ group }}] == "on" %}
这样,我们就实现了给用户组设置权限的功能。
2 查看权限
查看权限就非常简单了,就是把修改权限页面的表单的checkbox换成对应的对钩和叉子图片即可,后端则和ModifyGroupPrivilege的get方法一样,这里就只贴后端代码了:
# user_app.py
class ViewGroupPrivilege(BaseHandler):
def get(self):
result = {}
groups = session.query(UserGroup)
for group in groups:
result[group.groupname] = ''
privilegepath = gettemplatepath('viewgroupprivilege.html')
groupprivilege = getgroupprivilege()
self.render(privilegepath,groups = result,funclist=ALLFUNCTIONLIST,groupprivilege=groupprivilege)
# main.py
def make_app():
routelist = [
# ...
(r"/viewgroupprivilege",ViewGroupPrivilege)
# ...
]
# ...
3 检查权限
我们将检查权限放在BaseHandler中,这样只需修改少量代码就可以让每个功能都被权限系统控制起来。
# server/apps/basehandler.py
# ...
class BaseHandler(tornado.web.RequestHandler)
# ...
def checkprivilege(self):
functionname = self.__class__.__name__
functionlist = getprivilegefunclist(self.get_current_user()).split(',')
if functionname in functionlist:
return True
else:
return False
def render(self, template_name, **kwargs):
currentuser = self.get_current_user()
usergroup = getusergroup(currentuser)
if not self.checkprivilege():
resultpath = gettemplatepath('result.html')
super(BaseHandler, self).render(resultpath, currentuser=currentuser, currentusergroup=usergroup,
result='当前用户无此权限')
return
super(BaseHandler, self).render(template_name,currentuser=currentuser,currentusergroup=usergroup,**kwargs)
这里实现一个新函数checkprivilege,它会检查当前功能(RequestHandler)是否在当前用户所处的用户组中。如果当前功能在用户组的权限中,返回True,否则返回False;在render函数中,我们调用这个checkprivilege函数,如果结果是False,那么直接渲染无此权限的页面,否则继续执行正常的渲染函数。
getprivilegefunclist在userutil.py中实现:
# userutil.py
# ...
def getprivilegefunclist(usernaame):
group = getusergroup(usernaame)
funclist = session.query(GroupPrivilege).filter(GroupPrivilege.groupname == group).first()
if funclist is None:
return ''
return funclist.funclist
一个很简单的小函数。
4 初始化权限
我们的权限系统已经很完善了,现在需要我们提供一个初始化权限,以便新建的用户组可以基于这个权限开始工作,或增删权限。
在globalsettings.py中声明一个名为DEFAULTFUNCTIONLIST的变量,在其中定义一些我们的基本功能作为新用户组的默认权限:
# globalsettings.py
DEFAULTFUNCTIONLIST='Index,Register,ViewUserGroup,Login,PersonalInfo,LogOut,FillTimeSheet,ViewTimeSheet,CreateTimeSheetEvent,TimeSheetIndex,UserOrganization,ApproveTimeSheetIndex,ApproveTimeSheet,ApproveTimeSheet,RejectTimeSheet,CreateTimeSheetEventCategory,CreateVacationApply,ViewVacationApply,ApproveVacationApply,ViewGroupPrivilege'
然后,修改userutil.py中的createusergroup函数:
# userutil.py
from setting.globalsettings import DEFAULTFUNCTIONLIST
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)
# 创建用户组权限
if result == 'Success':
newgroupprivilege = GroupPrivilege(groupname=groupname,funclist=DEFAULTFUNCTIONLIST)
result = insertdata(newgroupprivilege)
return result
这样,当我们建立新的用户组时,也同时使用默认配置为其建立了默认权限。
十七 总结
这个系列到此就结束了。在这个系列的博客中,和大家分享了一些tornado框架搭建网站的过程,也算是自己在学习过程中的笔记和总结。我们这个系统实现了基本的用户注册/登录系统、核心的考勤/请假系统以及最后的权限管控部分,也算是“麻雀虽小,五脏俱全”了。在之后,我会好好思考一下下一个系列是什么,希望可以继续和大家分享程序开发中的一些经历,也希望大家继续关注~