调查问卷通常采用选择题来方便问卷对象更简单快速完成调查。而且为了统计分析更加有针对性和明确性,调查问卷题目设计成单选题更合理有效。即使是现实情况下涉及多条选项更合理的题目,也可以用“最喜欢”,答案“都有”,或者答案组合(1)(3)(6)设计为单个选项等还用单选题的形式来收集答案。下面介绍类似调查问卷的单选题考试的H5微应用。
题目使用记事本格式保存:
。答案也使用记事本文本保存:
看一下完成后的效果:
该小应用后台没有使用任何数据库,题目和答案存储在记事本文件中。不同的用户随机抽取到的不同题目集以及每个用户的答题进度和记录全部存储在cookie中,前端使用bootstrap框架,后台使用Python web框架tornado开发:
接下来介绍一下前端代码,前端使用了tornado的默认的模板系统mako语法:
开始答题页面index.html
index.js
正在答题页面page.html
page.js
考试结果report.html
服务端代码:
代码语言:python代码运行次数:0复制import tornado.web
from tornado.options import options,define
import tornado.ioloop
from tornado.escape import json_decode,json_encode,url_escape,url_unescape
from datetime import datetime
import re
import random
class Index_Handler(tornado.web.RequestHandler):
def get(self):
start=self.get_cookie('start')
self.render('index.html',start=start,exercise=exercise)
class Report_Handler(tornado.web.RequestHandler):
def get(self):
start=self.get_cookie('start')
if not start:
self.redirect('/')
size=self.get_cookie('size')
size=int(size)
ranges=self.get_cookie('ranges')
ranges=ranges.split('-')
ranges=list(map(int,ranges))
answers=self.get_cookie('answers','{}')
answers=url_unescape(answers)
answers=json_decode(answers)
self.render('report.html',size=size,
questions0=questions0,answers0=answers0,
ranges=ranges,answers=answers,start=start)
class Page_Handler(tornado.web.RequestHandler):
def get(self,num):
answers=self.get_cookie('answers','{}')
answers=url_unescape(answers)
answers=json_decode(answers)
num=int(num)
if(num==0):
size=self.get_argument('size')
self.set_cookie('size',size)
start=datetime.now()
start=start.strftime('%Y-%m-%d_%H:%M:%S')
self.set_cookie('start',start)
num=num 1
size=int(size)
print(size)
ranges=list(range(len(questions0)))
print(ranges)
ranges=random.sample(ranges,size)
ranges=list(map(str,ranges))
ranges='-'.join(ranges)
print(ranges)
self.set_cookie('ranges',ranges)
self.redirect('/page/1')
return
elif(num==10000):
self.clear_cookie('size')
self.clear_cookie('start')
self.clear_cookie('answers')
self.clear_cookie('ranges')
self.redirect('/')
return
elif(num==20000):
self.redirect('/report')
return
else:
size=self.get_cookie('size')
start=self.get_cookie('start')
ranges=self.get_cookie('ranges')
ranges=ranges.split('-')
ranges=list(map(int,ranges))
print(ranges)
size=int(size)
question=questions0[ranges[num-1]].splitlines()
print(question)
end=datetime.now()
self.render('page.html',num=num,question=question,
size=size,start=start,end=end,answers=answers)
def post(self,num):
num=self.get_argument('num')
num=int(num)
select=self.get_argument('select')
answers=self.get_cookie('answers','{}')
answers=url_unescape(answers)
answers=json_decode(answers)
answers[num]=select
self.set_cookie('answers',url_escape(json_encode(answers)))
self.write("success")
def make_app():
handlers=[('/',Index_Handler),
('/report',Report_Handler),
('/page/(d )',Page_Handler)]
app=tornado.web.Application(handlers,
static_path='static',
template_path='template',
debug=False)
return app
global answers0
global questions0
if __name__=='__main__':
define('port',default=5000)
define('exercise',default='政治')
options.parse_command_line()
global answers0
global questions0
global exercise
exercise=options.exercise
f=open('answer/%s.txt'%options.exercise,encoding='utf-8')
answers0=f.read()
f.close()
answers0=re.sub('[^A-Za-z]*','',answers0[1:]).upper()
answers0=list(map(lambda x:ord(x)-64,answers0))
print(answers0)
f=open('question/%s.txt'%options.exercise,encoding='utf-8')
questions0=f.read()
f.close()
questions0=questions0.split('nn')
print(len(questions0))
print(questions0[0])
app=make_app()
server=tornado.httpserver.HTTPServer(app)
server.listen(options.port)
tornado.ioloop.IOLoop.current().start()
前端index.html:
代码语言:html复制<!DOCTYPE html>
<html lang="zh-CN">
<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标签*必须*放在最前面,任何其他内容都*必须*跟随其后! -->
<title>考试系统</title>
<!-- Bootstrap -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@3.3.7/dist/css/bootstrap.min.css" rel="stylesheet">
<!-- HTML5 shim 和 Respond.js 是为了让 IE8 支持 HTML5 元素和媒体查询(media queries)功能 -->
<!-- 警告:通过 file:// 协议(就是直接将 html 页面拖拽到浏览器中)访问页面时 Respond.js 不起作用 -->
<!--[if lt IE 9]>
<script src="https://cdn.jsdelivr.net/npm/html5shiv@3.7.3/dist/html5shiv.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/respond.js@1.4.2/dest/respond.min.js"></script>
<![endif]-->
</head>
<body>
<div class="panel panel-default">
<!-- Default panel contents -->
<div class="panel-heading"><h1 style="text-align: center;">欢迎来测验答题</h1></div>
<div class="btn-group btn-group-justified {%if start %}hide{%end%}" role="group" aria-label="...">
<a role="button" class="btn btn-primary" data-size=25>25题</a>
<a role="button" class="btn btn-default" data-size=50>50题</a>
<a role="button" class="btn btn-default" data-size=100>100题</a>
</div>
<div class="panel-body ">
<div class="well center-block" style="max-width: 400px;" data-role='pages'>
{%if not start%}
<button type="button" class="btn btn-default btn-lg btn-block" data-num=0>开始{{exercise }}考试</button>
{%else%}
<button type="button" class="btn btn-primary btn-lg btn-block" data-num=1>继续{{exercise }}考试</button>
{%end%}
</div>
</div>
</div>
<div class="modal fade" tabindex="-1" role="dialog">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">×</span></button>
<h4 class="modal-title">确认考试开始</h4>
</div>
<div class="modal-body">
<p id='message'></p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">取消</button>
<button type="button" class="btn btn-primary" id='submit'>开始考试</button>
</div>
</div><!-- /.modal-content -->
</div><!-- /.modal-dialog -->
</div><!-- /.modal -->
<!-- jQuery (Bootstrap 的所有 JavaScript 插件都依赖 jQuery,所以必须放在前边) -->
<script src="https://cdn.jsdelivr.net/npm/jquery@1.12.4/dist/jquery.min.js"></script>
<!-- 加载 Bootstrap 的所有 JavaScript 插件。你也可以根据需要只加载单个插件。 -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@3.3.7/dist/js/bootstrap.min.js"></script>
<script src="static/index.js"></script>
</body>
</html>
前端index.js:
代码语言:javascript复制$(function () {
$('[data-size]').click(function () {
$(this).addClass('btn-primary').removeClass('btn-default').siblings().addClass('btn-default').removeClass('btn-primary')
});
$('[data-role=pages] button').click(function () {
var $btn = $(this);
var $dialog = $('[role=dialog]');
$dialog.find('#message').html($(this).text());
$dialog.modal('show');
$dialog.find('#submit').click(function () {
var size = $('[data-size].btn-primary').data('size');
document.location = `/page/${$btn.data('num')}?` $.param({
'size': size
});
})
})
})
前端page.html:
代码语言:html复制<!DOCTYPE html>
<html lang="zh-CN">
<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标签*必须*放在最前面,任何其他内容都*必须*跟随其后! -->
<title>欢迎考试</title>
<!-- Bootstrap -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@3.3.7/dist/css/bootstrap.min.css" rel="stylesheet">
<!-- HTML5 shim 和 Respond.js 是为了让 IE8 支持 HTML5 元素和媒体查询(media queries)功能 -->
<!-- 警告:通过 file:// 协议(就是直接将 html 页面拖拽到浏览器中)访问页面时 Respond.js 不起作用 -->
<!--[if lt IE 9]>
<script src="https://cdn.jsdelivr.net/npm/html5shiv@3.7.3/dist/html5shiv.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/respond.js@1.4.2/dest/respond.min.js"></script>
<![endif]-->
</head>
<body>
<div class="progress">
{% set percent=int(len(answers)*100/size) %}
<div class="progress-bar" role="progressbar" aria-valuenow="{{len(answers)}}" aria-valuemin="0" aria-valuemax="{{size}}" style="width: {{percent}}%;">
{{percent}} %
</div>
</div>
<div class="panel panel-info" data-num={{num}}>
<div class="panel-heading">
<h3 class="panel-title">当前第{{num}}道题(已经回答{{ len(answers)}}道题,共有{{size}}道题)</h3>
</div>
<div class="panel-body">
<form>
{%import re%}
<div class="form-group">
<p class="help-block"><b>{{num}}.</b>{{re.sub('[0-9] .','',question[0])}}</p>
</div>
{% for i,ans in enumerate(question[1:]) %}
<div class="radio">
<label>
<input type="radio" name="optionsRadios" value="{{i 1}}" {%if str(i 1)==answers.get(str(num),-1)%}checked{%end%}>
{{ans}}
</label>
</div>
{% end %}
{% if num>1 %}
<a role="button" class="btn btn-primary" href="/page/{{num-1}}">上一题</a>
{% end %}
{% if num<size %}
<a role="button" class="btn btn-primary" href="/page/{{num 1}}">下一题</a>
{% end %}
<a role="button" id="submit" class="btn btn-danger" href="javascript:void(0)">马上交卷</a>
</form>
</div>
{%from datetime import datetime %}
{% set start2=datetime.strptime(start,'%Y-%m-%d_%H:%M:%S') %}
{%set seconds=(end-start2).seconds%}
<div class="panel-footer">{{start.replace('_',' ')}}开始,已经用时<span id='minutes'>{{seconds//60}}</span>分钟<span id='seconds'>{{seconds`}}</span>秒</div>
</div>
<div class="well">
{% for index in list(range(size)) %}
{% set ans_mine=int(answers.get(str(index 1),-1))%}
<a href="/page/{{index 1}}">
<span class="label {% if ans_mine>0%}label-primary{%else%}label-default{%end%}">{{index 1}}</span>
</a>
{%end%}
</div>
<input type="hidden" value="{{start}}" id="start">
<input type="hidden" value="{{num}}" id="num">
<!-- jQuery (Bootstrap 的所有 JavaScript 插件都依赖 jQuery,所以必须放在前边) -->
<script src="https://cdn.jsdelivr.net/npm/jquery@1.12.4/dist/jquery.min.js"></script>
<!-- 加载 Bootstrap 的所有 JavaScript 插件。你也可以根据需要只加载单个插件。 -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@3.3.7/dist/js/bootstrap.min.js"></script>
<script src="/static/page.js"></script>
</body>
</html>
前端page.js:
代码语言:javascript复制$(function () {
var start = new Date($('#start').val().replace('_', ' '));
function timer() {
now = new Date();
var seconds = parseInt((now - start) / 1000);
var minutes = parseInt(seconds / 60);
if (!isNaN(seconds)) {
$('#minutes').text(minutes);
$('#seconds').text(seconds % 60);
}
}
timer();
setInterval(function () {
timer();
}, 1000);
$('.panel input[type=radio]').change(function () {
var select = $(this).val();
var num = $('.panel').data('num');
console.log(select, num);
$.post('/page/0', {
'num': num,
'select': select
}, function (data) {
console.log(data);
})
});
$('#submit').click(function () {
if ($('.well .label').hasClass('label-default')) {
if ($('.well .label-default').length > 1) {
if (confirm('还有题目未作答,你确实要现在交卷!')) {
document.location = '/page/20000'
}
return;
} else {
if ($('.well .label-default:first').text() == $('#num').val() && !$('.panel input[type=radio]:checked')[0]) {
if (confirm('还有题目未作答,你确实要现在交卷!')) {
document.location = '/page/20000'
}
return;
} else if ($('.well .label-default:first').text() == $('#num').val()) {
if (confirm('你确实要现在交卷,你还可以再检查一下!')) {
document.location = '/page/20000'
}
return;
} else {
if (confirm('还有题目未作答,你确实要现在交卷!')) {
document.location = '/page/20000'
}
}
}
} else {
if (confirm('你确实要现在交卷,你还可以再检查一下!')) {
document.location = '/page/20000'
}
return;
}
})
})
前端report.html:
代码语言:html复制<!DOCTYPE html>
<html lang="zh-CN">
{%import re%}
<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标签*必须*放在最前面,任何其他内容都*必须*跟随其后! -->
<title>考试成绩</title>
<!-- Bootstrap -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@3.3.7/dist/css/bootstrap.min.css" rel="stylesheet">
<!-- HTML5 shim 和 Respond.js 是为了让 IE8 支持 HTML5 元素和媒体查询(media queries)功能 -->
<!-- 警告:通过 file:// 协议(就是直接将 html 页面拖拽到浏览器中)访问页面时 Respond.js 不起作用 -->
<!--[if lt IE 9]>
<script src="https://cdn.jsdelivr.net/npm/html5shiv@3.7.3/dist/html5shiv.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/respond.js@1.4.2/dest/respond.min.js"></script>
<![endif]-->
<style>
#back-to-top {
bottom: 1.25rem;
position: fixed;
right: 1.25rem;
}
.panel-danger {
background: #ebccd1;
}
.panel-warning {
background: #faebcc;
}
.panel-success {
background: #d6e9c6;
}
</style>
</head>
<body>
{%set a= list(map(lambda x:str(answers0[x]),ranges)) %}
{% set b= list(map(lambda x:answers.get(str(x)),range(1,size 1))) %}
{% set c1=list(filter(lambda x:x[0]==x[1],zip(a,b))) %}
{% set c2=list(filter(lambda x:x==None,b)) %}
{% set c3=list(filter(lambda x:x[0]!=x[1] and x[1]!=None,zip(a,b))) %}
{% set percent1=int(len(c1)*100/size) %}
{% set percent2=int(len(c2)*100/size) %}
{% set percent3=int(len(c3)*100/size) %}
<div class="well">
{%from datetime import datetime %}
{%set end=datetime.now()%}
{% set start2=datetime.strptime(start,'%Y-%m-%d_%H:%M:%S') %}
<table class="table table-bordered">
<tbody>
{%set seconds=(end-start2).seconds%}
<tr>
<th scope="row">总题数: {{size}}</th>
<th >用时: {{seconds//60}}分钟{{seconds`}}秒</th>
</tr>
<tr>
<th scope="row">开始时间:{{start2.strftime('%Y-%m-%d %H:%M:%S')}}</th>
<th>结束时间: {{end.strftime('%Y-%m-%d %H:%M:%S')}}</th>
</tr>
<tr>
<th scope="row" class="panel-warning">未答数:{{len(c2)}}</th>
<th class="panel-danger">答错数:{{len(c3)}}</th>
</tr>
<tr>
<th scope="row" class="panel-success">答对数:{{len(c1)}}</th>
<th>正确率:{{'{:2.0%}'.format(len(c1)/size)}}</th>
</tr>
</tbody>
</table>
<a href="/" class="btn btn-primary btn-lg " role="button">重新开始</a>
</div>
<div class="well">
{% for index,num in enumerate(ranges) %}
{% set ans_mine=int(answers.get(str(index 1),-1))%}
<a href="#{{index 1}}">
<span class='label {% if -1== ans_mine%}label-warning {%elif answers0[num]==ans_mine%}label-success{%else%}label-danger{%end%}' > {{index 1}}</span>
</a>
{%end%}
</div>
<div class="progress">
<div class="progress-bar progress-bar-success" style="width: {{percent1}}%">
<span class="sr-only">{{percent1}}% 正确 </span>
</div>
<div class="progress-bar progress-bar-warning" style="width: {{percent2}}%">
<span class="sr-only">{{percent2}}% 未填写 </span>
</div>
<div class="progress-bar progress-bar-danger" style="width: {{percent3}}%">
<span class="sr-only">{{percent3}}% 错误 </span>
</div>
</div>
{% for index,num in enumerate(ranges) %}
{%set question=questions0[num].splitlines() %}
{% set ans_mine=int(answers.get(str(index 1),-1))%}
<div id="{{index 1}}" class="panel {% if -1== ans_mine%}panel-warning {%elif answers0[num]==ans_mine%}panel-success{%else%}panel-danger{%end%}">
<div class="panel-heading">
</div>
<div class="panel-body">
<form>
<div class="form-group">
<p class="help-block"><b>{{index 1}}.</b>{{re.sub('[0-9] .','',question[0])}}</p>
</div>
{% for i,ans in enumerate(question[1:]) %}
<div class="radio">
<label class=" {%if int(ans_mine)==(i 1) and answers0[num]==(i 1)%}label-success{%elif int(ans_mine)==(i 1)%}label-danger{%elif answers0[num]==(i 1)%}label-success{%end%}">
<input type="radio" name="optionsRadios" value="{{i 1}}" disabled >
{{ans}}
</label>
</div>
{% end %}
</form>
</div>
</div>
{% end %}
<a id='back-to-top' href='#' role='button' class='btn btn-default'>
<i class="fas fa-chevron-up"></i>
返回顶部
</a>
<input type="hidden" value="{{start}}" id="start">
<!-- jQuery (Bootstrap 的所有 JavaScript 插件都依赖 jQuery,所以必须放在前边) -->
<script src="https://cdn.jsdelivr.net/npm/jquery@1.12.4/dist/jquery.min.js"></script>
<!-- 加载 Bootstrap 的所有 JavaScript 插件。你也可以根据需要只加载单个插件。 -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@3.3.7/dist/js/bootstrap.min.js"></script>
<script type="text/javascript">
$(function(){
$.get('/page/10000',function(){});
})
</script>
</body>
</html>