You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
the_online_test_system/一、在线习题测试系统背景.md

25 KiB

1.可行性分析报告:

1.1引言:

随着时代的不断发展,互联网已进入千家万户,社会进入网络时代,计算机网络已经成为社会发展的强大动力。网络不仅给我们带来无穷的信息,也带来了许多便利。不仅企业、政府的正常工作离不开网络,教育事业同样需要网络。远程教育已经成为现代教育技术的发展的主要方向之一,在线习题测试作为远程教育的一个子系统也成为一个重要研究领域。

网络技术的发展使得学生习题的技术手段和载体发生了革命性的变化,网络的开放性、分布性和基于网络的巨大计算能力使学生做题和测试突破了空间和时间的限制。基于网络的习题测试系统正成为人们的研究热点之一。与传统习题测试相比,网络在线测试系统不仅能节约大量时间、人力财力。在线习题测试系统产生的背景正是当今教育信息化的趋势及我国高等教育信息化建设;目的是充分利用学校现有的计算机软硬件资源和网络资源实现无纸化习题测试以避免传统测试的不足。

1.2项目背景:

项目名称:在线习题测试系统(灯塔)

项目的开发者:唐三萌、何鹏瞩、郑培炤、张舜尧、磨文龙、陈德聪

用户:各高校师生

1.3参考资料:

1、https://www.djangoproject.com/

2、https://blog.csdn.net/weixin_45110404/article/details/90758243 3、《跟老齐学PythonDjango实战第2版》作者: 齐伟 电子工业出版社 出版年: 2019-1 4、https://docs.djangoproject.com/zh-hans/3.0/ 5、学长项目https://github.com/EnJoy-git/OnlineExerciseTest

2.可行性研究的前提:

2.1要求:

1、前端设计和后台设计#1、前端设计和后台设计 2、角色划分设计 3、题库管理习题分类单选题、多选题、填空题教师可在线录入也可以excel或word文件方式上传试题提供给教师题库模板 4、自动判题计分功能 5、其他特色功能

2.2目标:

1.设计出简洁优美的前端用户交汇界面,首页应包括用户登录、注册两大必备功能。在此基础上,可添加如验证码、记住密码等额外选项。

2.软件用户分为教师和学生,可建立相应数据库对用户信息进行保存。

3.学生端登入可进行文件的上传、习题的练习、查看老师发布的测试信息并参与,系统可自动对学生提交的选择题答案进行判断正误,并将学生最终成绩进行一个排名处理。

4.教师服务端教师服务端

3.对目前系统的分析:

1、后端数据库

导入django中的models包并用migrations中的CreateModel函数建立数据库的表其属性包括身份、姓名、学号/教师编号、手机号、账号、密码。其代码如下:

name='UserInfo',
fields=[
    ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
    ('identity', models.CharField(max_length=4, verbose_name='身份')),
    ('name', models.CharField(max_length=32, verbose_name='姓名')),
    ('id_number', models.CharField(max_length=32, verbose_name='学号/教师编号')),
    ('phone', models.CharField(max_length=32, unique=True, verbose_name='手机')),
    ('username', models.CharField(max_length=32, unique=True, verbose_name='账号')),
    ('password', models.CharField(max_length=32, verbose_name='密码')),
],

2、登入主界面

主界面包括注册和登录注册时使用request.POST来接收前端传过来的数据经过与数据库现有数据比对后确定用户名和手机号未被注册即可将数据导入数据库完成注册代码如下

identity = request.POST.get("identity")
name = request.POST.get("name")
id_number = request.POST.get("id_number")
phone = request.POST.get("phone")
username = request.POST.get("username")
password = request.POST.get("password")

#  添加到数据库
UserInfo.objects.create(identity=identity, name=name, id_number=id_number, phone=phone, username=username, password=password)

到此完成注册并跳转到登录界面登录时采用request.COOKIES.get方法从cookie中获取用户名和密码。在登录过程中采用了验证码技术有效防止某个黑客对某一个特定注册用户用特定程序暴力破解方式进行不断的登陆尝试。

验证码的实现先从PIL导入Image, ImageDraw, ImageFont, ImageFilter包可定义两函数如下生成随机的字符串和RGB颜色

def random_str(length=4):
return ''.join(random.sample(string.ascii_letters, length))	#随机字符串 默认长度 4

def random_color(s=1, e=255):
return random.randint(s, e), random.randint(s, e), random.randint(s, e)	#随机 RGB 颜色

