# coding=utf-8 import random import re import base64 from io import BytesIO from PIL import Image from django.db import models from django.utils import timezone from django.conf import settings from docx import Document from docx.shared import Inches, Pt from docx.enum.text import WD_PARAGRAPH_ALIGNMENT from docx.oxml.ns import qn from docx.oxml import OxmlElement from bs4 import BeautifulSoup from utils.exceptions import CustomError from apps.foundation.models import Subject from apps.examination.examquestion.models import ExamQuestion, ExamQuestionOption class ExamPaper(models.Model): MOCK = 1 FORMAL = 2 RANDOM = 3 TYPE_CHOICES = ( (MOCK, u'模拟试卷'), (FORMAL, u'正式试卷'), (RANDOM, u'随机试卷'), ) TYPE_JSON = [{'id': item[0], 'value': item[1]} for item in TYPE_CHOICES] name = models.CharField(max_length=200, verbose_name=u"名称") subject = models.ForeignKey(Subject, verbose_name=u"科目", on_delete=models.PROTECT) type = models.PositiveSmallIntegerField(choices=TYPE_CHOICES, verbose_name=u"类型") passline = models.IntegerField(verbose_name=u'及格线') desc = models.TextField(verbose_name=u"备注", null=True, blank=True) single_simple_count = models.IntegerField(verbose_name=u'简单单选题数量', default=0) multiple_simple_count = models.IntegerField(verbose_name=u'简单多选题数量', default=0) fill_simple_count = models.IntegerField(verbose_name=u'简单填空题数量', default=0) judgment_simple_count = models.IntegerField(verbose_name=u'简单判断题数量', default=0) discuss_simple_count = models.IntegerField(verbose_name=u'简单论述题数量', default=0) single_mid_count = models.IntegerField(verbose_name=u'中等单选题数量', default=0) multiple_mid_count = models.IntegerField(verbose_name=u'中等多选题数量', default=0) fill_mid_count = models.IntegerField(verbose_name=u'中等填空题数量', default=0) judgment_mid_count = models.IntegerField(verbose_name=u'中等判断题数量', default=0) discuss_mid_count = models.IntegerField(verbose_name=u'中等论述题数量', default=0) single_hard_count = models.IntegerField(verbose_name=u'困难单选题数量', default=0) multiple_hard_count = models.IntegerField(verbose_name=u'困难多选题数量', default=0) fill_hard_count = models.IntegerField(verbose_name=u'困难填空题数量', default=0) judgment_hard_count = models.IntegerField(verbose_name=u'困难判断题数量', default=0) discuss_hard_count = models.IntegerField(verbose_name=u'困难论述题数量', default=0) single_scores = models.IntegerField(verbose_name=u'单选题单题分数', default=0) multiple_scores = models.IntegerField(verbose_name=u'多选题单题分数', default=0) fill_scores = models.IntegerField(verbose_name=u'填空题单题分数', default=0) judgment_scores = models.IntegerField(verbose_name=u'判断题单题分数', default=0) discuss_scores = models.IntegerField(verbose_name=u'论述题单题分数', default=0) single_total_count = models.IntegerField(verbose_name=u'单选题总数量', default=0, editable=False) multiple_total_count = models.IntegerField(verbose_name=u'多选题总数量', default=0, editable=False) fill_total_count = models.IntegerField(verbose_name=u'填空题总数量', default=0, editable=False) judgment_total_count = models.IntegerField(verbose_name=u'判断题总数量', default=0, editable=False) discuss_total_count = models.IntegerField(verbose_name=u'论述题总数量', default=0, editable=False) single_total_scores = models.IntegerField(verbose_name=u'单选题总分数', default=0, editable=False) multiple_total_scores = models.IntegerField(verbose_name=u'多选题总分数', default=0, editable=False) fill_total_scores = models.IntegerField(verbose_name=u'填空题总分数', default=0, editable=False) judgment_total_scores = models.IntegerField(verbose_name=u'判断题总分数', default=0, editable=False) discuss_total_scores = models.IntegerField(verbose_name=u'论述题总分数', default=0, editable=False) question_total_count = models.IntegerField(verbose_name=u'试题总数量', default=0, editable=False) question_total_scores = models.IntegerField(verbose_name=u'试题总分数', default=0, editable=False) create_user = models.ForeignKey(settings.AUTH_USER_MODEL, verbose_name=u'添加人', 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) did_count = models.IntegerField(verbose_name=u'做过人数', default=0, editable=False) class Meta: db_table = "exam_paper" ordering = ['-id'] verbose_name = u"试卷管理" default_permissions = () @staticmethod def getById(id): instance = ExamPaper.objects.filter(pk=id).first() if not instance: raise CustomError(u'未找到相应的试卷') return instance def generate_passline(self): self.passline = int(self.question_total_scores * 0.6) def clear_detail(self): ExamPaperDetail.objects.filter(main=self).update(delete=True) def generate_detail(self): begin_order = 1 if self.single_simple_count: self._generate_detail(self.single_simple_count, ExamQuestion.SINGLE, ExamQuestion.SIMPLE, begin_order) begin_order += self.single_simple_count if self.single_mid_count: self._generate_detail(self.single_mid_count, ExamQuestion.SINGLE, ExamQuestion.MID, begin_order) begin_order += self.single_mid_count if self.single_hard_count: self._generate_detail(self.single_hard_count, ExamQuestion.SINGLE, ExamQuestion.HARD, begin_order) begin_order += self.single_hard_count if self.multiple_simple_count: self._generate_detail(self.multiple_simple_count, ExamQuestion.MULTIPLE, ExamQuestion.SIMPLE, begin_order) begin_order += self.multiple_simple_count if self.multiple_mid_count: self._generate_detail(self.multiple_mid_count, ExamQuestion.MULTIPLE, ExamQuestion.MID, begin_order) begin_order += self.multiple_mid_count if self.multiple_hard_count: self._generate_detail(self.multiple_hard_count, ExamQuestion.MULTIPLE, ExamQuestion.HARD, begin_order) begin_order += self.multiple_hard_count if self.fill_simple_count: self._generate_detail(self.fill_simple_count, ExamQuestion.FILL, ExamQuestion.SIMPLE, begin_order) begin_order += self.fill_simple_count if self.fill_mid_count: self._generate_detail(self.fill_mid_count, ExamQuestion.FILL, ExamQuestion.MID, begin_order) begin_order += self.fill_mid_count if self.fill_hard_count: self._generate_detail(self.fill_hard_count, ExamQuestion.FILL, ExamQuestion.HARD, begin_order) begin_order += self.fill_hard_count if self.judgment_simple_count: self._generate_detail(self.judgment_simple_count, ExamQuestion.JUDGMENT, ExamQuestion.SIMPLE, begin_order) begin_order += self.judgment_simple_count if self.judgment_mid_count: self._generate_detail(self.judgment_mid_count, ExamQuestion.JUDGMENT, ExamQuestion.MID, begin_order) begin_order += self.judgment_mid_count if self.judgment_hard_count: self._generate_detail(self.judgment_hard_count, ExamQuestion.JUDGMENT, ExamQuestion.HARD, begin_order) begin_order += self.judgment_hard_count if self.discuss_simple_count: self._generate_detail(self.discuss_simple_count, ExamQuestion.DISCUSS, ExamQuestion.SIMPLE, begin_order) begin_order += self.discuss_simple_count if self.discuss_mid_count: self._generate_detail(self.discuss_mid_count, ExamQuestion.DISCUSS, ExamQuestion.MID, begin_order) begin_order += self.discuss_mid_count if self.discuss_hard_count: self._generate_detail(self.discuss_hard_count, ExamQuestion.DISCUSS, ExamQuestion.HARD, begin_order) begin_order += self.discuss_hard_count def update_count(self): self.single_total_count = self.single_simple_count + self.single_mid_count + self.single_hard_count self.multiple_total_count = self.multiple_simple_count + self.multiple_mid_count + self.multiple_hard_count self.fill_total_count = self.fill_simple_count + self.fill_mid_count + self.fill_hard_count self.judgment_total_count = self.judgment_simple_count + self.judgment_mid_count + self.judgment_hard_count self.discuss_total_count = self.discuss_simple_count + self.discuss_mid_count + self.discuss_hard_count self.single_total_scores = self.single_scores * self.single_total_count self.multiple_total_scores = self.multiple_scores * self.multiple_total_count self.fill_total_scores = self.fill_scores * self.fill_total_count self.judgment_total_scores = self.judgment_scores * self.judgment_total_count self.discuss_total_scores = self.discuss_scores * self.discuss_total_count self.question_total_count = self.single_total_count + self.multiple_total_count + self.fill_total_count + self.judgment_total_count + self.discuss_total_count self.question_total_scores = self.single_total_scores + self.multiple_total_scores + self.fill_total_scores + self.judgment_total_scores+ self.discuss_total_scores return self def _generate_detail(self, count, type, difficulty, begin_order): questions = ExamQuestion.objects.filter( chapter__subject=self.subject, difficulty = difficulty, type = type, delete=False ) questions_count = questions.count() if questions_count < count: raise CustomError(u'[%s][%s]数量不足!' % (ExamQuestion.DIFFICULTY_CHOICES[difficulty-1][1], ExamQuestion.TYPE_CHOICES[type-1][1])) question_ids = questions.values_list('id', flat=True) rows = random.sample(list(question_ids), count) random.shuffle(rows) data = [] for row in rows: item = ExamPaperDetail( main=self, question_id=row, order=begin_order ) begin_order += 1 data.append(item) ExamPaperDetail.objects.bulk_create(data) def _html_to_docx(self, html, doc, para): soup = BeautifulSoup(html, 'html.parser') for element in soup.descendants: if element.name is None: # Text node run = para.add_run(element) elif element.name == 'b' or element.name == 'strong': run = para.add_run(element.get_text()) run.bold = True elif element.name == 'i' or element.name == 'em': run = para.add_run(element.get_text()) run.italic = True elif element.name == 'u': run = para.add_run(element.get_text()) run.underline = True elif element.name == 'br': para.add_run().add_break() elif element.name == 'p': para.add_run().add_break() elif element.name == 'span': run = para.add_run(element.get_text()) elif element.name == 'img' and element.get('src').startswith('data:image'): # 如果是 base64 编码的图片 img_data = element.get('src').split(',', 1)[1] # 提取 base64 数据部分 try: img_bytes = base64.b64decode(img_data) img_stream = BytesIO(img_bytes) image = Image.open(img_stream) # 插入图片到文档中 doc.add_picture(img_stream, width=Inches(4.0)) # 根据需要调整宽度 image.close() except Exception as e: print(f"Error inserting image: {e}") def _add_category_title(self, doc, category, num_index): num_chars = ('一、', '二、', '三、', '四、', '五、', ) para = doc.add_paragraph() text = f'{num_chars[num_index]}{category}:本大题共有 {self.single_total_count}小题,每小题{self.single_scores}分,共{self.single_scores * self.single_total_count}分。' run = para.add_run(text) run.font.size = Pt(12) # 设置字体大小 run.bold = True # 设置加粗 def _add_title(self, doc, order, title): if title.startswith('

