Tornado笔记——用Tornado搭建假单统计系统(四)
目前,我们已经实现了基本的用户系统,现在让我们进入考勤部分的开发。
十 考勤管理
1 考勤系统总览
我们初步实现如下几个功能:创建考勤事件、考勤填写以及考勤查看。创建考勤事件可以让我们创建不同的考勤事件,如正常出勤、事假、病假等;考勤填写顾名思义,是用来填写考勤的;而考勤查看用于查看已经填写好的考勤。我们的考勤系统为月度考勤,即每次填写一个月的考勤记录。
可以看到,我们左边的导航栏发生了一些变化:原本的创建用户组和查看用户组现在被放在了用户组管理的一级菜单下;原本的用户管理现在也变成了一个新的一级菜单,底下将会放置和用户管理相关的功能;而我们的考勤系统的功能将会放在考勤管理的菜单下面。
查看考勤的界面如下所示:
在这里可以看到此用户在1月每天填写的考勤状况,以及这张考勤表的状态和审批人。
在填写考勤界面,我们可以看到工作日有五种事件可以选择:正常出勤、病假、调休、年假以及事假,而周末的选择只有两种:默认的横杠-和加班。 对于不在本月的月份,我们仍然将其显示出来,但标志其不可用(N/A)。
下面就先让我们看看这两个功能的后端设计,先从数据库开始吧。
2 考勤表设计
目前我们有两张表用于考勤系统:TimeSheet表和TimeSheetEvent表。前者用于存储用户的考勤记录,后者用于存储考勤事件以供选择。
我们在database包下新建tbltimesheet.py文件,建立TimeSheet表:
# database/tbltimesheet.py
from database.tablebase import Base
from sqlalchemy import Column,String,Integer,Date,DateTime
class TimeSheet(Base):
__tablename__ = 'timesheet'
id = Column(Integer,autoincrement=True,primary_key=True)
username = Column(String,nullable=False)
approveusername = Column(String,nullable=False)
year = Column(Integer,nullable=False)
month = Column(Integer,nullable=False)
day1 = Column(String)
day2 = Column(String)
day3 = Column(String)
day4 = Column(String)
day5 = Column(String)
day6 = Column(String)
day7 = Column(String)
day8 = Column(String)
day9 = Column(String)
day10 = Column(String)
day11 = Column(String)
day12 = Column(String)
day13 = Column(String)
day14 = Column(String)
day15 = Column(String)
day16 = Column(String)
day17 = Column(String)
day18 = Column(String)
day19 = Column(String)
day20 = Column(String)
day21 = Column(String)
day22 = Column(String)
day23 = Column(String)
day24 = Column(String)
day25 = Column(String)
day26 = Column(String)
day27 = Column(String)
day28 = Column(String)
day29 = Column(String)
day30 = Column(String)
day31 = Column(String)
state = Column(String,nullable=False)
submitdate = Column(Date)
approvedate = Column(Date)
def __repr__(self):
return '<timesheet(username=%s,state=%s,submitdate=%s)>' % (self.username,self.state,self.submitdate)
各字段的含义如下:
- username:用户名,谁填写了这条记录
- approveusername:审批的用户名,每条记录需要别的用户审批
- year:考勤记录的年份
- month:考勤记录的月份
- day1-day31:每月的第1到第31天
- state:考勤表状态
- submitdate:考勤记录的提交时间
- approvedate:考勤记录的审批时间
通过以上设计,我们可以做到每人每月的考勤数据为一条记录。当然,这只是我想出来的设计,感觉用day1-day31这种字段设计有点奇葩,如果大家有更好的设计,也欢迎来讨论。
我们再新建tbltimesheetevent.py文件,建立考勤事件表:
# database/tbltimesheet.py
from database.tablebase import Base
from sqlalchemy import Column,String,Integer,Date,DateTime
class TimeSheetEvent(Base):
__tablename__ = 'timesheetevent'
id = Column(Integer,autoincrement=True,primary_key=True)
eventcode = Column(String,nullable=False,unique=True)
nickname = Column(String,nullable=False,unique=True)
def __repr__(self):
return '<timesheetevent(eventcode=%s,state=%s)>' % (self.eventcode,self.nickname)
这个表相对简单,eventcode表示事件在系统的内部名称,nickname表示它被显示出来的名称。
现在我们已经建立了数据表,下面让我们设计一个Calendar类,以便在前端创建我们的表单。
3 Calendar类的设计
由于我懒(不)得(会)找(用)前端的第三方日历插件,因此决定自己设计一个Calendar类用于显示每个月的日期以及星期,这个Calendar类应包括以下特点:
- 根据输入的年和月得到该月的日历
- 前端可以按行显示该月的日历,每行为一周,若天数不足一周的,则从下月日期中补齐
- 正确处理闰二月
我们在util包下建立timesheet包,再在其中建立timesheetutil.py文件,开始实现我们的Calendar类:
# util/timesheet/timesheetutil.py
import datetime
class TimeSheetCalendar:
def __init__(self,year,month):
self.__year = year
self.__month = month
self.__week_list = []
self.__monthday_map = {}
def __generatemonthmap(self):
firsttoday = datetime.datetime(self.__year, self.__month, 1)
oneday = datetime.timedelta(days=1)
if self.__month in [1, 3, 5, 7, 8, 10, 12]:
while firsttoday.day <= 31 and firsttoday.month == self.__month:
self.__monthday_map[firsttoday.strftime('%Y-%m-%d')] = firsttoday.weekday()
firsttoday += oneday
elif self.__month in [4, 6, 9, 11]:
while firsttoday.day <= 30 and firsttoday.month == self.__month:
self.__monthday_map[firsttoday.strftime('%Y-%m-%d')] = firsttoday.weekday()
firsttoday += oneday
elif self.__month == 2:
if (self.__year % 4 == 0 and self.__year % 100 != 0) or self.__year % 400 == 0:
while firsttoday.day <= 29 and firsttoday.month == 2:
self.__monthday_map[firsttoday.strftime('%Y-%m-%d')] = firsttoday.weekday()
firsttoday += oneday
else:
while firsttoday.day <= 28 and firsttoday.month == 2:
self.__monthday_map[firsttoday.strftime('%Y-%m-%d')] = firsttoday.weekday()
firsttoday += oneday
else:
print('Invalid month')
for monthday in self.__monthday_map:
if self.__monthday_map[monthday] == 0:
self.__monthday_map[monthday] = 'Mon'
elif self.__monthday_map[monthday] == 1:
self.__monthday_map[monthday] = 'Tues'
elif self.__monthday_map[monthday] == 2:
self.__monthday_map[monthday] = 'Wed'
elif self.__monthday_map[monthday] == 3:
self.__monthday_map[monthday] = 'Thur'
elif self.__monthday_map[monthday] == 4:
self.__monthday_map[monthday] = 'Fri'
elif self.__monthday_map[monthday] == 5:
self.__monthday_map[monthday] = 'Sat'
else:
self.__monthday_map[monthday] = 'Sun'
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 days % 7 == 0:
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)
self.__monthday_map.update(extra_monthday_map)
def generatecalendar(self):
self.__generatemonthmap()
self.__generateweeklist()
def getmonthmap(self):
return self.__monthday_map
def getweeklist(self):
return self.__week_list
让我们先来看一下这个类的成员变量:
- __year:年
- __month:月
- __week_list:list,用于存放每周的日期list,换句话说,它的元素也是list,在其中存的是这周的日期。
- __monthday_map:字典,key为格式化后的日期,value为星期几。
这个类最核心的两个函数是__generatemonthmap和__generateweeklist。在前者中,我们将根据输入的年与月生成该月的日期,并将其存入到__monthday_map中。而在__generateweeklist中,我们会遍历__monthday_map,以七天为周期将其存入到__week_list中,即__week_list的元素为一个七天的list;如果到了月末不足七天的,则从下月的日期中进行补齐,并一并更新到__monthday_map中。
generatecalendar函数只是调用了以上两个函数,getmonthmap和getweeklist分别用于获得__monthday_map和__week_list。
在实现了这个核心类之后,我们就可以实现填写考勤功能了。
4 考勤系统前端页面
如前文所述,我们的考勤系统通过一个二级菜单进入,因此我们需要修改导航栏文件base_nav.html,这里只贴出aside部分的代码,其余代码没有改动:
<!--base_nav.html-->
<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-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>
</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>
<!-- End Sidebar navigation -->
</div>
<!-- End Sidebar scroll-->
</aside>
在这里可以看到,我们建立了名为考勤管理的一级菜单,下列创建考勤事件和考勤两个菜单项。其中,考勤菜单项作为考勤功能的“主页”,将用户导向对考勤事件的操作:填写、查看或修改:
我们打开main.py,开始编写考勤“主页”的后端代码:
# main.py
from database.tbltimesheet import TimeSheet
# ...
class TimeSheetIndex(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')
timesheetindex = gettemplatepath('timesheetindex.html')
year = datetime.datetime.today().year
monthlist = range(1,13)
monthoperation = {}
for month in monthlist:
timesheet = session.query(TimeSheet).filter(and_(TimeSheet.username == username,TimeSheet.year == year, TimeSheet.month == month)).first()
if type(timesheet) is TimeSheet:
if timesheet.state == 'Approved':
monthoperation[month] = 'View'
else:
monthoperation[month] = 'Modify'
else:
monthoperation[month] = 'Fill'
self.render(timesheetindex,year=year,monthlist=monthlist,monthoperation=monthoperation)
def make_app():
routelist = [
# ...
(r"/timesheetindex",TimeSheetIndex),
# ...
]
return tornado.web.Application(routelist,cookie_secret='12f6352#527',autoreload=True,debug=True)
这个后端代码比较简单,根据当前的年数显示出1-12月,并给出对应月份考勤的操作。如果该月考勤记录不存在,则对应的操作为Fill;如果该月考勤记录存在但未批准,对应的操作为Modify;如果该月考勤已被批准,对应的操作为View,其对应的前端代码如下:
<!--timesheetindex.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>
</tr>
</thead>
<tbody>
{% for month in monthlist %}
<tr>
<td>{{ year }}</td>
<td>{{ month }}</td>
{% if monthoperation[month] == 'View' %}
<td><a href="/viewtimesheet/year={{ year }}&month={{ month }}">查看</a></td>
{% elif monthoperation[month] == 'Modify' %}
<td><a href="/filltimesheet/year={{ year }}&month={{ month }}">修改</a>|<a href="/viewtimesheet/year={{ year }}&month={{ month }}">查看</a></td>
{% elif monthoperation[month] == 'Fill' %}
<td><a href="/filltimesheet/year={{ year }}&month={{ month }}">填写</a></td>
{% end %}
</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 %}
可以看到,对于Modify的操作,我们在前端提供了修改和查看两个操作,如果用户只想查看,可以通过查看的选项进入查看页面。
由于篇幅限制,这期博客就先到这里。在这期博客中,我们实现了考勤系统所需的两张数据表timesheet和timesheetevent,以及实现了考勤系统的核心类——Calendar类;此外,我们修改了前端的导航栏,通过使用折叠菜单的设计可以让我们的系统容纳更多功能。在下一期博客中,将为大家介绍填写考勤和查看考勤这两大功能的实现,希望大家继续关注~