diff --git a/client/.env.development b/client/.env.development
index aacde3e..b2cc6c5 100644
--- a/client/.env.development
+++ b/client/.env.development
@@ -3,8 +3,8 @@ ENV = 'development'
# base api
#VUE_APP_BASE_API = 'http://10.0.11.127:8000/api'
-#VUE_APP_BASE_API = 'http://127.0.0.1:8000/api'
-VUE_APP_BASE_API = 'https://testsearch.ctc.ac.cn/api'
+VUE_APP_BASE_API = 'http://127.0.0.1:2222/api'
+#VUE_APP_BASE_API = 'https://testsearch.ctc.ac.cn/api'
#VUE_APP_BASE_API = 'http://47.95.0.242:9101/api'
diff --git a/client_mp/App.vue b/client_mp/App.vue
index 051fa44..0453561 100644
--- a/client_mp/App.vue
+++ b/client_mp/App.vue
@@ -60,4 +60,5 @@
diff --git a/client_mp/common/http.api.js b/client_mp/common/http.api.js
index 90377a5..4904415 100644
--- a/client_mp/common/http.api.js
+++ b/client_mp/common/http.api.js
@@ -44,6 +44,10 @@ const install = (Vue, vm) => {
let getVideos = (params={})=>vm.$u.get('/vod/video/', params);//点播视频
let getDickey = (params={})=>vm.$u.get('/system/dict/', params);//查询字典
let putMyVideoView =(id)=>vm.$u.put(`/vod/video/${id}/myview/`);//更新本人观看信息
+
+
+ //考试有关
+ let getExamList = (params={})=>vm.$u.get('/exam/exam/', params);//考试列表
vm.$u.api = {getUserInfo,
getCode,
codeLogin,
@@ -64,7 +68,9 @@ const install = (Vue, vm) => {
getDocument,
getVideos,
getDickey,
- putMyVideoView
+ putMyVideoView,
+
+ getExamList
};
}
diff --git a/client_mp/common/uni-ui.scss b/client_mp/common/uni-ui.scss
new file mode 100644
index 0000000..71bb980
--- /dev/null
+++ b/client_mp/common/uni-ui.scss
@@ -0,0 +1,119 @@
+.uni-flex {
+ display: flex;
+}
+
+.uni-flex-row {
+ @extend .uni-flex;
+ flex-direction: row;
+ box-sizing: border-box;
+}
+
+.uni-flex-column {
+ @extend .uni-flex;
+ flex-direction: column;
+}
+
+.uni-color-gary {
+ color: #3b4144;
+}
+
+/* 标题 */
+.uni-title {
+ display: flex;
+ margin-bottom: $uni-spacing-col-base;
+ font-size: $uni-font-size-lg;
+ font-weight: bold;
+ color: #3b4144;
+}
+
+.uni-title-sub {
+ display: flex;
+ // margin-bottom: $uni-spacing-col-base;
+ font-size: $uni-font-size-base;
+ font-weight: 500;
+ color: #3b4144;
+}
+
+/* 描述 额外文本 */
+.uni-note {
+ margin-top: 10px;
+ color: #999;
+ font-size: $uni-font-size-sm;
+}
+
+/* 列表内容 */
+.uni-list-box {
+ @extend .uni-flex-row;
+ flex: 1;
+ margin-top: 10px;
+}
+
+/* 略缩图 */
+.uni-thumb {
+ flex-shrink: 0;
+ margin-right: $uni-spacing-row-base;
+ width: 125px;
+ height: 75px;
+ border-radius: $uni-border-radius-lg;
+ overflow: hidden;
+ border: 1px #f5f5f5 solid;
+ image {
+ width: 100%;
+ height: 100%;
+ }
+}
+
+.uni-media-box {
+ @extend .uni-flex-row;
+ // margin-bottom: $uni-spacing-col-base;
+ border-radius: $uni-border-radius-lg;
+ overflow: hidden;
+ .uni-thumb {
+ margin: 0;
+ margin-left: 4px;
+ flex-shrink: 1;
+ width: 33%;
+ border-radius:0;
+ &:first-child {
+ margin: 0;
+ }
+ }
+}
+
+/* 内容 */
+.uni-content {
+ @extend .uni-flex-column;
+ justify-content: space-between;
+}
+
+/* 列表footer */
+.uni-footer {
+ @extend .uni-flex-row;
+ justify-content: space-around;
+ margin-top: $uni-spacing-col-lg;
+}
+.uni-footer-text {
+ font-size: $uni-font-size-sm;
+ color: $uni-text-color-grey;
+ margin-left: 5px;
+}
+
+/* 标签 */
+
+.uni-tag {
+ flex-shrink: 0;
+ padding: 0 5px;
+ border: 1px $uni-border-color solid;
+ margin-right: $uni-spacing-row-sm;
+ border-radius: $uni-border-radius-base;
+ background: $uni-bg-color-grey;
+ color: $uni-text-color;
+ font-size: $uni-font-size-sm;
+}
+
+/* 链接 */
+.uni-link {
+ margin-left: 10px;
+ color: $uni-text-color;
+ text-decoration: underline;
+}
\ No newline at end of file
diff --git a/client_mp/pages.json b/client_mp/pages.json
index 3305407..d388705 100644
--- a/client_mp/pages.json
+++ b/client_mp/pages.json
@@ -124,7 +124,52 @@
"path" : "pages/vod/video",
"style" :
{
- "navigationBarTitleText": "云点播",
+ "navigationBarTitleText": "视频观看",
+ "enablePullDownRefresh": false
+ }
+
+ }
+ ,{
+ "path" : "pages/exam/index",
+ "style" :
+ {
+ "navigationBarTitleText": "考试列表",
+ "enablePullDownRefresh": false
+ }
+
+ }
+ ,{
+ "path" : "pages/exam/preview",
+ "style" :
+ {
+ "navigationBarTitleText": "考试须知",
+ "enablePullDownRefresh": false
+ }
+
+ }
+ ,{
+ "path" : "pages/exam/main",
+ "style" :
+ {
+ "navigationBarTitleText": "答题中",
+ "enablePullDownRefresh": false
+ }
+
+ }
+ ,{
+ "path" : "pages/exam/record",
+ "style" :
+ {
+ "navigationBarTitleText": "答题记录",
+ "enablePullDownRefresh": false
+ }
+
+ }
+ ,{
+ "path" : "pages/exam/result",
+ "style" :
+ {
+ "navigationBarTitleText": "答题结束",
"enablePullDownRefresh": false
}
@@ -147,12 +192,18 @@
"selectedIconPath": "static/common/homec.png",
"text": "主页"
},
- /* {
+ {
+ "pagePath": "pages/exam/index",
+ "iconPath": "static/common/play.png",
+ "selectedIconPath": "static/common/playc.png",
+ "text": "答题"
+ },
+ {
"pagePath": "pages/vod/video",
"iconPath": "static/common/play.png",
"selectedIconPath": "static/common/playc.png",
"text": "点播"
- }, */
+ },
{
"pagePath": "pages/my/my",
"iconPath": "static/common/me.png",
diff --git a/client_mp/pages/exam/exam.vue b/client_mp/pages/exam/exam.vue
new file mode 100644
index 0000000..8183fd0
--- /dev/null
+++ b/client_mp/pages/exam/exam.vue
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+
+
diff --git a/client_mp/pages/exam/index.vue b/client_mp/pages/exam/index.vue
new file mode 100644
index 0000000..085c88e
--- /dev/null
+++ b/client_mp/pages/exam/index.vue
@@ -0,0 +1,60 @@
+
+
+
+
+
+
+ {{item.name}}
+
+
+
+
+ 开启时间: {{item.open_time}}至{{item.close_time}}
+ 考试机会: {{item.chance}}次
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/client_mp/pages/exam/main.vue b/client_mp/pages/exam/main.vue
new file mode 100644
index 0000000..8183fd0
--- /dev/null
+++ b/client_mp/pages/exam/main.vue
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+
+
diff --git a/client_mp/pages/exam/preview.vue b/client_mp/pages/exam/preview.vue
new file mode 100644
index 0000000..17397dd
--- /dev/null
+++ b/client_mp/pages/exam/preview.vue
@@ -0,0 +1,76 @@
+
+
+
+ 考试信息
+
+
+ 考试名称:{{currentExam.name}}
+ 考试总分:{{currentExam.paper_.total_score}}分
+ 考试时长:{{currentExam.limit}}分钟
+ 考试时长:不限时长
+ 题目分布:单选{{currentExam.paper_.danxuan_count}}道;多选{{currentExam.paper_.duoxuan_count}}道;判断{{currentExam.paper_.panduan_count}}道
+ 判分规则:单选{{currentExam.paper_.danxuan_score}}分,多选{{currentExam.paper_.duoxuan_score}}分,多选{{currentExam.paper_.panduan_score}}分,错选少选均不得分
+
+ 答题须知
+
+
+ - 1.进入答题后请不要后退或返回。
+ - 2.可点击上一题/下一题切换。
+ - 3.可点击答题卡复查
+ - 4.请合理安排时间答题,可提前交卷
+
+
+ 开始答题
+
+
+
+
+
+
diff --git a/client_mp/pages/exam/record.vue b/client_mp/pages/exam/record.vue
new file mode 100644
index 0000000..c716c20
--- /dev/null
+++ b/client_mp/pages/exam/record.vue
@@ -0,0 +1,133 @@
+
+
+
+
+
+
+
+
+
+ 模拟练习
+ 正式竞赛
+ (补)
+
+
+ 耗时:
+ {{item.took}}
+ s
+ -
+ 提交时间:{{item.create_time}}
+
+
+ 总分:{{item.total_score}}分
+ -
+ 得分:
+ {{item.score}}
+ 分
+
+
+
+
+
+
+
+
+ {{loadingText}}
+
+
+
+
+
+
diff --git a/client_mp/pages/exam/result.vue b/client_mp/pages/exam/result.vue
new file mode 100644
index 0000000..a79e1e3
--- /dev/null
+++ b/client_mp/pages/exam/result.vue
@@ -0,0 +1,97 @@
+
+
+
+
+ 恭喜您完成考试!
+
+
+ {{currentExam.name}}
+
+ 总分:{{currentExam.total_score}}分
+
+ 得分:{{currentExam.score}} 分
+
+
+ 查看答卷
+ 返回首页
+
+
+
+
+
+
+
diff --git a/client_mp/pages/vod/video.vue b/client_mp/pages/vod/video.vue
index 8a5a0d6..d306c83 100644
--- a/client_mp/pages/vod/video.vue
+++ b/client_mp/pages/vod/video.vue
@@ -31,7 +31,7 @@
{{video.name}}
- {{video.viewsp}}人观看
+ {{video.views}}次播放
diff --git a/client_mp/uview-ui/theme.scss b/client_mp/uview-ui/theme.scss
index f3bb36d..37264d5 100644
--- a/client_mp/uview-ui/theme.scss
+++ b/client_mp/uview-ui/theme.scss
@@ -2,6 +2,7 @@
// uni.scss中引入的样式会同时混入到全局样式文件和单独每一个页面的样式中,造成微信程序包太大,
// 故uni.scss只建议放scss变量名相关样式,其他的样式可以通过main.js或者App.vue引入
+$theme-color:#0080d1;
$u-main-color: #303133;
$u-content-color: #606266;
$u-tips-color: #909399;
@@ -36,3 +37,9 @@ $u-type-info-light: #f4f4f5;
$u-form-item-height: 70rpx;
$u-form-item-border-color: #dcdfe6;
+
+/* 文字尺寸 */
+$u-font-size-sm:24rpx;
+$u-font-size-lg:32rpx;
+$u-font-size-base:28rpx;
+$u-font-size-title:36rpx;
\ No newline at end of file
diff --git a/server/apps/ability/views.py b/server/apps/ability/views.py
index ce3ec75..16a02ee 100644
--- a/server/apps/ability/views.py
+++ b/server/apps/ability/views.py
@@ -19,6 +19,7 @@ from apps.supervision.models import Content, Record
from apps.supervision.serializers import ContentSerializer, RecordCreateSerializer, RecordSerializer
from apps.system.mixins import CreateUpdateCustomMixin
from utils.queryset import get_child_queryset2
+from apps.system.permission_data import RbacFilterSet
from django.utils import timezone
from apps.supervision.permission import RecordPermission
from django.utils.decorators import method_decorator
@@ -60,7 +61,7 @@ class AbilityContentViewSet(CreateUpdateCustomMixin, ModelViewSet):
def perform_update(self, serializer):
serializer.save(update_by = self.request.user)
-class AbilityRecordViewSet(PageOrNot, CreateUpdateCustomMixin, ModelViewSet):
+class AbilityRecordViewSet(RbacFilterSet, PageOrNot, CreateUpdateCustomMixin, ModelViewSet):
perms_map = {'get': '*', 'post': '*',
'put': '*', 'delete': '*'}
queryset = Record.objects.filter(content__cate=2)
@@ -70,25 +71,6 @@ class AbilityRecordViewSet(PageOrNot, CreateUpdateCustomMixin, ModelViewSet):
ordering = ['-task', 'content__sortnum', '-create_time']
filterset_fields = ['content','content__cate', 'belong_dept', 'state']
- def get_queryset(self):
- queryset = self.queryset
- if hasattr(self.get_serializer_class(), 'setup_eager_loading'):
- queryset = self.get_serializer_class().setup_eager_loading(queryset)
- if self.request.user.is_superuser:
- pass
- if hasattr(queryset.model, 'belong_dept'):
- user = self.request.user
- roles = user.roles
- data_range = roles.values_list('datas', flat=True)
- if '全部' in data_range:
- pass
- elif '本级及以下' in data_range:
- belong_depts = get_child_queryset2(user.dept)
- queryset = queryset.filter(belong_dept__in = belong_depts)
- elif '本级' in data_range:
- queryset = queryset.filter(belong_dept = user.dept)
- return queryset
-
def filter_queryset(self, queryset):
if not self.request.query_params.get('pageoff', None):
queryset = queryset.exclude(state='待发布')
diff --git a/server/apps/ability/views_qtask.py b/server/apps/ability/views_qtask.py
index 7e29bc6..dbe800a 100644
--- a/server/apps/ability/views_qtask.py
+++ b/server/apps/ability/views_qtask.py
@@ -13,6 +13,7 @@ from rest_framework.exceptions import ParseError
from rest_framework import serializers
from rest_framework.exceptions import ParseError
from utils.queryset import get_child_queryset2
+from apps.system.permission_data import RbacFilterSet
class QualiLibViewSet(PageOrNot, ListModelMixin, GenericViewSet):
@@ -112,7 +113,7 @@ def cal_count(qtask, org):
qorg.save()
-class QActionViewSet(PageOrNot, ListModelMixin, DestroyModelMixin, RetrieveModelMixin, GenericViewSet):
+class QActionViewSet(RbacFilterSet, PageOrNot, ListModelMixin, DestroyModelMixin, RetrieveModelMixin, GenericViewSet):
perms_map = {'get': '*', 'delete': 'qaction_delete'}
queryset = QAction.objects.select_related(
'file', 'atype', 'afield', 'qtask', 'belong_dept', 'create_by')
@@ -126,24 +127,7 @@ class QActionViewSet(PageOrNot, ListModelMixin, DestroyModelMixin, RetrieveModel
return QActionDetailSerializer
return super().get_serializer_class()
- def get_queryset(self):
- queryset = self.queryset
- if hasattr(self.get_serializer_class(), 'setup_eager_loading'):
- queryset = self.get_serializer_class().setup_eager_loading(queryset)
- if self.request.user.is_superuser:
- pass
- if hasattr(queryset.model, 'belong_dept'):
- user = self.request.user
- roles = user.roles
- data_range = roles.values_list('datas', flat=True)
- if '全部' in data_range:
- pass
- elif '本级及以下' in data_range:
- belong_depts = get_child_queryset2(user.dept)
- queryset = queryset.filter(belong_dept__in=belong_depts)
- elif '本级' in data_range:
- queryset = queryset.filter(belong_dept=user.dept)
- return queryset
+
# @action(methods=['get'], detail=False, perms_map={'get': '*'})
# def my(self, request, *args, **kwargs):
diff --git a/server/apps/exam/filters.py b/server/apps/exam/filters.py
new file mode 100644
index 0000000..2e7dca0
--- /dev/null
+++ b/server/apps/exam/filters.py
@@ -0,0 +1,20 @@
+from django_filters import rest_framework as filters
+from .models import ExamRecord, Exam
+class ExamRecordFilter(filters.FilterSet):
+ class Meta:
+ model = ExamRecord
+ fields = {
+ 'start_time': ['exact', 'gte', 'lte'],
+ 'is_pass': ['exact'],
+ 'type': ['exact'],
+ 'is_submited': ['exact'],
+ }
+
+class ExamFilter(filters.FilterSet):
+ class Meta:
+ model = Exam
+ fields = {
+ 'close_time': ['exact', 'gte', 'lte'],
+ 'paper': ['exact'],
+ 'code': ['exact']
+ }
\ No newline at end of file
diff --git a/server/apps/exam/migrations/0002_auto_20221107_1356.py b/server/apps/exam/migrations/0002_auto_20221107_1356.py
new file mode 100644
index 0000000..dc77a2c
--- /dev/null
+++ b/server/apps/exam/migrations/0002_auto_20221107_1356.py
@@ -0,0 +1,153 @@
+# Generated by Django 3.0.5 on 2022-11-07 05:56
+
+from django.conf import settings
+import django.contrib.postgres.fields.jsonb
+from django.db import migrations, models
+import django.db.models.deletion
+import django.utils.timezone
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+ ('exam', '0001_initial'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='AnswerDetail',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('create_time', models.DateTimeField(default=django.utils.timezone.now, help_text='创建时间', verbose_name='创建时间')),
+ ('update_time', models.DateTimeField(auto_now=True, help_text='修改时间', verbose_name='修改时间')),
+ ('is_deleted', models.BooleanField(default=False, help_text='删除标记', verbose_name='删除标记')),
+ ('user_answer', django.contrib.postgres.fields.jsonb.JSONField(blank=True, null=True)),
+ ('score', models.FloatField(default=0, verbose_name='本题得分')),
+ ('is_right', models.BooleanField(default=False, verbose_name='是否正确')),
+ ],
+ options={
+ 'verbose_name': '答题记录',
+ 'verbose_name_plural': '答题记录',
+ },
+ ),
+ migrations.CreateModel(
+ name='Exam',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('create_time', models.DateTimeField(default=django.utils.timezone.now, help_text='创建时间', verbose_name='创建时间')),
+ ('update_time', models.DateTimeField(auto_now=True, help_text='修改时间', verbose_name='修改时间')),
+ ('is_deleted', models.BooleanField(default=False, help_text='删除标记', verbose_name='删除标记')),
+ ('code', models.CharField(blank=True, max_length=100, null=True, unique=True, verbose_name='考试编号')),
+ ('name', models.CharField(max_length=100, verbose_name='名称')),
+ ('place', models.CharField(blank=True, max_length=100, null=True, verbose_name='考试地点')),
+ ('open_time', models.DateTimeField(blank=True, null=True, verbose_name='开启时间')),
+ ('close_time', models.DateTimeField(blank=True, null=True, verbose_name='关闭时间')),
+ ('proctor_name', models.CharField(blank=True, max_length=100, null=True, verbose_name='监考人姓名')),
+ ('proctor_phone', models.CharField(blank=True, max_length=100, null=True, verbose_name='监考人联系方式')),
+ ('chance', models.IntegerField(default=3, verbose_name='考试机会')),
+ ('create_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='exam_create_by', to=settings.AUTH_USER_MODEL, verbose_name='创建人')),
+ ],
+ options={
+ 'abstract': False,
+ },
+ ),
+ migrations.CreateModel(
+ name='Paper',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('create_time', models.DateTimeField(default=django.utils.timezone.now, help_text='创建时间', verbose_name='创建时间')),
+ ('update_time', models.DateTimeField(auto_now=True, help_text='修改时间', verbose_name='修改时间')),
+ ('is_deleted', models.BooleanField(default=False, help_text='删除标记', verbose_name='删除标记')),
+ ('name', models.CharField(max_length=200, verbose_name='名称')),
+ ('limit', models.IntegerField(default=0, verbose_name='限时(分钟)')),
+ ('total_score', models.FloatField(default=0, verbose_name='满分')),
+ ('pass_score', models.FloatField(default=0, verbose_name='通过分数')),
+ ('danxuan_count', models.IntegerField(default=0, verbose_name='单选数量')),
+ ('danxuan_score', models.FloatField(default=2, verbose_name='单选分数')),
+ ('duoxuan_count', models.IntegerField(default=0, verbose_name='多选数量')),
+ ('duoxuan_score', models.FloatField(default=4, verbose_name='多选分数')),
+ ('panduan_count', models.IntegerField(default=0, verbose_name='判断数量')),
+ ('panduan_score', models.FloatField(default=2, verbose_name='判断分数')),
+ ('create_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='paper_create_by', to=settings.AUTH_USER_MODEL, verbose_name='创建人')),
+ ],
+ options={
+ 'verbose_name': '押题卷',
+ 'verbose_name_plural': '押题卷',
+ },
+ ),
+ migrations.CreateModel(
+ name='PaperQuestion',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('create_time', models.DateTimeField(default=django.utils.timezone.now, help_text='创建时间', verbose_name='创建时间')),
+ ('update_time', models.DateTimeField(auto_now=True, help_text='修改时间', verbose_name='修改时间')),
+ ('is_deleted', models.BooleanField(default=False, help_text='删除标记', verbose_name='删除标记')),
+ ('total_score', models.FloatField(default=0, verbose_name='单题满分')),
+ ('sort', models.PositiveSmallIntegerField(default=1)),
+ ('paper', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='exam.Paper', verbose_name='试卷')),
+ ('question', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='exam.Question', verbose_name='试题')),
+ ],
+ options={
+ 'abstract': False,
+ },
+ ),
+ migrations.AddField(
+ model_name='paper',
+ name='questions',
+ field=models.ManyToManyField(through='exam.PaperQuestion', to='exam.Question'),
+ ),
+ migrations.AddField(
+ model_name='paper',
+ name='update_by',
+ field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='paper_update_by', to=settings.AUTH_USER_MODEL, verbose_name='最后编辑人'),
+ ),
+ migrations.CreateModel(
+ name='ExamRecord',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('create_time', models.DateTimeField(default=django.utils.timezone.now, help_text='创建时间', verbose_name='创建时间')),
+ ('update_time', models.DateTimeField(auto_now=True, help_text='修改时间', verbose_name='修改时间')),
+ ('is_deleted', models.BooleanField(default=False, help_text='删除标记', verbose_name='删除标记')),
+ ('name', models.CharField(max_length=200, verbose_name='名称')),
+ ('type', models.CharField(choices=[('自助模考', '自助模考'), ('押卷模考', '押卷模考'), ('正式考试', '正式考试')], default='自助模考', max_length=50, verbose_name='考试类型')),
+ ('limit', models.IntegerField(default=0, verbose_name='限时(分钟)')),
+ ('total_score', models.FloatField(default=0, verbose_name='总分')),
+ ('score', models.FloatField(default=0, verbose_name='得分')),
+ ('took', models.IntegerField(default=0, verbose_name='耗时(秒)')),
+ ('start_time', models.DateTimeField(verbose_name='开始答题时间')),
+ ('end_time', models.DateTimeField(verbose_name='结束答题时间')),
+ ('is_pass', models.BooleanField(default=True, verbose_name='是否通过')),
+ ('questions', django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=list, verbose_name='下发的题目列表')),
+ ('create_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='examrecord_create_by', to=settings.AUTH_USER_MODEL, verbose_name='创建人')),
+ ('detail', models.ManyToManyField(through='exam.AnswerDetail', to='exam.Question', verbose_name='答题记录')),
+ ('exam', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='exam.Exam', verbose_name='关联的正式考试')),
+ ('paper', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='exam.Paper', verbose_name='所用试卷')),
+ ('update_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='examrecord_update_by', to=settings.AUTH_USER_MODEL, verbose_name='最后编辑人')),
+ ],
+ options={
+ 'verbose_name': '考试记录',
+ 'verbose_name_plural': '考试记录',
+ },
+ ),
+ migrations.AddField(
+ model_name='exam',
+ name='paper',
+ field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='exam.Paper', verbose_name='使用的试卷'),
+ ),
+ migrations.AddField(
+ model_name='exam',
+ name='update_by',
+ field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='exam_update_by', to=settings.AUTH_USER_MODEL, verbose_name='最后编辑人'),
+ ),
+ migrations.AddField(
+ model_name='answerdetail',
+ name='examrecord',
+ field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='exam.ExamRecord'),
+ ),
+ migrations.AddField(
+ model_name='answerdetail',
+ name='question',
+ field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='exam.Question'),
+ ),
+ ]
diff --git a/server/apps/exam/migrations/0003_auto_20221108_0901.py b/server/apps/exam/migrations/0003_auto_20221108_0901.py
new file mode 100644
index 0000000..64aedfc
--- /dev/null
+++ b/server/apps/exam/migrations/0003_auto_20221108_0901.py
@@ -0,0 +1,31 @@
+# Generated by Django 3.0.5 on 2022-11-08 01:01
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('exam', '0002_auto_20221107_1356'),
+ ]
+
+ operations = [
+ migrations.RemoveField(
+ model_name='examrecord',
+ name='questions',
+ ),
+ migrations.RemoveField(
+ model_name='paperquestion',
+ name='sort',
+ ),
+ migrations.AddField(
+ model_name='answerdetail',
+ name='total_score',
+ field=models.FloatField(default=0, verbose_name='该题满分'),
+ ),
+ migrations.AddField(
+ model_name='examrecord',
+ name='is_submited',
+ field=models.BooleanField(default=False),
+ ),
+ ]
diff --git a/server/apps/exam/models.py b/server/apps/exam/models.py
index bb71fd5..dd35c3f 100644
--- a/server/apps/exam/models.py
+++ b/server/apps/exam/models.py
@@ -1,6 +1,7 @@
from django.db import models
from apps.system.models import CommonAModel
from django.contrib.postgres.fields import JSONField
+from utils.model import BaseModel
# Create your models here.
class Questioncat(CommonAModel):
@@ -44,4 +45,90 @@ class Question(CommonAModel):
verbose_name_plural = verbose_name
def __str__(self):
- return self.name
\ No newline at end of file
+ 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='单题满分')
+
+
+class Exam(CommonAModel):
+ """
+ 组织的正式考试
+ """
+ code = models.CharField('考试编号', max_length=100, null=True, blank=True, unique=True)
+ name = models.CharField('名称', max_length=100)
+ place = models.CharField('考试地点', max_length=100, null=True, blank=True)
+ open_time = models.DateTimeField('开启时间', null=True, blank=True)
+ close_time = models.DateTimeField('关闭时间', null=True, blank=True)
+ proctor_name = models.CharField('监考人姓名', max_length=100, null=True, blank=True)
+ proctor_phone = models.CharField('监考人联系方式', max_length=100, null=True, blank=True)
+ chance = models.IntegerField('考试机会', default=3)
+ paper = models.ForeignKey(Paper, verbose_name='使用的试卷', on_delete=models.CASCADE, null=True, blank=True)
+
+ def __str__(self):
+ return self.name
+
+class ExamRecord(CommonAModel):
+ '''
+ 考试记录表
+ '''
+ type_choices = (
+ ('自助模考', '自助模考'),
+ ('押卷模考', '押卷模考'),
+ ('正式考试', '正式考试')
+ )
+ name = models.CharField(max_length=200, verbose_name='名称')
+ type = models.CharField(max_length=50, default='自助模考',choices = type_choices, verbose_name='考试类型')
+ limit = models.IntegerField(default=0, verbose_name='限时(分钟)')
+ paper = models.ForeignKey(Paper, on_delete=models.SET_NULL, verbose_name='所用试卷', null=True, blank=True)
+ total_score = models.FloatField(default=0, verbose_name='总分')
+ score = models.FloatField(default=0, verbose_name='得分')
+ took = models.IntegerField(default=0, verbose_name='耗时(秒)')
+ start_time = models.DateTimeField(verbose_name='开始答题时间')
+ end_time = models.DateTimeField(verbose_name='结束答题时间')
+ detail = models.ManyToManyField(Question, verbose_name='答题记录', through='AnswerDetail')
+ is_pass = models.BooleanField(default=True, verbose_name='是否通过')
+ exam = models.ForeignKey(Exam, verbose_name='关联的正式考试', null=True, blank=True, on_delete= models.SET_NULL)
+ is_submited = models.BooleanField(default=False)
+
+ class Meta:
+ verbose_name = '考试记录'
+ verbose_name_plural = verbose_name
+
+
+
+class AnswerDetail(BaseModel):
+ examrecord = models.ForeignKey(ExamRecord, on_delete=models.CASCADE)
+ total_score = models.FloatField(default=0, verbose_name='该题满分')
+ question = models.ForeignKey(Question, on_delete=models.CASCADE)
+ user_answer = JSONField(null=True,blank=True)
+ score = models.FloatField(default=0, verbose_name='本题得分')
+ is_right = models.BooleanField(default=False, verbose_name='是否正确')
+
+ class Meta:
+ verbose_name = '答题记录'
+ verbose_name_plural = verbose_name
+
\ No newline at end of file
diff --git a/server/apps/exam/serializers.py b/server/apps/exam/serializers.py
index 5363655..9313fbf 100644
--- a/server/apps/exam/serializers.py
+++ b/server/apps/exam/serializers.py
@@ -1,6 +1,7 @@
-from rest_framework.serializers import ModelSerializer
+from rest_framework.serializers import ModelSerializer, CharField, Serializer, SerializerMethodField, FloatField
+from rest_framework import serializers
-from apps.exam.models import Question, Questioncat
+from apps.exam.models import Question, Questioncat, Paper, Exam, PaperQuestion, ExamRecord, AnswerDetail
class QuestioncatSerializer(ModelSerializer):
@@ -10,6 +11,174 @@ class QuestioncatSerializer(ModelSerializer):
class QuestionSerializer(ModelSerializer):
+ questioncat_name = serializers.CharField(source='questioncat.name', read_only=True)
class Meta:
model = Question
- fields = '__all__'
\ No newline at end of file
+ fields = '__all__'
+
+
+class PaperSerializer(ModelSerializer):
+ class Meta:
+ model = Paper
+ exclude = ('questions',)
+
+
+class PaperQuestionSerializer(ModelSerializer):
+ class Meta:
+ model = PaperQuestion
+ fields = ['question', 'total_score']
+
+
+class PaperCreateUpdateSerializer(ModelSerializer):
+ questions_ = PaperQuestionSerializer(many=True)
+
+ class Meta:
+ model = Paper
+ fields = '__all__'
+
+
+class PaperQuestionDetailSerializer(ModelSerializer):
+ name = serializers.ReadOnlyField(source='question.name')
+ options = serializers.ReadOnlyField(source='question.options')
+ right = serializers.ReadOnlyField(source='question.right')
+ type = serializers.ReadOnlyField(source='question.type')
+ img = serializers.ReadOnlyField(source='question.img')
+ questioncat_name = serializers.ReadOnlyField(
+ source='question.questioncat.name')
+ level = serializers.ReadOnlyField(source='question.level')
+
+ class Meta:
+ model = PaperQuestion
+ fields = ('id', 'name', 'options', 'right', 'type', 'level',
+ 'total_score', 'questioncat_name', 'img', 'question')
+
+
+class PaperQuestionShortSerializer(ModelSerializer):
+ class Meta:
+ model = PaperQuestion
+ fields = '__all__'
+
+
+class PaperDetailSerializer(ModelSerializer):
+ questions_ = SerializerMethodField()
+
+ class Meta:
+ model = Paper
+ fields = '__all__'
+
+ def get_questions_(self, instance):
+ pqs = PaperQuestion.objects.filter(paper=instance)
+ return PaperQuestionDetailSerializer(pqs, many=True).data
+
+
+class ExamCreateUpdateSerializer(ModelSerializer):
+ class Meta:
+ model = Exam
+ fields = ['name', 'place', 'open_time',
+ 'close_time', 'proctor_name', 'proctor_phone', 'chance', 'paper']
+
+
+class ExamListSerializer(ModelSerializer):
+ create_by_name = CharField(source='create_by.name', read_only=True)
+ paper_ = PaperSerializer(source='paper', read_only=True)
+
+ class Meta:
+ model = Exam
+ fields = '__all__'
+
+
+class ExamDetailSerializer(ModelSerializer):
+ create_by_name = CharField(source='create_by.name', read_only=True)
+ paper_ = PaperSerializer(source='paper', read_only=True)
+
+ class Meta:
+ model = Exam
+ fields = '__all__'
+
+
+class ExamAttendSerializer(Serializer):
+ code = CharField(label="考试编号")
+
+
+class ExamRecordListSerializer(serializers.ModelSerializer):
+ """
+ 考试列表序列化
+ """
+ took_format = serializers.SerializerMethodField()
+ create_by_name = serializers.CharField(
+ source='create_by.name', read_only=True)
+
+ class Meta:
+ model = ExamRecord
+ exclude = ('detail',)
+
+ def get_took_format(self, obj):
+ m, s = divmod(obj.took, 60)
+ h, m = divmod(m, 60)
+ return "%02d:%02d:%02d" % (h, m, s)
+
+
+class ExamRecordDetailSerializer(serializers.ModelSerializer):
+ """
+ 考试详情序列化
+ """
+ took_format = serializers.SerializerMethodField()
+ questions_ = serializers.SerializerMethodField()
+
+ class Meta:
+ model = ExamRecord
+ exclude = ('detail',)
+
+ def get_took_format(self, obj):
+ m, s = divmod(obj.took, 60)
+ h, m = divmod(m, 60)
+ return "%02d:%02d:%02d" % (h, m, s)
+
+ def get_questions_(self, obj):
+ objs = AnswerDetail.objects.select_related('question').filter(
+ examrecord=obj).order_by('question__type')
+ return AnswerDetailSerializer(instance=objs, many=True).data
+
+
+class AnswerDetailUpdateSerializer(serializers.Serializer):
+ id = serializers.CharField(label='下发ID')
+ user_answer = serializers.JSONField(label='作答')
+
+
+class ExamRecordSubmitSerializer(serializers.ModelSerializer):
+ questions_ = AnswerDetailUpdateSerializer(many=True)
+
+ class Meta:
+ model = ExamRecord
+ fields = ['questions_']
+
+
+class AnswerDetailSerializer(ModelSerializer):
+ name = serializers.ReadOnlyField(source='question.name')
+ options = serializers.ReadOnlyField(source='question.options')
+ right = serializers.ReadOnlyField(source='question.right')
+ type = serializers.ReadOnlyField(source='question.type')
+ img = serializers.ReadOnlyField(source='question.img')
+ questioncat_name = serializers.ReadOnlyField(
+ source='question.questioncat.name')
+ level = serializers.ReadOnlyField(source='question.level')
+
+ class Meta:
+ model = PaperQuestion
+ fields = ['id', 'question', 'name', 'options', 'right', 'type', 'level',
+ 'total_score', 'questioncat_name', 'img', 'user_answer', 'score', 'is_right']
+
+
+class AnswerDetailOutSerializer(ModelSerializer):
+ name = serializers.ReadOnlyField(source='question.name')
+ options = serializers.ReadOnlyField(source='question.options')
+ type = serializers.ReadOnlyField(source='question.type')
+ img = serializers.ReadOnlyField(source='question.img')
+ questioncat_name = serializers.ReadOnlyField(
+ source='question.questioncat.name')
+ level = serializers.ReadOnlyField(source='question.level')
+
+ class Meta:
+ model = PaperQuestion
+ fields = ['id', 'question', 'name', 'options', 'type', 'level',
+ 'total_score', 'questioncat_name', 'img', 'user_answer', 'score', 'is_right']
diff --git a/server/apps/exam/urls.py b/server/apps/exam/urls.py
index 86eb1b5..93ba0f1 100644
--- a/server/apps/exam/urls.py
+++ b/server/apps/exam/urls.py
@@ -1,13 +1,16 @@
from django.urls import path, include
from rest_framework import routers
-from apps.exam.views import QuestionViewSet, QuestioncatViewSet
+from apps.exam.views import QuestionViewSet, QuestioncatViewSet, PaperViewSet, ExamViewSet, ExamRecordViewSet
API_BASE_URL = 'api/exam/'
HTML_BASE_URL = 'exam/'
router = routers.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))
]
diff --git a/server/apps/exam/views.py b/server/apps/exam/views.py
index 4b9ba88..b80ef21 100644
--- a/server/apps/exam/views.py
+++ b/server/apps/exam/views.py
@@ -1,50 +1,95 @@
from django.shortcuts import render
-from rest_framework.viewsets import ModelViewSet
+from rest_framework.viewsets import ModelViewSet, GenericViewSet
+from rest_framework.mixins import ListModelMixin, DestroyModelMixin
from apps.exam.exports import export_question
-from apps.exam.models import Question, Questioncat
-from apps.exam.serializers import QuestionSerializer, QuestioncatSerializer
+from apps.exam.models import Question, Questioncat, PaperQuestion
+from apps.exam.serializers import (QuestionSerializer, QuestioncatSerializer, PaperSerializer, ExamDetailSerializer, ExamRecordDetailSerializer, ExamListSerializer,
+ ExamCreateUpdateSerializer, ExamListSerializer, ExamRecordSubmitSerializer, PaperDetailSerializer, PaperCreateUpdateSerializer, AnswerDetailOutSerializer, ExamRecordListSerializer)
from rest_framework.decorators import action
from rest_framework.response import Response
from rest_framework.permissions import IsAuthenticated
+from rest_framework.exceptions import ParseError
from openpyxl import Workbook, load_workbook
from django.conf import settings
+from apps.exam.models import Paper, Exam, ExamRecord, AnswerDetail
+from django.utils import timezone
+from django.db import transaction
+from rest_framework.serializers import Serializer
+from datetime import datetime
+from apps.exam.filters import ExamRecordFilter, ExamFilter
+from datetime import timedelta
+from apps.system.mixins import CreateUpdateCustomMixin
# Create your views here.
-class QuestioncatViewSet(ModelViewSet):
- perms_map = {'*': '*'}
+def enctry(s):
+ k = 'ez9z3a4m*$%srn9ve_t71yd!v+&xn9@0k(e(+l6#g1h=e5i4da'
+ encry_str = ""
+ for i, j in zip(s, k):
+ # i为字符,j为秘钥字符
+ temp = str(ord(i)+ord(j))+'_' # 加密字符 = 字符的Unicode码 + 秘钥的Unicode码
+ encry_str = encry_str + temp
+ return encry_str
+
+
+# 解密
+def dectry(p):
+ k = 'ez9z3a4m*$%srn9ve_t71yd!v+&xn9@0k(e(+l6#g1h=e5i4da'
+ dec_str = ""
+ for i, j in zip(p.split("_")[:-1], k):
+ # i 为加密字符,j为秘钥字符
+ # 解密字符 = (加密Unicode码字符 - 秘钥字符的Unicode码)的单字节字符
+ temp = chr(int(i) - ord(j))
+ dec_str = dec_str+temp
+ return dec_str
+
+
+class QuestioncatViewSet(CreateUpdateCustomMixin, ModelViewSet):
+ perms_map = {'get': '*', 'post':'question', 'put':'question', 'delete':'question'}
queryset = Questioncat.objects.all()
serializer_class = QuestioncatSerializer
filterset_fields = ['parent']
search_fields = ['name']
-class QuestionViewSet(ModelViewSet):
- perms_map = {'*': '*'}
+class QuestionViewSet(CreateUpdateCustomMixin, ModelViewSet):
+ perms_map = {'get': '*', 'post':'question', 'put':'question', 'delete':'question'}
queryset = Question.objects.all()
serializer_class = QuestionSerializer
filterset_fields = ['level', 'type', 'year']
search_fields = ['name', 'options', 'resolution']
@action(methods=['get'], detail=False,
- url_path='export', url_name='export_question', perms_map=[{'get': '*'}])
+ url_path='export', url_name='export_question', perms_map=[{'get': '*'}], serializer_class=Serializer)
def export_question(self, request):
+ """
+ 导出题目
+
+ 导出题目
+ """
queryset = self.filter_queryset(self.get_queryset())
path = export_question(queryset)
return Response({'path': path})
- @action(methods=['post'], detail=False, url_name='enable_question', permission_classes=[IsAuthenticated])
+ @action(methods=['post'], detail=False, url_name='enable_question', perms_map={'post': 'question'}, serializer_class=Serializer)
def enable(self, request):
+ """
+ 启用题目
+
+ 启用题目
+ """
ids = request.data.get('ids', None)
if ids:
Question.objects.filter(pk__in=ids).update(enabled=True)
return Response(status=200)
@action(methods=['post'], detail=False,
- url_path='import', url_name='import_question', perms_map=[{'post': '*'}])
+ url_path='import', url_name='import_question', perms_map={'post': 'question'}, serializer_class=Serializer)
def import_question(self, request):
"""
+ 导入题目
+
导入题目
"""
xlsxpath = request.data['path']
@@ -167,3 +212,207 @@ class QuestionViewSet(ModelViewSet):
obj.save()
i = i + 1
return Response(notinlist, status=200)
+
+
+class PaperViewSet(ModelViewSet):
+ """
+ 试卷增删改查
+ """
+ perms_map = {'get': '*', 'post':'paper', 'put':'paper', 'delete':'paper'}
+ queryset = Paper.objects.all()
+ serializer_class = PaperSerializer
+ ordering = ['id']
+ search_fields = ('name',)
+
+ def get_serializer_class(self):
+ if self.action in ['retrieve']:
+ return PaperDetailSerializer
+ elif self.action in ['create', 'update']:
+ return PaperCreateUpdateSerializer
+ return super().get_serializer_class()
+
+ def create(self, request, *args, **kwargs):
+ sr = PaperCreateUpdateSerializer(data=request.data)
+ sr.is_valid(raise_exception=True)
+ vdata = sr.validated_data
+ vdata['create_by'] = request.user
+ questions_ = vdata.pop('questions_')
+ paper = Paper.objects.create(**vdata)
+ q_list = []
+ for i in questions_:
+ q_list.append(PaperQuestion(question=i['question'], total_score=i['total_score'], paper=paper))
+ PaperQuestion.objects.bulk_create(q_list)
+ return Response(status=201)
+
+ def update(self, request, *args, **kwargs):
+ # 有考试在执行,不可更新
+ now = timezone.now()
+ paper = self.get_object()
+ if Exam.objects.filter(close_time__gte=now, paper=paper).exists():
+ raise ParseError('存在考试,不可编辑')
+ sr = PaperCreateUpdateSerializer(instance=paper, data=request.data)
+ sr.is_valid(raise_exception=True)
+ vdata = sr.validated_data
+ questions_ = vdata.pop('questions_')
+ vdata['update_by'] = request.user
+ Paper.objects.filter(id=paper.id).update(**vdata)
+ q_list = []
+ for i in questions_:
+ q_list.append(PaperQuestion(question=i['question'], total_score=i['total_score'], paper=paper))
+ PaperQuestion.objects.filter(paper=paper).delete()
+ PaperQuestion.objects.bulk_create(q_list)
+ return Response()
+
+
+class ExamViewSet(CreateUpdateCustomMixin, ModelViewSet):
+ perms_map = {'get': '*', 'post':'exam', 'put':'exam', 'delete':'exam'}
+ queryset = Exam.objects.all().select_related('paper', 'create_by')
+ ordering = ['-id']
+ search_fields = ('name',)
+ serializer_class = ExamListSerializer
+ filterset_class = ExamFilter
+
+ def get_serializer_class(self):
+ if self.action in ['create', 'update']:
+ return ExamCreateUpdateSerializer
+ elif self.action in ['retrieve']:
+ return ExamDetailSerializer
+ return super().get_serializer_class()
+
+ def destroy(self, request, *args, **kwargs):
+ instance = self.get_object()
+ if ExamRecord.objects.filter(exam=instance).exists():
+ raise ParseError('存在考试记录,禁止删除')
+ instance.delete(soft=False)
+ return Response(status=204)
+
+ @action(methods=['post'], detail=True, perms_map=[{'post': '*'}], serializer_class=Serializer, permission_classes = [IsAuthenticated])
+ def start(self, request, *args, **kwargs):
+ """
+ 开始考试
+
+ 开始考试具体题目信息
+ """
+ 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:
+ er = ExamRecord()
+ er.type = '正式考试'
+ er.name = '正式考试' + now.strftime('%Y%m%d%H%M')
+ er.limit = exam.paper.limit
+ er.paper = exam.paper
+ er.total_score = exam.paper.total_score
+ er.start_time = now
+ er.is_pass = False
+ er.exam = exam
+ er.save()
+ ret = {}
+ ret['examrecord'] = er.id
+ pqs = PaperQuestion.objects.filter(paper=exam.paper).order_by('id')
+ details = []
+ for i in pqs:
+ details.append(AnswerDetail(examrecord=er, question=i.question, total_score=i.total_score))
+ AnswerDetail.objects.bulk_create(details)
+ ads = AnswerDetail.objects.select_related('question').filter(examrecord=er).order_by('id')
+ ret['questions_'] = AnswerDetailOutSerializer(instance=ads, many=True).data
+ return Response(ret)
+ raise ParseError('暂不支持')
+
+
+class ExamRecordViewSet(ListModelMixin, DestroyModelMixin, GenericViewSet):
+ """
+ 考试记录列表和详情
+ """
+ perms_map = {'get': 'examrecord', 'post': '*', 'delete':'examrecord'}
+ queryset = ExamRecord.objects.select_related('create_by')
+ serializer_class = ExamRecordListSerializer
+ ordering_fields = ['create_time', 'score', 'took', 'update_time']
+ ordering = ['-update_time']
+ search_fields = ('create_by__name', 'create_by__username')
+ filterset_class = ExamRecordFilter
+
+ def get_serializer_class(self):
+ if self.action == 'retrieve':
+ return ExamRecordDetailSerializer
+ return super().get_serializer_class()
+
+ def perform_destroy(self, instance): # 考试记录物理删除
+ instance.delete(soft=False)
+
+ @action(methods=['post'], detail=False, perms_map=[{'post': '*'}], serializer_class=Serializer, permission_classes = [IsAuthenticated])
+ def clear(self, request, pk=None):
+ """
+ 清除七日前未提交的考试记录
+
+ 清除七日前未提交的考试记录
+ """
+ now = timezone.now
+ days7_ago = now - timedelta(days=7)
+ ExamRecord.objects.filter(create_time__lte=days7_ago, is_submited=False).delete(soft=False)
+ return Response(status=False)
+
+ @action(methods=['get'], detail=False, permission_classes=[IsAuthenticated])
+ def self(self, request, pk=None):
+ '''
+ 个人考试记录
+
+ 个人考试记录
+ '''
+ queryset = ExamRecord.objects.filter(create_by=request.user).order_by('-update_time')
+ page = self.paginate_queryset(queryset)
+ serializer = self.get_serializer(page, many=True)
+ return self.get_paginated_response(serializer.data)
+
+ @action(methods=['post'], detail=True, perms_map=[{'post': '*'}], serializer_class=ExamRecordSubmitSerializer, permission_classes = [IsAuthenticated])
+ def submit(self, request, pk=None):
+ '''
+ 提交答卷
+
+ 提交答卷
+ '''
+ er = self.get_object()
+ now = timezone.now()
+ if er.create_by != request.user:
+ raise ParseError('提交人有误')
+ exam = er.exam
+ if not exam:
+ raise ParseError('暂不支持')
+ if now > exam.close_time + timedelta(minutes=30):
+ raise ParseError('考试时间已过, 提交失败')
+ serializer = ExamRecordSubmitSerializer(data = request.data)
+ serializer.is_valid(raise_exception=True)
+ vdata = serializer.validated_data
+ questions_ = vdata['questions_']
+ # 后端判卷
+ ads = AnswerDetail.objects.select_related('question').filter(examrecord=er).order_by('id').values('id', 'question__right', 'total_score', 'question__type')
+ total_score = 0
+ try:
+ for index, ad in enumerate(ads):
+ ad.user_answer = questions_[index]['user_answer']
+ if ad.question__type == '多选':
+ if set(ad.question__right) == set(ad.user_answer):
+ ad.is_right = True
+ ad.score = ad.total_score
+ else:
+ if ad.question__right == ad.user_answer:
+ ad.is_right = True
+ ad.score = ad.total_score
+ ad.save()
+ total_score = total_score + ad.score
+ except Exception as e:
+ raise ParseError('判卷失败, 请检查试卷:' + str(e))
+ er.score = total_score
+ if er.score > 0.6*er.total_score:
+ er.is_pass = True
+ er.took = (now - er.create_time).total_seconds()
+ er.end_time = now
+ er.is_submited = True
+ er.save()
+ return Response()
\ No newline at end of file
diff --git a/server/apps/quality/views.py b/server/apps/quality/views.py
index 484e209..57f8f82 100644
--- a/server/apps/quality/views.py
+++ b/server/apps/quality/views.py
@@ -98,7 +98,7 @@ class SubtaskViewSet(PageOrNot, CreateUpdateCustomMixin, OptimizationMixin, Mode
if has_permission('inspecttask_create', self.request.user):
return queryset
else:
- return queryset.filter(team_subtask__member=self.request.user).exclude(state='待发布')
+ return queryset.filter(team_subtask__member__id=self.request.user.id).exclude(state='待发布')
@action(methods=['get'], detail=False, perms_map = {'get':'*'})
def self(self, request, *args, **kwargs):
diff --git a/server/apps/supervision/views.py b/server/apps/supervision/views.py
index 5ff50e4..c6c307a 100644
--- a/server/apps/supervision/views.py
+++ b/server/apps/supervision/views.py
@@ -209,7 +209,7 @@ from utils.queryset import get_child_queryset2
from .permission import RecordPermission
-class RecordViewSet(PageOrNot, CreateUpdateCustomMixin, ModelViewSet):
+class RecordViewSet(RbacFilterSet, PageOrNot, CreateUpdateCustomMixin, ModelViewSet):
perms_map = {'get': '*', 'post': 'record_create',
'put': 'record_update', 'delete': 'record_delete'}
queryset = Record.objects.filter(content__cate=1)
@@ -219,25 +219,6 @@ class RecordViewSet(PageOrNot, CreateUpdateCustomMixin, ModelViewSet):
ordering = ['-task', 'content__sortnum', '-create_time']
filter_class = RecordFilter # 过滤类
- def get_queryset(self):
- queryset = self.queryset
- if hasattr(self.get_serializer_class(), 'setup_eager_loading'):
- queryset = self.get_serializer_class().setup_eager_loading(queryset)
- if self.request.user.is_superuser:
- pass
- if hasattr(queryset.model, 'belong_dept'):
- user = self.request.user
- roles = user.roles
- data_range = roles.values_list('datas', flat=True)
- if '全部' in data_range:
- pass
- elif '本级及以下' in data_range:
- belong_depts = get_child_queryset2(user.dept)
- queryset = queryset.filter(belong_dept__in = belong_depts)
- elif '本级' in data_range:
- queryset = queryset.filter(belong_dept = user.dept)
- return queryset
-
def filter_queryset(self, queryset):
if not self.request.query_params.get('pageoff', None):
queryset = queryset.exclude(state='待发布')
diff --git a/server/apps/system/permission.py b/server/apps/system/permission.py
index 590c1f1..7fbc595 100644
--- a/server/apps/system/permission.py
+++ b/server/apps/system/permission.py
@@ -12,7 +12,7 @@ def get_permission_list(user):
perms_list = ['admin']
else:
perms = Permission.objects.none()
- roles = user.roles.all()
+ roles = user.roles.all() if hasattr(user, 'roles') else None
if roles:
for i in roles:
perms = perms | i.perms.all()
diff --git a/server/apps/system/permission_data.py b/server/apps/system/permission_data.py
index 65bf5c9..ad42ec0 100644
--- a/server/apps/system/permission_data.py
+++ b/server/apps/system/permission_data.py
@@ -34,7 +34,9 @@ class RbacFilterSet(object):
if hasattr(queryset.model, 'belong_dept'):
user = self.request.user
- roles = user.roles
+ roles = user.roles if hasattr(user, 'roles') else []
+ if not roles:
+ return queryset.none()
data_range = roles.values_list('datas', flat=True)
if '全部' in data_range:
return queryset
@@ -57,6 +59,8 @@ class RbacFilterSet(object):
elif '仅本人' in data_range:
queryset = queryset.filter(Q(create_by=user)|Q(update_by=user))
return queryset
+ else:
+ return queryset.none()
return queryset
diff --git a/server/apps/vod/views.py b/server/apps/vod/views.py
index 3aedc6f..402529c 100644
--- a/server/apps/vod/views.py
+++ b/server/apps/vod/views.py
@@ -133,7 +133,7 @@ class MyViewRecordAPIView(APIView):
video.save()
if request.data.get('current', None):
record.current = request.data.get('current')
- if timezone.now() > record.update_time + timedelta(hours=6):
+ if timezone.now() > record.update_time + timedelta(minutes=30):
record.views = record.views + 1
video.views = video.views + 1
video.save()