软工第3次作业-结对编程
1. 小组成员
2017202110031 王枫
2017282110255 刘烨
2. 项目github地址
3. PSP时间预估
PSP | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|---|
Planning | 计划 | 20 | |
· Estimate | · 估计这个任务需要多少时间 | 20 | |
Development | 开发 | 1650 | |
· Analysis | · 需求分析 (包括学习新技术) | 300 | |
· Design Spec | · 生成设计文档 | 180 | |
· Design Review | · 设计复审 (和同事审核设计文档) | 60 | |
· Coding Standard | · 代码规范 (为目前的开发制定合适的规范) | 30 | |
· Design | · 具体设计 | 180 | |
· Coding | · 具体编码 | 600 | |
· Code Review | · 代码复审 | 60 | |
· Test | · 测试(自我测试,修改代码,提交修改) | 240 | |
Reporting | 报告 | 100 | |
· Test Report | · 测试报告 | 60 | |
· Size Measurement | · 计算工作量 | 10 | |
·Postmortem&Process Improvement Plan | · 事后总结, 并提出过程改进计划 | 30 | |
合计 | 1770 |
4. 解题思路
4.1 需求分析
- 基于web可响应式
- 可记录并保存用户名
- 用户可自行选择题目数量
- 可对用户答案进行核对
- 显示答题分数及答题时间
- 可查看上次答题情况
- 用户再次进入系统时分数可在上一次答题基础上进行增量计算
- 语言的切换(简体中文、繁体中文、英语)
考虑到python本身wxpython和pyqt包不太好用且网页开发为当今GUI主流,我们查阅资料后决定使用基于python flask网页框架来搭建我们的GUI平台,并且采用前后端分离的方式分别开发。
4.2 前端(255刘烨)
前端主要采用bootstrap框架,以及 html+css+javascript开发语言进行编写。
将不同功能模块使用不同页面进行实现。推荐一个bootstrap学习网站
菜鸟教程 bootstrap开发网页。
4.3 后端(031王枫)
后端采用flask框搭建。在上次作业的基础上,调用源代码中Equation类,即可获取一个随机的正确表达式,并获取一个答案。用form接收前端的输入处理,再用传递api的方式发送给前端。
5. 设计实现过程
5.1 前端逻辑图
5.2 Axure原型设计
为便于本次作业的具体实现,采用了Axure软件对项目进行原型设计。
github项目中的arithmetic.rp即为针对需求分析进行的Axure原型设计文件。
《axure设计解释文档》是对原型设计进行的文档解释以及对开发进行地初步规范。以便于前后端在后期的具体实现。
axure设计--生成网页.rar,即为使用Axure软件设计原型过程中生成的网页,便于开发者直接了解网页的需要实现的效果和功能。
5.3 前端页面介绍
为实现网页的响应式以及较为简洁美观的效果,前端主要采用了bootstrap框架。且针对不同功能模块使用了不同页面来进行实现。本次项目主要含有以下几个前端页面:
- base.html:主要实现整个页面的heading部分,包含主页、历史成绩、github项目地址、语言切换四种功能按钮。该页面可供其它页面直接调用,减少冗余代码,使得整个项目代码较为简洁。
- index.html:为主页面,主要实现用户信息的登录以及本次四则运算测试的题数设置。
- answer.html:为答题页面,显示随机生成的规定数目的四则运算式子,以及用户答题已花费的时间。该页面可供用户进行答题,也可进行答案的查询。
- finish.html:为一次答题成绩显示页面,显示用户名、本次答题总数、本次答题正确数以及答题耗时。
- hisScore.html:为历史答题成绩页面,记录用户之前累计答题成绩,以及答题耗时。
5.4 数据库设计
设计数据库是为了满足要求1中,重新登陆会记录历史数据这一功能而设计。
字段 | 数据类型 | 功能 |
---|---|---|
nickname | String(64) | 记录用户名 |
numberOfQuestions | Integer | 存储用户答题数 |
correct | Integer | 用户答题正确数 |
time | Integer | 统计时间 |
5.5 计时器设计
计时器采用前端js直接实现,在答题结束后通过submit将当前的label值传到后端数据库上记录。具体实现可查看下一章代码说明。
5.6 多语言处理
最初的想法是直接写几个网页根据语言的不同进行切换,这种方法工作量大且结构冗余,在查阅大量资料后我们筛选找到了flask包中自带的flask-babel框架进行语言翻译。
参考资料:
以上网站提供了简单的实现多语言网站的框架搭建,我们在此基础上进行拓展,构建了translation文件夹,包含en,zh_Hans_CN,zh_Hant_TW三个翻译版本,并创建了.po文件翻译,大致实现流程如下:
- 将网站上每一个需要翻译的地方用babel的内置函数gettext()框住。
- 生成翻译模板文件message.po。
- 根据模板将每一个文件中的每一句话翻译成对应语言的文字。
- 生成编译文件。
- 在后端的按钮中添加刷新事件并根据选项改变language值。
6. 代码说明
6.1 前端
6.1.1 秒表功能:
<!--向txt中输入时间 -->
<script type="text/javascript">
function startTime()
{
var today=new Date()
var h=today.getHours()
var m=today.getMinutes()
var s=today.getSeconds()
// add a zero in front of numbers<10
m=checkTime(m)
s=checkTime(s)
document.getElementById('txt').innerHTML=h+":"+m+":"+s
t=setTimeout('startTime()',500)
}
function checkTime(i)
{
if (i<10)
{i="0" + i}
return i
}
</script>
<!--计时器 -->
<script type="text/javascript">
var c=0
var t
function timedCount()
{
document.getElementById('txt').innerHTML=c
c=c+1
t=setTimeout("timedCount()",1000)
}
</script>
6.1.2 base页面
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- 上述3个meta标签*必须*放在最前面,任何其他内容都*必须*跟随其后! -->
<meta name="description" content="">
<meta name="author" content="">
<link rel="icon" href="../../favicon.ico">
{% if title %}
<title>{{title}} - equation</title>
{% else %}
<title>Equation</title>
{% endif %}
<!-- Bootstrap core CSS -->
<link href="https://cdn.bootcss.com/bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet">
<!-- Bootstrap theme -->
<link href="../../dist/css/bootstrap-theme.min.css" rel="stylesheet">
<!-- IE10 viewport hack for Surface/desktop Windows 8 bug -->
<link href="../../assets/css/ie10-viewport-bug-workaround.css" rel="stylesheet">
<!-- Custom styles for this template -->
<link href="theme.css" rel="stylesheet">
<!-- Custom styles for this template -->
<link href="signin.css" rel="stylesheet">
<!-- Just for debugging purposes. Don't actually copy these 2 lines! -->
<!--[if lt IE 9]><script src="../../assets/js/ie8-responsive-file-warning.js"></script><![endif]-->
<script src="../../assets/js/ie-emulation-modes-warning.js"></script>
<!-- HTML5 shim and Respond.js for IE8 support of HTML5 elements and media queries -->
<!--[if lt IE 9]>
<script src="https://cdn.bootcss.com/html5shiv/3.7.3/html5shiv.min.js"></script>
<script src="https://cdn.bootcss.com/respond.js/1.4.2/respond.min.js"></script>
<![endif]-->
<!--向txt中输入时间 -->
<script type="text/javascript">
function startTime()
{
var today=new Date()
var h=today.getHours()
var m=today.getMinutes()
var s=today.getSeconds()
// add a zero in front of numbers<10
m=checkTime(m)
s=checkTime(s)
document.getElementById('txt').innerHTML=h+":"+m+":"+s
t=setTimeout('startTime()',500)
}
function checkTime(i)
{
if (i<10)
{i="0" + i}
return i
}
</script>
<!--计时器 -->
<script type="text/javascript">
var c=0
var t
function timedCount()
{
document.getElementById('txt').innerHTML=c
c=c+1
t=setTimeout("timedCount()",1000)
}
</script>
<!--cookie-->
<script type="text/javascript">
function setCookie(){
document.cookie = document.getElementById('txt').innerHTML
}
</script>
<script type="text/javascript">
function gettime(){
document.getElementById('time').innerHTML = document.cookie
}
</script>
<style type="text/css">
h4.title{font-weight:bold; font-size: 170%;margin-left: -60px}
h4.haveAns{font-size: 140%}
div.start{margin: 60px;height: 30px}
div.content{height:40px}
input{height: 34px;width: 82px}
</style>
</head>
<nav class="navbar navbar-inverse">
<div class="container">
<div class="navbar-header">
<button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target=".navbar-collapse">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="/index">{{ _("四则运算器") }}</a>
</div>
<div class="navbar-collapse collapse">
<ul class="nav navbar-nav">
<li class="active"><a href="/index">{{ _("主 页 ") }}</a></li>
<li><a href="/hisScore">{{ _("历史成绩") }}</a></li>
<li><a href="https://github.com/wapleeeeee/webEquation">{{ _("github地址 ") }}</a></li>
<li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">{{ _("语言切换 ") }}<span class="caret"></span></a>
<ul class="dropdown-menu">
<li><a href="{{url_for('index',language='zh_Hans_CN')}}">{{ _("简体中文 ") }}</a></li>
<li><a href="{{url_for('index',language='zh_Hant_TW')}}">{{ _("繁体中文 ") }}</a></li>
<li><a href="{{url_for('index',language='en')}}">{{ _("英语 ") }}</a></li>
</ul>
</li>
</ul>
</div><!--/.nav-collapse -->
</div>
</nav>
<div class="container">
<div class = "starter-template">
{% with messages = get_flashed_messages() %}
{% if messages %}
<ul>
{% for message in messages %}
<li>{{ message }} </li>
{% endfor %}
</ul>
{% endif %}
{% endwith %}
</div>
</div>
{% block content %}{% endblock %}
<!-- Bootstrap core JavaScript
================================================== -->
<!-- Placed at the end of the document so the pages load faster -->
<script src="https://cdn.bootcss.com/jquery/1.12.4/jquery.min.js"></script>
<script>window.jQuery || document.write('<script src="../../assets/js/vendor/jquery.min.js"><\/script>')</script>
<script src="https://cdn.bootcss.com/bootstrap/3.3.7/js/bootstrap.min.js"></script>
<script src="../../assets/js/docs.min.js"></script>
<!-- IE10 viewport hack for Surface/desktop Windows 8 bug -->
<script src="../../assets/js/ie10-viewport-bug-workaround.js"></script>
</html>
6.1.3 欢迎页面
{% extends "base.html" %}
{% block content %}
<body>
<div class="container">
<div class="jumbotron">
<h2>{{ _("欢迎来到四则运算系统! ") }}</h2><br>
<p>{{ _("首次登陆请输入用户名(1-15位)及答题数(1-20)开始答题。 ") }}</p>
<form class="form-horizontal" action="" method="post" name="login">
{{form.hidden_tag()}}
<p>{{ _("请输入您的用户名: ") }}</p>
<input name="username" type="text" class="form-control" placeholder="Username ">
{% for error in form.username.errors %}
<span class="help-inline">[{{ error }}]</span>
{% endfor %}<br>
<p>{{ _("请输入您本次答题数: ") }}</p>
<input name="numbers" type="text" class="form-control" placeholder="Number ">
{% for error in form.numbers.errors %}
<span class="help-inline">[{{ error }}]</span>
{% endfor %}<br>
<br>
<div class="control-group">
<div class="controls">
<input class="btn btn-primary" type="submit" value="{{ _('开始答题') }}">
</div>
</div>
</form>
</div>
</div><!-- /.container -->
</body>
{% endblock %}
6.1.4 回答界面
{% extends "base.html" %}
{% block content %}
<link rel="stylesheet" href="http://cdn.static.runoob.com/libs/bootstrap/3.3.7/css/bootstrap.min.css">
<script src="http://cdn.static.runoob.com/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="http://cdn.static.runoob.com/libs/bootstrap/3.3.7/js/bootstrap.min.js"></script>
<body onload="timedCount()">
<div class="container">
<div class="well">
<div class="page-header">
<h2 style="display: inline;">{{_('开始答题:')}} </h2>
<div align="right"><strong>{{_('用时:')}}</strong> <span class="label label-default" name = "txt" id="txt"></span><strong> s</strong>
</div>
</div>
<form class="form-horizontal" action="" method="post" name="ans">
{{form.hidden_tag()}}
{{ form.csrf_token }}
{% for post in posts %}
<br><h4>{{post.num}}. {{post.equ}}</h4>
<div class="well">
<button type="button" class="btn btn-primary" title="{{_('正确答案')}}"
data-container="body" data-toggle="popover" data-placement="right"
data-content="{{post.ans}}">
{{_('答案提示')}}
</button>
</div>
<script>
$(function () {
$("[data-toggle='popover']").popover();
});
</script>
<input name="answer-{{post.num}}" type="text" class="form-control" placeholder="{{_('请输入答案')}}">
{% for error in form.answer.errors %}
<span class="help-inline">[{{ error }}]</span>
{% endfor %}
{% endfor %}
<br>
<p class="text-center"><input class="btn btn-primary" type="submit" value="{{_('提交答案')}}" ></p>
</form>
</div>
</div><!-- /.container -->
</body>
{% endblock %}
6.2 后端
6.2.1 用户欢迎
@app.route('/', methods = ['GET', 'POST'])
@app.route('/index', methods = ['GET', 'POST'])
@app.route('/index/<language>', methods = ['GET', 'POST'])
def index(language = "zh_Hans_CN"):
session["language"] = language
refresh()
form = MyLoginForm()
if flask.request.method == "GET":
return render_template("index.html",form = form)
else:
if form.validate_on_submit():
session["name"] = form.username.data
return redirect(url_for('answer',user=form.username.data,num=form.numbers.data))
else:
return render_template("index.html", form = form)
6.2.2 回答界面
@app.route('/answer/<user>/<num>', methods = ['GET', 'POST'])
def answer(user,num):
form = AnswerForm()
if form.validate_on_submit():
time = int(ti())-session["starttime"]
counter = 0
for index,entry in enumerate(form.answer.entries):
a = "answer_"+str(index+1)
if session[a] == entry.data:
counter += 1
#整理数据库
#如果存在记录
if models.User.query.filter_by(nickname = user).all():
old_db = models.User.query.filter_by(nickname = user).first()
old_db.numberOfQuestions = int(num) + old_db.numberOfQuestions
old_db.correct = counter + old_db.correct
old_db.time = time + old_db.time
db.session.add(old_db)
else:
u = models.User(nickname = user,numberOfQuestions = num,correct = counter,time = time)
db.session.add(u)
db.session.commit()
return redirect(url_for('finish',correct=counter,user = user,num=num,time=time))
posts = []
for i in range(1,1+int(num)):
ansstring = "answer_"+str(i)
equclass = homework2.Equation()
equclass.start()
_Dict = {'num':i, 'equ':equclass.equ, 'ans':equclass.answer}
posts.append(_Dict)
session[ansstring] = str(equclass.answer)
session["starttime"] = int(ti())
return render_template("answer.html",
title = 'Answer',
form = form,
posts = posts)
6.2.3 表单配置
from flask.ext.wtf import Form
from wtforms import StringField, BooleanField, IntegerField, FieldList
from wtforms.validators import DataRequired,Length,InputRequired,NumberRange
class LoginForm(Form):
openid = StringField('openid', validators=[DataRequired()])
remember_me = BooleanField('remember_me', default=False)
class MyLoginForm(Form):
username = StringField('waple', validators=[Length(min=1,max=15,message="用户名长度错误")])
numbers = IntegerField(1, validators=[NumberRange(min=1,max=20,message="请输入1-20的整数")])
class AnswerForm(Form):
def __init__(self, *args, **kwargs):
kwargs['csrf_enabled'] = False
super(AnswerForm, self).__init__(*args, **kwargs)
answer = FieldList(StringField(validators=[DataRequired(),Length(1,10)]), label = '答案列表', min_entries=1)
7. 测试运行
8. 遇到的问题
8.1 王枫
基本上是从零开始摸索,跟着教程一步一步开始学,碰到了很多后端问题,在这里放几个靠谱的解决方法:
- flask搭建的问题 从零开始教如何搭建flask。
- bootstrap模板 bootsrap入门模仿搭建网站必备。
- Flask WTForms always give false on validate_on_submit() flask表达提交validate_on_submit总是false的原因,也就是表单提交到后台无响应。 解决方法:添加csrf key验证。
- 前后端逻辑混乱 参考廖雪峰老师的网站逻辑结构图。
- 定时器的写法 简单易懂的js计时器实现。
- 数据库环境搭建 flask配置数据库环境,简单易上手。
- 多语言问题,上文5.6已经详细列出了三个相关链接。
以上提供的链接都是在根本上解决问题
8.2 刘烨
- 在python中安装flask_sqlalchemy包是出现安装失败的问题:
在查阅资料后了解到需要修改python某个源文件的内容。具体解决方法已上传至博客。 - 在使用flask时遇到的markupsafe._compat包缺失的问题:
在查阅资料后知道了在python的markupsafe文件夹中缺失了_compat.py文件,在该文件夹中新建一个 _compat.py文件即可。具体解决方法已上传至博客 - 在试图寻找使用前端的方法来实现语言切换功能遇到问题:
在网上查阅资料,找到一种使用jQuery+jQuery.i18n.properties框架的方法进行语言的切换,该方法通过建立多种语言的键值对文件,并在前端通过jQuery触发来选择相应文件的内容进行前台的显示。但在进行该方法demo的编写和实现过程中(使用官方demo也会出现以下问题)发现了以下两个问题:
(1)使用该方法编写的html文件无法在google以及360浏览器上进行正常显示,但能够在firefox上进行页面的正常显示。
(2)在使用firefox进行语言选择操作时,选择好语言后页面能够正常刷新,但是不能成功地显示对应语言的页面。(不知道是数据未成功传到cookie中还是刷新页面时没有读取新的cookie内容)
鉴于以上问题暂时放弃使用前端的方法实现语言切换的功能,后在查询中获知可使用python flask框架中的flask-babel进行该功能实现(由于我对flask框架不是很熟悉,因此该功能最终由搭档王枫成功实现了)。
9. 合作情况
9.1 PSP实际时间
PSP | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|---|
Planning | 计划 | 20 | 30 |
· Estimate | · 估计这个任务需要多少时间 | 20 | 30 |
Development | 开发 | 1650 | 2050 |
· Analysis | · 需求分析 (包括学习新技术) | 300 | 420 |
· Design Spec | · 生成设计文档 | 180 | 240 |
· Design Review | · 设计复审 (和同事审核设计文档) | 60 | 60 |
· Coding Standard | · 代码规范 (为目前的开发制定合适的规范) | 30 | 30 |
· Design | · 具体设计 | 180 | 300 |
· Coding | · 具体编码 | 600 | 720 |
· Code Review | · 代码复审 | 60 | 100 |
· Test | · 测试(自我测试,修改代码,提交修改) | 240 | 180 |
Reporting | 报告 | 100 | 70 |
· Test Report | · 测试报告 | 60 | 30 |
· Size Measurement | · 计算工作量 | 10 | 10 |
·Postmortem&Process Improvement Plan | · 事后总结, 并提出过程改进计划 | 30 | 30 |
合计 | 1770 | 2150 |
9.2 王枫
第一次进行结对编程,深刻地意识到在计算机开发领域上1+1>2这一不变真理。由于从没有做过网页编程,我只能从我的伙伴那里吸取经验。我通过学习构建了一个后端的框架,将后端能提供的参数以及需求交给我的伙伴。借此让我们双方在自己擅长的领域扮演领航员的角色。
这种前后端分离的开发方法让我不用费太多的时间去学习前段的html,js等知识,相反,我可以保证更多的时间来处理后端的函数调用,数据库建立以及多语言等问题,这种开发方法切实地体现了我们两个人的开发特性,是一次很成功的项目开发经历。
9.3 刘烨
由于是第一次进行结对合作,且我和搭档对于使用python编写GUI项目都没有任何的经验,因此期初对如何开展项目有些困惑。
考虑到我在本科期间进行过web开发,对前端的开发较为熟悉,因此我们决定使用前后端分离的形式进行本次作业的完成。
在开始进行代码的编写前,我们初步讨论了本项目的项目需求以及大致的界面设计。
由于假期,面对面沟通存在一定的障碍,于是决定先采用Axure软件将项目原型画出来,并生成初步的网页,以供后期项目具体实现时进行参考。且在文档中对每个页面需实现的功能以及实现效果进行了解释和规范。
在前端开发中我与搭档扮演的角色是领航员与驾驶员的角色,对前端功能的具体解释以及合理的代码规范能够方便搭档进行后台的实现。
10. 项目小结
10.1 王枫
由于从没有做过web开发,本次选题当我下定决心要做一个web时,我和伙伴都吓了一跳。但是光从发展上都可以看到,python的pyqt和wxpython等图形化界面发展缓慢,远不如django,flask等web框架迭代的快,复杂性及各种效果的可扩展性有目共睹。
这次作业,我边看边学flask框架,从0完成了后端的搭建同时提供了前端的模板,把最基本的flask搭建网站的流程都走了一遍,收获颇丰。不仅如此,通过对数据库的链接,多语言的处理等熟悉了flask的其他开发配置,也算是丰富了自己的见识。
在前端的开发上,搭档给我耐心细致地讲解了html、css等基本页面的写法,也通过自己的努力尝试写出了一个js函数。在bootstrap学习当中,我体会到了前端框架的强大,今后的学习可以以此为基础继续努力。
10.2 刘烨
虽然之前进行过web开发,但一两年之久未碰过前端开发,使得期初进行代码的编写时还是出了一定的问题。即是本次页面设计较为简单,但由于对语言的不够熟悉,使得需要反复进行资料的查阅,耗费了较多的时间。
但本次作业还是收获比遗憾多。在请教已工作的朋友后了解到,可使用Axure软件进行原型的设计,于是开始尝试使用该软件进行本次作业的初步设计,虽然设计的过程很坎坷,但对于自己来说,正是学会使用一种新工具的好机会。同时也能够便于后期我和搭档对本次项目的具体实现。
本次作业过程中,我从搭档身上也学到了很多,他高效的执行力、查阅资料的能力以及高效的学习能力都是我要学习的,并且他也给我讲解了如何使用后端python的flask框架进行代码的编写,以及一些功能模块是如何进行实现的。
11. 结对照片
12. 中程汇报
中期汇报
2017282110255 刘烨
2017202110031 王枫
实现情况:
采用python中flask框架+bootstrap编写。
因为之前学习过pyqt和wxpython不是很好看,于是考虑使用web开发,flask是一个基于python的网页开发后段框架。
之前两人都没接触过网页开发所以学习的时间比较长,现在实现了网页的基本功能,并完成了计时功能。
待实现功能:
正在构建用户数据库用于实现记录用户登录情况功能,后面需要添加多语言处理功能。
分工情况:
刘烨负责前端逻辑结构的设计以及前段代码的实现,我负责后端框架搭建。在github上可以看到代码提交情况。
@王枫,OK,优先实现多语言,赞前后端分离开发方式
多语言功能已实现。
13. 待实现功能
目前在本地开启的服务器 http://192.168.167.196:5000/ 可以接受本地(软工所实验室)的访问,但由于是内网,无法接受外网的访问,还在努力探索这个问题。
项目可以通过github readme上提供的方法在本地运行。
10.12更新
http://10.133.228.232:8080/ 已经可以再任意网络上查看我们的demo。
但由于网络限制,仅限10.12早上和每晚22:00-8:00开放。