Kaynağa Gözat

导入试题

jiaweiqi 3 yıl önce
ebeveyn
işleme
3d899c9102

+ 334 - 0
apps/api/admin/examquestion/views.py

@@ -1,5 +1,6 @@
 # coding=utf-8
 import json
+import traceback
 from django.db.models import Sum, F
 from rest_framework.decorators import action
 from rest_framework.views import APIView
@@ -9,9 +10,11 @@ from django.db.models import Q
 from utils.custom_modelviewset import CustomModelViewSet
 from utils import response_ok, response_error
 from utils.permission import IsAdministrator
+from utils.format import ExcelImporter
 from apps.examination.examquestion.serializers import *
 from apps.examination.examquestion.filters import *
 from apps.system.models import SysLog
+from apps.foundation.models import Subject, Chapter
 
 class ExamQuestionViewSet(CustomModelViewSet):
     permission_classes = [IsAdministrator, ]
@@ -49,6 +52,337 @@ class ExamQuestionViewSet(CustomModelViewSet):
             SysLog.objects.addnew(request.user, SysLog.INSERT, u"删除试题库试题,id=%d" % instance.id)
         return response_ok()
 
+    @action(methods=['post'], detail=False)
+    def import_single(self, request):
+        file = request.FILES.get('excel_file')
+        status = ExcelImporter.validity(file)
+        if not status['success']:
+            return response_error(status['errors'])
+
+        data = status['data']
+        question_count = 0
+        line = 2
+        try:
+            with transaction.atomic():
+                cur_question = None
+                for row in data:
+                    title = row['试题内容']
+                    subject = row['科目']
+                    chapter = row['章节']
+                    difficulty_text = row['难度']
+                    scores= row['分数']
+                    analysis = row['解析']
+                    content = row['选项']
+                    right = row['正确答案']
+
+                    if title and content:
+                        raise CustomError(u'第%d行:数据格式不正确' % line)
+                    if title and not content:
+                        if not subject:
+                            raise CustomError(u'第%d行:科目不能为空' % line)
+                        if not chapter:
+                            raise CustomError(u'第%d行:章节不能为空' % line)
+                        if not difficulty_text:
+                            raise CustomError(u'第%d行:难度不能为空' % line)
+                        if not scores:
+                            raise CustomError(u'第%d行:分数不能为空' % line)
+
+                        if difficulty_text == u'简单':
+                            difficulty = ExamQuestion.SIMPLE
+                        elif difficulty_text == u'中等':
+                            difficulty = ExamQuestion.MID
+                        elif difficulty_text == u'困难':
+                            difficulty = ExamQuestion.HARD
+                        else:
+                            raise CustomError(u'第%d行:难度为无效数据' % line)
+
+                        subject = Subject.objects.filter(name=subject, delete=False).first()
+                        if not subject:
+                            raise CustomError(u'第%d行:科目为无效数据' % line)
+                        chapter = Chapter.objects.filter(subject=subject, name=chapter, delete=False).first()
+                        if not chapter:
+                            raise CustomError(u'第%d行:章节为无效数据' % line)
+                        try:
+                            scores = int(scores)
+                        except:
+                            raise CustomError(u'第%d行:分数为无效数据' % line)
+
+                        cur_question = ExamQuestion.objects.create(
+                            chapter=chapter,
+                            type=ExamQuestion.SINGLE,
+                            difficulty=difficulty,
+                            scores=scores,
+                            title=title,
+                            analysis=analysis,
+                            create_user=request.user,
+                            create_time=timezone.now()
+                        )
+                        question_count += 1
+                    if content and not title:
+                        if not cur_question:
+                            continue
+                        if right:
+                            ExamQuestionOption.objects.create(main=cur_question, content=content, right=True)
+                        else:
+                            ExamQuestionOption.objects.create(main=cur_question, content=content)
+
+                    line += 1
+                SysLog.objects.addnew(request.user, SysLog.IMPORT, u"导入[%d]道单选题" % (question_count))
+        except CustomError as e:
+            return response_error(e.get_error_msg())
+        except Exception as e:
+            traceback.print_exc()
+            return response_error(str(e))
+        return response_ok()
+
+    @action(methods=['post'], detail=False)
+    def import_multiple(self, request):
+        file = request.FILES.get('excel_file')
+        status = ExcelImporter.validity(file)
+        if not status['success']:
+            return response_error(status['errors'])
+
+        data = status['data']
+        question_count = 0
+        line = 2
+        try:
+            with transaction.atomic():
+                cur_question = None
+                for row in data:
+                    title = row['试题内容']
+                    subject = row['科目']
+                    chapter = row['章节']
+                    difficulty_text = row['难度']
+                    scores = row['分数']
+                    analysis = row['解析']
+                    content = row['选项']
+                    right = row['正确答案']
+
+                    if title and content:
+                        raise CustomError(u'第%d行:数据格式不正确' % line)
+                    if title and not content:
+                        if not subject:
+                            raise CustomError(u'第%d行:科目不能为空' % line)
+                        if not chapter:
+                            raise CustomError(u'第%d行:章节不能为空' % line)
+                        if not difficulty_text:
+                            raise CustomError(u'第%d行:难度不能为空' % line)
+                        if not scores:
+                            raise CustomError(u'第%d行:分数不能为空' % line)
+
+                        if difficulty_text == u'简单':
+                            difficulty = ExamQuestion.SIMPLE
+                        elif difficulty_text == u'中等':
+                            difficulty = ExamQuestion.MID
+                        elif difficulty_text == u'困难':
+                            difficulty = ExamQuestion.HARD
+                        else:
+                            raise CustomError(u'第%d行:难度为无效数据' % line)
+
+                        subject = Subject.objects.filter(name=subject, delete=False).first()
+                        if not subject:
+                            raise CustomError(u'第%d行:科目为无效数据' % line)
+                        chapter = Chapter.objects.filter(subject=subject, name=chapter, delete=False).first()
+                        if not chapter:
+                            raise CustomError(u'第%d行:章节为无效数据' % line)
+                        try:
+                            scores = int(scores)
+                        except:
+                            raise CustomError(u'第%d行:分数为无效数据' % line)
+
+                        cur_question = ExamQuestion.objects.create(
+                            chapter=chapter,
+                            type=ExamQuestion.MULTIPLE,
+                            difficulty=difficulty,
+                            scores=scores,
+                            title=title,
+                            analysis=analysis,
+                            create_user=request.user,
+                            create_time=timezone.now()
+                        )
+                        question_count += 1
+                    if content and not title:
+                        if not cur_question:
+                            continue
+                        if right:
+                            ExamQuestionOption.objects.create(main=cur_question, content=content, right=True)
+                        else:
+                            ExamQuestionOption.objects.create(main=cur_question, content=content)
+
+                    line += 1
+                SysLog.objects.addnew(request.user, SysLog.IMPORT, u"导入[%d]道多选题" % (question_count))
+        except CustomError as e:
+            return response_error(e.get_error_msg())
+        except Exception as e:
+            traceback.print_exc()
+            return response_error(str(e))
+        return response_ok()
+
+    @action(methods=['post'], detail=False)
+    def import_fill(self, request):
+        file = request.FILES.get('excel_file')
+        status = ExcelImporter.validity(file)
+        if not status['success']:
+            return response_error(status['errors'])
+
+        data = status['data']
+        question_count = 0
+        line = 2
+        try:
+            with transaction.atomic():
+                cur_question = None
+                order = 0
+                for row in data:
+                    title = row['试题内容']
+                    subject = row['科目']
+                    chapter = row['章节']
+                    difficulty_text = row['难度']
+                    scores = row['分数']
+                    analysis = row['解析']
+                    content = row['答案']
+
+                    if title and not content:
+                        raise CustomError(u'第%d行:答案不能为空' % line)
+                    if title and content:
+                        if not subject:
+                            raise CustomError(u'第%d行:科目不能为空' % line)
+                        if not chapter:
+                            raise CustomError(u'第%d行:章节不能为空' % line)
+                        if not difficulty_text:
+                            raise CustomError(u'第%d行:难度不能为空' % line)
+                        if not scores:
+                            raise CustomError(u'第%d行:分数不能为空' % line)
+
+                        if difficulty_text == u'简单':
+                            difficulty = ExamQuestion.SIMPLE
+                        elif difficulty_text == u'中等':
+                            difficulty = ExamQuestion.MID
+                        elif difficulty_text == u'困难':
+                            difficulty = ExamQuestion.HARD
+                        else:
+                            raise CustomError(u'第%d行:难度为无效数据' % line)
+
+                        subject = Subject.objects.filter(name=subject, delete=False).first()
+                        if not subject:
+                            raise CustomError(u'第%d行:科目为无效数据' % line)
+                        chapter = Chapter.objects.filter(subject=subject, name=chapter, delete=False).first()
+                        if not chapter:
+                            raise CustomError(u'第%d行:章节为无效数据' % line)
+                        try:
+                            scores = int(scores)
+                        except:
+                            raise CustomError(u'第%d行:分数为无效数据' % line)
+
+                        order = 1
+                        cur_question = ExamQuestion.objects.create(
+                            chapter=chapter,
+                            type=ExamQuestion.FILL,
+                            difficulty=difficulty,
+                            scores=scores,
+                            title=title,
+                            analysis=analysis,
+                            create_user=request.user,
+                            create_time=timezone.now()
+                        )
+                        ExamQuestionFill.objects.create(main=cur_question, content=content, order=order)
+                        question_count += 1
+                        order += 1
+                    if content and not title:
+                        if (not cur_question) or (not order):
+                            continue
+                        ExamQuestionFill.objects.create(main=cur_question, content=content, order=order)
+                        order += 1
+
+                    line += 1
+                SysLog.objects.addnew(request.user, SysLog.IMPORT, u"导入[%d]道填空题" % (question_count))
+        except CustomError as e:
+            return response_error(e.get_error_msg())
+        except Exception as e:
+            traceback.print_exc()
+            return response_error(str(e))
+        return response_ok()
+
+    @action(methods=['post'], detail=False)
+    def import_judgment(self, request):
+        file = request.FILES.get('excel_file')
+        status = ExcelImporter.validity(file)
+        if not status['success']:
+            return response_error(status['errors'])
+
+        data = status['data']
+        line = 2
+        try:
+            with transaction.atomic():
+                for row in data:
+                    title = row['试题内容']
+                    subject = row['科目']
+                    chapter = row['章节']
+                    difficulty_text = row['难度']
+                    scores = row['分数']
+                    analysis = row['解析']
+                    judgment_text = row['答案']
+
+                    if not title:
+                        raise CustomError(u'第%d行:试题内容不能为空' % line)
+                    if not subject:
+                        raise CustomError(u'第%d行:科目不能为空' % line)
+                    if not chapter:
+                        raise CustomError(u'第%d行:章节不能为空' % line)
+                    if not difficulty_text:
+                        raise CustomError(u'第%d行:难度不能为空' % line)
+                    if not scores:
+                        raise CustomError(u'第%d行:分数不能为空' % line)
+                    if not judgment_text:
+                        raise CustomError(u'第%d行:答案不能为空' % line)
+
+                    if difficulty_text == u'简单':
+                        difficulty = ExamQuestion.SIMPLE
+                    elif difficulty_text == u'中等':
+                        difficulty = ExamQuestion.MID
+                    elif difficulty_text == u'困难':
+                        difficulty = ExamQuestion.HARD
+                    else:
+                        raise CustomError(u'第%d行:难度为无效数据' % line)
+
+                    if judgment_text == u'正确':
+                        judgment = True
+                    elif judgment_text == u'错误':
+                        judgment = False
+                    else:
+                        raise CustomError(u'第%d行:答案为无效数据' % line)
+
+                    subject = Subject.objects.filter(name=subject, delete=False).first()
+                    if not subject:
+                        raise CustomError(u'第%d行:科目为无效数据' % line)
+                    chapter = Chapter.objects.filter(subject=subject, name=chapter, delete=False).first()
+                    if not chapter:
+                        raise CustomError(u'第%d行:章节为无效数据' % line)
+                    try:
+                        scores = int(scores)
+                    except:
+                        raise CustomError(u'第%d行:分数为无效数据' % line)
+
+                    ExamQuestion.objects.create(
+                        chapter=chapter,
+                        type=ExamQuestion.JUDGMENT,
+                        difficulty=difficulty,
+                        scores=scores,
+                        title=title,
+                        judgment=judgment,
+                        analysis=analysis,
+                        create_user=request.user,
+                        create_time=timezone.now()
+                    )
+
+                    line += 1
+                SysLog.objects.addnew(request.user, SysLog.IMPORT, u"导入[%d]道判断题" % (line - 2))
+        except CustomError as e:
+            return response_error(e.get_error_msg())
+        except Exception as e:
+            traceback.print_exc()
+            return response_error(str(e))
+        return response_ok()
+
 class ExamQuestionFeedbackViewSet(ReadOnlyModelViewSet):
     permission_classes = [IsAdministrator, ]
     queryset = ExamQuestionFeedback.objects.filter().order_by('status', '-id')

