Complete Odoo 19 backend with 25 custom addons: - encoach_core: user/entity/role management - encoach_api: REST API + JWT auth - encoach_ai: OpenAI integration, AI settings, generation - encoach_ai_course: AI-powered English & IELTS course generation - encoach_exam_template/session: exam creation, structures, sessions - encoach_scoring: AI auto-grading + manual approval - encoach_vector: pgvector RAG integration - encoach_adaptive: adaptive learning engine - encoach_placement: placement testing - encoach_taxonomy/resources: content taxonomy & resource management - Plus 14 more modules for courses, branding, portal, etc. Includes docs: user guide, generation report, developer workflow. Made-with: Cursor
231 lines
8.4 KiB
Python
231 lines
8.4 KiB
Python
import json
|
|
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, _paginate
|
|
)
|
|
|
|
_logger = logging.getLogger(__name__)
|
|
|
|
TAXONOMY_MODELS = {
|
|
'subject': 'encoach.subject',
|
|
'domain': 'encoach.domain',
|
|
'topic': 'encoach.topic',
|
|
'learning_objective': 'encoach.learning.objective',
|
|
}
|
|
|
|
PARENT_FIELD_MAP = {
|
|
'domain': 'subject_id',
|
|
'topic': 'domain_id',
|
|
'learning_objective': 'topic_id',
|
|
}
|
|
|
|
|
|
class EncoachTaxonomyController(http.Controller):
|
|
|
|
# ------------------------------------------------------------------
|
|
# GET /api/taxonomy/subjects
|
|
# ------------------------------------------------------------------
|
|
@http.route('/api/taxonomy/subjects', type='http', auth='none',
|
|
methods=['GET'], csrf=False)
|
|
@jwt_required
|
|
def get_subjects(self, **kw):
|
|
try:
|
|
Subject = request.env['encoach.subject'].sudo()
|
|
subjects = Subject.search([('active', '=', True)])
|
|
|
|
items = []
|
|
for s in subjects:
|
|
items.append({
|
|
'id': s.id,
|
|
'name': s.name,
|
|
'code': s.code or '',
|
|
'description': s.description or '',
|
|
'domain_count': len(s.domain_ids),
|
|
})
|
|
|
|
return _json_response({'subjects': items})
|
|
|
|
except Exception as e:
|
|
_logger.exception('get_subjects failed')
|
|
return _error_response(str(e), 500)
|
|
|
|
# ------------------------------------------------------------------
|
|
# GET /api/taxonomy/tree
|
|
# ------------------------------------------------------------------
|
|
@http.route('/api/taxonomy/tree', type='http', auth='none',
|
|
methods=['GET'], csrf=False)
|
|
@jwt_required
|
|
def get_tree(self, **kw):
|
|
try:
|
|
Subject = request.env['encoach.subject'].sudo()
|
|
subjects = Subject.search([('active', '=', True)])
|
|
|
|
tree = []
|
|
for s in subjects:
|
|
domains = []
|
|
for d in s.domain_ids:
|
|
topics = []
|
|
for t in d.topic_ids:
|
|
objectives = []
|
|
for o in t.learning_objective_ids:
|
|
objectives.append({
|
|
'id': o.id,
|
|
'name': o.name,
|
|
'bloom_level': o.bloom_level or '',
|
|
'description': o.description or '',
|
|
})
|
|
topics.append({
|
|
'id': t.id,
|
|
'name': t.name,
|
|
'description': t.description or '',
|
|
'learning_objectives': objectives,
|
|
})
|
|
domains.append({
|
|
'id': d.id,
|
|
'name': d.name,
|
|
'description': d.description or '',
|
|
'topics': topics,
|
|
})
|
|
tree.append({
|
|
'id': s.id,
|
|
'name': s.name,
|
|
'code': s.code or '',
|
|
'description': s.description or '',
|
|
'domains': domains,
|
|
})
|
|
|
|
return _json_response({'tree': tree})
|
|
|
|
except Exception as e:
|
|
_logger.exception('get_tree failed')
|
|
return _error_response(str(e), 500)
|
|
|
|
# ------------------------------------------------------------------
|
|
# POST /api/taxonomy/node
|
|
# ------------------------------------------------------------------
|
|
@http.route('/api/taxonomy/node', type='http', auth='none',
|
|
methods=['POST'], csrf=False)
|
|
@jwt_required
|
|
def create_node(self, **kw):
|
|
try:
|
|
body = _get_json_body()
|
|
node_type = body.get('type')
|
|
if not node_type or node_type not in TAXONOMY_MODELS:
|
|
return _error_response(
|
|
f'type must be one of: {", ".join(TAXONOMY_MODELS.keys())}', 400,
|
|
)
|
|
|
|
name = body.get('name')
|
|
if not name:
|
|
return _error_response('name is required', 400)
|
|
|
|
model_name = TAXONOMY_MODELS[node_type]
|
|
Model = request.env[model_name].sudo()
|
|
|
|
vals = {'name': name}
|
|
if body.get('description'):
|
|
vals['description'] = body['description']
|
|
|
|
if node_type == 'subject':
|
|
code = body.get('code')
|
|
if not code:
|
|
return _error_response('code is required for subjects', 400)
|
|
vals['code'] = code
|
|
else:
|
|
parent_id = body.get('parent_id')
|
|
if not parent_id:
|
|
parent_field = PARENT_FIELD_MAP[node_type]
|
|
return _error_response(
|
|
f'parent_id ({parent_field}) is required for {node_type}', 400,
|
|
)
|
|
vals[PARENT_FIELD_MAP[node_type]] = int(parent_id)
|
|
|
|
if node_type == 'learning_objective' and body.get('bloom_level'):
|
|
vals['bloom_level'] = body['bloom_level']
|
|
|
|
record = Model.create(vals)
|
|
|
|
return _json_response({
|
|
'id': record.id,
|
|
'type': node_type,
|
|
'name': record.name,
|
|
}, 201)
|
|
|
|
except Exception as e:
|
|
_logger.exception('create_node failed')
|
|
return _error_response(str(e), 500)
|
|
|
|
# ------------------------------------------------------------------
|
|
# PUT /api/taxonomy/node/<int:node_id>
|
|
# ------------------------------------------------------------------
|
|
@http.route('/api/taxonomy/node/<int:node_id>', type='http', auth='none',
|
|
methods=['PUT'], csrf=False)
|
|
@jwt_required
|
|
def update_node(self, node_id, **kw):
|
|
try:
|
|
body = _get_json_body()
|
|
node_type = body.get('type')
|
|
if not node_type or node_type not in TAXONOMY_MODELS:
|
|
return _error_response(
|
|
f'type must be one of: {", ".join(TAXONOMY_MODELS.keys())}', 400,
|
|
)
|
|
|
|
model_name = TAXONOMY_MODELS[node_type]
|
|
Model = request.env[model_name].sudo()
|
|
record = Model.browse(node_id)
|
|
if not record.exists():
|
|
return _error_response(f'{node_type} not found', 404)
|
|
|
|
vals = {}
|
|
if body.get('name'):
|
|
vals['name'] = body['name']
|
|
if 'description' in body:
|
|
vals['description'] = body.get('description') or ''
|
|
|
|
if node_type == 'subject':
|
|
if body.get('code'):
|
|
vals['code'] = body['code']
|
|
if 'active' in body:
|
|
vals['active'] = bool(body['active'])
|
|
elif node_type == 'learning_objective' and body.get('bloom_level'):
|
|
vals['bloom_level'] = body['bloom_level']
|
|
|
|
if vals:
|
|
record.write(vals)
|
|
|
|
return _json_response(record.to_api_dict())
|
|
|
|
except Exception as e:
|
|
_logger.exception('update_node failed')
|
|
return _error_response(str(e), 500)
|
|
|
|
# ------------------------------------------------------------------
|
|
# DELETE /api/taxonomy/node/<int:node_id>
|
|
# ------------------------------------------------------------------
|
|
@http.route('/api/taxonomy/node/<int:node_id>', type='http', auth='none',
|
|
methods=['DELETE'], csrf=False)
|
|
@jwt_required
|
|
def delete_node(self, node_id, **kw):
|
|
try:
|
|
body = _get_json_body()
|
|
node_type = body.get('type') or kw.get('type')
|
|
if not node_type or node_type not in TAXONOMY_MODELS:
|
|
return _error_response(
|
|
f'type must be one of: {", ".join(TAXONOMY_MODELS.keys())}', 400,
|
|
)
|
|
|
|
model_name = TAXONOMY_MODELS[node_type]
|
|
Model = request.env[model_name].sudo()
|
|
record = Model.browse(node_id)
|
|
if not record.exists():
|
|
return _error_response(f'{node_type} not found', 404)
|
|
|
|
record.unlink()
|
|
return _json_response({}, 204)
|
|
|
|
except Exception as e:
|
|
_logger.exception('delete_node failed')
|
|
return _error_response(str(e), 500)
|