Files
shcool/college/controllers/exam_controller.py
T
2026-08-21 19:30:28 +08:00

186 lines
8.3 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import json
import logging
from odoo import http, fields
from odoo.http import request
from odoo.exceptions import UserError, AccessError
_logger = logging.getLogger(__name__)
class ExamController(http.Controller):
"""学生答题卡式考试页面控制器。"""
def _check_paper_access(self, paper_id, states=None):
"""校验当前登录用户是否为该答卷的学员本人。"""
paper = request.env['yuthon.school.exam.paper'].sudo().browse(paper_id)
if not paper.exists():
raise http.NotFound()
if paper.user_id.id != request.env.user.id:
raise AccessError('您无权访问该答卷。')
if states and paper.state not in states:
raise UserError('当前答卷状态不允许该操作。')
return paper
# ------------------------------------------------------------------
# 1. 开始考试:初始化答题记录并跳转到答题页面
# ------------------------------------------------------------------
@http.route('/exam/start/<int:paper_id>', type='http', auth='user')
def exam_start(self, paper_id, **kw):
paper = self._check_paper_access(paper_id, states=('pending', 'in_progress'))
try:
paper.with_user(request.env.user).action_start()
except UserError as e:
return request.render('college.exam_error_page', {'message': str(e)})
return request.redirect('/exam/page/%s' % paper_id)
# ------------------------------------------------------------------
# 2. 答题主页面
# ------------------------------------------------------------------
@http.route('/exam/page/<int:paper_id>', type='http', auth='user', website=False)
def exam_page(self, paper_id, **kw):
paper = self._check_paper_access(paper_id)
if paper.state == 'pending':
# 未开始则自动开始
try:
paper.with_user(request.env.user).action_start()
except UserError as e:
return request.render('college.exam_error_page', {'message': str(e)})
if paper.state not in ('in_progress', 'submitted', 'graded'):
return request.render('college.exam_error_page', {
'message': '该答卷当前状态为 %s,无法进入答题页面。' % dict(paper._fields['state'].selection).get(paper.state)
})
answers = paper.answer_ids.sorted(lambda a: (a.sequence, a.id))
exam = paper.exam_id
# 计算剩余时间(秒)
remaining_seconds = 0
if paper.start_time and exam.duration_minutes:
end_dt = paper.start_time + fields.Datetime.timedelta(minutes=exam.duration_minutes)
remaining_seconds = max(0, int((end_dt - fields.Datetime.now()).total_seconds()))
values = {
'paper': paper,
'exam': exam,
'course': paper.course_id,
'chapters': paper.chapter_ids,
'student': paper.student_id,
'answers': answers,
'answer_json': json.dumps([self._answer_to_dict(a) for a in answers]),
'remaining_seconds': remaining_seconds,
'duration_minutes': exam.duration_minutes or 0,
'question_types': dict(request.env['yuthon.school.exam.question']._fields['question_type'].selection),
}
return request.render('college.exam_take_page', values)
def _answer_to_dict(self, answer):
"""将答题记录转为前端 JSON 可用的字典。"""
q = answer.question_id
return {
'id': answer.id,
'sequence': answer.sequence or 0,
'question_type': answer.question_type,
'question_text': answer.question_text or '',
'question_text_short': answer.question_text_short or '',
'max_score': answer.max_score,
'options': [{
'id': opt.id,
'label': opt.label,
'content': opt.content or '',
'text': opt.name or '%s. %s' % (opt.label, opt._strip_html(opt.content or '')),
} for opt in q.option_ids.sorted(lambda o: (o.sequence, o.id))],
# 当前作答
'selected_option_id': answer.selected_option_id.id or False,
'selected_option_ids': answer.selected_option_ids.ids,
'answer_choice': answer.answer_choice or False,
'answer_text': answer.answer_text or '',
'is_answered': bool(
answer.selected_option_id or
answer.selected_option_ids or
answer.answer_choice or
(answer.answer_text and answer._strip_html(answer.answer_text))
),
}
# ------------------------------------------------------------------
# 3. AJAX 保存单题答案
# ------------------------------------------------------------------
@http.route('/exam/save', type='json', auth='user', methods=['POST'])
def exam_save_answer(self, **kw):
# 注意:type='json' 路由的参数在 request.params(JSON-RPC 内层 params),
# 而非 request.jsonrequest(那是 envelope {jsonrpc, method, params, id})。
params = request.params or {}
paper_id = params.get('paper_id')
answer_id = params.get('answer_id')
data = params.get('data', {})
paper = self._check_paper_access(paper_id, states=('in_progress',))
answer = request.env['yuthon.school.exam.answer'].sudo().browse(int(answer_id))
if not answer.exists() or answer.paper_id.id != paper.id:
return {'status': 'error', 'message': '题目不属于当前答卷。'}
qtype = answer.question_type
vals = {}
if qtype == 'single_choice':
vals['selected_option_id'] = int(data.get('selected_option_id') or 0) or False
if vals['selected_option_id']:
vals['selected_option_ids'] = [(6, 0, [vals['selected_option_id']])]
else:
vals['selected_option_ids'] = [(5,)]
elif qtype == 'multiple_choice':
opt_ids = [int(x) for x in data.get('selected_option_ids', []) if x]
vals['selected_option_ids'] = [(6, 0, opt_ids)]
vals['selected_option_id'] = opt_ids[0] if opt_ids else False
elif qtype == 'true_false':
vals['answer_choice'] = data.get('answer_choice') or False
elif qtype in ('fill_blank', 'essay'):
vals['answer_text'] = data.get('answer_text') or False
answer.write(vals)
return {
'status': 'ok',
'is_answered': bool(
answer.selected_option_id or
answer.selected_option_ids or
answer.answer_choice or
(answer.answer_text and answer._strip_html(answer.answer_text))
),
}
# ------------------------------------------------------------------
# 4. AJAX 提交整卷
# ------------------------------------------------------------------
@http.route('/exam/submit', type='json', auth='user', methods=['POST'])
def exam_submit(self, **kw):
params = request.params or {}
paper_id = params.get('paper_id')
paper = self._check_paper_access(paper_id, states=('in_progress',))
try:
paper.with_user(request.env.user).action_submit()
except UserError as e:
return {'status': 'error', 'message': str(e)}
return {
'status': 'ok',
'redirect': '/exam/result/%s' % paper.id,
}
# ------------------------------------------------------------------
# 5. 结果页面
# ------------------------------------------------------------------
@http.route('/exam/result/<int:paper_id>', type='http', auth='user', website=False)
def exam_result(self, paper_id, **kw):
paper = self._check_paper_access(paper_id)
if paper.state not in ('submitted', 'graded'):
return request.redirect('/exam/page/%s' % paper_id)
answers = paper.answer_ids.sorted(lambda a: (a.sequence, a.id))
values = {
'paper': paper,
'exam': paper.exam_id,
'answers': answers,
'question_types': dict(request.env['yuthon.school.exam.question']._fields['question_type'].selection),
}
return request.render('college.exam_result_page', values)