models.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277
  1. # coding=utf-8
  2. import itertools
  3. import string
  4. from django.db import models
  5. from django.utils import timezone
  6. from django.conf import settings
  7. from utils.exceptions import CustomError
  8. from apps.foundation.models import Chapter
  9. class ExamQuestion(models.Model):
  10. SINGLE = 1
  11. MULTIPLE = 2
  12. FILL = 3
  13. JUDGMENT = 4
  14. DISCUSS = 5
  15. TYPE_CHOICES = (
  16. (SINGLE, u'单选题'),
  17. (MULTIPLE, u'多选题'),
  18. (FILL, u'填空题'),
  19. (JUDGMENT, u'判断题'),
  20. (DISCUSS, u'论述题'),
  21. )
  22. TYPE_JSON = [{'id': item[0], 'value': item[1]} for item in TYPE_CHOICES]
  23. SIMPLE = 1
  24. MID = 2
  25. HARD = 3
  26. DIFFICULTY_CHOICES = (
  27. (SIMPLE, u'简单'),
  28. (MID, u'中等'),
  29. (HARD, u'困难'),
  30. )
  31. DIFFICULTY_JSON = [{'id': item[0], 'value': item[1]} for item in DIFFICULTY_CHOICES]
  32. chapter = models.ForeignKey(Chapter, verbose_name=u"章节", on_delete=models.PROTECT)
  33. type = models.PositiveSmallIntegerField(choices=TYPE_CHOICES, verbose_name=u"题型")
  34. difficulty = models.PositiveSmallIntegerField(choices=DIFFICULTY_CHOICES, verbose_name=u"难度")
  35. scores = models.IntegerField(verbose_name=u'分数')
  36. title = models.TextField(verbose_name=u"题目")
  37. judgment = models.BooleanField(verbose_name=u'判断题答案', default=False)
  38. analysis = models.TextField(verbose_name=u"解析", null=True, blank=True)
  39. discuss_answer = models.TextField(verbose_name=u"论述题答案", null=True, blank=True)
  40. create_user = models.ForeignKey(settings.AUTH_USER_MODEL, verbose_name=u'录入人',related_name='exam_question_user', editable=False, on_delete=models.PROTECT)
  41. create_time = models.DateTimeField(verbose_name=u"录入时间", default=timezone.now, editable=False)
  42. delete = models.BooleanField(verbose_name=u'删除', default=False, editable=False)
  43. class Meta:
  44. db_table = "exam_question"
  45. ordering = ['-id']
  46. verbose_name = u"试题管理"
  47. default_permissions = ()
  48. @classmethod
  49. def batch_create(cls, subject, data, user):
  50. for row in data:
  51. # print("=====>", row)
  52. num = row.get('num', None)
  53. if not num:
  54. raise CustomError('序号不能为空')
  55. if not num.isdigit():
  56. raise CustomError('序号必须为数字')
  57. chapter = row.get('chapter', None)
  58. if not chapter:
  59. raise CustomError(f'题目{num}章节不能为空')
  60. else:
  61. chapter = Chapter.objects.filter(subject=subject, name=chapter).first()
  62. if not chapter:
  63. raise CustomError(f'题目{num}章节不存在')
  64. title = row.get('title', None)
  65. if not title:
  66. raise CustomError(f'题目{num}题文不能为空')
  67. type = row.get('type', None)
  68. if not type:
  69. raise CustomError(f'题目{num}题型不能为空')
  70. found = False
  71. for item in ExamQuestion.TYPE_CHOICES:
  72. if item[1] == type:
  73. type = item[0]
  74. found = True
  75. break
  76. if not found:
  77. raise CustomError(f'题目{num}题型不存在,可选类型有:填空题、单选题、多选题、判断题、论述题')
  78. difficulty = row.get('difficulty', None)
  79. if not difficulty:
  80. raise CustomError(f'题目{num}题型不能为空')
  81. found = False
  82. for item in ExamQuestion.DIFFICULTY_CHOICES:
  83. if item[1] == difficulty:
  84. difficulty = item[0]
  85. found = True
  86. break
  87. if not found:
  88. raise CustomError(f'题目{num}难度无效,可选类型有:简单、中等、困难')
  89. score = row.get('score', None)
  90. if not score:
  91. raise CustomError(f'题目{num}分数不能为空')
  92. if not score.isdigit():
  93. raise CustomError(f'题目{num}分数必须为数字')
  94. answer = row.get('answer', None)
  95. if not answer:
  96. raise CustomError(f'题目{num}答案不能为空')
  97. if type == ExamQuestion.JUDGMENT and not answer in ('正确', '错误',):
  98. raise CustomError(f'题目{num}答案无效,可选类型有:正确、错误')
  99. images = row.get('images', None)
  100. if images:
  101. for image in images:
  102. img = f'<img src="data:image/png;base64,{image}" />'
  103. title = title + '<br>' + img
  104. judgment = False
  105. if type == ExamQuestion.JUDGMENT:
  106. judgment = answer == '正确'
  107. discuss_answer = ''
  108. if type == ExamQuestion.DISCUSS:
  109. discuss_answer = answer
  110. analysis = row.get('analysis', None)
  111. question = cls.objects.create(
  112. title=title,
  113. type=type,
  114. difficulty=difficulty,
  115. scores=score,
  116. discuss_answer=discuss_answer,
  117. judgment=judgment,
  118. chapter=chapter,
  119. analysis=analysis,
  120. create_user=user
  121. )
  122. if type == ExamQuestion.SINGLE or type == ExamQuestion.MULTIPLE:
  123. options = row.get('options', None)
  124. options_data = ExamQuestionOption.validate(type, options, answer)
  125. for option in options_data:
  126. ExamQuestionOption.objects.create(main=question, content=option['content'], right=option['right'])
  127. if type == ExamQuestion.FILL:
  128. fill_data = ExamQuestionFill.validate(answer)
  129. for item in fill_data:
  130. ExamQuestionFill.objects.create(main=question, content=item['content'], order=item['order'])
  131. class ExamQuestionOption(models.Model):
  132. main = models.ForeignKey(ExamQuestion, verbose_name=u"试题", on_delete=models.PROTECT)
  133. content = models.TextField(verbose_name=u"内容")
  134. right = models.BooleanField(verbose_name=u'正确答案', default=False)
  135. delete = models.BooleanField(verbose_name=u'删除', default=False, editable=False)
  136. class Meta:
  137. db_table = "exam_question_option"
  138. ordering = ['id']
  139. verbose_name = u"选择题选项" # 单选、多选
  140. default_permissions = ()
  141. @classmethod
  142. def validate(cls, type, options, answer):
  143. # 检查选项有效性
  144. if not options:
  145. raise CustomError(f'选项不能为空')
  146. if len(options) < 2:
  147. raise CustomError(f'选项不能少于2个')
  148. if len(options) > 10:
  149. raise CustomError(f'选项不能多于10个')
  150. # 处理答案
  151. answers = list(answer) # 转换答案为列表,支持多个字符
  152. answers_len = len(answers)
  153. # 单选题只能选择一个答案
  154. if type == ExamQuestion.SINGLE and answers_len > 1:
  155. raise CustomError(f'单选题答案只能有一个')
  156. # 验证答案中的字母是否合法
  157. alphabet = string.ascii_uppercase # 包含 A 到 Z 的字母序列
  158. valid_answers = set(alphabet[:len(options)]) # 只考虑合法的字母选项
  159. if not all(answer in valid_answers for answer in answers):
  160. raise CustomError(f'答案选项无效,可选项有:{", ".join(valid_answers)}')
  161. # 生成验证数据
  162. validation_data = []
  163. for index, option in enumerate(options):
  164. item = {
  165. 'content': option,
  166. 'right': False
  167. }
  168. if chr(ord('A') + index) in answers: # 使用字母索引进行匹配
  169. item['right'] = True
  170. validation_data.append(item)
  171. return validation_data
  172. class ExamQuestionFill(models.Model):
  173. main = models.ForeignKey(ExamQuestion, verbose_name=u"试题", on_delete=models.PROTECT)
  174. content = models.TextField(verbose_name=u"内容")
  175. order = models.IntegerField(verbose_name=u'排序')
  176. delete = models.BooleanField(verbose_name=u'删除', default=False, editable=False)
  177. class Meta:
  178. db_table = "exam_question_fill"
  179. ordering = ['order', 'id']
  180. verbose_name = u"试题填空"
  181. default_permissions = ()
  182. @classmethod
  183. def validate(cls, answer):
  184. answers = answer.split('$')
  185. validation_data = []
  186. index = 1
  187. for option in answers:
  188. item = {
  189. 'content': option,
  190. 'order': index
  191. }
  192. index += 1
  193. validation_data.append(item)
  194. return validation_data
  195. class ExamQuestionFeedback(models.Model):
  196. TYPE = 1
  197. ANSWER = 2
  198. ANALYSIS = 3
  199. OTHER = 4
  200. TYPE_CHOICES = (
  201. (TYPE, u'题目类型'),
  202. (ANSWER, u'题目答案'),
  203. (ANALYSIS, u'题目解析'),
  204. (OTHER, u'其它'),
  205. )
  206. TYPE_JSON = [{'id': item[0], 'value': item[1]} for item in TYPE_CHOICES]
  207. UNTREATED = 1
  208. PROCESSED = 2
  209. STATUS_CHOICES = (
  210. (UNTREATED, u'未处理'),
  211. (PROCESSED, u'已处理'),
  212. )
  213. STATUS_JSON = [{'id': item[0], 'value': item[1]} for item in STATUS_CHOICES]
  214. main = models.ForeignKey(ExamQuestion, verbose_name=u"试题", on_delete=models.PROTECT)
  215. type = models.PositiveSmallIntegerField(choices=TYPE_CHOICES, verbose_name=u"错误类型")
  216. desc = models.TextField(verbose_name=u"错误描述", null=True, blank=True)
  217. create_user = models.ForeignKey(settings.AUTH_USER_MODEL, verbose_name=u'反馈人',related_name='exam_question_feedback_user', editable=False, on_delete=models.PROTECT)
  218. create_time = models.DateTimeField(verbose_name=u"反馈时间", default=timezone.now, editable=False)
  219. status = models.PositiveSmallIntegerField(choices=STATUS_CHOICES, verbose_name=u'处理状态', default=UNTREATED)
  220. 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")
  221. process_time = models.DateTimeField(verbose_name=u'处理时间', null=True)
  222. class Meta:
  223. db_table = "exam_question_feedback"
  224. ordering = ['-id']
  225. verbose_name = u"错误反馈"
  226. default_permissions = ()
  227. @staticmethod
  228. def getById(id):
  229. instance = ExamQuestionFeedback.objects.filter(pk=id).first()
  230. if not instance:
  231. raise CustomError(u'未找到相应的错误反馈')
  232. return instance
  233. def process(self, user):
  234. self.process_user = user
  235. self.status = ExamQuestionFeedback.PROCESSED
  236. self.process_time = timezone.now()