接着创建Image、Draw、Font对象调用Draw中的point方法令随机颜色填充每个像素并调用text方法将随机颜色验证码写到图片上具体实现代码如下

image = Image.new('RGB', (width, height), (255, 255, 255))
# 创建Font对象
file = os.path.dirname(os.path.abspath(__file__))
font = ImageFont.truetype(f'{file}/FZSTK.ttf', size)
# 创建Draw对象
draw = ImageDraw.Draw(image)
# 随机颜色填充每个像素
for x in range(0, width, 2):
    for y in range(height):
        draw.point((x, y), fill=random_color(64, 255))
# 验证码
code = random_str(length)
# 随机颜色验证码写到图片上
for t in range(length):
    draw.text((40 * t + 5, 5), code[t], font=font, fill=random_color(32, 127))
return image, code

当用户完成用户名、密码和验证码的输入后显然要进行一系列的判断这个过程使用dologin函数实现首先使用HTTP协议请求方法中的POST方法获取验证码再利用会话控制session对象获取保存的code。判断验证码和输入的code与session对象中保存的一致后再使用HTTP协议POST方法从登录表单中获取用户名和密码及是否勾选了记住密码rember即为判断是否勾选了记住密码的标志并将返回的响应赋给response。完成上述操作后再进行数据库的遍历对比找出符合输入用户名和密码的数据并将其存入到session对象中若 勾选了记住用户名和密码将用户名和密码保存到cookie中否则 删除cookie中的之前保存用户名和密码。之后再进行登入用户身份的判断并跳转到不同的端口学生端或教师服务端

若用户密码或验证码输入不匹配,则将输入的用户名和密码清空,若密码错误则返回“用户名或密码错误”后重新返回登录界面,验证码错误则返回“验证码错误”后重新返回登录界面。

def dologin(request):

    # 获取表单中提交的验证码
    check_code = request.POST.get('check_code')
    # 获取session会话中保存的code
    session_checkcode = request.session.get('check_code')
    if check_code and check_code.lower() == session_checkcode.lower():
        # 从登录表单中获取用户名和密码及是否勾选了记住密码
        username = request.POST.get('username')
        password = request.POST.get('password')
        rember = request.POST.get('rember')
        response = HttpResponse()

        temp_RawQuerySet = UserInfo.objects.raw('select * from login_userinfo')

        for temp in temp_RawQuerySet:
            if temp.username == username and temp.password == password:
                # ## 存入基本信息到session ## #
                request.session['username'] = username
                request.session['password'] = password
                request.session['id_number'] = temp.id_number
                request.session['name'] = temp.name
                if rember == 'rember':
                    # 勾选了记住用户名和密码
                    # 将用户名和密码保存到cookie中
                    response.set_cookie('username', username, max_age=3 * 24 * 3600)
                    response.set_signed_cookie('pwd', password, salt='pwdsalt', max_age=3 * 24 * 3600)
                else:
                    # 删除cookie中的之前保存用户名和密码
                    response.delete_cookie('username')
                    response.delete_cookie('pwd')

                if temp.identity == '老师':
                    return redirect('teacher_client:teacher_client')
                    # return render(request, 'teacher_client.html', {'username': temp.username})
                else:
                    return redirect('student_client:student_client')

        # 用户密码不匹配
        response.delete_cookie('username')
        response.delete_cookie('pwd')
        error_msg = '用户名或密码错误'
        return render(request, 'login.html', {'error_msg1': error_msg})
    else:
        error_msg = '验证码错误'
        return render(request, 'login.html', {'error_msg': error_msg})

3.学生端口:成功登录后跳转学生端口,对于学生和老师端口的设计,首先要设计相关的数据库来存储相关的数据,包括姓名、学号/教师编号、加课码、课程名、题目信息、A、B、C、D选项、答案、所属班级、存入时间、考试时间等数据信息。

class teacherClass(models.Model):
    """记录班级加课码"""
    name = models.CharField(max_length=32, verbose_name="姓名", blank=False, null=False)
    id_number = models.CharField(max_length=32, verbose_name="学号/教师编号", blank=False, null=False)
    classCode = models.CharField(max_length=20, verbose_name="加课码", default='')
    classname = models.CharField(max_length=20, verbose_name="课程名", default='')


class classTable(models.Model):
    """班级表"""
    name = models.CharField(max_length=32, verbose_name="姓名", blank=False, null=False)
    id_number = models.CharField(max_length=32, verbose_name="学号/教师编号", blank=False, null=False)
    phone = models.CharField(max_length=32, verbose_name="手机", unique=True, blank=False, null=False)
    classCode = models.CharField(max_length=6, verbose_name="加课码", default='')


