Tornado笔记——用Tornado搭建假单统计系统(五)
在上篇博客中,我们搭建了考勤部分的主页,提供填写考勤和查看考勤的入口。在这篇博客中,将继续为大家带来填写考勤和查看考勤部分的代码。
目前,本系列博客的所有代码都已上传到github,库地址为[email protected]:CapLiu/LeaveManage.git,大家可以上去查看,之后的代码也将更新到这个库中。
5 填写考勤
在介绍填写考勤部分之前,我们先对上篇博文中提到的Calendar类做一点小改动。
这是我们上篇博客的图,可见这个日历是从周三开始的,而不是从周一开始的。事实上,在之前的Calendar日历中,每月的一号是星期几,它就从星期几开始显示,和一般的日历相比有点混乱。
因此,我们要对Calendar类的代码做一些改动,使其永远从周一开始显示日历:
可见,在改动了Calendar类之后,日历会从周一开始显示。
为此,我们要修改TimeSheetCalendar类的__generateweeklist函数,让它找到每月一号所在的那个周一。
# util/timesheet/timesheetutil.py
def __generateweeklist(self):
extra_monthday_map = {}
days = 0
one_week = []
oneday = datetime.timedelta(days=1)
for monthday in self.__monthday_map:
one_week.append(monthday)
days += 1
if len(one_week) == 7:
self.__week_list.append(one_week)
one_week = []
elif days == len(self.__monthday_map):
tmptimeinfo = monthday.split('-')
tmpday = datetime.datetime(int(tmptimeinfo[0]), int(tmptimeinfo[1]), int(tmptimeinfo[2]))
while len(one_week) < 7:
tmpday += oneday
one_week.append(tmpday.strftime('%Y-%m-%d'))
extra_monthday_map[tmpday.strftime('%Y-%m-%d')] = tmpday.weekday()
self.__week_list.append(one_week)
elif days == 1:
# 每月1号若不是周一,则往前找到最近的周一
if self.__monthday_map[monthday] != 'Mon':
tmptimeinfo = monthday.split('-')
tmpday = datetime.datetime(int(tmptimeinfo[0]), int(tmptimeinfo[1]), int(tmptimeinfo[2]))
while tmpday.weekday() != 0:
tmpday -= oneday
one_week.append(tmpday.strftime('%Y-%m-%d'))
extra_monthday_map[tmpday.strftime('%Y-%m-%d')] = tmpday.weekday()
one_week.reverse()
if len(one_week) == 7:
self.__week_list.append(one_week)
one_week = []
self.__monthday_map.update(extra_monthday_map)
在第二个elif块中,我们会判断每月的1号是否为周一,如果不是的话,则往前找,直至找到第一个周一为止,并将这些日期插入one_week。在找到第一个周一后,对one_week做反转,从而得到正序的一周日期;如果算上往前找的天数已经够7天,则会另起一周开始剩余日期的处理。这样就确保了每月1号所在的周要从周一开始。
下面让我们看看填写考勤的后端部分。我们在main.py中建立FillTimeSheet类,用于填写考勤。
# server/main.py
# ...
class FillTimeSheet(BaseHandler):
def get(self,year,month):
#today_date = datetime.datetime.today()
year = int(year.split('=')[1])
month = int(month.split('=')[1])
timesheetcalendar = TimeSheetCalendar(year,month)
timesheetpath = gettemplatepath('timesheet.html')
timesheetcalendar.generatecalendar()
monthday_map = timesheetcalendar.getmonthmap()
week_list = timesheetcalendar.getweeklist()
self.render(timesheetpath, monthdaymap=monthday_map,weeklist=week_list,year=year,month=month)
def make_app():
routelist = [
# ...
(r"/filltimesheet/(year=\d*)&(month=\d*)",FillTimeSheet),
# ...
]
# ...
由于我们要根据不同的年月来生成对应的Calendar,因此我们采用带参数的路由来处理这部分。可以看到,我们路由的写法为/filltimesheet/(year=\d*)&(month=\d*),其含义为在/filltimesheet后要跟year=年&month=月,即/filltimesheet/year=xx&month=xx,小括号为正则表达式中的对字符分组,本身并不是url内容。
在get函数中,我们要先将传入的year具体内容解析出来。当我们访问/filltimesheet/year=yy&month=mm后,我们得到的year变量和month变量的值分别为我们在路由中写的正则表达式的值,即year='year=yy',month='month=mm'。因此,我们使用int(year.split('-')[1])和int(month.split('-')[1])来得到具体的年与月,并将其转换为int型。随后,我们就可以利用year和month来生成一个Calendar对象,并得到它的周列表和月列表,再将其显示在前端上。
我们在template文件夹下建立timesheet.html,将我们获得的日历显示出来:
<!--template/timesheet.html-->
{% extends "base_nav.html" %}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<!-- Tell the browser to be responsive to screen width -->
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content="">
<meta name="author" content="">
<!-- Favicon icon -->
<link rel="icon" type="image/png" sizes="16x16" href="../assets/images/favicon.png">
<title>填写考勤</title>
<!-- Bootstrap Core CSS -->
<link href="../assets/plugins/bootstrap/css/bootstrap.min.css" rel="stylesheet">
{% block css %}
<link href="../css/style.css" rel="stylesheet">
<!-- You can change the theme colors from here -->
<link href="../css/colors/blue.css" id="theme" rel="stylesheet">
{% end %}
<!-- HTML5 Shim and Respond.js IE8 support of HTML5 elements and media queries -->
<!-- WARNING: Respond.js doesn't work if you view the page via file:// -->
<!--[if lt IE 9]>
<script src="https://oss.maxcdn.com/libs/html5shiv/3.7.0/html5shiv.js"></script>
<script src="https://oss.maxcdn.com/libs/respond.js/1.4.2/respond.min.js"></script>
<![endif]-->
</head>
<body class="fix-header card-no-border">
<!-- ============================================================== -->
<!-- Preloader - style you can find in spinners.css -->
<!-- ============================================================== -->
<div class="preloader">
<svg class="circular" viewBox="25 25 50 50">
<circle class="path" cx="50" cy="50" r="20" fill="none" stroke-width="2" stroke-miterlimit="10" /> </svg>
</div>
<!-- ============================================================== -->
<!-- Main wrapper - style you can find in pages.scss -->
<!-- ============================================================== -->
<div id="main-wrapper">
<!-- ============================================================== -->
<!-- Page wrapper -->
<!-- ============================================================== -->
{% block content %}
{% end %}
<!-- ============================================================== -->
<!-- End Page wrapper -->
<!-- ============================================================== -->
</div>
<!-- ============================================================== -->
<!-- End Wrapper -->
<!-- ============================================================== -->
<!-- ============================================================== -->
<!-- All Jquery -->
<!-- ============================================================== -->
{% block jquery %}
<script src="https://cdn.staticfile.org/jquery/3.2.1/jquery.min.js"></script>
<script src="https://cdn.staticfile.org/popper.js/1.15.0/umd/popper.min.js"></script>
<script src="https://cdn.staticfile.org/twitter-bootstrap/4.3.1/js/bootstrap.min.js"></script>
<!--<script src="../assets/plugins/jquery/jquery.min.js"></script>-->
<!-- Bootstrap tether Core JavaScript -->
<script src="../assets/plugins/bootstrap/js/tether.min.js"></script>
<!--<script src="../assets/plugins/bootstrap/js/bootstrap.min.js"></script>-->
<!-- slimscrollbar scrollbar JavaScript -->
<script src="../js/jquery.slimscroll.js"></script>
<!--Wave Effects -->
<script src="../js/waves.js"></script>
<!--Menu sidebar -->
<script src="../js/sidebarmenu.js"></script>
<!--stickey kit -->
<script src="../assets/plugins/sticky-kit-master/dist/sticky-kit.min.js"></script>
<!--Custom JavaScript -->
<script src="../js/custom.min.js"></script>
<!-- ============================================================== -->
<!-- This page plugins -->
<!-- ============================================================== -->
<!-- Flot Charts JavaScript -->
<script src="../assets/plugins/flot/jquery.flot.js"></script>
<script src="../assets/plugins/flot.tooltip/js/jquery.flot.tooltip.min.js"></script>
<script src="../js/flot-data.js"></script>
<!-- ============================================================== -->
<!-- Style switcher -->
<!-- ============================================================== -->
<script src="../assets/plugins/styleswitcher/jQuery.style.switcher.js"></script>
{% end %}
</body>
</html>
这里先将content块省略,是因为这里我们引入了两个新的块:css和jquery。因为/filltimesheet/year=yy&month=mm属于二级路由,所以如果按照之前的css和jquery路径的话,系统会无法找到对应的文件,因此这里将css和jquery的部分也做成block的形式,以便可以针对不同的页面来修改寻找文件的路径。对应的代码大家可以在base_nav.html中自行添加。可见,在timesheet.html中,css和jquery块中的文件搜索路径都向上找了一层。
下面让我们具体看看content块的内容,看看我们是如何构建这个表单的。
<!--templatee/timesheet.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="/timesheetindex">考勤</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 method="post" action="/filltimesheet/year={{ year }}&month={{ month }}">
{% 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>
{% if monthdaymap[day] == 'Mon' or monthdaymap[day] == 'Tues' or monthdaymap[day] == 'Wed' or monthdaymap[day] == 'Thur' or monthdaymap[day] == 'Fri' %}
<select class="form-control form-control-line" name="{{ day }}" id="{{ day }}" >
<option value="Normal">正常出勤</option>
<option value="Sick">病假</option>
<option value="Shift">调休</option>
<option value="AnnualLeave">年假</option>
<option value="BusinessLeave">事假</option>
</select>
{% elif monthdaymap[day] == 'Sat' or monthdaymap[day] == 'Sun' %}
<select class="form-control form-control-line" name="{{ day }}" id="{{ day }}" >
<option value="Weekend">-</option>
<option value="OT">加班</option>
</select>
{% else %}
<select class="form-control form-control-line" disabled=true >
<option>-</option>
</select>
{% end %}
</select>
</td>
{% end %}
</tr>
</tbody>
</table>
{% end %}
<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 %}
<!--...-->
这个表单本质上是由多个表格组成。我们最外层的循环为遍历weeklist,并在表头和表体部分分别对此周的日期进行遍历,从而生成每周的表单。我们的表头部分用于显示日期以及星期几,若该日期不是本月日期,则星期几处显示(N/A);在表格体部分,则会根据该日期是否为工作日显示不同的下拉事项。工作日提供正常出勤、病假、事假、年假和调休5个选项,而周末就只有周末(用-表示)和加班这两个选项。若该日期不是本月日期,则下拉框的disabled属性为true。可以注意到,我们表单元素的id和name均为日期,这为我们之后获取表单数据提供了方便。
现在让我们看看表单对应的post方法:
# server/main.py
# ..
class FillTimeSheet(BaseHandler):
# ...
def post(self,year,month):
username = ''
bytes_user = self.get_secure_cookie('currentuser')
if type(bytes_user) is bytes:
username = str(bytes_user, encoding='utf-8')
resultpath = gettemplatepath('timesheetfail.html')
year = int(year.split('=')[1])
month = int(month.split('=')[1])
monthday_map = generatemonthday(year, month)
business_list = {}
for monthday in monthday_map:
business_list[monthday] = self.get_argument(monthday)
result = filltimesheet(username,year,month,business_list)
if result == 'Fail':
self.render(resultpath,result=result)
else:
redirecturl = '/viewtimesheet/year=' + str(year) + '&month=' + str(month)
self.redirect(redirecturl)
在post方法中,我们要从cookie中得到当前登录的user,并将其从bytes类型转换为str类型。随后我们使用generatemonthday函数根据年月获得本月所有日期,此函数和Calendar类中的__generatemonthmap代码一样,这里不再赘述。在得到本月所有日期后,我们就可以得到每个日期对应的表单选项数据,将其存在business_list中。business_list是key为日期,value为表单选项数据的字典。最后, 我们将年、月以及business_list丢进filltimesheet函数中,完成表单的填写。
filltimesheet函数在util/timesheet/timesheetutil.py中,代码如下:
# util/timesheet/timesheetutil.py
# ...
def filltimesheet(username,year,month,business_list):
timesheet = session.query(TimeSheet).filter(and_(TimeSheet.username == username,TimeSheet.year == year, TimeSheet.month == month)).first()
result = 'Fail'
if type(timesheet) is not TimeSheet:
# Create new timesheet
daysbusiness = {}
newtimesheet = TimeSheet(username=username,approveusername='N/A',year=year,month=month,state='WaitForApprove',submitdate=datetime.date.today(),approvedate=datetime.date.today())
for days in business_list:
tmpday = int(days.split('-')[2])
dayinfo = 'day' + str(tmpday)
setattr(newtimesheet,dayinfo,business_list[days])
result = insertdata(newtimesheet)
else:
# Recall
for days in business_list:
tmpday = int(days.split('-')[2])
dayinfo = 'day' + str(tmpday)
setattr(timesheet, dayinfo, business_list[days])
timesheet.submitdate = datetime.datetime.today()
result = insertdata(timesheet)
return result
# ...
这个函数分两部分:填写新的考勤和修改已有考勤。如果根据输入的用户名、年份和月份没有找到TimeSheet,即为填写新的考勤,否则为对已有的考勤记录进行修改。
对于新建考勤,我们要以输入的用户名、月份和年份新建一个TimeSheet对象,state默认为WaitForApprove,提交日期和批准日期为today。随后,我们要对传入的business_list遍历,为数据库中day1-day31的字段填入值。注意,我们这里使用了setattr方法,这个方法可以让我们对对象的指定属性赋值,这样当我们构造出day1-day31的属性名后,就可以很容易地为数据库中这些字段赋值了。
对于修改已有考勤,我们只是修改day1-day31字段的值,以及修改一下提交日期。
在考勤填写完成后,根据结果会返回到填写失败页面或跳转到查看考勤页面。
填写失败页面timesheetfail.html代码如下,这里只贴content块,其他部分和timesheet.html相同:
<!--template/timesheetfail.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="javascript:void(0)">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">
<div class="col-12">
<div class="card">
<div class="card-block">
{{ result }}
</div>
</div>
</div>
</div>
<!-- ============================================================== -->
<!-- End PAge Content -->
<!-- ============================================================== -->
</div>
<!-- ============================================================== -->
<!-- End Container fluid -->
<!-- ============================================================== -->
<!-- ============================================================== -->
<!-- footer -->
<!-- ============================================================== -->
<footer class="footer text-center">
© 2020 Tornado考勤系统
</footer>
<!-- ============================================================== -->
<!-- End footer -->
<!-- ============================================================== -->
</div>
{% end %}
这样,我们就完成了填写考勤功能:
在这篇博客中,我们完成了填写考勤功能,并且构造了一个足够复杂的表单来实现这个功能。下篇博客中,我们将实现查考考勤的功能,以及要开始为审批考勤功能做一些准备,希望大家继续关注~
下一篇: 详解Python判断上传文件类型