考试api基本完成

This commit is contained in:
caoqianming 2022-11-07 18:16:48 +08:00
parent d45afb9861
commit 295d5f886d
5 changed files with 418 additions and 37 deletions

View File

@ -0,0 +1,9 @@
from django_filters import rest_framework as filters
from .models import ExamRecord
class ExamRecordFilter(filters.FilterSet):
start_date = filters.DateFilter(field_name="update_time", lookup_expr='gte')
end_date = filters.DateFilter(field_name="update_time", lookup_expr='lte')
class Meta:
model = ExamRecord
fields = ['is_pass', 'type', 'start_date', 'end_date', 'is_submited']

View File

@ -0,0 +1,153 @@
# Generated by Django 3.0.5 on 2022-11-07 05:56
from django.conf import settings
import django.contrib.postgres.fields.jsonb
from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('exam', '0001_initial'),
]
operations = [
migrations.CreateModel(
name='AnswerDetail',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('create_time', models.DateTimeField(default=django.utils.timezone.now, help_text='创建时间', verbose_name='创建时间')),
('update_time', models.DateTimeField(auto_now=True, help_text='修改时间', verbose_name='修改时间')),
('is_deleted', models.BooleanField(default=False, help_text='删除标记', verbose_name='删除标记')),
('user_answer', django.contrib.postgres.fields.jsonb.JSONField(blank=True, null=True)),
('score', models.FloatField(default=0, verbose_name='本题得分')),
('is_right', models.BooleanField(default=False, verbose_name='是否正确')),
],
options={
'verbose_name': '答题记录',
'verbose_name_plural': '答题记录',
},
),
migrations.CreateModel(
name='Exam',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('create_time', models.DateTimeField(default=django.utils.timezone.now, help_text='创建时间', verbose_name='创建时间')),
('update_time', models.DateTimeField(auto_now=True, help_text='修改时间', verbose_name='修改时间')),
('is_deleted', models.BooleanField(default=False, help_text='删除标记', verbose_name='删除标记')),
('code', models.CharField(blank=True, max_length=100, null=True, unique=True, verbose_name='考试编号')),
('name', models.CharField(max_length=100, verbose_name='名称')),
('place', models.CharField(blank=True, max_length=100, null=True, verbose_name='考试地点')),
('open_time', models.DateTimeField(blank=True, null=True, verbose_name='开启时间')),
('close_time', models.DateTimeField(blank=True, null=True, verbose_name='关闭时间')),
('proctor_name', models.CharField(blank=True, max_length=100, null=True, verbose_name='监考人姓名')),
('proctor_phone', models.CharField(blank=True, max_length=100, null=True, verbose_name='监考人联系方式')),
('chance', models.IntegerField(default=3, verbose_name='考试机会')),
('create_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='exam_create_by', to=settings.AUTH_USER_MODEL, verbose_name='创建人')),
],
options={
'abstract': False,
},
),
migrations.CreateModel(
name='Paper',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('create_time', models.DateTimeField(default=django.utils.timezone.now, help_text='创建时间', verbose_name='创建时间')),
('update_time', models.DateTimeField(auto_now=True, help_text='修改时间', verbose_name='修改时间')),
('is_deleted', models.BooleanField(default=False, help_text='删除标记', verbose_name='删除标记')),
('name', models.CharField(max_length=200, verbose_name='名称')),
('limit', models.IntegerField(default=0, verbose_name='限时(分钟)')),
('total_score', models.FloatField(default=0, verbose_name='满分')),
('pass_score', models.FloatField(default=0, verbose_name='通过分数')),
('danxuan_count', models.IntegerField(default=0, verbose_name='单选数量')),
('danxuan_score', models.FloatField(default=2, verbose_name='单选分数')),
('duoxuan_count', models.IntegerField(default=0, verbose_name='多选数量')),
('duoxuan_score', models.FloatField(default=4, verbose_name='多选分数')),
('panduan_count', models.IntegerField(default=0, verbose_name='判断数量')),
('panduan_score', models.FloatField(default=2, verbose_name='判断分数')),
('create_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='paper_create_by', to=settings.AUTH_USER_MODEL, verbose_name='创建人')),
],
options={
'verbose_name': '押题卷',
'verbose_name_plural': '押题卷',
},
),
migrations.CreateModel(
name='PaperQuestion',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('create_time', models.DateTimeField(default=django.utils.timezone.now, help_text='创建时间', verbose_name='创建时间')),
('update_time', models.DateTimeField(auto_now=True, help_text='修改时间', verbose_name='修改时间')),
('is_deleted', models.BooleanField(default=False, help_text='删除标记', verbose_name='删除标记')),
('total_score', models.FloatField(default=0, verbose_name='单题满分')),
('sort', models.PositiveSmallIntegerField(default=1)),
('paper', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='exam.Paper', verbose_name='试卷')),
('question', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='exam.Question', verbose_name='试题')),
],
options={
'abstract': False,
},
),
migrations.AddField(
model_name='paper',
name='questions',
field=models.ManyToManyField(through='exam.PaperQuestion', to='exam.Question'),
),
migrations.AddField(
model_name='paper',
name='update_by',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='paper_update_by', to=settings.AUTH_USER_MODEL, verbose_name='最后编辑人'),
),
migrations.CreateModel(
name='ExamRecord',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('create_time', models.DateTimeField(default=django.utils.timezone.now, help_text='创建时间', verbose_name='创建时间')),
('update_time', models.DateTimeField(auto_now=True, help_text='修改时间', verbose_name='修改时间')),
('is_deleted', models.BooleanField(default=False, help_text='删除标记', verbose_name='删除标记')),
('name', models.CharField(max_length=200, verbose_name='名称')),
('type', models.CharField(choices=[('自助模考', '自助模考'), ('押卷模考', '押卷模考'), ('正式考试', '正式考试')], default='自助模考', max_length=50, verbose_name='考试类型')),
('limit', models.IntegerField(default=0, verbose_name='限时(分钟)')),
('total_score', models.FloatField(default=0, verbose_name='总分')),
('score', models.FloatField(default=0, verbose_name='得分')),
('took', models.IntegerField(default=0, verbose_name='耗时(秒)')),
('start_time', models.DateTimeField(verbose_name='开始答题时间')),
('end_time', models.DateTimeField(verbose_name='结束答题时间')),
('is_pass', models.BooleanField(default=True, verbose_name='是否通过')),
('questions', django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=list, verbose_name='下发的题目列表')),
('create_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='examrecord_create_by', to=settings.AUTH_USER_MODEL, verbose_name='创建人')),
('detail', models.ManyToManyField(through='exam.AnswerDetail', to='exam.Question', verbose_name='答题记录')),
('exam', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='exam.Exam', verbose_name='关联的正式考试')),
('paper', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='exam.Paper', verbose_name='所用试卷')),
('update_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='examrecord_update_by', to=settings.AUTH_USER_MODEL, verbose_name='最后编辑人')),
],
options={
'verbose_name': '考试记录',
'verbose_name_plural': '考试记录',
},
),
migrations.AddField(
model_name='exam',
name='paper',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='exam.Paper', verbose_name='使用的试卷'),
),
migrations.AddField(
model_name='exam',
name='update_by',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='exam_update_by', to=settings.AUTH_USER_MODEL, verbose_name='最后编辑人'),
),
migrations.AddField(
model_name='answerdetail',
name='examrecord',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='exam.ExamRecord'),
),
migrations.AddField(
model_name='answerdetail',
name='question',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='exam.Question'),
),
]