+ 2 - 0
apps/system/models.py

@@ -27,10 +27,12 @@ class SysLog(models.Model):
     INSERT = 1
     UPDATE = 2
     DELETE = 3
+    IMPORT = 4
     TYPE_CHOICES = (
         (INSERT, u'添加'),
         (UPDATE, u'修改'),
         (DELETE, u'删除'),
+        (IMPORT, u'导入'),
     )
     TYPE_JSON = [{'id': item[0], 'value': item[1]} for item in TYPE_CHOICES]
 

+ 1 - 0
ks/settings.py

@@ -165,6 +165,7 @@ USE_TZ = False
 # https://docs.djangoproject.com/en/2.2/howto/static-files/
 
 STATIC_URL = '/static/'
+STATICFILES_DIRS = (os.path.join(BASE_DIR, "static"),)
 
 MEDIA_URL = '/media/'
 MEDIA_ROOT = os.path.join(BASE_DIR, "uis/media/")

+ 3 - 1
requirements

@@ -2,4 +2,6 @@ django==2.2.5
 django-filter
 djangorestframework
 djangorestframework-jwt
-django-cors-headers
+django-cors-headers
+tablib==3.1.0
+openpyxl==3.0.9

BIN
static/xls/判断题导入模版.xlsx


BIN
static/xls/单选题导入模版.xlsx


