539 lines
23 KiB
Python
539 lines
23 KiB
Python
# -*- coding: utf-8 -*-
|
||
"""drag 模块——拖拽编程画布。
|
||
|
||
提供:
|
||
- drag_graph 画布表(blocks JSON + connections JSON + 编译产物 content)
|
||
- drag_template 模板表
|
||
- 图校验(validate_graph)/编译(compile_graph)/块定义(get_block_defs)
|
||
- 画布 CRUD + graph-save(保存即校验+编译) + compile + validate 端点函数
|
||
|
||
通过 load_drag() 挂载到 ServerEnv,宿主应用 load 后 /drag/api/*.dspy 可用。
|
||
取库名一律 ServerEnv().get_module_dbname('drag'),禁止硬编码。
|
||
|
||
注意:ahserver/appPublic 依赖全部为函数内惰性导入——保证 blocks/validator/
|
||
compiler 核心逻辑可在无宿主环境独立测试,部署时 ahserver 环境可用。
|
||
"""
|
||
|
||
import json
|
||
|
||
from .blocks import BLOCK_DEFS, get_block_defs
|
||
from .validator import validate_graph
|
||
from .compiler import compile_graph
|
||
|
||
|
||
def _server_env():
|
||
"""惰性获取 ServerEnv 单例(宿主环境)。"""
|
||
from ahserver.serverenv import ServerEnv
|
||
return ServerEnv()
|
||
|
||
|
||
def _now():
|
||
from appPublic.timeutils import curDateString
|
||
return curDateString()
|
||
|
||
|
||
def _new_id():
|
||
from appPublic.getID import getID
|
||
return getID()
|
||
|
||
|
||
def _dumps(obj):
|
||
from appPublic.jsonUtils import dumps
|
||
return dumps(obj)
|
||
|
||
|
||
async def get_drag_graph_dbname():
|
||
"""drag 业务库名(宿主 get_module_dbname 决定)。"""
|
||
try:
|
||
return _server_env().get_module_dbname('drag')
|
||
except Exception:
|
||
return 'scense'
|
||
|
||
|
||
# ═══════════════ 块定义(前端画布 + 校验 + 编译的唯一事实源) ═══════════════
|
||
|
||
async def get_block_defs_api():
|
||
"""返回 {categories, blocks} 供前端渲染左侧块面板。"""
|
||
return get_block_defs()
|
||
|
||
|
||
# ═══════════════ 画布 CRUD ═══════════════
|
||
|
||
async def list_drag_graphs(params=None):
|
||
"""分页列表:{list, total},支持 name/status 过滤 + sort/order。"""
|
||
params = params or {}
|
||
dbname = await get_drag_graph_dbname()
|
||
try:
|
||
async with _server_env().sqlorContext(dbname) as sor:
|
||
where, args = [], []
|
||
name = params.get('name')
|
||
status = params.get('status')
|
||
if name:
|
||
where.append('name LIKE %s')
|
||
args.append('%%%s%%' % name)
|
||
if status:
|
||
where.append('status = %s')
|
||
args.append(status)
|
||
wsql = (' WHERE ' + ' AND '.join(where)) if where else ''
|
||
ns = {'page': int(params.get('page', 1) or 1),
|
||
'rows': int(params.get('rows', 20) or 20),
|
||
'sort': params.get('sort', 'updated_at'),
|
||
'order': params.get('order', 'desc')}
|
||
recs = await sor.sqlPaging(
|
||
'SELECT id, name, description, status, created_at, updated_at '
|
||
'FROM drag_graph%s' % wsql, ns, args)
|
||
rows = [{'id': r.id, 'name': r.name, 'description': getattr(r, 'description', ''),
|
||
'status': r.status, 'created_at': str(getattr(r, 'created_at', '')),
|
||
'updated_at': str(getattr(r, 'updated_at', ''))} for r in recs]
|
||
return {'list': rows, 'total': ns.get('total', len(rows))}
|
||
except Exception as e:
|
||
return {'code': 'DB_ERROR', 'message': '查询画布列表失败: %s' % e}
|
||
|
||
|
||
async def get_drag_graph(ns):
|
||
"""获取画布详情(含 blocks/connections 解析)。"""
|
||
gid = (ns or {}).get('id')
|
||
if not gid:
|
||
return {'code': 'PARAM_REQUIRED', 'message': '缺少 id', 'field': 'id'}
|
||
dbname = await get_drag_graph_dbname()
|
||
try:
|
||
async with _server_env().sqlorContext(dbname) as sor:
|
||
recs = await sor.R('drag_graph', {'id': gid})
|
||
if not recs:
|
||
return {'code': 'NOT_FOUND', 'message': '画布不存在: %s' % gid}
|
||
r = recs[0]
|
||
data = {'id': r.id, 'name': r.name, 'description': getattr(r, 'description', ''),
|
||
'status': r.status, 'created_at': str(getattr(r, 'created_at', '')),
|
||
'updated_at': str(getattr(r, 'updated_at', ''))}
|
||
try:
|
||
data['blocks'] = json.loads(r.blocks or '[]')
|
||
except Exception:
|
||
data['blocks'] = []
|
||
try:
|
||
data['connections'] = json.loads(r.connections or '[]')
|
||
except Exception:
|
||
data['connections'] = []
|
||
data['content'] = r.content or ''
|
||
return {'data': data}
|
||
except Exception as e:
|
||
return {'code': 'DB_ERROR', 'message': '查询画布失败: %s' % e}
|
||
|
||
|
||
async def create_drag_graph(ns):
|
||
"""创建画布:校验 blocks/connections → 编译 → 落库。"""
|
||
name = (ns or {}).get('name')
|
||
if not name or not str(name).strip():
|
||
return {'code': 'PARAM_REQUIRED', 'message': '画布名称必填', 'field': 'name'}
|
||
if len(str(name)) > 100:
|
||
return {'code': 'FIELD_TOO_LONG', 'message': '画布名称不能超过 100 字符', 'field': 'name'}
|
||
blocks = ns.get('blocks')
|
||
conns = ns.get('connections')
|
||
if isinstance(blocks, str):
|
||
try:
|
||
blocks = json.loads(blocks)
|
||
except Exception:
|
||
return {'code': 'PARAM_TYPE', 'message': 'blocks 不是合法 JSON', 'field': 'blocks'}
|
||
if isinstance(conns, str):
|
||
try:
|
||
conns = json.loads(conns)
|
||
except Exception:
|
||
return {'code': 'PARAM_TYPE', 'message': 'connections 不是合法 JSON', 'field': 'connections'}
|
||
if blocks is None:
|
||
blocks = []
|
||
if conns is None:
|
||
conns = []
|
||
graph = {'blocks': blocks, 'connections': conns}
|
||
v = validate_graph(graph)
|
||
result = compile_graph(graph, {'name': name})
|
||
gid = _new_id()
|
||
content = result.get('content', '') if result.get('success') else ''
|
||
dbname = await get_drag_graph_dbname()
|
||
try:
|
||
async with _server_env().sqlorContext(dbname) as sor:
|
||
await sor.C('drag_graph', {
|
||
'id': gid, 'name': str(name).strip(),
|
||
'description': str(ns.get('description') or ''),
|
||
'blocks': _dumps(blocks), 'connections': _dumps(conns),
|
||
'content': content, 'status': '1',
|
||
'created_at': _now(), 'updated_at': _now()})
|
||
return {'success': True, 'id': gid, 'valid': v['valid'],
|
||
'errors': v['errors'], 'content': content}
|
||
except Exception as e:
|
||
return {'code': 'DB_ERROR', 'message': '保存画布失败: %s' % e}
|
||
|
||
|
||
async def update_drag_graph(ns):
|
||
"""更新画布:校验+编译后写回 content。"""
|
||
gid = (ns or {}).get('id')
|
||
if not gid:
|
||
return {'code': 'PARAM_REQUIRED', 'message': '缺少 id', 'field': 'id'}
|
||
blocks = ns.get('blocks')
|
||
conns = ns.get('connections')
|
||
if isinstance(blocks, str):
|
||
try:
|
||
blocks = json.loads(blocks)
|
||
except Exception:
|
||
return {'code': 'PARAM_TYPE', 'message': 'blocks 不是合法 JSON', 'field': 'blocks'}
|
||
if isinstance(conns, str):
|
||
try:
|
||
conns = json.loads(conns)
|
||
except Exception:
|
||
return {'code': 'PARAM_TYPE', 'message': 'connections 不是合法 JSON', 'field': 'connections'}
|
||
if blocks is None:
|
||
blocks = []
|
||
if conns is None:
|
||
conns = []
|
||
graph = {'blocks': blocks, 'connections': conns}
|
||
v = validate_graph(graph)
|
||
result = compile_graph(graph, {'name': ns.get('name') or gid})
|
||
content = result.get('content', '') if result.get('success') else ''
|
||
dbname = await get_drag_graph_dbname()
|
||
try:
|
||
async with _server_env().sqlorContext(dbname) as sor:
|
||
recs = await sor.R('drag_graph', {'id': gid})
|
||
if not recs:
|
||
return {'code': 'NOT_FOUND', 'message': '画布不存在: %s' % gid}
|
||
up = {'blocks': _dumps(blocks), 'connections': _dumps(conns),
|
||
'content': content, 'updated_at': _now()}
|
||
if ns.get('name'):
|
||
up['name'] = str(ns['name']).strip()
|
||
if ns.get('description') is not None:
|
||
up['description'] = str(ns['description'])
|
||
if ns.get('status'):
|
||
up['status'] = str(ns['status'])
|
||
await sor.U('drag_graph', up, {'id': gid})
|
||
return {'success': True, 'id': gid, 'valid': v['valid'],
|
||
'errors': v['errors'], 'content': content}
|
||
except Exception as e:
|
||
return {'code': 'DB_ERROR', 'message': '更新画布失败: %s' % e}
|
||
|
||
|
||
async def delete_drag_graph(ns):
|
||
"""删除画布。"""
|
||
gid = (ns or {}).get('id')
|
||
if not gid:
|
||
return {'code': 'PARAM_REQUIRED', 'message': '缺少 id', 'field': 'id'}
|
||
dbname = await get_drag_graph_dbname()
|
||
try:
|
||
async with _server_env().sqlorContext(dbname) as sor:
|
||
await sor.D('drag_graph', {'id': gid})
|
||
return {'success': True, 'id': gid}
|
||
except Exception as e:
|
||
return {'code': 'DB_ERROR', 'message': '删除画布失败: %s' % e}
|
||
|
||
|
||
# ═══════════════ 画布保存(保存即校验+编译) ═══════════════
|
||
|
||
async def graph_save(ns):
|
||
"""保存画布(新建或更新):blocks/connections 校验+编译后落库。
|
||
|
||
ns: {id?, name, description?, blocks, connections}
|
||
返回 {success, id, valid, errors, content}
|
||
"""
|
||
gid = (ns or {}).get('id')
|
||
if gid:
|
||
return await update_drag_graph(ns)
|
||
return await create_drag_graph(ns)
|
||
|
||
|
||
# ═══════════════ 编译 / 校验 ═══════════════
|
||
|
||
async def graph_compile(ns):
|
||
"""编译画布 → script_engine 可执行脚本(不落库)。"""
|
||
blocks = (ns or {}).get('blocks')
|
||
conns = (ns or {}).get('connections')
|
||
if isinstance(blocks, str):
|
||
try:
|
||
blocks = json.loads(blocks)
|
||
except Exception:
|
||
return {'code': 'PARAM_TYPE', 'message': 'blocks 不是合法 JSON', 'field': 'blocks'}
|
||
if isinstance(conns, str):
|
||
try:
|
||
conns = json.loads(conns)
|
||
except Exception:
|
||
return {'code': 'PARAM_TYPE', 'message': 'connections 不是合法 JSON', 'field': 'connections'}
|
||
graph = {'blocks': blocks or [], 'connections': conns or []}
|
||
return compile_graph(graph, {'name': (ns or {}).get('name')})
|
||
|
||
|
||
async def graph_validate(ns):
|
||
"""校验画布 → {valid, errors, warnings}(不落库不编译)。"""
|
||
blocks = (ns or {}).get('blocks')
|
||
conns = (ns or {}).get('connections')
|
||
if isinstance(blocks, str):
|
||
try:
|
||
blocks = json.loads(blocks)
|
||
except Exception:
|
||
return {'valid': False, 'errors': [{'code': 'PARAM_TYPE', 'message': 'blocks 不是合法 JSON', 'field': 'blocks'}]}
|
||
if isinstance(conns, str):
|
||
try:
|
||
conns = json.loads(conns)
|
||
except Exception:
|
||
return {'valid': False, 'errors': [{'code': 'PARAM_TYPE', 'message': 'connections 不是合法 JSON', 'field': 'connections'}]}
|
||
return validate_graph({'blocks': blocks or [], 'connections': conns or []})
|
||
|
||
|
||
# ═══════════════ 编译产物发布到 script_engine ═══════════════
|
||
|
||
async def graph_publish(ns):
|
||
"""把画布编译产物写入 script_engine(script_type=0 Python 可执行脚本)。
|
||
|
||
ns: {id 画布id, script_name?}
|
||
返回 {success, script_id, script_name}
|
||
"""
|
||
gid = (ns or {}).get('id')
|
||
if not gid:
|
||
return {'code': 'PARAM_REQUIRED', 'message': '缺少画布 id', 'field': 'id'}
|
||
got = await get_drag_graph({'id': gid})
|
||
if got.get('code'):
|
||
return got
|
||
data = got['data']
|
||
result = compile_graph({'blocks': data['blocks'], 'connections': data['connections']},
|
||
{'name': data['name'], 'id': gid})
|
||
if not result.get('success'):
|
||
return {'code': 'COMPILE_ERROR', 'message': '画布校验未通过,无法发布',
|
||
'errors': result.get('errors', [])}
|
||
script_name = (ns or {}).get('script_name') or ('drag_%s' % data['name'])
|
||
se = _server_env()
|
||
try:
|
||
create_script = getattr(se, 'create_script', None) or getattr(se, 'create_scripts', None)
|
||
if create_script is None:
|
||
return {'code': 'MODULE_MISSING', 'message': 'script_engine 未挂载,无法发布'}
|
||
r = await create_script({'script_name': script_name, 'script_type': '0',
|
||
'content': result['content'],
|
||
'description': 'drag 画布编译产物 (graph=%s)' % gid})
|
||
return {'success': True, 'script_id': r.get('id'), 'script_name': script_name,
|
||
'script_result': r}
|
||
except Exception as e:
|
||
return {'code': 'DB_ERROR', 'message': '发布到 script_engine 失败: %s' % e}
|
||
|
||
|
||
# ═══════════════ 模板 CRUD ═══════════════
|
||
|
||
async def list_drag_templates(params=None):
|
||
"""模板分页列表:{list, total}。"""
|
||
params = params or {}
|
||
dbname = await get_drag_graph_dbname()
|
||
try:
|
||
async with _server_env().sqlorContext(dbname) as sor:
|
||
where, args = [], []
|
||
name = params.get('name')
|
||
if name:
|
||
where.append('name LIKE %s')
|
||
args.append('%%%s%%' % name)
|
||
wsql = (' WHERE ' + ' AND '.join(where)) if where else ''
|
||
ns = {'page': int(params.get('page', 1) or 1),
|
||
'rows': int(params.get('rows', 20) or 20),
|
||
'sort': params.get('sort', 'updated_at'),
|
||
'order': params.get('order', 'desc')}
|
||
recs = await sor.sqlPaging(
|
||
'SELECT id, name, category, description, created_at FROM drag_template%s' % wsql,
|
||
ns, args)
|
||
rows = [{'id': r.id, 'name': r.name, 'category': r.category,
|
||
'description': getattr(r, 'description', ''),
|
||
'created_at': str(getattr(r, 'created_at', ''))} for r in recs]
|
||
return {'list': rows, 'total': ns.get('total', len(rows))}
|
||
except Exception as e:
|
||
return {'code': 'DB_ERROR', 'message': '查询模板失败: %s' % e}
|
||
|
||
|
||
async def get_drag_template(ns):
|
||
"""模板详情(含 blocks/connections 解析)。"""
|
||
tid = (ns or {}).get('id')
|
||
if not tid:
|
||
return {'code': 'PARAM_REQUIRED', 'message': '缺少 id', 'field': 'id'}
|
||
dbname = await get_drag_graph_dbname()
|
||
try:
|
||
async with _server_env().sqlorContext(dbname) as sor:
|
||
recs = await sor.R('drag_template', {'id': tid})
|
||
if not recs:
|
||
return {'code': 'NOT_FOUND', 'message': '模板不存在: %s' % tid}
|
||
r = recs[0]
|
||
data = {'id': r.id, 'name': r.name, 'category': r.category,
|
||
'description': getattr(r, 'description', '')}
|
||
try:
|
||
data['blocks'] = json.loads(r.blocks or '[]')
|
||
except Exception:
|
||
data['blocks'] = []
|
||
try:
|
||
data['connections'] = json.loads(r.connections or '[]')
|
||
except Exception:
|
||
data['connections'] = []
|
||
return {'data': data}
|
||
except Exception as e:
|
||
return {'code': 'DB_ERROR', 'message': '查询模板失败: %s' % e}
|
||
|
||
|
||
async def create_drag_template(ns):
|
||
"""创建模板:blocks/connections 校验。"""
|
||
name = (ns or {}).get('name')
|
||
if not name or not str(name).strip():
|
||
return {'code': 'PARAM_REQUIRED', 'message': '模板名称必填', 'field': 'name'}
|
||
blocks = ns.get('blocks') or []
|
||
conns = ns.get('connections') or []
|
||
if isinstance(blocks, str):
|
||
try:
|
||
blocks = json.loads(blocks)
|
||
except Exception:
|
||
return {'code': 'PARAM_TYPE', 'message': 'blocks 不是合法 JSON', 'field': 'blocks'}
|
||
if isinstance(conns, str):
|
||
try:
|
||
conns = json.loads(conns)
|
||
except Exception:
|
||
return {'code': 'PARAM_TYPE', 'message': 'connections 不是合法 JSON', 'field': 'connections'}
|
||
v = validate_graph({'blocks': blocks, 'connections': conns})
|
||
dbname = await get_drag_graph_dbname()
|
||
try:
|
||
async with _server_env().sqlorContext(dbname) as sor:
|
||
tid = _new_id()
|
||
await sor.C('drag_template', {
|
||
'id': tid, 'name': str(name).strip(),
|
||
'category': str(ns.get('category') or '通用'),
|
||
'description': str(ns.get('description') or ''),
|
||
'blocks': _dumps(blocks), 'connections': _dumps(conns),
|
||
'created_at': _now(), 'updated_at': _now()})
|
||
return {'success': True, 'id': tid, 'valid': v['valid'], 'errors': v['errors']}
|
||
except Exception as e:
|
||
return {'code': 'DB_ERROR', 'message': '保存模板失败: %s' % e}
|
||
|
||
|
||
async def update_drag_template(ns):
|
||
"""更新模板。"""
|
||
tid = (ns or {}).get('id')
|
||
if not tid:
|
||
return {'code': 'PARAM_REQUIRED', 'message': '缺少 id', 'field': 'id'}
|
||
blocks = ns.get('blocks')
|
||
conns = ns.get('connections')
|
||
if isinstance(blocks, str):
|
||
try:
|
||
blocks = json.loads(blocks)
|
||
except Exception:
|
||
return {'code': 'PARAM_TYPE', 'message': 'blocks 不是合法 JSON', 'field': 'blocks'}
|
||
if isinstance(conns, str):
|
||
try:
|
||
conns = json.loads(conns)
|
||
except Exception:
|
||
return {'code': 'PARAM_TYPE', 'message': 'connections 不是合法 JSON', 'field': 'connections'}
|
||
dbname = await get_drag_graph_dbname()
|
||
try:
|
||
async with _server_env().sqlorContext(dbname) as sor:
|
||
recs = await sor.R('drag_template', {'id': tid})
|
||
if not recs:
|
||
return {'code': 'NOT_FOUND', 'message': '模板不存在: %s' % tid}
|
||
up = {'updated_at': _now()}
|
||
if blocks is not None:
|
||
up['blocks'] = _dumps(blocks)
|
||
if conns is not None:
|
||
up['connections'] = _dumps(conns)
|
||
if ns.get('name'):
|
||
up['name'] = str(ns['name']).strip()
|
||
if ns.get('category'):
|
||
up['category'] = str(ns['category'])
|
||
if ns.get('description') is not None:
|
||
up['description'] = str(ns['description'])
|
||
await sor.U('drag_template', up, {'id': tid})
|
||
return {'success': True, 'id': tid}
|
||
except Exception as e:
|
||
return {'code': 'DB_ERROR', 'message': '更新模板失败: %s' % e}
|
||
|
||
|
||
async def delete_drag_template(ns):
|
||
"""删除模板。"""
|
||
tid = (ns or {}).get('id')
|
||
if not tid:
|
||
return {'code': 'PARAM_REQUIRED', 'message': '缺少 id', 'field': 'id'}
|
||
dbname = await get_drag_graph_dbname()
|
||
try:
|
||
async with _server_env().sqlorContext(dbname) as sor:
|
||
await sor.D('drag_template', {'id': tid})
|
||
return {'success': True, 'id': tid}
|
||
except Exception as e:
|
||
return {'code': 'DB_ERROR', 'message': '删除模板失败: %s' % e}
|
||
|
||
|
||
async def template_use(ns):
|
||
"""应用模板:按模板生成新画布。ns: {template_id, name} → 新画布 id。"""
|
||
tid = (ns or {}).get('template_id') or (ns or {}).get('id')
|
||
if not tid:
|
||
return {'code': 'PARAM_REQUIRED', 'message': '缺少 template_id', 'field': 'template_id'}
|
||
got = await get_drag_template({'id': tid})
|
||
if got.get('code'):
|
||
return got
|
||
tpl = got['data']
|
||
return await create_drag_graph({'name': ns.get('name') or (tpl['name'] + '_副本'),
|
||
'description': '由模板 %s 生成' % tpl['name'],
|
||
'blocks': tpl['blocks'], 'connections': tpl['connections']})
|
||
|
||
|
||
# ═══════════════ 实体选择器(下拉数据源) ═══════════════
|
||
|
||
async def get_entity_options(ns=None):
|
||
"""实体下拉选项 [{value, text}]。复用 entity 模块的实体列表。"""
|
||
se = _server_env()
|
||
list_entities = getattr(se, 'list_entities', None)
|
||
if list_entities is None:
|
||
return [{'value': 'hero', 'text': 'hero(示例)'}, {'value': 'coin', 'text': 'coin(示例)'}]
|
||
try:
|
||
r = await list_entities({'page': 1, 'rows': 500})
|
||
items = r.get('list') or r.get('data') or []
|
||
if isinstance(r, list):
|
||
items = r
|
||
out = []
|
||
for it in items:
|
||
if isinstance(it, dict):
|
||
out.append({'value': it.get('id'), 'text': it.get('name') or it.get('id')})
|
||
else:
|
||
out.append({'value': getattr(it, 'id', ''), 'text': getattr(it, 'name', '') or getattr(it, 'id', '')})
|
||
return out or [{'value': 'hero', 'text': 'hero(示例)'}, {'value': 'coin', 'text': 'coin(示例)'}]
|
||
except Exception:
|
||
return [{'value': 'hero', 'text': 'hero(示例)'}, {'value': 'coin', 'text': 'coin(示例)'}]
|
||
|
||
|
||
async def get_template_category_options(ns=None):
|
||
"""模板分类下拉。"""
|
||
return [{'value': '通用', 'text': '通用'}, {'value': '移动', 'text': '移动'},
|
||
{'value': '弹球', 'text': '弹球'}, {'value': '计时', 'text': '计时'}]
|
||
|
||
|
||
# ═══════════════ 辅助:脚本校验白名单(供 script_engine 执行 drag 产物) ═══════════════
|
||
|
||
def _action_whitelist():
|
||
"""drag 编译产物可调用的运行时动作白名单(宿主演绎层实现)。"""
|
||
return ['move', 'rotate', 'scale', 'set_property', 'play_animation',
|
||
'play_sound', 'show_hide', 'camera', 'wait']
|
||
|
||
|
||
def load_drag():
|
||
"""挂载 drag 模块函数到 ServerEnv(宿主 init() 调用)。"""
|
||
env = _server_env()
|
||
env.get_block_defs_api = get_block_defs_api
|
||
env.list_drag_graphs = list_drag_graphs
|
||
env.get_drag_graph = get_drag_graph
|
||
env.create_drag_graph = create_drag_graph
|
||
env.update_drag_graph = update_drag_graph
|
||
env.delete_drag_graph = delete_drag_graph
|
||
env.graph_save = graph_save
|
||
env.graph_compile = graph_compile
|
||
env.graph_validate = graph_validate
|
||
env.graph_publish = graph_publish
|
||
env.list_drag_templates = list_drag_templates
|
||
env.get_drag_template = get_drag_template
|
||
env.create_drag_template = create_drag_template
|
||
env.update_drag_template = update_drag_template
|
||
env.delete_drag_template = delete_drag_template
|
||
env.template_use = template_use
|
||
env.get_entity_options = get_entity_options
|
||
env.get_template_category_options = get_template_category_options
|
||
# CRUD 复数别名(CRUD 框架约定:create_xxx/update_xxx/delete_xxx)
|
||
env.create_drag_graphs = create_drag_graph
|
||
env.update_drag_graphs = update_drag_graph
|
||
env.delete_drag_graphs = delete_drag_graph
|
||
env.create_drag_templates = create_drag_template
|
||
env.update_drag_templates = update_drag_template
|
||
env.delete_drag_templates = delete_drag_template
|
||
# 内部导出(测试/扩展用)
|
||
env.drag_action_whitelist = _action_whitelist
|
||
env.drag_validate_graph = validate_graph
|
||
env.drag_compile_graph = compile_graph
|
||
print('[drag] load_drag() OK: %d block types registered' % len(BLOCK_DEFS))
|