feat: 添加edu模块

This commit is contained in:
caoqianming 2024-05-31 15:42:15 +08:00
parent 0fcecf9a64
commit 49f3341a01
9 changed files with 709 additions and 3 deletions

View File

@ -3,4 +3,4 @@ from django.apps import AppConfig
class EduConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'edu'
name = 'apps.edu'

47
apps/edu/filters.py Normal file
View File

@ -0,0 +1,47 @@
from django_filters import rest_framework as filters
from django.utils import timezone
from apps.system.models import User
from .models import Exam, ExamRecord
class ExamFilter(filters.FilterSet):
can_attend = filters.BooleanFilter(method='filter_can_attend')
is_my = filters.BooleanFilter(method='filter_is_my')
class Meta:
model = Exam
fields = {
'close_time': ['gte', 'lte'],
'paper': ['exact'],
'is_public': ['exact'],
}
def filter_can_attend(self, queryset, name, value):
if value:
now = timezone.now()
return queryset.filter(open_time__lte=now, close_time__gte=now)| queryset.filter(close_time__isnull=True)
return queryset
def filter_is_my(self, queryset, name, value):
if value:
user:User = self.request.user
dept = user.belong_dept
qs = queryset.filter(is_public=True)
qs = qs|qs.filter(p_users=user)
if dept:
qs = qs|qs.filter(p_depts=dept)
return qs
return queryset
class ExamRecordFilter(filters.FilterSet):
is_my = filters.BooleanFilter(method='filter_is_my')
class Meta:
model = ExamRecord
fields = {
'start_time': ['exact', 'gte', 'lte'],
'is_pass': ['exact'],
'is_submited': ['exact'],
}
def filter_is_my(self, queryset, name, value):
if value:
user = self.request.user
return queryset.filter(create_by=user)
return queryset

View File