class questionBank(models.Model):
    """用于保存用户上传提交的题目"""
    qus_imfomation = models.CharField(max_length=1000, verbose_name="题目信息", blank=False, null=False)
    qus_A = models.CharField(max_length=1000, verbose_name="选项A", blank=False, null=False)
    qus_B = models.CharField(max_length=1000, verbose_name="选项B", blank=False, null=False)
    qus_C = models.CharField(max_length=1000, verbose_name="选项C", blank=False, null=False)
    qus_D = models.CharField(max_length=1000, verbose_name="选项D")
    qus_ans = models.CharField(max_length=1000, verbose_name="答案", blank=False, null=False)
    que_classcode = models.CharField(max_length=1000, verbose_name="所属班级", blank=False, null=False)
    que_time = models.CharField(max_length=200, verbose_name="存入时间", default="")


class testQuestionBank(models.Model):
    """考试信息表"""
    qus_imfomation = models.CharField(max_length=1000, verbose_name="题目信息", blank=False, null=False)
    qus_A = models.CharField(max_length=1000, verbose_name="选项A", blank=False, null=False)
    qus_B = models.CharField(max_length=1000, verbose_name="选项B", blank=False, null=False)
    qus_C = models.CharField(max_length=1000, verbose_name="选项C", blank=False, null=False)
    qus_D = models.CharField(max_length=1000, verbose_name="选项D")
    qus_ans = models.CharField(max_length=1000, verbose_name="答案")
    test_code = models.CharField(max_length=1000, verbose_name="所属班级", blank=False, null=False)
    test_time = models.CharField(max_length=200, verbose_name="考试时间", default='')

进入学生端口后,要先定义一系列如下变量及列表如下:以变量记录 总题目数、单选题题目数、多选题题目数、填空题题目数、正确的题目数量、错误的题目数量,列表记录单选题标准答案、多选题标准答案、填空题标准答案、单选题、多选题、填空题。

total_number = 0  # 总题目数
ans = []
single_choice_number = 0  # 单选题题目数
multiple_choice_number = 0  # 多选题题目数
gap_filling_number = 0  # 填空题题目数
single_choice_ans = []  # 单选题标准答案
multiple_choice_ans = []  # 多选题标准答案
gap_filling_ans = []  # 填空题标准答案
right_num = 0  # 正确的题目数量
wrong_num = 0  # 错误的题目数量
single_choice = []  # 单选题
multiple_choice = []  # 多选题
gap_filling = []  # 填空题

接着若收到GET请求则将session对象中的“username”和“password”赋给username和password若username和password同时不为空则将username返回前端否则返回登录界面重新登录。

def student_client(request):
    """学生端口"""
    if request.method == "GET":
        username = request.session.get('username')
        password = request.session.get('password')
        if username and password:
            StudentID = request.session.get('id_number')
            StudentName = request.session.get('classcode')
            return rernde(request, "student_client.html", {"username": username})
        else:
            return redirect("http://127.0.0.1:8000/")

通过构造函数test_join来接受加入测试的请求前面的if判断同上即接着若收到GET请求则将session对象中的“username”和“password”赋给username和password若username和password同时不为空则将username返回前端否则返回登录界面重新登录。接着将获得的testcode赋给test_code如果test_code存在则将test_code存入session对象中并跳转test_client界面进入测试。

def test_join(request):
    """加入测试"""
    if request.method == "GET":
        username = request.session.get('username')
        password = request.session.get('password')
        if username and password:
            StudentID= request.session.get('id_number')
            StudentName = request.session.get('classcode')
        else:
            return redirect("http://127.0.0.1:8000/")

    test_code = request.POST.get("testcode")
    if test_code:
        request.session['test_code'] = test_code
        return redirect("student_client:test_client")

在进入test_client考试界面后可从数据库中获取题目,并将获取的题目赋给 temp_RawQuerySetquestion

questionSelect = "select id, qus_imfomation, " \
                 "qus_A, qus_B, qus_C, qus_D, " \
                 "qus_ans," \
                 "test_time " \
                 "from teacher_client_testquestionbank " \
                 "where test_code = " + str(test_code)
 temp_RawQuerySetquestion = models.questionBank.objects.raw(questionSelect)