View File

@ -112,7 +112,7 @@ class ExamRecord(CommonAModel):
detail = models.ManyToManyField(Question, verbose_name='答题记录', through='AnswerDetail')
is_pass = models.BooleanField(default=True, verbose_name='是否通过')
exam = models.ForeignKey(Exam, verbose_name='关联的正式考试', null=True, blank=True, on_delete= models.SET_NULL)
questions = JSONField(default=list, verbose_name='下发的题目列表', blank=True)
is_submited = models.BooleanField(default=False)
class Meta:
verbose_name = '考试记录'
@ -122,6 +122,7 @@ class ExamRecord(CommonAModel):
class AnswerDetail(BaseModel):
examrecord = models.ForeignKey(ExamRecord, on_delete=models.CASCADE)
total_score = models.FloatField(default=0, verbose_name='该题满分')
question = models.ForeignKey(Question, on_delete=models.CASCADE)
user_answer = JSONField(null=True,blank=True)
score = models.FloatField(default=0, verbose_name='本题得分')
@ -129,4 +130,5 @@ class AnswerDetail(BaseModel):
class Meta:
verbose_name = '答题记录'
verbose_name_plural = verbose_name
verbose_name_plural = verbose_name

View File

@ -1,7 +1,7 @@
from rest_framework.serializers import ModelSerializer, CharField, Serializer, SerializerMethodField, FloatField
from rest_framework import serializers
from apps.exam.models import Question, Questioncat, Paper, Exam, PaperQuestion
from apps.exam.models import Question, Questioncat, Paper, Exam, PaperQuestion, ExamRecord, AnswerDetail
class QuestioncatSerializer(ModelSerializer):
@ -22,54 +22,162 @@ class PaperSerializer(ModelSerializer):
exclude = ('questions',)
class QuestionReadSerializer(ModelSerializer):
class PaperQuestionSerializer(ModelSerializer):
class Meta:
model = Question
exclude = ['right']
model = PaperQuestion
fields = ['question', 'total_score', 'sort']
class PaperQuestionSerializer(Serializer):
id = CharField(label='题目ID')
total_score = FloatField(label='单题满分')
class PaperCreateUpdateSerializer(ModelSerializer):
questions_ = PaperQuestionSerializer(many=True)
class Meta:
model = Paper
fields = '__all__'
class PaperQuestionDetailSerializer(ModelSerializer):
id = serializers.IntegerField(source='question.id')
name = serializers.ReadOnlyField(source='question.name')
options = serializers.ReadOnlyField(source='question.options')
right = serializers.ReadOnlyField(source='question.right')
type = serializers.ReadOnlyField(source='question.type')
img = serializers.ReadOnlyField(source='question.img')
questioncat_name = serializers.ReadOnlyField(source='question.questioncat.name')
questioncat_name = serializers.ReadOnlyField(
source='question.questioncat.name')
level = serializers.ReadOnlyField(source='question.level')
class Meta:
model = PaperQuestion
fields = ('id','name','options','right','type','level','total_score','questioncat_name', 'img')
fields = ('id', 'name', 'options', 'right', 'type', 'level',
'total_score', 'questioncat_name', 'img', 'question')
class PaperQuestionShortSerializer(ModelSerializer):
class Meta:
model = PaperQuestion
fields = '__all__'
class PaperDetailSerializer(ModelSerializer):
questions_ = SerializerMethodField()
class Meta:
model = Paper
fields = '__all__'
def get_questions_(self, instance):
pqs = PaperQuestion.objects.filter(paper=instance)
return PaperQuestionDetailSerializer(pqs, many=True).data
class ExamCreateUpdateSerializer(ModelSerializer):
class Meta:
model = Exam
fields = ['name', 'place', 'open_time', 'close_time', 'proctor_name', 'proctor_phone']
fields = ['name', 'place', 'open_time',
'close_time', 'proctor_name', 'proctor_phone']
class ExamListSerializer(ModelSerializer):
create_by_name = CharField(source='create_by.name', read_only=True)
paper_ = PaperSerializer(source='paper', read_only=True)
class Meta:
model = Exam
fields = '__all__'
class ExamDetailSerializer(ModelSerializer):
create_by_name = CharField(source='create_by.name', read_only=True)
paper_ = PaperSerializer(source='paper', read_only=True)
class Meta:
model = Exam
fields = '__all__'
class ExamAttendSerializer(Serializer):
code = CharField(label="考试编号")
code = CharField(label="考试编号")
class ExamRecordListSerializer(serializers.ModelSerializer):
"""
考试列表序列化
"""
took_format = serializers.SerializerMethodField()
create_by_name = serializers.CharField(
source='create_by.name', read_only=True)
class Meta:
model = ExamRecord
exclude = ('detail',)
def get_took_format(self, obj):
m, s = divmod(obj.took, 60)
h, m = divmod(m, 60)
return "%02d:%02d:%02d" % (h, m, s)
class ExamRecordDetailSerializer(serializers.ModelSerializer):
"""
考试详情序列化
"""
took_format = serializers.SerializerMethodField()
questions_ = serializers.SerializerMethodField()
class Meta:
model = ExamRecord
exclude = ('detail',)
def get_took_format(self, obj):
m, s = divmod(obj.took, 60)
h, m = divmod(m, 60)
return "%02d:%02d:%02d" % (h, m, s)
def get_questions(self, obj):
objs = AnswerDetail.objects.select_related('question').filter(
examrecord=obj).order_by('question__type')
return AnswerDetailSerializer(instance=objs, many=True).data
class AnswerDetailUpdateSerializer(serializers.Serializer):
id = serializers.CharField(label='下发ID')
user_answer = serializers.JSONField(label='作答')
class ExamRecordSubmitSerializer(serializers.ModelSerializer):
questions_ = AnswerDetailUpdateSerializer(many=True)
class Meta:
model = ExamRecord
fields = ['questions_']
class AnswerDetailSerializer(ModelSerializer):
name = serializers.ReadOnlyField(source='question.name')
options = serializers.ReadOnlyField(source='question.options')
right = serializers.ReadOnlyField(source='question.right')
type = serializers.ReadOnlyField(source='question.type')
img = serializers.ReadOnlyField(source='question.img')
questioncat_name = serializers.ReadOnlyField(
source='question.questioncat.name')
level = serializers.ReadOnlyField(source='question.level')
class Meta:
model = PaperQuestion
fields = ['id', 'question', 'name', 'options', 'right', 'type', 'level',
'total_score', 'questioncat_name', 'img', 'user_answer', 'score', 'is_right']
class AnswerDetailOutSerializer(ModelSerializer):
name = serializers.ReadOnlyField(source='question.name')
options = serializers.ReadOnlyField(source='question.options')
type = serializers.ReadOnlyField(source='question.type')
img = serializers.ReadOnlyField(source='question.img')
questioncat_name = serializers.ReadOnlyField(
source='question.questioncat.name')
level = serializers.ReadOnlyField(source='question.level')
class Meta:
model = PaperQuestion
fields = ['id', 'question', 'name', 'options', 'type', 'level',
'total_score', 'questioncat_name', 'img', 'user_answer', 'score', 'is_right']

