models.py 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365
  1. # coding=utf-8
  2. import random
  3. import re
  4. import base64
  5. from io import BytesIO
  6. from PIL import Image
  7. from django.db import models
  8. from django.utils import timezone
  9. from django.conf import settings
  10. from docx import Document
  11. from docx.shared import Inches, Pt
  12. from docx.enum.text import WD_PARAGRAPH_ALIGNMENT
  13. from docx.oxml.ns import qn
  14. from docx.oxml import OxmlElement
  15. from bs4 import BeautifulSoup
  16. from utils.exceptions import CustomError
  17. from apps.foundation.models import Subject
  18. from apps.examination.examquestion.models import ExamQuestion, ExamQuestionOption
  19. class ExamPaper(models.Model):
  20. MOCK = 1
  21. FORMAL = 2
  22. RANDOM = 3
  23. TYPE_CHOICES = (
  24. (MOCK, u'模拟试卷'),
  25. (FORMAL, u'正式试卷'),
  26. (RANDOM, u'随机试卷'),
  27. )
  28. TYPE_JSON = [{'id': item[0], 'value': item[1]} for item in TYPE_CHOICES]
  29. name = models.CharField(max_length=200, verbose_name=u"名称")
  30. subject = models.ForeignKey(Subject, verbose_name=u"科目", on_delete=models.PROTECT)
  31. type = models.PositiveSmallIntegerField(choices=TYPE_CHOICES, verbose_name=u"类型")
  32. passline = models.IntegerField(verbose_name=u'及格线')
  33. desc = models.TextField(verbose_name=u"备注", null=True, blank=True)
  34. single_simple_count = models.IntegerField(verbose_name=u'简单单选题数量', default=0)
  35. multiple_simple_count = models.IntegerField(verbose_name=u'简单多选题数量', default=0)
  36. fill_simple_count = models.IntegerField(verbose_name=u'简单填空题数量', default=0)
  37. judgment_simple_count = models.IntegerField(verbose_name=u'简单判断题数量', default=0)
  38. discuss_simple_count = models.IntegerField(verbose_name=u'简单论述题数量', default=0)
  39. single_mid_count = models.IntegerField(verbose_name=u'中等单选题数量', default=0)
  40. multiple_mid_count = models.IntegerField(verbose_name=u'中等多选题数量', default=0)
  41. fill_mid_count = models.IntegerField(verbose_name=u'中等填空题数量', default=0)
  42. judgment_mid_count = models.IntegerField(verbose_name=u'中等判断题数量', default=0)
  43. discuss_mid_count = models.IntegerField(verbose_name=u'中等论述题数量', default=0)
  44. single_hard_count = models.IntegerField(verbose_name=u'困难单选题数量', default=0)
  45. multiple_hard_count = models.IntegerField(verbose_name=u'困难多选题数量', default=0)
  46. fill_hard_count = models.IntegerField(verbose_name=u'困难填空题数量', default=0)
  47. judgment_hard_count = models.IntegerField(verbose_name=u'困难判断题数量', default=0)
  48. discuss_hard_count = models.IntegerField(verbose_name=u'困难论述题数量', default=0)
  49. single_scores = models.IntegerField(verbose_name=u'单选题单题分数', default=0)
  50. multiple_scores = models.IntegerField(verbose_name=u'多选题单题分数', default=0)
  51. fill_scores = models.IntegerField(verbose_name=u'填空题单题分数', default=0)
  52. judgment_scores = models.IntegerField(verbose_name=u'判断题单题分数', default=0)
  53. discuss_scores = models.IntegerField(verbose_name=u'论述题单题分数', default=0)
  54. single_total_count = models.IntegerField(verbose_name=u'单选题总数量', default=0, editable=False)
  55. multiple_total_count = models.IntegerField(verbose_name=u'多选题总数量', default=0, editable=False)
  56. fill_total_count = models.IntegerField(verbose_name=u'填空题总数量', default=0, editable=False)
  57. judgment_total_count = models.IntegerField(verbose_name=u'判断题总数量', default=0, editable=False)
  58. discuss_total_count = models.IntegerField(verbose_name=u'论述题总数量', default=0, editable=False)
  59. single_total_scores = models.IntegerField(verbose_name=u'单选题总分数', default=0, editable=False)
  60. multiple_total_scores = models.IntegerField(verbose_name=u'多选题总分数', default=0, editable=False)
  61. fill_total_scores = models.IntegerField(verbose_name=u'填空题总分数', default=0, editable=False)
  62. judgment_total_scores = models.IntegerField(verbose_name=u'判断题总分数', default=0, editable=False)
  63. discuss_total_scores = models.IntegerField(verbose_name=u'论述题总分数', default=0, editable=False)
  64. question_total_count = models.IntegerField(verbose_name=u'试题总数量', default=0, editable=False)
  65. question_total_scores = models.IntegerField(verbose_name=u'试题总分数', default=0, editable=False)
  66. create_user = models.ForeignKey(settings.AUTH_USER_MODEL, verbose_name=u'添加人', editable=False, on_delete=models.PROTECT)
  67. create_time = models.DateTimeField(verbose_name=u"添加时间", default=timezone.now, editable=False)
  68. delete = models.BooleanField(verbose_name=u'删除', default=False, editable=False)
  69. did_count = models.IntegerField(verbose_name=u'做过人数', default=0, editable=False)
  70. class Meta:
  71. db_table = "exam_paper"
  72. ordering = ['-id']
  73. verbose_name = u"试卷管理"
  74. default_permissions = ()
  75. @staticmethod
  76. def getById(id):
  77. instance = ExamPaper.objects.filter(pk=id).first()
  78. if not instance:
  79. raise CustomError(u'未找到相应的试卷')
  80. return instance
  81. def generate_passline(self):
  82. self.passline = int(self.question_total_scores * 0.6)
  83. def clear_detail(self):
  84. ExamPaperDetail.objects.filter(main=self).update(delete=True)
  85. def generate_detail(self):
  86. begin_order = 1
  87. if self.single_simple_count:
  88. self._generate_detail(self.single_simple_count, ExamQuestion.SINGLE, ExamQuestion.SIMPLE, begin_order)
  89. begin_order += self.single_simple_count
  90. if self.single_mid_count:
  91. self._generate_detail(self.single_mid_count, ExamQuestion.SINGLE, ExamQuestion.MID, begin_order)
  92. begin_order += self.single_mid_count
  93. if self.single_hard_count:
  94. self._generate_detail(self.single_hard_count, ExamQuestion.SINGLE, ExamQuestion.HARD, begin_order)
  95. begin_order += self.single_hard_count
  96. if self.multiple_simple_count:
  97. self._generate_detail(self.multiple_simple_count, ExamQuestion.MULTIPLE, ExamQuestion.SIMPLE, begin_order)
  98. begin_order += self.multiple_simple_count
  99. if self.multiple_mid_count:
  100. self._generate_detail(self.multiple_mid_count, ExamQuestion.MULTIPLE, ExamQuestion.MID, begin_order)
  101. begin_order += self.multiple_mid_count
  102. if self.multiple_hard_count:
  103. self._generate_detail(self.multiple_hard_count, ExamQuestion.MULTIPLE, ExamQuestion.HARD, begin_order)
  104. begin_order += self.multiple_hard_count
  105. if self.fill_simple_count:
  106. self._generate_detail(self.fill_simple_count, ExamQuestion.FILL, ExamQuestion.SIMPLE, begin_order)
  107. begin_order += self.fill_simple_count
  108. if self.fill_mid_count:
  109. self._generate_detail(self.fill_mid_count, ExamQuestion.FILL, ExamQuestion.MID, begin_order)
  110. begin_order += self.fill_mid_count
  111. if self.fill_hard_count:
  112. self._generate_detail(self.fill_hard_count, ExamQuestion.FILL, ExamQuestion.HARD, begin_order)
  113. begin_order += self.fill_hard_count
  114. if self.judgment_simple_count:
  115. self._generate_detail(self.judgment_simple_count, ExamQuestion.JUDGMENT, ExamQuestion.SIMPLE, begin_order)
  116. begin_order += self.judgment_simple_count
  117. if self.judgment_mid_count:
  118. self._generate_detail(self.judgment_mid_count, ExamQuestion.JUDGMENT, ExamQuestion.MID, begin_order)
  119. begin_order += self.judgment_mid_count
  120. if self.judgment_hard_count:
  121. self._generate_detail(self.judgment_hard_count, ExamQuestion.JUDGMENT, ExamQuestion.HARD, begin_order)
  122. begin_order += self.judgment_hard_count
  123. if self.discuss_simple_count:
  124. self._generate_detail(self.discuss_simple_count, ExamQuestion.DISCUSS, ExamQuestion.SIMPLE, begin_order)
  125. begin_order += self.discuss_simple_count
  126. if self.discuss_mid_count:
  127. self._generate_detail(self.discuss_mid_count, ExamQuestion.DISCUSS, ExamQuestion.MID, begin_order)
  128. begin_order += self.discuss_mid_count
  129. if self.discuss_hard_count:
  130. self._generate_detail(self.discuss_hard_count, ExamQuestion.DISCUSS, ExamQuestion.HARD, begin_order)
  131. begin_order += self.discuss_hard_count
  132. def update_count(self):
  133. self.single_total_count = self.single_simple_count + self.single_mid_count + self.single_hard_count
  134. self.multiple_total_count = self.multiple_simple_count + self.multiple_mid_count + self.multiple_hard_count
  135. self.fill_total_count = self.fill_simple_count + self.fill_mid_count + self.fill_hard_count
  136. self.judgment_total_count = self.judgment_simple_count + self.judgment_mid_count + self.judgment_hard_count
  137. self.discuss_total_count = self.discuss_simple_count + self.discuss_mid_count + self.discuss_hard_count
  138. self.single_total_scores = self.single_scores * self.single_total_count
  139. self.multiple_total_scores = self.multiple_scores * self.multiple_total_count
  140. self.fill_total_scores = self.fill_scores * self.fill_total_count
  141. self.judgment_total_scores = self.judgment_scores * self.judgment_total_count
  142. self.discuss_total_scores = self.discuss_scores * self.discuss_total_count
  143. self.question_total_count = self.single_total_count + self.multiple_total_count + self.fill_total_count + self.judgment_total_count + self.discuss_total_count
  144. self.question_total_scores = self.single_total_scores + self.multiple_total_scores + self.fill_total_scores + self.judgment_total_scores+ self.discuss_total_scores
  145. return self
  146. def _generate_detail(self, count, type, difficulty, begin_order):
  147. questions = ExamQuestion.objects.filter(
  148. chapter__subject=self.subject,
  149. difficulty = difficulty,
  150. type = type,
  151. delete=False
  152. )
  153. questions_count = questions.count()
  154. if questions_count < count:
  155. raise CustomError(u'[%s][%s]数量不足!' % (ExamQuestion.DIFFICULTY_CHOICES[difficulty-1][1], ExamQuestion.TYPE_CHOICES[type-1][1]))
  156. question_ids = questions.values_list('id', flat=True)
  157. rows = random.sample(list(question_ids), count)
  158. random.shuffle(rows)
  159. data = []
  160. for row in rows:
  161. item = ExamPaperDetail(
  162. main=self,
  163. question_id=row,
  164. order=begin_order
  165. )
  166. begin_order += 1
  167. data.append(item)
  168. ExamPaperDetail.objects.bulk_create(data)
  169. def _html_to_docx(self, html, doc, para):
  170. soup = BeautifulSoup(html, 'html.parser')
  171. for element in soup.descendants:
  172. if element.name is None: # Text node
  173. run = para.add_run(element)
  174. elif element.name == 'b' or element.name == 'strong':
  175. run = para.add_run(element.get_text())
  176. run.bold = True
  177. elif element.name == 'i' or element.name == 'em':
  178. run = para.add_run(element.get_text())
  179. run.italic = True
  180. elif element.name == 'u':
  181. run = para.add_run(element.get_text())
  182. run.underline = True
  183. elif element.name == 'br':
  184. para.add_run().add_break()
  185. elif element.name == 'p':
  186. para.add_run().add_break()
  187. elif element.name == 'span':
  188. run = para.add_run(element.get_text())
  189. elif element.name == 'img' and element.get('src').startswith('data:image'):
  190. # 如果是 base64 编码的图片
  191. img_data = element.get('src').split(',', 1)[1] # 提取 base64 数据部分
  192. try:
  193. img_bytes = base64.b64decode(img_data)
  194. img_stream = BytesIO(img_bytes)
  195. image = Image.open(img_stream)
  196. # 插入图片到文档中
  197. doc.add_picture(img_stream, width=Inches(4.0)) # 根据需要调整宽度
  198. image.close()
  199. except Exception as e:
  200. print(f"Error inserting image: {e}")
  201. def _add_category_title(self, doc, category, num_index):
  202. num_chars = ('一、', '二、', '三、', '四、', '五、', )
  203. para = doc.add_paragraph()
  204. text = f'{num_chars[num_index]}{category}:本大题共有 {self.single_total_count}小题,每小题{self.single_scores}分,共{self.single_scores * self.single_total_count}分。'
  205. run = para.add_run(text)
  206. run.font.size = Pt(12) # 设置字体大小
  207. run.bold = True # 设置加粗
  208. def _add_title(self, doc, order, title):
  209. if title.startswith('<p>') and title.endswith('</p>'):
  210. title = title[3:-4]
  211. para = doc.add_paragraph()
  212. html_content = f'{order}.{title}'
  213. self._html_to_docx(html_content, doc, para)
  214. def _add_option(self, doc, index, content):
  215. option_chars = ('A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z')
  216. pattern = r'^[A-Z]\..*'
  217. if re.match(pattern, content) is None:
  218. content = f'{option_chars[index]}.{content}'
  219. para = doc.add_paragraph()
  220. para.add_run(content)
  221. def create_docx(self):
  222. # 创建一个新的文档
  223. doc = Document()
  224. para = doc.add_paragraph()
  225. run = para.add_run(self.name)
  226. run.font.size = Pt(16) # 设置字体大小
  227. run.bold = True # 设置加粗
  228. para.alignment = WD_PARAGRAPH_ALIGNMENT.CENTER # 添加居中对齐的段落
  229. para = doc.add_paragraph()
  230. run = para.add_run("姓名:___________________________日期:___________________________分数:___________________________")
  231. num_index = 0
  232. # 单选题
  233. rows = ExamPaperDetail.objects.filter(main=self, question__type=ExamQuestion.SINGLE, delete=False).order_by('order')
  234. if rows.count() > 0:
  235. self._add_category_title(doc, '单选题', num_index)
  236. seq = 1
  237. for row in rows:
  238. self._add_title(doc, seq, row.question.title)
  239. seq += 1
  240. options = ExamQuestionOption.objects.filter(main=row.question, delete=False)
  241. index = 0
  242. for option in options:
  243. self._add_option(doc, index, option.content)
  244. index += 1
  245. num_index += 1
  246. # 多选题
  247. rows = ExamPaperDetail.objects.filter(main=self, question__type=ExamQuestion.MULTIPLE, delete=False).order_by('order')
  248. if rows.count() > 0:
  249. self._add_category_title(doc, '多选题', num_index)
  250. seq = 1
  251. for row in rows:
  252. self._add_title(doc, seq, row.question.title)
  253. seq += 1
  254. options = ExamQuestionOption.objects.filter(main=row.question, delete=False)
  255. index = 0
  256. for option in options:
  257. self._add_option(doc, index, option.content)
  258. index += 1
  259. num_index += 1
  260. # 填空题
  261. rows = ExamPaperDetail.objects.filter(main=self, question__type=ExamQuestion.FILL, delete=False).order_by('order')
  262. if rows.count() > 0:
  263. self._add_category_title(doc, '填空题', num_index)
  264. seq = 1
  265. for row in rows:
  266. self._add_title(doc, seq, row.question.title)
  267. seq += 1
  268. num_index += 1
  269. # 判断题
  270. rows = ExamPaperDetail.objects.filter(main=self, question__type=ExamQuestion.JUDGMENT, delete=False).order_by('order')
  271. if rows.count() > 0:
  272. self._add_category_title(doc, '判断题', num_index)
  273. seq = 1
  274. for row in rows:
  275. self._add_title(doc, seq, row.question.title)
  276. seq += 1
  277. num_index += 1
  278. # 论述题
  279. rows = ExamPaperDetail.objects.filter(main=self, question__type=ExamQuestion.DISCUSS, delete=False).order_by('order')
  280. if rows.count() > 0:
  281. self._add_category_title(doc, '论述题', num_index)
  282. seq = 1
  283. for row in rows:
  284. #print(row.question.title)
  285. self._add_title(doc, seq, row.question.title)
  286. doc.add_paragraph('')
  287. doc.add_paragraph('')
  288. doc.add_paragraph('')
  289. doc.add_paragraph('')
  290. doc.add_paragraph('')
  291. seq += 1
  292. num_index += 1
  293. return doc
  294. class ExamPaperDetail(models.Model):
  295. main = models.ForeignKey(ExamPaper, verbose_name=u"试卷", on_delete=models.PROTECT)
  296. question = models.ForeignKey(ExamQuestion, verbose_name=u"试题", on_delete=models.PROTECT)
  297. order = models.IntegerField(verbose_name=u'序号', editable=False)
  298. delete = models.BooleanField(verbose_name=u'删除', default=False, editable=False)
  299. class Meta:
  300. db_table = "exam_paper_detail"
  301. ordering = ['order']
  302. verbose_name = u"试卷明细"
  303. default_permissions = ()