由于得到的题目中包括单选题、多选题及填空题,可通过循环来判断分别都是哪些题型,若选项为空,则为填空题,否则若选项只有一个,则为单选题,否则则为多选。详细代码如下:

for temp in temp_RawQuerySetquestion:
    temp_testtime = temp.test_time
    ans.append(temp.qus_ans)
    if temp.qus_ans is None:
        gap_filling.append(temp)
        gap_filling_ans.append(temp.qus_A)
    elif len(temp.qus_ans) == 1:
        single_choice.append(temp)
        single_choice_ans.append(temp.qus_ans)
    else:
        multiple_choice.append(temp)
        multiple_choice_ans.append(temp.qus_ans)

完成后对总题数、单选题题数、多选题题数、填空题题数进行一次统计:

total_number = len(gap_filling) + len(multiple_choice) + len(single_choice)
total_list = [x + 1 for x in range(total_number)]
single_choice_number = len(single_choice)
multiple_choice_number = len(multiple_choice)
gap_filling_number = len(gap_filling)

完成后将获得的相应信息返回到前端:

return render(request, "test1.html", {"total_num": total_number,
                                      "total_list": total_list,
                                      "single_choice": single_choice,
                                      "multiple_choice": multiple_choice,
                                      "gap_filling": gap_filling,
                                      "test_time": temp_testtime,
                                      "single_choice_number": single_choice_number,
                                      "multiple_choice_number": multiple_choice_number,
                                      "gap_filling_number": gap_filling_number
                                      })

定义三个列表分别保存用户输入的单选题、多选题、填空题答案,再从前端获得输入的答案后存入相应的列表中:

        single_choice_answer = []  # 用户输入的单选题答案
        multiple_choice_answer = []  # 用户输入的多选题答案
        gap_filling_answer = []  # 用户输入的填空题答案
        for temp in range(total_number):
            pass
        # 获取前端传递的单选题答案
        for temp in range(1, single_choice_number+1):
            single_str = "choose" + str(temp)
            temp_single_choice_answer = request.POST.get(single_str)
            single_choice_answer.append(temp_single_choice_answer)
        # 获取前端传递的多选题答案
        for temp in range(1, multiple_choice_number+1):
            multiple_str = "chooseA" + str(temp)
            temp_multiple_choice_answer = request.POST.getlist(multiple_str)
            multiple_choice_answer.append(temp_multiple_choice_answer)
        # 获取前端传递的填空题答案
        for temp in range(1, gap_filling_number+1):
            gap_filling_str = "chooseB" + str(temp)
            temp_gap_filling_answer = request.POST.getlist(gap_filling_str)
            gap_filling_answer.append(temp_gap_filling_answer)

接着进入判题阶段将前端获得的答案与数据库中的答案进行一一比对由于有填空题、单选题、多选题三种不同题型故用三个循环来比对答案相同则right_num+1否则wrong_num+1。

for ans, answer in zip(single_choice_ans, single_choice_ans):
    if answer == ans:
        right_num += 1
    else:
        wrong_num += 1
# 判断多选题 ans:标准答案 answer:用户答案
for ans, answer in zip(multiple_choice_ans, multiple_choice_answer):
    if answer == ans:
        right_num += 1
    else:
        wrong_num += 1
# 判断填空题 ans:标准答案 answer:用户答案
for ans, answer in zip(gap_filling_ans, gap_filling_answer):
    if answer == ans:
        right_num += 1
    else:
        wrong_num += 1

4.教师端口开始与学生端一致收到来自前端的GET请求后则将session会话中的“username”和“password”赋给username和password若username和password同时不为空则将username返回前端否则返回登录界面重新登录。

接着在数据库中通过用户名检索出该名老师的信息并放入temp_RawQuerySetClass中

