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

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

程序员文章站 2022-04-30 17:00:18
...

在上篇博客中,我们搭建了考勤部分的主页,提供填写考勤和查看考勤的入口。在这篇博客中,将继续为大家带来填写考勤和查看考勤部分的代码。

目前,本系列博客的所有代码都已上传到github,库地址为[email protected]:CapLiu/LeaveManage.git,大家可以上去查看,之后的代码也将更新到这个库中。

5 填写考勤

在介绍填写考勤部分之前,我们先对上篇博文中提到的Calendar类做一点小改动。

这是我们上篇博客的图,可见这个日历是从周三开始的,而不是从周一开始的。事实上,在之前的Calendar日历中,每月的一号是星期几,它就从星期几开始显示,和一般的日历相比有点混乱。

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

 因此,我们要对Calendar类的代码做一些改动,使其永远从周一开始显示日历:

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

 可见,在改动了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 %}

这样,我们就完成了填写考勤功能:

Tornado笔记——用Tornado搭建假单统计系统(五) 在这篇博客中,我们完成了填写考勤功能,并且构造了一个足够复杂的表单来实现这个功能。下篇博客中,我们将实现查考考勤的功能,以及要开始为审批考勤功能做一些准备,希望大家继续关注~