BIN
static/xls/填空题导入模版.xlsx


BIN
static/xls/多选题导入模版.xlsx


+ 115 - 2
uis/admin/examquestion/index.html

@@ -26,7 +26,7 @@
         }
 
         .LAY-btns .layui-nav .layui-nav-item {
-            line-height: 40px;
+            line-height: 30px;
         }
 
         .LAY-btns .layui-nav .layui-nav-child {
@@ -67,6 +67,28 @@
                             <button class="layui-btn layui-btn-sm" id="exam_question_add">
                                 <i class="layui-icon layui-icon-add-circle"></i>添加
                             </button>
+                              <button class="layui-nav">
+                                <div class="layui-nav-item">
+                                  <a href="javascript:;" style="color:#fff;" >导入</a>
+                                  <dl class="layui-nav-child"> <!-- 二级菜单 -->
+                                    <dd><a href="#" id="btn_add_single">单选题</a></dd>
+                                    <dd><a href="#" id="btn_add_multiple">多选题</a></dd>
+                                    <dd><a href="#" id="btn_add_fill">填空题</a></dd>
+                                    <dd><a href="#" id="btn_add_judgment">判断题</a></dd>
+                                  </dl>
+                                </div>
+                              </button>
+                            <button class="layui-nav">
+                                <div class="layui-nav-item">
+                                  <a href="javascript:;" style="color:#fff;" >下载模版</a>
+                                  <dl class="layui-nav-child"> <!-- 二级菜单 -->
+                                    <dd><a href="#" id="btn_download_single">单选题模版</a></dd>
+                                    <dd><a href="#" id="btn_download_multiple">多选题模版</a></dd>
+                                    <dd><a href="#" id="btn_download_fill">填空题模版</a></dd>
+                                    <dd><a href="#" id="btn_download_judgment">判断题模版</a></dd>
+                                  </dl>
+                                </div>
+                              </button>
                         </div>
                         <form class="layui-form" lay-filter="query-form-element">
                             <div class="seach_items">
@@ -109,10 +131,11 @@
         base: '../../../layuiadmin/' //静态资源所在路径
     }).extend({
         index: 'lib/index' //主入口模块
-    }).use(['index', 'table', 'form', 'admin',], function () {
+    }).use(['index', 'table', 'form', 'admin', 'upload', ], function () {
         var $ = layui.$;
         var table = layui.table
             , admin = layui.admin
+            ,upload = layui.upload
             , form = layui.form;
 
         table.render({
@@ -211,6 +234,96 @@
                 content: 'edit.html'
             });
         });