@ -0,0 +1,194 @@
# Generated by Django 3.2.12 on 2024-05-31 07:26
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone
class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('system', '0003_alter_permission_parent'),
]
operations = [
migrations.CreateModel(
name='AnswerDetail',
fields=[
('id', models.CharField(editable=False, help_text='主键ID', max_length=20, 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='该题满分')),
('user_answer', models.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.CharField(editable=False, help_text='主键ID', max_length=20, 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=100, verbose_name='名称')),
('open_time', models.DateTimeField(verbose_name='开启时间')),
('close_time', models.DateTimeField(blank=True, null=True, verbose_name='关闭时间')),
('chance', models.IntegerField(default=1, verbose_name='考试机会')),
('is_public', models.BooleanField(default=False, verbose_name='是否公开')),
('paper_json', models.JSONField(blank=True, null=True, verbose_name='试卷JSON')),
('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='创建人')),
('p_depts', models.ManyToManyField(blank=True, to='system.Dept', verbose_name='指定部门')),
('p_users', models.ManyToManyField(blank=True, to=settings.AUTH_USER_MODEL, verbose_name='指定用户')),
],
options={
'verbose_name': '在线考试',
'verbose_name_plural': '在线考试',
},
),
migrations.CreateModel(
name='Paper',
fields=[
('id', models.CharField(editable=False, help_text='主键ID', max_length=20, 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='Questioncat',
fields=[
('id', models.CharField(editable=False, help_text='主键ID', max_length=20, 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='名称')),
('sort', models.PositiveIntegerField(default=0, verbose_name='排序')),
('create_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='questioncat_create_by', to=settings.AUTH_USER_MODEL, verbose_name='创建人')),
('parent', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='cate_parent', to='edu.questioncat', verbose_name='父级')),
('update_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='questioncat_update_by', to=settings.AUTH_USER_MODEL, verbose_name='最后编辑人')),
],
options={
'verbose_name': '题库分类',
'verbose_name_plural': '题库分类',
},
),
migrations.CreateModel(
name='Question',
fields=[
('id', models.CharField(editable=False, help_text='主键ID', max_length=20, 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.TextField(verbose_name='题干')),
('img', models.TextField(blank=True, null=True, verbose_name='题干图片')),
('type', models.PositiveSmallIntegerField(choices=[(10, '单选'), (20, '多选'), (30, '判断')], verbose_name='题型')),
('level', models.PositiveSmallIntegerField(choices=[(10, '简单'), (20, '一般'), (30, '困难')], default=20, verbose_name='难度')),
('options', models.JSONField(verbose_name='选项')),
('right', models.JSONField(verbose_name='正确答案')),
('resolution', models.TextField(blank=True, null=True, verbose_name='解析')),
('enabled', models.BooleanField(default=False, verbose_name='是否启用')),
('create_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='question_create_by', to=settings.AUTH_USER_MODEL, verbose_name='创建人')),
('questioncat', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='questioncat', to='edu.questioncat', verbose_name='所属题库')),
('update_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='question_update_by', to=settings.AUTH_USER_MODEL, verbose_name='最后编辑人')),
],
options={
'verbose_name': '题目',
'verbose_name_plural': '题目',
},
),
migrations.CreateModel(
name='PaperQuestion',
fields=[
('id', models.CharField(editable=False, help_text='主键ID', max_length=20, 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.IntegerField(default=0, verbose_name='排序')),
('paper', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='edu.paper', verbose_name='试卷')),
('question', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='edu.question', verbose_name='试题')),
],
options={
'abstract': False,
},
),
migrations.AddField(
model_name='paper',
name='questions',
field=models.ManyToManyField(through='edu.PaperQuestion', to='edu.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.CharField(editable=False, help_text='主键ID', max_length=20, 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='删除标记')),
('score', models.FloatField(default=0, verbose_name='得分')),
('took', models.IntegerField(default=0, verbose_name='耗时(秒)')),
('start_time', models.DateTimeField(verbose_name='开始答题时间')),
('end_time', models.DateTimeField(blank=True, null=True, verbose_name='结束答题时间')),
('is_pass', models.BooleanField(default=True, verbose_name='是否通过')),
('is_submited', models.BooleanField(default=False, verbose_name='是否提交')),
('is_last', models.BooleanField(default=False, 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='创建人')),
('exam', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='record_exam', to='edu.exam', verbose_name='关联考试')),
('questions', models.ManyToManyField(through='edu.AnswerDetail', to='edu.Question', 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.SET_NULL, to='edu.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, related_name='detail_er', to='edu.examrecord'),
),
migrations.AddField(
model_name='answerdetail',
name='question',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='edu.question'),
),
]

View File

@ -1,3 +1,115 @@
from django.db import models
from apps.utils.models import CommonAModel, BaseModel, CommonADModel
from apps.system.models import User, Dept
# Create your models here.
class Questioncat(CommonAModel):
name = models.CharField(max_length=200, verbose_name='名称')
parent = models.ForeignKey('self', on_delete=models.SET_NULL, null=True, blank=True, verbose_name='父级', related_name='cate_parent')
sort = models.PositiveIntegerField(default=0, verbose_name='排序')
class Meta:
verbose_name = '题库分类'
verbose_name_plural = verbose_name
def __str__(self):
return self.name
class Question(CommonAModel):
Q_DAN = 10
Q_DUO = 20
Q_PAN = 30
Q_EASY = 10
Q_SIMPLE = 20
Q_HARD = 30
name = models.TextField('题干')
img = models.TextField('题干图片', null=True, blank=True)
type = models.PositiveSmallIntegerField('题型', choices=((Q_DAN, '单选'), (Q_DUO, '多选'), (Q_PAN, '判断')))
level = models.PositiveSmallIntegerField('难度', default=Q_SIMPLE, choices=((Q_EASY, '简单'), (Q_SIMPLE, '一般'), (Q_HARD, '困难')))
questioncat = models.ForeignKey(Questioncat, blank=True, null=True, on_delete=models.SET_NULL, verbose_name='所属题库', related_name='questioncat')
options = models.JSONField('选项')
right = models.JSONField('正确答案')
resolution = models.TextField('解析', null=True, blank=True)
enabled = models.BooleanField('是否启用', default=False)
class Meta:
verbose_name = '题目'
verbose_name_plural = verbose_name
def __str__(self):
return self.name
class Paper(CommonAModel):
name = models.CharField(max_length=200, verbose_name='名称')
questions = models.ManyToManyField(Question, through='PaperQuestion')
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='判断分数')
class Meta:
verbose_name = '押题卷'
verbose_name_plural = verbose_name
def __str__(self):
return self.name
class PaperQuestion(BaseModel):
paper = models.ForeignKey(Paper, on_delete=models.CASCADE, verbose_name='试卷')
question = models.ForeignKey(Question, on_delete=models.CASCADE, verbose_name='试题')
total_score = models.FloatField(default=0, verbose_name='单题满分')
sort = models.IntegerField(default=0, verbose_name='排序')
class Exam(CommonADModel):
name = models.CharField('名称', max_length=100)
open_time = models.DateTimeField('开启时间')
close_time = models.DateTimeField('关闭时间', null=True, blank=True)
chance = models.IntegerField('考试机会', default=1) # 0表示不限次数
paper = models.ForeignKey(Paper, verbose_name='使用的试卷', on_delete=models.SET_NULL, null=True, blank=True)
is_public = models.BooleanField('是否公开', default=False)
p_users = models.ManyToManyField(User, verbose_name='指定用户', blank=True)
p_depts = models.ManyToManyField(Dept, verbose_name='指定部门', blank=True)
paper_json = models.JSONField('试卷JSON', null=True, blank=True)
class Meta:
verbose_name = '在线考试'
verbose_name_plural = verbose_name
def __str__(self):
return self.name
class ExamRecord(CommonADModel):
'''
考试记录表
'''
exam = models.ForeignKey(Exam, on_delete=models.CASCADE, verbose_name='关联考试', related_name='record_exam')
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='结束答题时间', null=True, blank=True)
questions = models.ManyToManyField(Question, verbose_name='答题记录', through='AnswerDetail')
is_pass = models.BooleanField(default=True, verbose_name='是否通过')
is_submited = models.BooleanField(default=False, verbose_name='是否提交')
is_last = models.BooleanField(default=False, verbose_name='是否最后一次考试记录')
class Meta:
verbose_name = '考试记录'
verbose_name_plural = verbose_name
class AnswerDetail(BaseModel):
examrecord = models.ForeignKey(ExamRecord, on_delete=models.CASCADE, related_name='detail_er')
total_score = models.FloatField(default=0, verbose_name='该题满分')
question = models.ForeignKey(Question, on_delete=models.CASCADE)
user_answer = models.JSONField(null=True,blank=True)
score = models.FloatField(default=0, verbose_name='本题得分')
is_right = models.BooleanField(default=False, verbose_name='是否正确')
class Meta:
verbose_name = '答题记录'
verbose_name_plural = verbose_name

161
apps/edu/serializers.py Normal file
View File

@ -0,0 +1,161 @@
from apps.utils.serializers import CustomModelSerializer
from rest_framework import serializers
from .models import Questioncat, Question, Paper, PaperQuestion, Exam, ExamRecord, AnswerDetail
from apps.utils.constants import EXCLUDE_FIELDS, EXCLUDE_FIELDS_BASE
from django.db import transaction
class QuestioncatSerializer(CustomModelSerializer):
class Meta:
model = Questioncat
fields = '__all__'
class QuestionSerializer(CustomModelSerializer):
class Meta:
model = Question
fields = '__all__'
class PaperQuestionSerializer(CustomModelSerializer):
class Meta:
model = PaperQuestion
fields = '__all__'
read_only_fields = EXCLUDE_FIELDS_BASE
class PaperListSerializer(CustomModelSerializer):
class Meta:
model = Paper
exclude = ["questions"]
class PaperPatchSerializer(CustomModelSerializer):
class Meta:
model = Paper
fields = ["name"]
class PaperSerializer(CustomModelSerializer):
detail = PaperQuestionSerializer(many=True)
class Meta:
model = Paper
exclude = ["questions"]
read_only_fields = ["editable"] + EXCLUDE_FIELDS
def create(self, validated_data):
detail = validated_data.pop("detail", [])
with transaction.atomic():
paper = super().create(validated_data)
qs = [PaperQuestion(paper=paper, question=item["question"], total_score=item["total_score"], sort=item["sort"]) for item in detail]
PaperQuestion.objects.bulk_create(qs)
return paper
def update(self, instance, validated_data):
detail = validated_data.pop("detail", [])
with transaction.atomic():
paper = super().update(instance, validated_data)
# 删除未有的数据
question_ids = [item["question"].id for item in detail]
PaperQuestion.objects.filter(paper=paper).exclude(question__id__in=question_ids).delete()
# 更新新数据
for item in detail:
PaperQuestion.objects.update_or_create(
paper=instance,
question=item["question"],
defaults={
"total_score": item["total_score"],
"sort": item["sort"]
}
)
return paper
def to_representation(self, instance):
representation = super().to_representation(instance)
detail_data = representation['detail']
sorted_detail_data = sorted(detail_data, key=lambda x: (x['sort'], x['create_time']))
representation['detail'] = sorted_detail_data
return sorted_detail_data
class ExamSerializer(CustomModelSerializer):
create_by_name = serializers.CharField(source='create_by.name', read_only=True)
paper = serializers.PrimaryKeyRelatedField(queryset=Paper.objects.all(), label='有考试记录,编辑时忽略')
class Meta:
model = Exam
fields = "__all__"
def update(self, instance, validated_data):
if ExamRecord.objects.filter(exam=instance).exists():
validated_data.pop("paper", None)
return super().update(instance, validated_data)
class AnswerDetailOutSerializer(CustomModelSerializer):
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 = AnswerDetail
fields = ['id', 'question', 'name', 'options', 'type', 'level',
'total_score', 'questioncat_name', 'img', 'user_answer', 'score', 'is_right']
class AnswerDetailSerializer(AnswerDetailOutSerializer):
right = serializers.ReadOnlyField(source='question.right')
class Meta:
model = AnswerDetail
fields = ['id', 'question', 'name', 'options', 'type', 'level', 'right',
'total_score', 'questioncat_name', 'img', 'user_answer', 'score', 'is_right']
class AnswerDetailUpdateSerializer(serializers.Serializer):
id = serializers.CharField(label='下发ID')
user_answer = serializers.JSONField(label='作答')
class ExamRecordInitSerizlier(CustomModelSerializer):
detail = serializers.SerializerMethodField()
class Meta:
model = ExamRecord
fields = ["id", "detail"]
def get_detail(self, obj):
objs = AnswerDetail.objects.select_related('question').filter(
examrecord=obj).order_by('id')
return AnswerDetailOutSerializer(instance=objs, many=True).data
class TookSerializerMixin:
took_format = serializers.SerializerMethodField()
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 ExamRecordSerializer(CustomModelSerializer, TookSerializerMixin):
create_by_name = serializers.CharField(source='create_by.name', read_only=True)
class Meta:
model = ExamRecord
exclude = ["questions"]
class ExamRecordDetailSerializer(ExamRecordSerializer, TookSerializerMixin):
detail = serializers.SerializerMethodField()
class Meta:
model = ExamRecord
fields = "__all__"
def get_detail(self, obj):
objs = AnswerDetail.objects.select_related('question').filter(
examrecord=obj).order_by('id')
return AnswerDetailSerializer(instance=objs, many=True).data
class ExamRecordSubmitSerializer(serializers.ModelSerializer):
detail = AnswerDetailUpdateSerializer(many=True)
class Meta:
model = ExamRecord
fields = ['detail']

16
apps/edu/urls.py Normal file
View File

@ -0,0 +1,16 @@
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from apps.edu.views import QuestioncatViewSet, QuestionViewSet, PaperViewSet, ExamViewSet, ExamRecordViewSet
API_BASE_URL = 'api/edu/'
HTML_BASE_URL = 'edu/'
router = DefaultRouter()
router.register('questioncat', QuestioncatViewSet, basename='questioncat')
router.register('question', QuestionViewSet, basename='question')
router.register('paper', PaperViewSet, basename='paper')
router.register('exam', ExamViewSet, basename='exam')
router.register('examrecord', ExamRecordViewSet, basename='examrecord')
urlpatterns = [
path(API_BASE_URL, include(router.urls)),
]

View File

@ -1,3 +1,177 @@
from django.shortcuts import render
from apps.utils.viewsets import CustomModelViewSet, CustomGenericViewSet
from apps.utils.mixins import ListModelMixin, DestroyModelMixin
from rest_framework.mixins import RetrieveModelMixin
from rest_framework.exceptions import ParseError
from rest_framework.decorators import action
from rest_framework.serializers import Serializer
from django.db import transaction
from .models import Questioncat, Question, Paper, PaperQuestion, Exam, ExamRecord, AnswerDetail
from .serializers import (QuestioncatSerializer, QuestionSerializer, ExamSerializer,
ExamRecordInitSerizlier, ExamRecordSerializer, ExamRecordDetailSerializer, ExamRecordSubmitSerializer,
PaperSerializer, PaperListSerializer, PaperPatchSerializer)
from django.utils import timezone
from rest_framework.response import Response
from rest_framework.permissions import IsAuthenticated
from drf_yasg.utils import swagger_auto_schema
from apps.utils.permission import has_perm
from .filters import ExamFilter, ExamRecordFilter
from apps.system.models import User
# Create your views here.
class QuestioncatViewSet(CustomModelViewSet):
queryset = Questioncat.objects.all()
serializer_class = QuestioncatSerializer
class QuestionViewSet(CustomModelViewSet):
queryset = Question.objects.all()
serializer_class = QuestionSerializer
filterset_fields = ["questioncat", "type", "enabled"]
def update(self, request, *args, **kwargs):
obj: Question = self.get_object()
if AnswerDetail.objects.filter(question=obj).exists():
raise ParseError("存在答题,该题目不可编辑")
return super().update(request, *args, **kwargs)
class PaperViewSet(CustomModelViewSet):
queryset = Paper.objects.all()
serializer_class = PaperSerializer
list_serializer_class = PaperListSerializer
partial_update_serializer_class = PaperPatchSerializer
def update(self, request, *args, **kwargs):
obj: Paper = self.get_object()
if Exam.objects.filter(paper=obj).exists():
raise ParseError("存在考试,该试卷不可编辑")
return super().update(request, *args, **kwargs)
class ExamViewSet(CustomModelViewSet):
queryset = Exam.objects.all()
serializer_class = ExamSerializer
filterset_class = ExamFilter
def get_queryset(self):
qs = super().get_queryset()
if has_perm(self.request.user, 'exam.view'):
return qs
user:User = self.request.user
dept = user.belong_dept
qs = qs.filter(is_public=True)
qs = qs|qs.filter(p_users=user)
if dept:
qs = qs|qs.filter(p_depts=dept)
return qs
def destroy(self, request, *args, **kwargs):
instance = self.get_object()
if ExamRecord.objects.filter(exam=instance).exists():
raise ParseError('存在考试记录,禁止删除')
return super().destroy(request, *args, **kwargs)
@swagger_auto_schema(request_body=Serializer, responses={200: ExamRecordInitSerizlier})
@action(methods=['post'], detail=True, perms_map=[{'post': '*'}], serializer_class=Serializer)
def attend(self, request, *args, **kwargs):
"""
参加考试
返回考试具体题目信息
"""
exam: Exam = self.get_object()
now = timezone.now()
if now < exam.open_time or now > exam.close_time:
raise ParseError('不在考试时间范围')
tests = ExamRecord.objects.filter(
exam=exam, create_by=request.user)
chance_used = tests.count()
if chance_used > exam.chance:
raise ParseError('考试机会已用完')
if exam.paper:
with transaction.atomic():
tests.update(is_last=False)
er = ExamRecord()
er.start_time = now
er.is_pass = False
er.is_submited = False
er.exam = exam
er.create_by = request.user
er.is_last = True
er.save()
pqs = PaperQuestion.objects.filter(paper=exam.paper).order_by('sort', 'id')
details = []
for i in pqs:
details.append(AnswerDetail(examrecord=er, question=i.question, total_score=i.total_score))
AnswerDetail.objects.bulk_create(details)
sr = ExamRecordInitSerizlier(er)
res_data = sr.data
res_data.update({"chance_used": chance_used})
return Response(sr.data, status=201)
raise ParseError('暂不支持')
class ExamRecordViewSet(ListModelMixin, DestroyModelMixin, RetrieveModelMixin, CustomGenericViewSet):
"""
考试记录
"""
perms_map = {"get": "*", "delete": "examrecord.delete"}
queryset = ExamRecord.objects.all()
list_serializer_class = ExamRecordSerializer
retrieve_serializer_class = ExamRecordDetailSerializer
search_fields = ('create_by__name', 'create_by__username', 'exam__name')
filterset_class = ExamRecordFilter
def get_queryset(self):
qs = super().get_queryset()
if has_perm(self.request.user, "examrecord.view"):
return qs
return qs.filter(create_by=self.request.user)
@swagger_auto_schema(request_body=ExamRecordSubmitSerializer, responses={200: ExamRecordSerializer})
@action(methods=['post'], detail=True, perms_map=[{'post': '*'}], serializer_class=ExamRecordSubmitSerializer, permission_classes = [IsAuthenticated])
@transaction.atomic
def submit(self, request, pk=None):
'''
提交答卷
提交答卷
'''
er: ExamRecord = self.get_object()
now = timezone.now()
if er.is_submited:
raise ParseError('该考试记录已提交')
exam:Exam = er.exam
if not exam:
raise ParseError('暂不支持')
took = (now - er.start_time).total_seconds()
if took > exam.paper.limit * 60:
raise ParseError('答题时间超时,提交失败')
serializer = ExamRecordSubmitSerializer(data = request.data)
serializer.is_valid(raise_exception=True)
vdata = serializer.validated_data
questions_ = vdata['detail_']
# 后端判卷
ads = AnswerDetail.objects.select_related('question').filter(examrecord=er).order_by('id')
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
er.took = took
er.end_time = now
er.is_submited = True
er.save()
return Response(ExamRecordSerializer(er).data)

View File

@ -76,7 +76,8 @@ INSTALLED_APPS = [
'apps.sam',
'apps.pum',
'apps.pm',
'apps.enp'
'apps.enp',
'apps.edu'
]
MIDDLEWARE = [

View File

@ -66,6 +66,7 @@ urlpatterns = [
path('', include('apps.pum.urls')),
path('', include('apps.pm.urls')),
path('', include('apps.enp.urls')),
path('', include('apps.edu.urls')),
# 前端页面入口
path('', TemplateView.as_view(template_name="index.html")),