feat: complete exam lifecycle — AI generation, submission, student session, and results
- Backend: AI generation fallbacks when OpenAI not configured, full exam submission saving all params (difficulty, rubric, entity, grading system, approval workflow) and creating linked question records per section - Backend: new exam session controller with get_session, autosave, submit, status, and results endpoints; student attempt/answer/score models - Backend: new controllers for entities, approval workflows, exam schedules - Frontend: exam session split-layout with passage panel, question types (MCQ, T/F/NG, gap-fill, writing, speaking), timer, and review dialog - Frontend: results page with percentage score, per-answer breakdown table - Frontend: generation page dynamic dropdowns, full payload submission - Frontend: updated types for ExamSessionSection, ExamQuestion options Made-with: Cursor
This commit is contained in:
@@ -3,14 +3,13 @@ import logging
|
||||
|
||||
from odoo import http
|
||||
from odoo.http import request
|
||||
from odoo.addons.encoach_api.controllers.base import (
|
||||
jwt_required, _json_response, _error_response, _get_json_body,
|
||||
)
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _json_response(data, status=200):
|
||||
return request.make_json_response(data, status=status)
|
||||
|
||||
|
||||
def _rubric_to_dict(rec):
|
||||
criteria_text = rec.criteria or ''
|
||||
criteria_count = 0
|
||||
@@ -26,6 +25,15 @@ def _rubric_to_dict(rec):
|
||||
except (json.JSONDecodeError, ValueError):
|
||||
criteria_count = len([l for l in criteria_text.split('\n') if l.strip()])
|
||||
|
||||
levels = ['A1', 'A2', 'B1', 'B2', 'C1', 'C2']
|
||||
if rec.levels:
|
||||
try:
|
||||
parsed_levels = json.loads(rec.levels)
|
||||
if isinstance(parsed_levels, list) and parsed_levels:
|
||||
levels = parsed_levels
|
||||
except (json.JSONDecodeError, ValueError):
|
||||
pass
|
||||
|
||||
return {
|
||||
'id': rec.id,
|
||||
'name': rec.name,
|
||||
@@ -33,15 +41,16 @@ def _rubric_to_dict(rec):
|
||||
'exam_type': rec.exam_type or '',
|
||||
'criteria': criteria_count or 1,
|
||||
'criteria_text': criteria_text,
|
||||
'levels': ['A1', 'A2', 'B1', 'B2', 'C1', 'C2'],
|
||||
'levels': levels,
|
||||
'created': rec.create_date.strftime('%Y-%m-%d') if rec.create_date else '',
|
||||
}
|
||||
|
||||
|
||||
class EncoachRubricController(http.Controller):
|
||||
|
||||
@http.route('/api/rubrics', type='http', auth='user',
|
||||
@http.route('/api/rubrics', type='http', auth='none',
|
||||
methods=['GET'], csrf=False)
|
||||
@jwt_required
|
||||
def list_rubrics(self, **kw):
|
||||
try:
|
||||
Rubric = request.env['encoach.rubric'].sudo()
|
||||
@@ -58,14 +67,15 @@ class EncoachRubricController(http.Controller):
|
||||
_logger.exception('rubrics list failed')
|
||||
return _json_response({'error': str(e)}, 500)
|
||||
|
||||
@http.route('/api/rubrics', type='http', auth='user',
|
||||
@http.route('/api/rubrics', type='http', auth='none',
|
||||
methods=['POST'], csrf=False)
|
||||
@jwt_required
|
||||
def create_rubric(self, **kw):
|
||||
try:
|
||||
body = json.loads(request.httprequest.data or '{}')
|
||||
body = _get_json_body()
|
||||
name = body.get('name', '').strip()
|
||||
if not name:
|
||||
return _json_response({'error': 'name is required'}, 400)
|
||||
return _error_response('name is required', 400)
|
||||
|
||||
vals = {
|
||||
'name': name,
|
||||
@@ -73,8 +83,140 @@ class EncoachRubricController(http.Controller):
|
||||
'criteria': body.get('criteria', ''),
|
||||
'exam_type': body.get('exam_type', 'academic'),
|
||||
}
|
||||
rec = Rubric = request.env['encoach.rubric'].sudo().create(vals)
|
||||
if 'levels' in body:
|
||||
vals['levels'] = json.dumps(body['levels'])
|
||||
rec = request.env['encoach.rubric'].sudo().create(vals)
|
||||
return _json_response(_rubric_to_dict(rec), 201)
|
||||
except Exception as e:
|
||||
_logger.exception('rubric create failed')
|
||||
return _json_response({'error': str(e)}, 500)
|
||||
|
||||
@http.route('/api/rubrics/<int:rubric_id>', type='http', auth='none',
|
||||
methods=['PUT'], csrf=False)
|
||||
@jwt_required
|
||||
def update_rubric(self, rubric_id, **kw):
|
||||
try:
|
||||
rec = request.env['encoach.rubric'].sudo().browse(rubric_id)
|
||||
if not rec.exists():
|
||||
return _error_response('Rubric not found', 404)
|
||||
|
||||
body = _get_json_body()
|
||||
vals = {}
|
||||
if 'name' in body:
|
||||
vals['name'] = body['name']
|
||||
if 'skill' in body:
|
||||
vals['skill'] = body['skill']
|
||||
if 'criteria' in body:
|
||||
vals['criteria'] = body['criteria']
|
||||
if 'exam_type' in body:
|
||||
vals['exam_type'] = body['exam_type']
|
||||
if 'levels' in body:
|
||||
vals['levels'] = json.dumps(body['levels'])
|
||||
|
||||
if vals:
|
||||
rec.write(vals)
|
||||
|
||||
return _json_response(_rubric_to_dict(rec))
|
||||
except Exception as e:
|
||||
_logger.exception('rubric update failed')
|
||||
return _json_response({'error': str(e)}, 500)
|
||||
|
||||
@http.route('/api/rubrics/<int:rubric_id>', type='http', auth='none',
|
||||
methods=['DELETE'], csrf=False)
|
||||
@jwt_required
|
||||
def delete_rubric(self, rubric_id, **kw):
|
||||
try:
|
||||
rec = request.env['encoach.rubric'].sudo().browse(rubric_id)
|
||||
if not rec.exists():
|
||||
return _error_response('Rubric not found', 404)
|
||||
rec.unlink()
|
||||
return _json_response({'success': True})
|
||||
except Exception as e:
|
||||
_logger.exception('rubric delete failed')
|
||||
return _json_response({'error': str(e)}, 500)
|
||||
|
||||
|
||||
def _group_to_dict(rec):
|
||||
return {
|
||||
'id': rec.id,
|
||||
'name': rec.name,
|
||||
'rubric_ids': rec.rubric_ids.ids,
|
||||
'rubric_names': [r.name for r in rec.rubric_ids],
|
||||
'created': rec.create_date.strftime('%Y-%m-%d') if rec.create_date else '',
|
||||
}
|
||||
|
||||
|
||||
class EncoachRubricGroupController(http.Controller):
|
||||
|
||||
@http.route('/api/rubric-groups', type='http', auth='none',
|
||||
methods=['GET'], csrf=False)
|
||||
@jwt_required
|
||||
def list_rubric_groups(self, **kw):
|
||||
try:
|
||||
Group = request.env['encoach.rubric.group'].sudo()
|
||||
limit = int(kw.get('limit', 50))
|
||||
offset = int(kw.get('offset', 0))
|
||||
records = Group.search([], limit=limit, offset=offset,
|
||||
order='create_date desc')
|
||||
total = Group.search_count([])
|
||||
return _json_response({
|
||||
'items': [_group_to_dict(r) for r in records],
|
||||
'total': total,
|
||||
})
|
||||
except Exception as e:
|
||||
_logger.exception('rubric-groups list failed')
|
||||
return _json_response({'error': str(e)}, 500)
|
||||
|
||||
@http.route('/api/rubric-groups', type='http', auth='none',
|
||||
methods=['POST'], csrf=False)
|
||||
@jwt_required
|
||||
def create_rubric_group(self, **kw):
|
||||
try:
|
||||
body = _get_json_body()
|
||||
name = body.get('name', '').strip()
|
||||
if not name:
|
||||
return _error_response('name is required', 400)
|
||||
rubric_ids = body.get('rubric_ids', [])
|
||||
rec = request.env['encoach.rubric.group'].sudo().create({
|
||||
'name': name,
|
||||
'rubric_ids': [(6, 0, rubric_ids)],
|
||||
})
|
||||
return _json_response(_group_to_dict(rec), 201)
|
||||
except Exception as e:
|
||||
_logger.exception('rubric-group create failed')
|
||||
return _json_response({'error': str(e)}, 500)
|
||||
|
||||
@http.route('/api/rubric-groups/<int:group_id>', type='http', auth='none',
|
||||
methods=['PUT'], csrf=False)
|
||||
@jwt_required
|
||||
def update_rubric_group(self, group_id, **kw):
|
||||
try:
|
||||
rec = request.env['encoach.rubric.group'].sudo().browse(group_id)
|
||||
if not rec.exists():
|
||||
return _error_response('Group not found', 404)
|
||||
body = _get_json_body()
|
||||
vals = {}
|
||||
if 'name' in body:
|
||||
vals['name'] = body['name']
|
||||
if 'rubric_ids' in body:
|
||||
vals['rubric_ids'] = [(6, 0, body['rubric_ids'])]
|
||||
if vals:
|
||||
rec.write(vals)
|
||||
return _json_response(_group_to_dict(rec))
|
||||
except Exception as e:
|
||||
_logger.exception('rubric-group update failed')
|
||||
return _json_response({'error': str(e)}, 500)
|
||||
|
||||
@http.route('/api/rubric-groups/<int:group_id>', type='http', auth='none',
|
||||
methods=['DELETE'], csrf=False)
|
||||
@jwt_required
|
||||
def delete_rubric_group(self, group_id, **kw):
|
||||
try:
|
||||
rec = request.env['encoach.rubric.group'].sudo().browse(group_id)
|
||||
if not rec.exists():
|
||||
return _error_response('Group not found', 404)
|
||||
rec.unlink()
|
||||
return _json_response({'success': True})
|
||||
except Exception as e:
|
||||
_logger.exception('rubric-group delete failed')
|
||||
return _json_response({'error': str(e)}, 500)
|
||||
|
||||
Reference in New Issue
Block a user