View File

@ -2,8 +2,8 @@ from django.shortcuts import render
from rest_framework.viewsets import ModelViewSet
from apps.exam.exports import export_question
from apps.exam.models import Question, Questioncat, PaperQuestion
from apps.exam.serializers import (QuestionSerializer, QuestioncatSerializer, PaperSerializer,
ExamCreateUpdateSerializer, ExamListSerializer, ExamAttendSerializer, PaperDetailSerializer, PaperCreateUpdateSerializer)
from apps.exam.serializers import (QuestionSerializer, QuestioncatSerializer, PaperSerializer, ExamDetailSerializer, ExamRecordDetailSerializer, ExamListSerializer,
ExamCreateUpdateSerializer, ExamListSerializer, ExamRecordSubmitSerializer, PaperDetailSerializer, PaperCreateUpdateSerializer, AnswerDetailOutSerializer, ExamRecordListSerializer)
from rest_framework.decorators import action
from rest_framework.response import Response
from rest_framework.permissions import IsAuthenticated
@ -13,6 +13,10 @@ from django.conf import settings
from apps.exam.models import Paper, Exam, ExamRecord, AnswerDetail
from django.utils import timezone
from django.db import transaction
from rest_framework.serializers import Serializer
from datetime import datetime
from apps.exam.filters import ExamRecordFilter
from datetime import timedelta
# Create your views here.
@ -221,24 +225,37 @@ class PaperViewSet(ModelViewSet):
paper = Paper.objects.create(**vdata)
q_list = []
for i in questions_:
question = Question.objects.get(id=i['id'])
q_list.append(PaperQuestion(question=question, total_score=i['total_score'], paper=paper))
q_list.append(PaperQuestion(question=i['question'], total_score=i['total_score'], paper=paper))
PaperQuestion.objects.bulk_create(q_list)
return Response(status=201)
def update(self, request, *args, **kwargs):
return super().update(request, *args, **kwargs)
# 有考试在执行,不可更新
paper = self.get_object()
sr = PaperCreateUpdateSerializer(instance=paper, data=request.data)
sr.is_valid(raise_exception=True)
vdata = sr.validated_data
questions_ = vdata.pop('questions_')
Paper.objects.filter(id=paper.id).update(**vdata)
q_list = []
for i in questions_:
q_list.append(PaperQuestion(question=i['question'], total_score=i['total_score'], paper=paper))
PaperQuestion.objects.filter(paper=paper).delete()
PaperQuestion.objects.bulk_create(q_list)
return Response()
class ExamViewSet(ModelViewSet):
perms_map = {'*': '*'}
queryset = Exam.objects.all()
queryset = Exam.objects.all().select_related('paper', 'create_by')
ordering = ['-id']
search_fields = ('name',)
def get_serializer_class(self):
if self.action in ['create', 'update']:
return ExamCreateUpdateSerializer
elif self.action in ['retrieve']:
return ExamDetailSerializer
return ExamListSerializer
def destroy(self, request, *args, **kwargs):
@ -248,24 +265,116 @@ class ExamViewSet(ModelViewSet):
instance.delete()
return Response(status=204)
@action(methods=['post'], detail=False, perms_map=[{'post': '*'}], serializer_class=ExamAttendSerializer)
def attend(self, request, *args, **kwargs):
@action(methods=['post'], detail=True, perms_map=[{'post': '*'}], serializer_class=Serializer, permission_classes = [IsAuthenticated])
def start(self, request, *args, **kwargs):
"""
参加考试
开始考试
参加考试
开始考试返回时间和题目信息
"""
sr = ExamAttendSerializer(data=request.data)
sr.is_valid(raise_exception=True)
vdata = sr.validated_data
code = vdata['code']
exam = self.get_object()
now = timezone.now()
exam = Exam.objects.filter(
code=code, open_time__lt=now, close_time__gt=now).first()
if exam:
tests = ExamRecord.objects.filter(
if now < exam.open_time or now > exam.close_time:
raise ParseError('不在考试时间范围')
tests = ExamRecord.objects.filter(
exam=exam, create_by=request.user)
if tests.count() < exam.chance: # 还有考试机会就可以接着考
return Response(ExamListSerializer(instance=exam).data)
chance_used = tests.count()
if chance_used > exam.chance:
raise ParseError('考试机会已用完')
raise ParseError('有效考试不存在')
if exam.paper:
er = ExamRecord()
er.type = '正式考试'
er.name = '正式考试' + now.strftime('%Y%m%d%H%M')
er.limit = exam.paper.limit
er.paper = exam.paper
er.total_score = exam.paper.total_score
er.start_time = now
er.is_pass = False
er.exam = exam
er.save()
ret = {}
ret['examrecord'] = er.id
pqs = PaperQuestion.objects.filter(paper=exam.paper).order_by('id')
details = []
for i in pqs:
details.append(AnswerDetail(examrecord=er, question=i.question, total_score=i.total_score))
AnswerDetail.objects.bulk_create(details)
ads = AnswerDetail.objects.select_related('question').filter(examrecord=er).order_by('id')
ret['questions_'] = AnswerDetailOutSerializer(instance=ads, many=True).data
return Response(ret)
raise ParseError('暂不支持')
class ExamRecordViewSet(ModelViewSet):
"""
考试记录列表和详情
"""
perms_map = {'get': '*', 'post': '*', 'delete':'examrecord_delete'}
queryset = ExamRecord.objects.select_related('create_by')
serializer_class = ExamRecordListSerializer
ordering_fields = ['create_time', 'score', 'took', 'update_time']
ordering = ['-update_time']
search_fields = ('create_by__name', 'create_by__username')
filterset_class = ExamRecordFilter
def get_serializer_class(self):
if self.action == 'retrieve':
return ExamRecordDetailSerializer
return ExamRecordListSerializer
@action(methods=['get'], detail=False, permission_classes=[IsAuthenticated])
def self(self, request, pk=None):
'''
个人考试记录
'''
queryset = ExamRecord.objects.filter(create_by=request.user).order_by('-update_time')
page = self.paginate_queryset(queryset)
serializer = self.get_serializer(page, many=True)
return self.get_paginated_response(serializer.data)
@action(methods=['post'], detail=True, perms_map=[{'post': '*'}], serializer_class=Serializer, permission_classes = [IsAuthenticated])
def submit(self, request, pk=None):
'''
提交答卷
提交答卷
'''
er = self.get_object()
now = timezone.now()
if er.create_by != request.user:
raise ParseError('提交人有误')
exam = er.exam
if not exam:
raise ParseError('暂不支持')
if now > exam.close_time + timedelta(minutes=30):
raise ParseError('考试时间已过, 提交失败')
serializer = ExamRecordSubmitSerializer(data = request.data)
serializer.is_valid(raise_exception=True)
vdata = serializer.validated_data
questions_ = vdata['questions_']
# 后端判卷
ads = AnswerDetail.objects.select_related('question').filter(examrecord=er).order_by('id').values('id', 'question__right', 'total_score', 'question__type')
total_score = 0
try:
for index, ad in enumerate(ads):
ad.user_answer = questions_[index]['user_answer']
if ad.question__type == '多选':
if set(ad.question__right) == set(ad.user_answer):
ad.is_right = True
ad.score = ad.total_score
else:
if ad.question__right == ad.user_answer:
ad.is_right = True
ad.score = ad.total_score
ad.save()
total_score = total_score + ad.score
except Exception as e:
raise ParseError('判卷失败, 请检查试卷:' + str(e))
er.score = total_score
if er.score > 0.6*er.total_score:
er.is_pass = True
er.took = (now - er.create_time).total_seconds()
er.end_time = now
er.is_submited = True
er.save()
return Response()