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:
@@ -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})
|
||||||
|
|||||||
@@ -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])
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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": (
|
||||||
|
|||||||
@@ -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(
|
||||||
[
|
[
|
||||||
|
|||||||
Reference in New Issue
Block a user