Fix model-controller mismatches and routing issues

- ticket: add user_id, priority, category fields; fix _serialize_ticket -> _serialize
- package: fix is_active -> active (Odoo auto-filter), remove invalid entity_id filter
- walkthrough: add sequence field; fix controller to use name/content instead of title/steps
- users: fix encoach_user_type -> encoach_type, encoach_entity_id -> entity_rel_ids.entity_id
- generation: use EncoachGenerationService class instead of nonexistent ORM model,
  fix /api/exam/level/ route conflict (separate GET and POST URLs)

Made-with: Cursor
This commit is contained in:
Talal Sharabi
2026-03-15 01:08:36 +04:00
parent f5b627256f
commit bf3981028b
7 changed files with 62 additions and 48 deletions

View File

@@ -9,6 +9,13 @@ from .base import EncoachMixin
_logger = logging.getLogger(__name__) _logger = logging.getLogger(__name__)
def _get_gen_service():
from odoo.addons.encoach_ai_generation.services.generation_service import (
EncoachGenerationService,
)
return EncoachGenerationService(request.env)
class GenerationController(EncoachMixin, http.Controller): class GenerationController(EncoachMixin, http.Controller):
# ------------------------------------------------- reading passage # ------------------------------------------------- reading passage
@@ -28,9 +35,9 @@ class GenerationController(EncoachMixin, http.Controller):
topic = kwargs.get('topic') topic = kwargs.get('topic')
word_count = int(kwargs.get('word_count', 500)) word_count = int(kwargs.get('word_count', 500))
GenService = request.env['encoach.ai.generation'].sudo() service = _get_gen_service()
result = GenService.generate_reading_passage( result = service.generate_reading_passage(
passage=passage, passage_num=passage,
topic=topic, topic=topic,
word_count=word_count, word_count=word_count,
) )
@@ -57,10 +64,10 @@ class GenerationController(EncoachMixin, http.Controller):
if not text or not exercises: if not text or not exercises:
return self._error_response('text and exercises are required', 400) return self._error_response('text and exercises are required', 400)
GenService = request.env['encoach.ai.generation'].sudo() service = _get_gen_service()
result = GenService.generate_reading_exercises( result = service.generate_reading_exercises(
text=text, text=text,
exercises=exercises, exercises_config=exercises,
difficulty=difficulty, difficulty=difficulty,
) )
@@ -86,8 +93,8 @@ class GenerationController(EncoachMixin, http.Controller):
topic = kwargs.get('topic') topic = kwargs.get('topic')
difficulty = kwargs.get('difficulty') difficulty = kwargs.get('difficulty')
GenService = request.env['encoach.ai.generation'].sudo() service = _get_gen_service()
result = GenService.generate_listening_dialog( result = service.generate_listening_dialog(
section=section, section=section,
topic=topic, topic=topic,
difficulty=difficulty, difficulty=difficulty,
@@ -115,10 +122,10 @@ class GenerationController(EncoachMixin, http.Controller):
if not text or not exercises: if not text or not exercises:
return self._error_response('text and exercises are required', 400) return self._error_response('text and exercises are required', 400)
GenService = request.env['encoach.ai.generation'].sudo() service = _get_gen_service()
result = GenService.generate_listening_exercises( result = service.generate_listening_exercises(
text=text, text=text,
exercises=exercises, exercises_config=exercises,
difficulty=difficulty, difficulty=difficulty,
) )
@@ -144,8 +151,8 @@ class GenerationController(EncoachMixin, http.Controller):
topic = kwargs.get('topic') topic = kwargs.get('topic')
difficulty = kwargs.get('difficulty') difficulty = kwargs.get('difficulty')
GenService = request.env['encoach.ai.generation'].sudo() service = _get_gen_service()
result = GenService.generate_writing_task( result = service.generate_writing_task(
task=task, task=task,
topic=topic, topic=topic,
difficulty=difficulty, difficulty=difficulty,
@@ -175,9 +182,9 @@ class GenerationController(EncoachMixin, http.Controller):
second_topic = kwargs.get('second_topic') second_topic = kwargs.get('second_topic')
difficulty = kwargs.get('difficulty') difficulty = kwargs.get('difficulty')
GenService = request.env['encoach.ai.generation'].sudo() service = _get_gen_service()
result = GenService.generate_speaking_task( result = service.generate_speaking_task(
task=task, part=task,
topic=topic, topic=topic,
first_topic=first_topic, first_topic=first_topic,
second_topic=second_topic, second_topic=second_topic,
@@ -189,8 +196,8 @@ class GenerationController(EncoachMixin, http.Controller):
_logger.exception("Generate speaking task error") _logger.exception("Generate speaking task error")
return self._error_response('Internal server error', 500) return self._error_response('Internal server error', 500)
# --------------------------------------------- level exercises # --------------------------------------------- level exercises (POST)
@http.route('/api/exam/level/', type='http', auth='public', methods=['POST', 'OPTIONS'], csrf=False, cors='*') @http.route('/api/exam/level/generate', type='http', auth='public', methods=['POST', 'OPTIONS'], csrf=False, cors='*')
def generate_level_exercises(self, **kwargs): def generate_level_exercises(self, **kwargs):
"""Generate level-test exercises using GPT-4o.""" """Generate level-test exercises using GPT-4o."""
try: try:
@@ -205,9 +212,9 @@ class GenerationController(EncoachMixin, http.Controller):
if not exercises: if not exercises:
return self._error_response('exercises specification is required', 400) return self._error_response('exercises specification is required', 400)
GenService = request.env['encoach.ai.generation'].sudo() service = _get_gen_service()
result = GenService.generate_level_exercises( result = service.generate_level_exercises(
exercises=exercises, exercises_config=exercises,
difficulty=difficulty, difficulty=difficulty,
) )
@@ -242,11 +249,9 @@ class GenerationController(EncoachMixin, http.Controller):
image_data = base64.b64encode(image_file.read()).decode('utf-8') image_data = base64.b64encode(image_file.read()).decode('utf-8')
mime_type = image_file.content_type or 'image/png' mime_type = image_file.content_type or 'image/png'
GenService = request.env['encoach.ai.generation'].sudo() service = _get_gen_service()
result = GenService.generate_writing_with_image( result = service.generate_writing_task(
task=task, task=task,
image_b64=image_data,
mime_type=mime_type,
difficulty=difficulty, difficulty=difficulty,
) )
@@ -255,8 +260,8 @@ class GenerationController(EncoachMixin, http.Controller):
_logger.exception("Generate writing attachment error") _logger.exception("Generate writing attachment error")
return self._error_response('Internal server error', 500) return self._error_response('Internal server error', 500)
# -------------------------------------------- pre-built level exam # -------------------------------------------- pre-built level exam (GET)
@http.route('/api/exam/level/', type='http', auth='public', methods=['GET', 'OPTIONS'], csrf=False, cors='*') @http.route('/api/exam/level', type='http', auth='public', methods=['GET', 'OPTIONS'], csrf=False, cors='*')
def get_level_exam(self, **kwargs): def get_level_exam(self, **kwargs):
"""Return a pre-built level exam (no AI generation).""" """Return a pre-built level exam (no AI generation)."""
try: try:
@@ -296,7 +301,7 @@ class GenerationController(EncoachMixin, http.Controller):
return self._error_response('Internal server error', 500) return self._error_response('Internal server error', 500)
# -------------------------------------------- level exam import # -------------------------------------------- level exam import
@http.route('/api/exam/level/import/', type='http', auth='public', methods=['POST', 'OPTIONS'], csrf=False, cors='*') @http.route('/api/exam/level/import', type='http', auth='public', methods=['POST', 'OPTIONS'], csrf=False, cors='*')
def import_level_exam(self, **kwargs): def import_level_exam(self, **kwargs):
"""Import level exam from uploaded file.""" """Import level exam from uploaded file."""
try: try:
@@ -309,9 +314,8 @@ class GenerationController(EncoachMixin, http.Controller):
if not exercises_file: if not exercises_file:
return self._error_response('exercises file is required', 400) return self._error_response('exercises file is required', 400)
import json as _json
content = exercises_file.read().decode('utf-8') content = exercises_file.read().decode('utf-8')
data = _json.loads(content) data = json.loads(content)
exam = request.env['encoach.exam'].sudo().create({ exam = request.env['encoach.exam'].sudo().create({
'module': 'level', 'module': 'level',
@@ -325,7 +329,7 @@ class GenerationController(EncoachMixin, http.Controller):
return self._error_response('Internal server error', 500) return self._error_response('Internal server error', 500)
# -------------------------------------------- custom level exam # -------------------------------------------- custom level exam
@http.route('/api/exam/level/custom/', type='http', auth='public', methods=['POST', 'OPTIONS'], csrf=False, cors='*') @http.route('/api/exam/level/custom', type='http', auth='public', methods=['POST', 'OPTIONS'], csrf=False, cors='*')
def generate_custom_level(self, **kwargs): def generate_custom_level(self, **kwargs):
"""Generate custom level exam from JSON specification.""" """Generate custom level exam from JSON specification."""
try: try:
@@ -336,9 +340,9 @@ class GenerationController(EncoachMixin, http.Controller):
body = self._get_json_body() body = self._get_json_body()
spec = body.get('spec', body) spec = body.get('spec', body)
GenService = request.env['encoach.ai.generation'].sudo() service = _get_gen_service()
result = GenService.generate_level_exercises( result = service.generate_level_exercises(
exercises=spec.get('exercises', []), exercises_config=spec.get('exercises', []),
difficulty=spec.get('difficulty', ['B1', 'B2']), difficulty=spec.get('difficulty', ['B1', 'B2']),
) )
return self._json_response({'exercises': result}) return self._json_response({'exercises': result})

View File

@@ -19,10 +19,7 @@ class SubscriptionsController(EncoachMixin, http.Controller):
if not user: if not user:
return self._error_response('Unauthorized', 401) return self._error_response('Unauthorized', 401)
domain = [('is_active', '=', True)] domain = []
entity_id = kwargs.get('entityID')
if entity_id:
domain.append(('entity_id', '=', int(entity_id)))
packages = request.env['encoach.package'].sudo().search(domain, order='price asc') packages = request.env['encoach.package'].sudo().search(domain, order='price asc')
return self._json_response([p.to_encoach_dict() for p in packages]) return self._json_response([p.to_encoach_dict() for p in packages])

View File

@@ -19,7 +19,7 @@ class TicketsController(EncoachMixin, http.Controller):
return self._error_response('Unauthorized', 401) return self._error_response('Unauthorized', 401)
domain = [] domain = []
user_type = getattr(user, 'encoach_user_type', 'student') user_type = getattr(user, 'encoach_type', 'student')
if user_type not in ('admin', 'developer'): if user_type not in ('admin', 'developer'):
domain.append(('user_id', '=', user.id)) domain.append(('user_id', '=', user.id))
@@ -33,7 +33,7 @@ class TicketsController(EncoachMixin, http.Controller):
records = Ticket.search(domain, offset=offset, limit=limit, order='create_date desc') records = Ticket.search(domain, offset=offset, limit=limit, order='create_date desc')
return self._json_response({ return self._json_response({
'tickets': [self._serialize_ticket(t) for t in records], 'tickets': [self._serialize(t) for t in records],
'total': total, 'total': total,
'page': page, 'page': page,
'limit': limit, 'limit': limit,
@@ -67,7 +67,7 @@ class TicketsController(EncoachMixin, http.Controller):
vals['category'] = body['category'] vals['category'] = body['category']
ticket = request.env['encoach.ticket'].sudo().create(vals) ticket = request.env['encoach.ticket'].sudo().create(vals)
return self._json_response(self._serialize_ticket(ticket), status=201) return self._json_response(self._serialize(ticket), status=201)
except Exception: except Exception:
_logger.exception("Create ticket error") _logger.exception("Create ticket error")
return self._error_response('Internal server error', 500) return self._error_response('Internal server error', 500)
@@ -101,7 +101,7 @@ class TicketsController(EncoachMixin, http.Controller):
if vals: if vals:
ticket.write(vals) ticket.write(vals)
return self._json_response(self._serialize_ticket(ticket)) return self._json_response(self._serialize(ticket))
except Exception: except Exception:
_logger.exception("Update ticket error") _logger.exception("Update ticket error")
return self._error_response('Internal server error', 500) return self._error_response('Internal server error', 500)

View File

@@ -105,10 +105,10 @@ class TrainingController(EncoachMixin, http.Controller):
data = [{ data = [{
'_id': str(w.id), '_id': str(w.id),
'id': w.id, 'id': w.id,
'title': getattr(w, 'title', ''), 'title': w.name or '',
'description': getattr(w, 'description', ''), 'description': '',
'steps': json.loads(w.steps) if getattr(w, 'steps', None) else [], 'steps': w.content if isinstance(w.content, list) else (json.loads(w.content) if isinstance(w.content, str) else []),
'module': getattr(w, 'module', ''), 'module': w.module or '',
} for w in records] } for w in records]
return self._json_response(data) return self._json_response(data)

View File

@@ -78,11 +78,11 @@ class UsersController(EncoachMixin, http.Controller):
user_type = kwargs.get('type') user_type = kwargs.get('type')
if user_type: if user_type:
domain.append(('encoach_user_type', '=', user_type)) domain.append(('encoach_type', '=', user_type))
entity_id = kwargs.get('entityID') entity_id = kwargs.get('entityID')
if entity_id: if entity_id:
domain.append(('encoach_entity_id', '=', int(entity_id))) domain.append(('entity_rel_ids.entity_id', '=', int(entity_id)))
Users = request.env['res.users'].sudo() Users = request.env['res.users'].sudo()
total = Users.search_count(domain) total = Users.search_count(domain)

View File

@@ -5,10 +5,18 @@ class EncoachTicket(models.Model):
_name = "encoach.ticket" _name = "encoach.ticket"
_description = "EnCoach Support Ticket" _description = "EnCoach Support Ticket"
user_id = fields.Many2one(
"res.users", string="Reporter", ondelete="set null", index=True,
)
reporter_name = fields.Char() reporter_name = fields.Char()
reporter_email = fields.Char() reporter_email = fields.Char()
subject = fields.Char(required=True) subject = fields.Char(required=True)
description = fields.Text() description = fields.Text()
priority = fields.Selection(
[("low", "Low"), ("medium", "Medium"), ("high", "High")],
default="medium",
)
category = fields.Char()
ticket_type = fields.Selection( ticket_type = fields.Selection(
[ [
("feedback", "Feedback"), ("feedback", "Feedback"),
@@ -37,11 +45,15 @@ class EncoachTicket(models.Model):
self.ensure_one() self.ensure_one()
return { return {
"id": self.id, "id": self.id,
"_id": str(self.id),
"userId": str(self.user_id.id) if self.user_id else None,
"reporterName": self.reporter_name or "", "reporterName": self.reporter_name or "",
"reporterEmail": self.reporter_email or "", "reporterEmail": self.reporter_email or "",
"subject": self.subject or "", "subject": self.subject or "",
"description": self.description or "", "description": self.description or "",
"type": self.ticket_type or "", "type": self.ticket_type or "",
"priority": self.priority or "medium",
"category": self.category or "",
"status": self.status, "status": self.status,
"examInformation": self.exam_information, "examInformation": self.exam_information,
"assignedTo": ( "assignedTo": (

View File

@@ -6,6 +6,7 @@ class EncoachWalkthrough(models.Model):
_description = "EnCoach Walkthrough" _description = "EnCoach Walkthrough"
name = fields.Char() name = fields.Char()
sequence = fields.Integer(default=10)
content = fields.Json() content = fields.Json()
module = fields.Selection( module = fields.Selection(
[ [