完善教育

This commit is contained in:
李鹏宇
2026-08-21 19:30:28 +08:00
parent b072b7f973
commit 4c03ef6a90
11 changed files with 2262 additions and 0 deletions
+1
View File
@@ -0,0 +1 @@
from . import exam_controller
+185
View File
@@ -0,0 +1,185 @@
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)