186 lines
8.3 KiB
Python
186 lines
8.3 KiB
Python
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)
|