# coding=utf-8 import itertools import string from django.db import models from django.utils import timezone from django.conf import settings from utils.exceptions import CustomError from apps.foundation.models import Chapter class ExamQuestion(models.Model): SINGLE = 1 MULTIPLE = 2 FILL = 3 JUDGMENT = 4 DISCUSS = 5 TYPE_CHOICES = ( (SINGLE, u'单选题'), (MULTIPLE, u'多选题'), (FILL, u'填空题'), (JUDGMENT, u'判断题'), (DISCUSS, u'论述题'), ) TYPE_JSON = [{'id': item[0], 'value': item[1]} for item in TYPE_CHOICES] SIMPLE = 1 MID = 2 HARD = 3 DIFFICULTY_CHOICES = ( (SIMPLE, u'简单'), (MID, u'中等'), (HARD, u'困难'), ) DIFFICULTY_JSON = [{'id': item[0], 'value': item[1]} for item in DIFFICULTY_CHOICES] chapter = models.ForeignKey(Chapter, verbose_name=u"章节", on_delete=models.PROTECT) type = models.PositiveSmallIntegerField(choices=TYPE_CHOICES, verbose_name=u"题型") difficulty = models.PositiveSmallIntegerField(choices=DIFFICULTY_CHOICES, verbose_name=u"难度") scores = models.IntegerField(verbose_name=u'分数') title = models.TextField(verbose_name=u"题目") judgment = models.BooleanField(verbose_name=u'判断题答案', default=False) analysis = models.TextField(verbose_name=u"解析", null=True, blank=True) discuss_answer = models.TextField(verbose_name=u"论述题答案", null=True, blank=True) create_user = models.ForeignKey(settings.AUTH_USER_MODEL, verbose_name=u'录入人',related_name='exam_question_user', editable=False, on_delete=models.PROTECT) create_time = models.DateTimeField(verbose_name=u"录入时间", default=timezone.now, editable=False) delete = models.BooleanField(verbose_name=u'删除', default=False, editable=False) class Meta: db_table = "exam_question" ordering = ['-id'] verbose_name = u"试题管理" default_permissions = () @classmethod def batch_create(cls, subject, data, user): for row in data: # print("=====>", row) num = row.get('num', None) if not num: raise CustomError('序号不能为空') if not num.isdigit(): raise CustomError('序号必须为数字') chapter = row.get('chapter', None) if not chapter: raise CustomError(f'题目{num}章节不能为空') else: chapter = Chapter.objects.filter(subject=subject, name=chapter).first() if not chapter: raise CustomError(f'题目{num}章节不存在') title = row.get('title', None) if not title: raise CustomError(f'题目{num}题文不能为空') type = row.get('type', None) if not type: raise CustomError(f'题目{num}题型不能为空') found = False for item in ExamQuestion.TYPE_CHOICES: if item[1] == type: type = item[0] found = True break if not found: raise CustomError(f'题目{num}题型不存在,可选类型有:填空题、单选题、多选题、判断题、论述题') difficulty = row.get('difficulty', None) if not difficulty: raise CustomError(f'题目{num}题型不能为空') found = False for item in ExamQuestion.DIFFICULTY_CHOICES: if item[1] == difficulty: difficulty = item[0] found = True break if not found: raise CustomError(f'题目{num}难度无效,可选类型有:简单、中等、困难') score = row.get('score', None) if not score: raise CustomError(f'题目{num}分数不能为空') if not score.isdigit(): raise CustomError(f'题目{num}分数必须为数字') answer = row.get('answer', None) if not answer: raise CustomError(f'题目{num}答案不能为空') if type == ExamQuestion.JUDGMENT and not answer in ('正确', '错误',): raise CustomError(f'题目{num}答案无效,可选类型有:正确、错误') images = row.get('images', None) if images: for image in images: img = f'' title = title + '
' + img judgment = False if type == ExamQuestion.JUDGMENT: judgment = answer == '正确' discuss_answer = '' if type == ExamQuestion.DISCUSS: discuss_answer = answer analysis = row.get('analysis', None) question = cls.objects.create( title=title, type=type, difficulty=difficulty, scores=score, discuss_answer=discuss_answer, judgment=judgment, chapter=chapter, analysis=analysis, create_user=user ) if type == ExamQuestion.SINGLE or type == ExamQuestion.MULTIPLE: options = row.get('options', None) options_data = ExamQuestionOption.validate(type, options, answer) for option in options_data: ExamQuestionOption.objects.create(main=question, content=option['content'], right=option['right']) if type == ExamQuestion.FILL: fill_data = ExamQuestionFill.validate(answer) for item in fill_data: ExamQuestionFill.objects.create(main=question, content=item['content'], order=item['order']) class ExamQuestionOption(models.Model): main = models.ForeignKey(ExamQuestion, verbose_name=u"试题", on_delete=models.PROTECT) content = models.TextField(verbose_name=u"内容") right = models.BooleanField(verbose_name=u'正确答案', default=False) delete = models.BooleanField(verbose_name=u'删除', default=False, editable=False) class Meta: db_table = "exam_question_option" ordering = ['id'] verbose_name = u"选择题选项" # 单选、多选 default_permissions = () @classmethod def validate(cls, type, options, answer): # 检查选项有效性 if not options: raise CustomError(f'选项不能为空') if len(options) < 2: raise CustomError(f'选项不能少于2个') if len(options) > 10: raise CustomError(f'选项不能多于10个') # 处理答案 answers = list(answer) # 转换答案为列表,支持多个字符 answers_len = len(answers) # 单选题只能选择一个答案 if type == ExamQuestion.SINGLE and answers_len > 1: raise CustomError(f'单选题答案只能有一个') # 验证答案中的字母是否合法 alphabet = string.ascii_uppercase # 包含 A 到 Z 的字母序列 valid_answers = set(alphabet[:len(options)]) # 只考虑合法的字母选项 if not all(answer in valid_answers for answer in answers): raise CustomError(f'答案选项无效,可选项有:{", ".join(valid_answers)}') # 生成验证数据 validation_data = [] for index, option in enumerate(options): item = { 'content': option, 'right': False } if chr(ord('A') + index) in answers: # 使用字母索引进行匹配 item['right'] = True validation_data.append(item) return validation_data class ExamQuestionFill(models.Model): main = models.ForeignKey(ExamQuestion, verbose_name=u"试题", on_delete=models.PROTECT) content = models.TextField(verbose_name=u"内容") order = models.IntegerField(verbose_name=u'排序') delete = models.BooleanField(verbose_name=u'删除', default=False, editable=False) class Meta: db_table = "exam_question_fill" ordering = ['order', 'id'] verbose_name = u"试题填空" default_permissions = () @classmethod def validate(cls, answer): answers = answer.split('$') validation_data = [] index = 1 for option in answers: item = { 'content': option, 'order': index } index += 1 validation_data.append(item) return validation_data class ExamQuestionFeedback(models.Model): TYPE = 1 ANSWER = 2 ANALYSIS = 3 OTHER = 4 TYPE_CHOICES = ( (TYPE, u'题目类型'), (ANSWER, u'题目答案'), (ANALYSIS, u'题目解析'), (OTHER, u'其它'), ) TYPE_JSON = [{'id': item[0], 'value': item[1]} for item in TYPE_CHOICES] UNTREATED = 1 PROCESSED = 2 STATUS_CHOICES = ( (UNTREATED, u'未处理'), (PROCESSED, u'已处理'), ) STATUS_JSON = [{'id': item[0], 'value': item[1]} for item in STATUS_CHOICES] main = models.ForeignKey(ExamQuestion, verbose_name=u"试题", on_delete=models.PROTECT) type = models.PositiveSmallIntegerField(choices=TYPE_CHOICES, verbose_name=u"错误类型") desc = models.TextField(verbose_name=u"错误描述", null=True, blank=True) create_user = models.ForeignKey(settings.AUTH_USER_MODEL, verbose_name=u'反馈人',related_name='exam_question_feedback_user', editable=False, on_delete=models.PROTECT) create_time = models.DateTimeField(verbose_name=u"反馈时间", default=timezone.now, editable=False) status = models.PositiveSmallIntegerField(choices=STATUS_CHOICES, verbose_name=u'处理状态', default=UNTREATED) process_user = models.ForeignKey(settings.AUTH_USER_MODEL, verbose_name=u"处理人", on_delete=models.PROTECT, null=True, related_name="exam_question_feedback_ref_process_user") process_time = models.DateTimeField(verbose_name=u'处理时间', null=True) class Meta: db_table = "exam_question_feedback" ordering = ['-id'] verbose_name = u"错误反馈" default_permissions = () @staticmethod def getById(id): instance = ExamQuestionFeedback.objects.filter(pk=id).first() if not instance: raise CustomError(u'未找到相应的错误反馈') return instance def process(self, user): self.process_user = user self.status = ExamQuestionFeedback.PROCESSED self.process_time = timezone.now()