feat: 添加edu模块
This commit is contained in:
parent
0fcecf9a64
commit
49f3341a01
|
@ -3,4 +3,4 @@ from django.apps import AppConfig
|
||||||
|
|
||||||
class EduConfig(AppConfig):
|
class EduConfig(AppConfig):
|
||||||
default_auto_field = 'django.db.models.BigAutoField'
|
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 django.db import models
|
||||||
|
from apps.utils.models import CommonAModel, BaseModel, CommonADModel
|
||||||
|
from apps.system.models import User, Dept
|
||||||
|
|
||||||
# Create your models here.
|
# 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.
|
# 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.sam',
|
||||||
'apps.pum',
|
'apps.pum',
|
||||||
'apps.pm',
|
'apps.pm',
|
||||||
'apps.enp'
|
'apps.enp',
|
||||||
|
'apps.edu'
|
||||||
]
|
]
|
||||||
|
|
||||||
MIDDLEWARE = [
|
MIDDLEWARE = [
|
||||||
|
|
|
@ -66,6 +66,7 @@ urlpatterns = [
|
||||||
path('', include('apps.pum.urls')),
|
path('', include('apps.pum.urls')),
|
||||||
path('', include('apps.pm.urls')),
|
path('', include('apps.pm.urls')),
|
||||||
path('', include('apps.enp.urls')),
|
path('', include('apps.enp.urls')),
|
||||||
|
path('', include('apps.edu.urls')),
|
||||||
|
|
||||||
# 前端页面入口
|
# 前端页面入口
|
||||||
path('', TemplateView.as_view(template_name="index.html")),
|
path('', TemplateView.as_view(template_name="index.html")),
|
||||||
|
|
Loading…
Reference in New Issue