') and title.endswith('

'): title = title[3:-4] para = doc.add_paragraph() html_content = f'{order}.{title}' self._html_to_docx(html_content, doc, para) def _add_option(self, doc, index, content): 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') pattern = r'^[A-Z]\..*' if re.match(pattern, content) is None: content = f'{option_chars[index]}.{content}' para = doc.add_paragraph() para.add_run(content) def create_docx(self): # 创建一个新的文档 doc = Document() para = doc.add_paragraph() run = para.add_run(self.name) run.font.size = Pt(16) # 设置字体大小 run.bold = True # 设置加粗 para.alignment = WD_PARAGRAPH_ALIGNMENT.CENTER # 添加居中对齐的段落 para = doc.add_paragraph() run = para.add_run("姓名:___________________________日期:___________________________分数:___________________________") num_index = 0 # 单选题 rows = ExamPaperDetail.objects.filter(main=self, question__type=ExamQuestion.SINGLE, delete=False).order_by('order') if rows.count() > 0: self._add_category_title(doc, '单选题', num_index) seq = 1 for row in rows: self._add_title(doc, seq, row.question.title) seq += 1 options = ExamQuestionOption.objects.filter(main=row.question, delete=False) index = 0 for option in options: self._add_option(doc, index, option.content) index += 1 num_index += 1 # 多选题 rows = ExamPaperDetail.objects.filter(main=self, question__type=ExamQuestion.MULTIPLE, delete=False).order_by('order') if rows.count() > 0: self._add_category_title(doc, '多选题', num_index) seq = 1 for row in rows: self._add_title(doc, seq, row.question.title) seq += 1 options = ExamQuestionOption.objects.filter(main=row.question, delete=False) index = 0 for option in options: self._add_option(doc, index, option.content) index += 1 num_index += 1 # 填空题 rows = ExamPaperDetail.objects.filter(main=self, question__type=ExamQuestion.FILL, delete=False).order_by('order') if rows.count() > 0: self._add_category_title(doc, '填空题', num_index) seq = 1 for row in rows: self._add_title(doc, seq, row.question.title) seq += 1 num_index += 1 # 判断题 rows = ExamPaperDetail.objects.filter(main=self, question__type=ExamQuestion.JUDGMENT, delete=False).order_by('order') if rows.count() > 0: self._add_category_title(doc, '判断题', num_index) seq = 1 for row in rows: self._add_title(doc, seq, row.question.title) seq += 1 num_index += 1 # 论述题 rows = ExamPaperDetail.objects.filter(main=self, question__type=ExamQuestion.DISCUSS, delete=False).order_by('order') if rows.count() > 0: self._add_category_title(doc, '论述题', num_index) seq = 1 for row in rows: #print(row.question.title) self._add_title(doc, seq, row.question.title) doc.add_paragraph('') doc.add_paragraph('') doc.add_paragraph('') doc.add_paragraph('') doc.add_paragraph('') seq += 1 num_index += 1 return doc class ExamPaperDetail(models.Model): main = models.ForeignKey(ExamPaper, verbose_name=u"试卷", on_delete=models.PROTECT) question = models.ForeignKey(ExamQuestion, verbose_name=u"试题", on_delete=models.PROTECT) order = models.IntegerField(verbose_name=u'序号', editable=False) delete = models.BooleanField(verbose_name=u'删除', default=False, editable=False) class Meta: db_table = "exam_paper_detail" ordering = ['order'] verbose_name = u"试卷明细" default_permissions = ()