在上一篇博客中,我们补全了一些用户系统的相关功能,这期让我们来实现用户的上下级关系以及考勤审批
十 用户上下级和考勤审批
在我们的系统中,每个用户只有一个上级,但每个用户可以有多个下级。因此,我们需要给User表加一个名为supervisor的字段,表明该用户的上级是谁。一个用户的可以审批其所有下级的考勤,他自身的考勤也只能被他的上级审批。
我们migrate目录下打开Powershell,输入以下命令:
alembic revision -m "add supervisor on user table"
alembic会在versions下生成如下命名方式的文件:ead049362c51_add_supervisor_on_user_table.py。开头的16进制字符串为当前数据库的版本,每人都是不同的。
在ead049362c51_add_supervisor_on_user_table.py中输入以下内容,为User表加入supervisor字段:
# add_supervisor_on_user_table.py
"""add supervisor on user table
Revision ID: ead049362c51
Revises: ac252cf2f6aa
Create Date: 2020-11-10 20:47:31.458590
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'ead049362c51'
down_revision = 'ac252cf2f6aa'
branch_labels = None
depends_on = None
def upgrade():
op.add_column('user', sa.Column('supervisor', sa.String))
def downgrade():
pass
然后回到powershell,输入以下命令完成对数据库的升版:
alembic upgrade head
再打开database/tbluser.py,把新的supervisor字段加到User类中:
# database/tbluser.py
from database.tablebase import Base
from sqlalchemy import Column,String,Integer,Date,DateTime
class User(Base):
__tablename__ = 'user'
id = Column(Integer,autoincrement=True,primary_key=True)
username = Column(String,unique=True,nullable=False)
password = Column(String,nullable=False)
email = Column(String,unique=True,nullable=False)
usergroup = Column(String,nullable=False)
state = Column(String)
registerdate = Column(Date)
lastlogintime = Column(DateTime)
supervisor = Column(String)
def __repr__(self):
return '<user(username=%s,email=%s,registerdate=%s)>' % (self.username,self.email,self.registerdate)
这样,我们数据库层面的准备工作就大功告成,下面让我们开始实现调整用户上下级的功能(俗称“挂人”,即把某个用户挂在某个用户的下面)。实现后的效果如下图:
在这个页面中,我们可以对用户的上下级关系进行调整,也可以对用户所属的用户组进行调整。
我们打开user_app/user_app.py,建立UserOrganization RequestHandler:
# user_app.py
# ...
class UserOrganization(BaseHandler):
def get(self):
userorganizationpath = gettemplatepath('userorganization.html')
users = session.query(User).filter(User.username != 'Root')
usergroups = getallusergroup()
userInfos = []
for user in users:
usergrouplist = []
usersupervisorlist = []
userInfo = {}
userInfo['username'] = user.username
userInfo['usergroup'] = user.usergroup
if user.usergroup not in usergrouplist:
usergrouplist.append(user.usergroup)
if user.supervisor not in usersupervisorlist and type(user.supervisor) is str:
usersupervisorlist.append(user.supervisor)
userInfo['formid'] = user.id
userInfo['groupid'] = str(user.id)+'_group'
userInfo['supervisorid'] = str(user.id) + '_supervisor'
for othergroup in usergroups:
if othergroup.groupname not in usergrouplist:
usergrouplist.append(othergroup.groupname)
for otheruser in users:
if otheruser.username not in usersupervisorlist:
usersupervisorlist.append(otheruser.username)
userInfo['usergrouplist'] = usergrouplist
userInfo['usersupervisorlist'] = usersupervisorlist
userInfos.append(userInfo)
self.render(userorganizationpath,userInfos=userInfos)
def post(self):
userid = self.get_argument('userid')
username = self.get_argument('username')
usergroupid = userid + '_group'
usergroup = self.get_argument(usergroupid)
supervisorid = userid + '_supervisor'
supervisor = self.get_argument(supervisorid)
result = 'Fail'
result = changeuserorganization(username,usergroup,supervisor)
resultpath = gettemplatepath('result.html')
if result == 'Success':
self.redirect('/userorganization')
else:
result = '操作失败!'
self.render(resultpath,result=result)
# main.py
# ...
routelist = [
# ...
(r"/userorganization",UserOrganization),
# ...
]
# ...
在get请求中,我们会获得除了Root用户外的所有用户,并将其相关信息返回到前端页面显示;在post页面则是根据选择的用户组和上级用户将选定用户的用户组和上级改变。
changeuserorganization函数位于userutil.py中,代码如下:
# util/users/userutil.py
def changeuserorganization(username,usergroup,supervisor):
print(username)
user = session.query(User).filter(User.username == username).first()
result = 'Fail'
if type(user) is User:
user.usergroup = usergroup
user.supervisor = supervisor
result = insertdata(user)
if result == 'Success':
pass
#sendapprovemail(user.username,usergroup,user.email)
return result
这个函数没啥说的,根据传入的用户名去修改用户组和上级,如果修改成功后,还可以调用之前写的email模块向用户发送邮件,这里就先用pass占个位。
调整用户组织的前端页面代码如下:
<!--userorganization.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">
<table class="table">
<thead>
<tr>
<th>#</th>
<th>用户名</th>
<th>用户组</th>
<th>直属领导</th>
<th>操作</th>
</tr>
</thead>
<tbody>
{% for userinfo in userInfos %}
<tr>
<form method="post" id="{
{ userinfo['formid'] }}" action="/userorganization">
<td><input type="text" value="{
{ userinfo['formid'] }}" readonly=true id='userid' name='userid'/></td>
<td><input type="text" value="{
{ escape(userinfo['username']) }}" readonly=true id='username' name='username'/></td>
<td><select class="form-control form-control-line" name="{
{ escape(userinfo['groupid']) }}" id="{
{ escape(userinfo['groupid']) }}" >
{% for usergroup in userinfo['usergrouplist'] %}
<option>{
{ escape(usergroup) }}</option>
{% end %}
</select>
</td>
<td><select class="form-control form-control-line" name="{
{ escape(userinfo['supervisorid']) }}" id="{
{ escape(userinfo['supervisorid']) }}" >
{% for supervisor in userinfo['usersupervisorlist'] %}
<option>{
{ escape(supervisor) }}</option>
{% end %}
</select>
</td>
<td><button type="submit" class="btn btn-success">修改</button></td>
</form>
</tr>
{% end %}
</tbody>
</table>
</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 %}
这个页面的代码与上篇博客中写的用户审批页面基本相似,都是为每个用户建立一个表单,用于对其修改。
最后,不要忘了在导航栏中加入这部分的导航,我们当前的导航栏代码应该是这个样子,包含了我们之前实现的所有功能,以及一些我们还没有介绍到的功能:
<!--base_nav.html-->
<nav class="sidebar-nav">
<ul id="sidebarnav">
<li>
<a href="/" class="waves-effect"><i class="fa fa-bank m-r-10" aria-hidden="true"></i>主页</a>
</li>
<li>
<a href="#" data-toggle="collapse" data-target="#timesheetmanage"><i class="fa fa-calendar m-r-10" aria-hidden="true"></i>考勤管理</a>
<ul id="timesheetmanage" class="collapse">
<li>
<a href="/createtimesheetevent" class="waves-effect"><i class="fa fa-clock-o m-r-10" aria-hidden="true"></i>创建考勤事件</a>
</li>
<li>
<a href="/timesheetindex" class="waves-effect"><i class="fa fa-pencil m-r-10" aria-hidden="true"></i>考勤</a>
</li>
<li>
<a href="/approvetimesheetindex" class="waves-effect"><i class="fa fa-check m-r-10" aria-hidden="true"></i>审批考勤</a>
</li>
</ul>
</li>
<li>
<a href="#" data-toggle="collapse" data-target="#usergroupmanage"><i class="fa fa-sitemap m-r-10" aria-hidden="true"></i>用户组管理</a>
<ul id="usergroupmanage" class="collapse">
<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-info-circle m-r-10" aria-hidden="true"></i>查看用户组</a>
</li>
</ul>
</li>
<li>
<a href="#" data-toggle="collapse" data-target="#usermanage"><i class="fa fa-user-circle m-r-10" aria-hidden="true"></i>用户管理</a>
<ul id="usermanage" class="collapse">
<li>
<a href="/usermanage" class="waves-effect"><i class="fa fa-user-o m-r-10" aria-hidden="true"></i>用户审批</a>
</li>
<li>
<a href="/userorganization" class="waves-effect"><i class="fa fa-info-circle m-r-10" aria-hidden="true"></i>调整用户组织</a>
</li>
</ul>
</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>
在把用户们“挂”好后,让我们开始实现审批考勤的功能。审批考勤功能主页面如下:
从图中可以看出,test用户下面挂着test1和test2两名用户,且test1填写了今年1月、10月和11月的考勤(这有点不太合理,之后会对功能做一些限制);test2填写了今年1月和3月的考勤。除了test1在11月的考勤已被批准外,剩下的考勤均处于待批准状态。
点开批准链接,可以进入到审批考勤页面:
同之前查看考勤的页面基本相同,只不过底下多了批准和拒绝的选项。
我们打开timesheet_app/timesheet_app.py,输入以下代码:
# timesheet_app/timesheet_app.py
# ...
class ApproveTimeSheetIndex(BaseHandler):
def get(self):
approveinfolist = []
username = ''
bytes_user = self.get_secure_cookie('currentuser')
if type(bytes_user) is bytes:
username = str(bytes_user, encoding='utf-8')
approvetimesheetindexpath = gettemplatepath('approvetimesheetindex.html')
employees = session.query(User).filter(User.supervisor==username)
year = datetime.datetime.today().year
for employee in employees:
approveinfo = {}
monthlist = []
approveinfo['employee'] = employee.username
timesheets = session.query(TimeSheet).filter(and_(TimeSheet.username == employee.username,TimeSheet.year == year))
for timesheet in timesheets:
monthlist.append(timesheet.month)
approveinfo[timesheet.month] = timesheet.state
approveinfo['monthlist'] = monthlist
approveinfolist.append(approveinfo)
self.render(approvetimesheetindexpath,approveinfolist=approveinfolist,year=year)
# ...
这里首先会选出当前用户的所有下级,并将他们今年填写过的考勤一并列出。
前端页面approvetimesheetindex.html如下:
<!--approvetimesheetindex.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">
<table class="table">
<thead>
<tr>
<th>员工</th>
<th>年</th>
<th>月</th>
<th>操作</th>
</tr>
</thead>
<tbody>
{% for approveinfo in approveinfolist %}
{% for month in approveinfo['monthlist'] %}
<tr>
<td>{
{ escape(approveinfo['employee']) }}</td>
<td>{
{ year }}</td>
<td>{
{ month }}</td>
{% if approveinfo[month] == 'Approved' %}
<td><a href="/approvetimesheet/year={
{ year }}&month={
{ month }}&employee={
{ approveinfo['employee'] }}">查看</a></td>
{% else %}
<td><a href="/approvetimesheet/year={
{ year }}&month={
{ month }}&employee={
{ approveinfo['employee'] }}">批准</a></td>
{% end %}
</tr>
{% end %}
{% end %}
</tbody>
</table>
</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 %}
这里会把后端的数据渲染到前端,并且根据当前考勤表的状态显示查看或批准的选项。
同样在main.py中添加其路由:
# main.py
routelist = [
# ...
(r"/approvetimesheetindex",ApproveTimeSheetIndex),
# ...
]
回到timesheet_app/timesheet_app.py,实现ApproveTimeSheet和RejectTimeSheet功能:
# timesheet_app/timesheet_app.py
class ApproveTimeSheet(BaseHandler):
def get(self,year,month,employee):
year = int(year.split('=')[1])
month = int(month.split('=')[1])
employee = employee.split('=')[1]
timesheetviewer = TimeSheetViewer(username=employee,year=year,month=month)
timesheetviewer.gettimesheetmap()
timesheetcalendar = TimeSheetCalendar(year, month)
timesheetcalendar.generatecalendar()
monthday_map = timesheetcalendar.getmonthmap()
week_list = timesheetcalendar.getweeklist()
timesheet_map = timesheetviewer.gettimesheetmap()
timesheet_state = timesheetviewer.getstate()
timesheet_approveuser = timesheetviewer.getapproveuser()
timesheet_approvedate = timesheetviewer.getapprovedate()
approvetimesheetpath = gettemplatepath('approvetimesheet.html')
self.render(approvetimesheetpath, monthdaymap=monthday_map,weeklist=week_list,timesheetmap=timesheet_map,timesheetstate=timesheet_state,
timesheetapprovedate=timesheet_approvedate,timesheetapproveuser=timesheet_approveuser,employee=employee,year=year,month=month)
def post(self):
username = ''
bytes_user = self.get_secure_cookie('currentuser')
if type(bytes_user) is bytes:
username = str(bytes_user, encoding='utf-8')
employee = self.get_argument('employee')
year = self.get_argument('year')
month = self.get_argument('month')
result = changetimesheetstate(username,employee,int(year),int(month),'Approved')
resultpath = gettemplatepath('timesheetfail.html')
if result == 'Success':
self.redirect('/approvetimesheetindex')
else:
result = '操作失败!'
self.render(resultpath,result=result)
class RejectTimeSheet(BaseHandler):
def get(self,year,month,employee):
username = ''
bytes_user = self.get_secure_cookie('currentuser')
if type(bytes_user) is bytes:
username = str(bytes_user, encoding='utf-8')
employee = employee.split('=')[1]
year = year.split('=')[1]
month = month.split('=')[1]
result = changetimesheetstate(username,employee,int(year),int(month),'Reject')
resultpath = gettemplatepath('timesheetfail.html')
if result == 'Success':
self.redirect('/approvetimesheetindex')
else:
result = '操作失败!'
self.render(resultpath,result=result)
我们的ApproveTimeSheet分两部分:get部分用于显示之前的页面,基本上可以视为ViewTimeSheet的翻版,只不过多传入了employee的信息;而post页面会调用changetimesheetstate函数来改变指定timesheet的状态,改变完毕后重定向到approvetimesheetindex页面中。changetimesheetstate函数位于timesheet/timesheetutil.py中:
# timesheet/timesheetutil.py
def changetimesheetstate(approver,employee,year,month,state):
timesheet = session.query(TimeSheet).filter(and_(TimeSheet.username == employee,TimeSheet.year == year, TimeSheet.month == month)).first()
result = 'Fail'
if type(timesheet) is TimeSheet:
timesheet.state = state
timesheet.approveusername = approver
timesheet.approvedate = datetime.datetime.today()
result = insertdata(timesheet)
return result
# ...
这个函数没什么说的,基本的数据修改操作。
RejectTimeSheet函数使用get请求方式,同样调用changetimesheetstate函数来拒绝掉考勤。
以上两个功能对应的前端代码如下:
<!--approvetimesheet.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">{
{ escape(employee) }} - {
{ year }} - {
{ month }}</h3>
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="/approvetimesheetindex">审批考勤</a></li>
<li class="breadcrumb-item active">{
{ escape(employee) }} - {
{ year }} - {
{ month }}</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">
{% for week in weeklist %}
<table class="table">
<thead>
<tr>
{% for day in week %}
<th>{
{ day }}
{% if monthdaymap[day] == 'Mon' or monthdaymap[day] == 'Tues' or monthdaymap[day] == 'Wed' or monthdaymap[day] == 'Thur' or monthdaymap[day] == 'Fri' or monthdaymap[day] == 'Sat' or monthdaymap[day] == 'Sun' %}
({
{ monthdaymap[day] }})
{% else %}
(N/A)
{% end %}
</th>
{% end %}
</tr>
</thead>
<tbody>
<tr>
{% for day in week %}
<td>
{
{ timesheetmap[day] }}
</td>
{% end %}
</tr>
</tbody>
</table>
{% end %}
{% if timesheetstate != 'Approved' %}
<table class="table">
<thead>
<tr>
<th>员工</th>
<th>年份</th>
<th>月份</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<tr>
<form method="post" action="/approvetimesheet">
<td><input type="text" value="{
{ employee }}" readonly=true id='employee' name='employee'/></td>
<td><input type="text" value="{
{ year }}" readonly=true id='year' name='year'/></td>
<td><input type="text" value="{
{ month }}" readonly=true id='month' name='month'/></td>
<td><button type="submit" class="btn btn-success">批准</button> |<a href="/rejecttimesheet/year={
{ year }}&month={
{ month }}&employee={
{ employee }}">拒绝</a></td>
</form>
</tr>
</tbody>
</table>
{% end %}
</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 %}
<!-- ... -->
这里需要注意的是,“批准”是作为表单的submit按钮实现的,因此“批准”所需的参数都可以从表单中获得,无需带路由的参数;但“拒绝”则是使用普通的a标签,因此这里需要带路由的参数:
# main.py
# ...
routelist = [
# ...
(r"/approvetimesheet/(year=\d*)&(month=\d*)&(employee=.*)",ApproveTimeSheet),
(r"/approvetimesheet", ApproveTimeSheet),
(r"/rejecttimesheet/(year=\d*)&(month=\d*)&(employee=.*)", RejectTimeSheet),
# ...
]
我们对ApproveTimeSheet设置了两种不同的路由:带参数的路由用于从ApproveTimeSheetIndex进入到具体的审批页面,而不带参数的用于具体的审批功能,因为年、月以及雇员都已经在审批页面中显示出来了,通过表单可直接获取到;RejectTimeSheet则是使用带参数的路由。
在这篇博客中,我们实现了用户的上下级关系调整以及审批考勤的功能,整个考勤系统初步完成。在之后的博客中,我们将继续开发请假系统以及权限控制相关的内容,希望大家继续关注~