feat(generation): rebuild Generation Page with full AI workflows
- Rebuild GenerationPage.tsx from static placeholder to production-parity exam generation wizard with all 4 IELTS modules (Reading, Listening, Writing, Speaking) plus Level and Industry - Add per-module config: timer, CEFR difficulty tags, access type, entities, approval workflow, rubric, grading system, shuffling - Reading: AI passage generation, 5 exercise types (MCQ, Fill Blanks, Write Blanks, True/False, Paragraph Match), categories/types - Listening: 4 section types, AI context generation, TTS audio generation - Writing: Task 1/2, AI instruction generation, word limits, marks - Speaking: 3 parts, AI script generation, avatar video generation with 7 avatar options - Wire ExamStructuresPage to real CRUD API (list/create/delete) - Add backend exam_structure model and controller (/api/exam-structures) - Enhance ai_controller with 5 specialized generation handlers (passage, exercises, writing instructions, speaking script, listening context) - Add POST /api/exam/generation/submit for exam creation workflow - Fix media.service avatar video endpoint alignment - All 12 API tests passed, browser-verified with real OpenAI calls Made-with: Cursor
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
from . import templates
|
||||
from . import ielts_exam
|
||||
from . import custom_exam
|
||||
from . import exam_structures
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
import json
|
||||
import logging
|
||||
|
||||
from odoo import http
|
||||
from odoo.http import request
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _json_body():
|
||||
try:
|
||||
return json.loads(request.httprequest.data or '{}')
|
||||
except Exception:
|
||||
return {}
|
||||
|
||||
|
||||
def _json_response(data, status=200):
|
||||
return request.make_json_response(data, status=status)
|
||||
|
||||
|
||||
class ExamStructureController(http.Controller):
|
||||
|
||||
@http.route('/api/exam-structures', type='http', auth='user', methods=['GET'], csrf=False)
|
||||
def list_structures(self, **kw):
|
||||
domain = [('active', '=', True)]
|
||||
entity_id = kw.get('entity_id')
|
||||
if entity_id:
|
||||
domain.append(('entity_id', '=', int(entity_id)))
|
||||
|
||||
limit = int(kw.get('limit', 50))
|
||||
offset = int(kw.get('offset', 0))
|
||||
records = request.env['encoach.exam.structure'].search(domain, limit=limit, offset=offset, order='create_date desc')
|
||||
total = request.env['encoach.exam.structure'].search_count(domain)
|
||||
|
||||
items = []
|
||||
for r in records:
|
||||
modules = []
|
||||
if r.modules:
|
||||
try:
|
||||
modules = json.loads(r.modules)
|
||||
except Exception:
|
||||
modules = []
|
||||
items.append({
|
||||
'id': r.id,
|
||||
'name': r.name,
|
||||
'entity_id': r.entity_id.id if r.entity_id else None,
|
||||
'entity_name': r.entity_id.name if r.entity_id else None,
|
||||
'industry': r.industry or '',
|
||||
'modules': modules,
|
||||
'config': json.loads(r.config) if r.config else {},
|
||||
})
|
||||
|
||||
return _json_response({'items': items, 'total': total})
|
||||
|
||||
@http.route('/api/exam-structures', type='http', auth='user', methods=['POST'], csrf=False)
|
||||
def create_structure(self, **kw):
|
||||
body = _json_body()
|
||||
name = body.get('name')
|
||||
if not name:
|
||||
return _json_response({'error': 'name is required'}, status=400)
|
||||
|
||||
vals = {
|
||||
'name': name,
|
||||
'industry': body.get('industry', ''),
|
||||
'modules': json.dumps(body.get('modules', [])),
|
||||
'config': json.dumps(body.get('config', {})),
|
||||
}
|
||||
entity_id = body.get('entity_id')
|
||||
if entity_id:
|
||||
vals['entity_id'] = int(entity_id)
|
||||
|
||||
record = request.env['encoach.exam.structure'].create(vals)
|
||||
return _json_response({
|
||||
'id': record.id,
|
||||
'name': record.name,
|
||||
'entity_id': record.entity_id.id if record.entity_id else None,
|
||||
'industry': record.industry or '',
|
||||
'modules': json.loads(record.modules) if record.modules else [],
|
||||
})
|
||||
|
||||
@http.route('/api/exam-structures/<int:structure_id>', type='http', auth='user', methods=['DELETE'], csrf=False)
|
||||
def delete_structure(self, structure_id, **kw):
|
||||
record = request.env['encoach.exam.structure'].browse(structure_id)
|
||||
if not record.exists():
|
||||
return _json_response({'error': 'Structure not found'}, status=404)
|
||||
record.unlink()
|
||||
return _json_response({'success': True})
|
||||
@@ -8,3 +8,4 @@ from . import speaking_card
|
||||
from . import exam_custom
|
||||
from . import exam_custom_section
|
||||
from . import exam_assignment
|
||||
from . import exam_structure
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
from odoo import models, fields
|
||||
|
||||
|
||||
class EncoachExamStructure(models.Model):
|
||||
_name = 'encoach.exam.structure'
|
||||
_description = 'Reusable Exam Structure'
|
||||
_order = 'create_date desc'
|
||||
|
||||
name = fields.Char(size=200, required=True)
|
||||
entity_id = fields.Many2one('encoach.entity', ondelete='set null')
|
||||
industry = fields.Char(size=100)
|
||||
modules = fields.Text(help='JSON list of module keys, e.g. ["reading","listening"]')
|
||||
config = fields.Text(help='JSON config: timer, difficulty, passage counts per module')
|
||||
active = fields.Boolean(default=True)
|
||||
@@ -9,3 +9,4 @@ access_encoach_rubric_user,encoach.rubric.user,model_encoach_rubric,base.group_u
|
||||
access_encoach_exam_custom_user,encoach.exam.custom.user,model_encoach_exam_custom,base.group_user,1,1,1,1
|
||||
access_encoach_exam_custom_section_user,encoach.exam.custom.section.user,model_encoach_exam_custom_section,base.group_user,1,1,1,1
|
||||
access_encoach_exam_assignment_user,encoach.exam.assignment.user,model_encoach_exam_assignment,base.group_user,1,1,1,1
|
||||
access_encoach_exam_structure_user,encoach.exam.structure.user,model_encoach_exam_structure,base.group_user,1,1,1,1
|
||||
|
||||
|
Reference in New Issue
Block a user