select_teacher_client_teacherclass = 'select id, id_number, classname, classcode ' \
                                     'from teacher_client_teacherclass ' \
                                     'where id_number = ' + str(teacherID)
 temp_RawQuerySetClass = teacherClass.objects.raw(select_teacher_client_teachercla

定义两个列表classname、classcode来存放一名老师的班级及相应加课码并对temp_RawQuerySetClass进行循环遍历将其班级和加课码放入列表中。

之后再通过加课码classcode进行数据库检索找出加入课堂的相应学生并定义相应列表Sname、Sno、Sphone 存放学生姓名、学号、手机号。

将以上数据返回到前端teacher_client.html

            return render(request, 'teacher_client.html',
                          {'classname': json.dumps(classname), 'res': json.dumps(res), 'classcode': classcode,
                           "student": temp_RawQuerySetStudent, 'username': username,
                           "class": temp_RawQuerySetClass}, )

若需要创建一个班级则在收到一个GET请求后跳转返回到createClass.html在该页面拿到班级名字classname后将该classname存入session会话中。

if request.method == "GET":
     return render(request, "createClass.html")
 classname = request.POST.get('classname')
 request.session['classname'] = classname

利用if判断获得的classname不为空利用get_code获得相应的加课码并将code存入session对象中之后再从session对象中获得username和id_number并将这些信息一并存入数据库中之后可跳转到老师客户端。

if classname != '':
    # global CLASS_ID
    # CLASS_ID += 1
    code = get_code()
    request.session['code'] = code
    username = request.session.get('username')
    id_number = request.session.get('id_number')
    teacherClass.objects.create(name=username, id_number=id_number, classCode=code, classname=classname)
    return redirect("teacher_client:teacher_client")

要实现教师端文件的上传可利用request.FILES.get('exc')拿到从前端获取的Excel文件通过循环遍历将题目的A、B、C、D、标准答案、班级码存入Excel的每一行中并将这些信息存入数据库中完成后跳转教师客户端。

qus_imfomation = row[0].value
qus_A = row[1].value
qus_B = row[2].value
qus_C = row[3].value
qus_D = row[4].value
qus_ans = row[5].value
que_classcode = row[6].value
questionBank.objects.create(qus_imfomation=qus_imfomation, qus_A=qus_A, qus_B=qus_B, qus_C=qus_C, qus_D=qus_D, qus_ans=qus_ans, que_classcode=que_classcode, que_time=now_time)

定义一函数test_release用于习题发布由于教师发布习题默认发布最近上传的习题显然在从数据库中拿出各个习题发布时间后还要对其进行排序查找的操作来确定应该发布哪些习题可定义一列表time_list将各习题发布时间存入该列表对该列表进行一次基数排序确认排序完成根据与目前系统时间比较后进行一次二分查找在找到离目前系统时间最近的一次发布时间target_time后以此为根据在数据库拿出题目信息、选项和答案。再利用三个列表分别存入单选题、多选题、填空题区分单选、多选、填空的方法与之前学生端一致。完成后记录总题数、单选题数、多选题数、填空题数并将以上所有信息返回给前端中。

在题目成功在前端显示后,需要教师设置考试时间即可。

time_list = []
time_select = "select id, que_time " \
              "from teacher_client_questionbank"
time_res = questionBank.objects.raw(time_select)

for temp in time_res:
    if temp.que_time not in time_list:
        time_list.append(temp.que_time)
time_list = [float(x) for x in time_list]
time_list = radix_sort(time_list)
print(time_list)
target_time = binary_search(time_list, time.time())
questionSelect = "select id, qus_imfomation, " \
                 "qus_A, qus_B, qus_C, qus_D, " \
                 "qus_ans, " \
                 "que_classcode " \
                 "from teacher_client_questionbank " \
                 "where que_time =" + str(target_time)
temp_RawQuerySetquestion = questionBank.objects.raw(questionSelect)
single_choice = []
multiple_choice = []
gap_filling = []
for temp in temp_RawQuerySetquestion:
    temp_classcode = temp.que_classcode
    if temp.qus_ans is None:
        gap_filling.append(temp)
    elif len(temp.qus_ans) == 1:
        single_choice.append(temp)
    else:
        multiple_choice.append(temp)

    # testQuestionBank.objects.create(qus_imfomation=temp.qus_imfomation, qus_A=temp.qus_A, qus_B=temp.qus_B,
    #                                 qus_C=temp.qus_C, qus_D=temp.qus_D, qus_ans=temp.qus_ans,
    #                                 test_code=TEST_CODE, test_time=test_time)

total_number = len(gap_filling) + len(multiple_choice) + len(single_choice)
total_list = [x + 1 for x in range(total_number)]
single_choice_number = len(single_choice)
multiple_choice_number = len(multiple_choice)
gap_filling_number = len(gap_filling)

return render(request, "test.html", {"total_num": total_number,
                                     "total_list": total_list,
                                     "single_choice": single_choice,
                                     "multiple_choice": multiple_choice,
                                     "gap_filling": gap_filling,
                                     "ClassCode": temp_classcode,
                                     "TEST_CODE": TEST_CODE,
                                     "single_choice_number": single_choice_number,
                                     "multiple_choice_number": multiple_choice_number,
                                     "gap_filling_number": gap_filling_number
                                     })