+
+        upload.render({
+            elem: '#btn_add_single'
+            ,url: '/admin/examquestion/import_single/'
+            ,accept:'file'
+            ,acceptMime:'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
+            ,exts:'xlsx'
+            ,field:'excel_file'
+            ,multiple:false
+            ,done: function(res){
+                if (res.code == 0) {
+                    table.reload('exam_question_datagrid',{});
+                } else {
+                    layer.msg(res.msg);
+                }
+            }
+            ,error: function(){
+              layer.msg('导入失败');
+            }
+        });
+        upload.render({
+            elem: '#btn_add_multiple'
+            ,url: '/admin/examquestion/import_multiple/'
+            ,accept:'file'
+            ,acceptMime:'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
+            ,exts:'xlsx'
+            ,field:'excel_file'
+            ,multiple:false
+            ,done: function(res){
+                if (res.code == 0) {
+                    table.reload('exam_question_datagrid',{});
+                } else {
+                    layer.msg(res.msg);
+                }
+            }
+            ,error: function(){
+              layer.msg('导入失败');
+            }
+        });
+        upload.render({
+            elem: '#btn_add_fill'
+            ,url: '/admin/examquestion/import_fill/'
+            ,accept:'file'
+            ,acceptMime:'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
+            ,exts:'xlsx'
+            ,field:'excel_file'
+            ,multiple:false
+            ,done: function(res){
+                if (res.code == 0) {
+                    table.reload('exam_question_datagrid',{});
+                } else {
+                    layer.msg(res.msg);
+                }
+            }
+            ,error: function(){
+              layer.msg('导入失败');
+            }
+        });
+        upload.render({
+            elem: '#btn_add_judgment'
+            ,url: '/admin/examquestion/import_judgment/'
+            ,accept:'file'
+            ,acceptMime:'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
+            ,exts:'xlsx'
+            ,field:'excel_file'
+            ,multiple:false
+            ,done: function(res){
+                if (res.code == 0) {
+                    table.reload('exam_question_datagrid',{});
+                } else {
+                    layer.msg(res.msg);
+                }
+            }
+            ,error: function(){
+              layer.msg('导入失败');
+            }
+        });
+
+        $('#btn_download_single').on('click', function () {
+            layui.view.download("/static/xls/单选题导入模版.xlsx");
+        });
+        $('#btn_download_multiple').on('click', function () {
+            layui.view.download("/static/xls/多选题导入模版.xlsx");
+        });
+        $('#btn_download_fill').on('click', function () {
+            layui.view.download("/static/xls/填空题导入模版.xlsx");
+        });
+        $('#btn_download_judgment').on('click', function () {
+            layui.view.download("/static/xls/判断题导入模版.xlsx");
+        });
     });
 
 </script>

