feat: 添加edu模块
This commit is contained in:
parent
0fcecf9a64
commit
49f3341a01
|
@ -3,4 +3,4 @@ from django.apps import AppConfig
|
|||
|
||||
class EduConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'edu'
|
||||
name = 'apps.edu'
|
||||
|
|
|
@ -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
|
|
@ -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'),
|
||||
),
|
||||
]
|
|
@ -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
|
|
@ -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']
|
|
@ -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)),
|
||||
]
|
|
@ -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)
|
|
@ -76,7 +76,8 @@ INSTALLED_APPS = [
|
|||
'apps.sam',
|
||||
'apps.pum',
|
||||
'apps.pm',
|
||||
'apps.enp'
|
||||
'apps.enp',
|
||||
'apps.edu'
|
||||
]
|
||||
|
||||
MIDDLEWARE = [
|
||||
|
|
|
@ -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")),
|
||||
|
|
Loading…
Reference in New Issue