135 lines
4.9 KiB
Python
135 lines
4.9 KiB
Python
# -*- coding: utf-8 -*-
|
||
import json
|
||
from odoo import http
|
||
from odoo.exceptions import AccessError
|
||
from odoo.http import request, Response
|
||
|
||
|
||
def _json(data, status=200):
|
||
"""统一的 JSON 响应。"""
|
||
return Response(json.dumps(data, ensure_ascii=False),
|
||
mimetype='application/json', status=status)
|
||
|
||
|
||
class BrainController(http.Controller):
|
||
"""前端脑图页面与数据接口。除“新建节点”外均为只读。"""
|
||
|
||
# ---------------- 只读查询接口(JSON)----------------
|
||
|
||
@http.route('/yuthon_brain/api/<string:key>/graph', type='http',
|
||
auth='user', methods=['GET'], csrf=False)
|
||
def api_graph(self, key, **kw):
|
||
"""返回某数据源的脑图结构:{root, nodes, edges, theme, layout}。"""
|
||
source = request.env['yuthon.brain.source'].search(
|
||
[('key', '=', key), ('active', '=', True)], limit=1)
|
||
if not source:
|
||
return Response(json.dumps({'error': 'not_found'}, ensure_ascii=False),
|
||
mimetype='application/json', status=404)
|
||
|
||
nodes = []
|
||
for n in source.node_ids:
|
||
try:
|
||
extra = json.loads(n.extra or '{}') if n.extra else {}
|
||
except (ValueError, TypeError):
|
||
extra = {}
|
||
nodes.append({
|
||
'id': n.id,
|
||
'label': n.label,
|
||
'parent_id': n.parent_id.id if n.parent_id else None,
|
||
'rel_type': n.rel_type,
|
||
'color': n.color or '#1E6FFF',
|
||
'icon': n.icon or '',
|
||
'level': n.level,
|
||
'extra': extra,
|
||
})
|
||
|
||
edges = [{
|
||
'id': e.id,
|
||
'from': e.from_id.id,
|
||
'to': e.to_id.id,
|
||
'rel_type': e.rel_type,
|
||
'label': e.label or '',
|
||
} for e in source.edge_ids]
|
||
|
||
payload = {
|
||
'root': source.root_id.id if source.root_id else None,
|
||
'theme': source.theme,
|
||
'layout': source.layout,
|
||
'source_name': source.name,
|
||
'nodes': nodes,
|
||
'edges': edges,
|
||
}
|
||
return Response(json.dumps(payload, ensure_ascii=False),
|
||
mimetype='application/json')
|
||
|
||
@http.route('/yuthon_brain/api/<string:key>/node', type='http',
|
||
auth='user', methods=['POST'], csrf=False)
|
||
def api_create_node(self, key, **kw):
|
||
"""新建脑图节点,body = {"parent_id": <id|null>, "label": "..."}。
|
||
|
||
只有系统管理员对 yuthon.brain.node 有 create 权限,其他账号返回 403。
|
||
前端拖拽端口后调用此接口把新节点落库。
|
||
"""
|
||
source = request.env['yuthon.brain.source'].search(
|
||
[('key', '=', key), ('active', '=', True)], limit=1)
|
||
if not source:
|
||
return _json({'error': 'not_found'}, 404)
|
||
|
||
try:
|
||
payload = json.loads(request.httprequest.get_data(as_text=True) or '{}')
|
||
except (ValueError, TypeError):
|
||
payload = {}
|
||
if not isinstance(payload, dict):
|
||
payload = {}
|
||
|
||
label = (payload.get('label') or '').strip()[:120]
|
||
if not label:
|
||
return _json({'error': 'empty_label'}, 400)
|
||
|
||
Node = request.env['yuthon.brain.node']
|
||
parent = None
|
||
pid = payload.get('parent_id')
|
||
if pid is not None and pid != '':
|
||
try:
|
||
cand = Node.browse(int(pid))
|
||
except (ValueError, TypeError):
|
||
cand = Node.browse([])
|
||
# 只允许挂到同一数据源下已存在的节点上
|
||
if cand.exists() and cand.source_id.id == source.id:
|
||
parent = cand.id
|
||
if parent is None:
|
||
parent = source.root_id.id or False
|
||
|
||
try:
|
||
node = Node.create({
|
||
'source_id': source.id,
|
||
'parent_id': parent,
|
||
'rel_type': 'child',
|
||
'label': label,
|
||
'auto': False, # 手动节点:重同步时不会被清理
|
||
})
|
||
except AccessError:
|
||
return _json({'error': 'forbidden'}, 403)
|
||
except Exception as exc:
|
||
msg = str(exc).lower()
|
||
if 'unique' in msg or 'duplicate' in msg:
|
||
return _json({'error': 'duplicate'}, 409)
|
||
raise
|
||
|
||
return _json({
|
||
'id': node.id,
|
||
'label': node.label,
|
||
'parent_id': node.parent_id.id or None,
|
||
'rel_type': node.rel_type,
|
||
})
|
||
|
||
@http.route('/yuthon_brain/api/sources', type='http',
|
||
auth='user', methods=['GET'], csrf=False)
|
||
def api_sources(self, **kw):
|
||
"""已启用数据源列表。"""
|
||
sources = request.env['yuthon.brain.source'].search(
|
||
[('active', '=', True)], order='sequence, id')
|
||
data = [{'key': s.key, 'name': s.name} for s in sources]
|
||
return Response(json.dumps(data, ensure_ascii=False),
|
||
mimetype='application/json')
|