+ 1 - 0
uis/admin/log/index.html

@@ -81,6 +81,7 @@
                                     <option value="1">添加</option>
                                     <option value="2">修改</option>
                                     <option value="3">删除</option>
+                                    <option value="4">导入</option>
                                 </select>
                             </div>
                             <div class="seach_items">

+ 34 - 0
utils/format.py

@@ -3,6 +3,11 @@
 import datetime
 from datetime import timedelta
 
+import traceback
+import tablib
+
+from openpyxl.utils.exceptions import InvalidFileException
+
 def strfdate(d):
     if d:
         return d.strftime('%Y-%m-%d')
@@ -48,3 +53,32 @@ def clean_datetime_range(data, fieldname):
         data[fieldname+'_before'] = t[1] + ' 23:59:59'
         data.pop(fieldname)
     return data
+
+
+class ExcelImporter():
+    @staticmethod
+    def validity(file):
+        status = {
+            'success': True,
+            'errors': '',
+            'data': []
+        }
+        if file == None:
+            status['success'] = False
+            status['errors'] = u"请上传数据文件"
+        else:
+            try:
+                data = tablib.import_set(file, format='xlsx').dict
+                if len(data) == 0:
+                    status['success'] = False
+                    status['errors'] = u"上传的文件内没有发现数据"
+                else:
+                    status['data'] = data
+            except InvalidFileException:
+                status['success'] = False
+                status['errors'] = u"请上传<strong>xlsx</strong>格式的数据文件,老版本的xls格式不被支持!"
+            except Exception as e:
+                traceback.print_exc()
+                status['success'] = False
+                status['errors'] = u"导入失败"
+        return status