bugfix
This commit is contained in:
commit
68da1a1aea
21
README.md
Normal file
21
README.md
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
# dataui
|
||||||
|
A set of tools which auto generates frontend ui and backend data operation for data in table in database.
|
||||||
|
|
||||||
|
## crud
|
||||||
|
crud module generates crud UI for data in table in database.
|
||||||
|
base on a description json file, this module wll generate code by a parameter:
|
||||||
|
* browser: genrates the data browser UI
|
||||||
|
* filter: generates the filter condition input UI
|
||||||
|
* add: generates the new data input UI
|
||||||
|
* edit: generates the data modify UI
|
||||||
|
* remove: generates the delete data UI
|
||||||
|
* choose: browser data for choose, has filter UI.
|
||||||
|
* insert:generates the new data insert backend script;
|
||||||
|
* update:generates the update data backend script
|
||||||
|
* select:generates the query data backend script
|
||||||
|
* delete:generates the delete data backend script
|
||||||
|
|
||||||
|
## tree
|
||||||
|
tree module generates tree UI for hierarchy data in table in database.
|
||||||
|
|
||||||
|
a plugin for gadget webserver, it generates data browser
|
0
crud/__init__.py
Normal file
0
crud/__init__.py
Normal file
15
crud/build_filter_sql.py
Normal file
15
crud/build_filter_sql.py
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
|
||||||
|
def build_filters_sql(table, ns):
|
||||||
|
"""
|
||||||
|
|
||||||
|
"""
|
||||||
|
sql = f"select * from {table} where 1=1"
|
||||||
|
for k,v in ns.items():
|
||||||
|
if isinstance(v, list):
|
||||||
|
sql = "%s and %s in ${%s}$" % (sql, k, k)
|
||||||
|
else:
|
||||||
|
if v == '_all_':
|
||||||
|
continue
|
||||||
|
sql = "%s and %s = ${%s}$" % (sql, k, k)
|
||||||
|
return sql
|
||||||
|
|
150
crud/code_keyvalue.py
Normal file
150
crud/code_keyvalue.py
Normal file
@ -0,0 +1,150 @@
|
|||||||
|
|
||||||
|
"""
|
||||||
|
## 实现方法
|
||||||
|
代码以一组数据表形式存储在数据库中, 按照配置,代码表可以存在独立的数据库中也可以存储在业务数据库中
|
||||||
|
多级代码,代码键值以“.“字符分割,可形成多级代码
|
||||||
|
|
||||||
|
其中
|
||||||
|
代码定义表定义每个代码的名称
|
||||||
|
代码码值表存储各个代码键值和码值
|
||||||
|
|
||||||
|
## 数据结构
|
||||||
|
代码定义表
|
||||||
|
md_code_definition
|
||||||
|
{
|
||||||
|
code_name,
|
||||||
|
code_desc
|
||||||
|
}
|
||||||
|
|
||||||
|
代码码值表
|
||||||
|
md_code_keyvalue
|
||||||
|
{
|
||||||
|
code_name,
|
||||||
|
code_key,
|
||||||
|
level,
|
||||||
|
code_value
|
||||||
|
}
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
from sqlor.dbpools import DBPools
|
||||||
|
|
||||||
|
class CodeKeyValue:
|
||||||
|
def __init__(self, database):
|
||||||
|
self.database = database
|
||||||
|
self.db = DBPools()
|
||||||
|
|
||||||
|
async def new_code(self, code_name:str, code_desc):
|
||||||
|
"""
|
||||||
|
新建一个代码
|
||||||
|
"""
|
||||||
|
with self.db.sqlorContext(self.database) as sor:
|
||||||
|
await sor.C('md_code_definition',
|
||||||
|
code_name=code_name,
|
||||||
|
code_desc=code_desc)
|
||||||
|
|
||||||
|
async def add_kv(self, code_name:str, code_key:str, code_value:str):
|
||||||
|
"""
|
||||||
|
新增一个代码
|
||||||
|
code_name:代码名称
|
||||||
|
code_key:代码键值
|
||||||
|
code_value:代码键值
|
||||||
|
"""
|
||||||
|
level = len(code_key.split('.'))
|
||||||
|
with self.db.sqlorContext(self.database) as sor:
|
||||||
|
await sor.C('md_code_keyvalue',
|
||||||
|
code_name=code_name,
|
||||||
|
code_key=code_key,
|
||||||
|
level=level,
|
||||||
|
code_value=code_value)
|
||||||
|
|
||||||
|
async def delete_kv(self, code_name:str, code_key:str):
|
||||||
|
"""
|
||||||
|
删除一个代码
|
||||||
|
code_name:
|
||||||
|
code_key:
|
||||||
|
"""
|
||||||
|
with self.db.sqlorContext(self.database) as sor:
|
||||||
|
await sor.D('md_code_keyvalue',
|
||||||
|
code_name=code_name,
|
||||||
|
code_key=code_key)
|
||||||
|
|
||||||
|
async def modify_kv(self, code_name:str, code_key:str, code_value:str):
|
||||||
|
"""
|
||||||
|
修改一个代码值
|
||||||
|
code_name:代码名称
|
||||||
|
code_key:代码键值
|
||||||
|
code_value:新代码键值
|
||||||
|
"""
|
||||||
|
|
||||||
|
level = len(code_key.split('.'))
|
||||||
|
with self.db.sqlorContext(self.database) as sor:
|
||||||
|
await sor.U('md_code_keyvalue',
|
||||||
|
code_name=code_name,
|
||||||
|
code_key=code_key,
|
||||||
|
level=level,
|
||||||
|
code_value=code_value)
|
||||||
|
|
||||||
|
async def get_code_value(self, code_name:str, code_key:str) -> str:
|
||||||
|
"""
|
||||||
|
得到代码码值
|
||||||
|
code_name:代码名称
|
||||||
|
code_key:代码键值
|
||||||
|
返回值:字符串,如果没找到返回None
|
||||||
|
|
||||||
|
"""
|
||||||
|
with self.db.sqlorContext(self.database) as sor:
|
||||||
|
r = await sor.R('md_code_keyvalue',
|
||||||
|
code_name=code_name,
|
||||||
|
code_key=code_key)
|
||||||
|
if len(r)>0:
|
||||||
|
return r[0]['code_value']
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def get_code_list(self, code_name:str, code_key_part:str=None) -> list:
|
||||||
|
"""
|
||||||
|
得到代码名称的代码列表,如果code_key_part不为空,则获得级联的下级代码的字典
|
||||||
|
code_name:代码名称
|
||||||
|
code_key:代码键值
|
||||||
|
返回值:字符串,如果没找到返回None
|
||||||
|
"""
|
||||||
|
filter = ''
|
||||||
|
ns = {
|
||||||
|
'code_name':code_name
|
||||||
|
}
|
||||||
|
if code_key_part is not None:
|
||||||
|
level = len(code_key_part.split('.')) + 1
|
||||||
|
ns['level'] = level
|
||||||
|
ns['parts'] = code_key_parts + '%'
|
||||||
|
filter = ' and level=${level}$ and code_key like ${parts}$'
|
||||||
|
sql = '''select * from md_code_keyvalue
|
||||||
|
where code_name=${code_name}$ ''' + filter
|
||||||
|
with self.db.sqlorContext(self.database) as sor:
|
||||||
|
r = await sor.sqlExe(sql, ns)
|
||||||
|
return r
|
||||||
|
|
||||||
|
def sql_completion(self, sql, codes):
|
||||||
|
"""
|
||||||
|
sql代码补全,补全每个代码的码值
|
||||||
|
sql:原始sql代码
|
||||||
|
codes:需要补全的代码数组,每个codes中的code有以下格式:
|
||||||
|
{
|
||||||
|
code_keyfield, sql中键值字段
|
||||||
|
code_valuefield, 补充的键值字段
|
||||||
|
code_name, 代码名称
|
||||||
|
}
|
||||||
|
返回值:补全的SQL代码
|
||||||
|
局限:补全的sql没有做有效性分析,不能保证能够执行。比如code_keyfield在原始sql的结果字段中不存在,代码库和原始sql的目标数据库不是同一个数据库。
|
||||||
|
|
||||||
|
"""
|
||||||
|
i = 0
|
||||||
|
dbp = DBPools()
|
||||||
|
dbname = dbp.get_dbname(self.database)
|
||||||
|
for c in codes:
|
||||||
|
sql = '''select a{i}.*, b{i}.code_value as {c['code_valuefield']}
|
||||||
|
from ({sql}) as a{i}, {dbname}.code_keyvalue as b{i}
|
||||||
|
where a{i}.{c['code_keyfield'] = b{i}.code_key
|
||||||
|
and b{i}.code_name = '{c["code_name"]}'
|
||||||
|
'''
|
||||||
|
i += 1
|
||||||
|
return sql
|
625
crud/crudProcessor.py
Normal file
625
crud/crudProcessor.py
Normal file
@ -0,0 +1,625 @@
|
|||||||
|
|
||||||
|
|
||||||
|
"""
|
||||||
|
table info:
|
||||||
|
md_metadata {
|
||||||
|
id,
|
||||||
|
ancestor,
|
||||||
|
name,
|
||||||
|
title,
|
||||||
|
type,
|
||||||
|
length,
|
||||||
|
dec,
|
||||||
|
nullable,
|
||||||
|
default,
|
||||||
|
uiattr
|
||||||
|
|
||||||
|
"""
|
||||||
|
from traceback import print_exc
|
||||||
|
import codecs
|
||||||
|
import ujson as json
|
||||||
|
from sqlor.dbpools import DBPools
|
||||||
|
from sqlor.filter import DBFilter
|
||||||
|
from ahserver.baseProcessor import TemplateProcessor
|
||||||
|
from .metadata import Metadata
|
||||||
|
|
||||||
|
class CRUDEngine:
|
||||||
|
def __init__(self, processor, crud_dic:dict):
|
||||||
|
self.processor = processor
|
||||||
|
self.database = crud_dic['database']
|
||||||
|
self.table = crud_dic['table']
|
||||||
|
self.db = DBPools()
|
||||||
|
self.crud_title = ''
|
||||||
|
self.crud_dic = crud_dic
|
||||||
|
self.metadata = Metadata()
|
||||||
|
self.cmds = {
|
||||||
|
'browser':self.build_browser,
|
||||||
|
'browser_data':self.build_browser_data,
|
||||||
|
'filter':self.build_filter_form,
|
||||||
|
'add':self.build_new_data_form,
|
||||||
|
'edit':self.build_edit_data_form,
|
||||||
|
'delete':self.build_delete_data,
|
||||||
|
"new_data":self.build_new_data,
|
||||||
|
"edit_data":self.build_edit_data,
|
||||||
|
"delete_data":self.build_delete_data
|
||||||
|
}
|
||||||
|
|
||||||
|
def is_legal_cmd(self, cmd:str) -> bool:
|
||||||
|
return cmd in self.cmds.keys()
|
||||||
|
|
||||||
|
def defaultcmd(self):
|
||||||
|
return 'browser'
|
||||||
|
|
||||||
|
async def render(self):
|
||||||
|
cmd = self.get_cmd()
|
||||||
|
return await self.dispatch(cmd)
|
||||||
|
|
||||||
|
async def dispatch(self, cmd:str):
|
||||||
|
f = self.cmds.get(cmd)
|
||||||
|
if f:
|
||||||
|
return await f()
|
||||||
|
return await self.build_crud()
|
||||||
|
|
||||||
|
async def get_uiattr(self, field:str) -> dict:
|
||||||
|
if not self.metadata:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
id = f'{self.database}.{self.table}.{field}'
|
||||||
|
ui = await self.metadata.get_metadata_uiattr(id)
|
||||||
|
if ui:
|
||||||
|
return ui
|
||||||
|
id = field
|
||||||
|
ui = await self.metadata.get_metadata_uiattr(id)
|
||||||
|
return ui
|
||||||
|
|
||||||
|
def get_cmd(self):
|
||||||
|
path_parts = self.processor.path.split('/')
|
||||||
|
cmd = ''
|
||||||
|
cmd = path_parts[-2]
|
||||||
|
if not self.is_legal_cmd(cmd):
|
||||||
|
cmd = ''
|
||||||
|
return cmd
|
||||||
|
|
||||||
|
def cmdid(self, cmd):
|
||||||
|
crud_id = self.crud_dic.get('crud_id', '')
|
||||||
|
if cmd == '':
|
||||||
|
return f'{crud_id}_{self.database}_{self.table}'
|
||||||
|
return f'{crud_id}_{self.database}_{self.table}_{cmd}'
|
||||||
|
|
||||||
|
def cmdurl(self, cmd):
|
||||||
|
p = self._cmdurl(cmd)
|
||||||
|
return self._env('entire_url')(p)
|
||||||
|
|
||||||
|
def _cmdurl(self, cmd):
|
||||||
|
if cmd != '' and not self.is_legal_cmd(cmd):
|
||||||
|
return
|
||||||
|
path_parts = self.processor.path.split('/')
|
||||||
|
ocmd = self.get_cmd()
|
||||||
|
if ocmd == cmd:
|
||||||
|
return
|
||||||
|
|
||||||
|
if self.is_legal_cmd(path_parts[-2]):
|
||||||
|
if cmd == '':
|
||||||
|
del path_parts[-2]
|
||||||
|
return '/'.join(path_parts)
|
||||||
|
path_parts[-2] = cmd
|
||||||
|
return '/'.join(path_parts)
|
||||||
|
|
||||||
|
path_parts.insert(-1, cmd)
|
||||||
|
return '/'.join(path_parts)
|
||||||
|
|
||||||
|
def build_menu_dic(self):
|
||||||
|
return {
|
||||||
|
"widgettype":"Menu",
|
||||||
|
"options":{
|
||||||
|
"single_expand":True,
|
||||||
|
"idField":"id",
|
||||||
|
"textField":"label",
|
||||||
|
"bgcolor":[0.2,0.2,0.2,1],
|
||||||
|
"target":f"root." + self.cmdid(''),
|
||||||
|
"data":[
|
||||||
|
{
|
||||||
|
"id":"browser",
|
||||||
|
"label":"Browser",
|
||||||
|
"url":self.cmdurl('browser')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id":"filter",
|
||||||
|
"label":"Filter",
|
||||||
|
"url":self.cmdurl('filter')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id":"add",
|
||||||
|
"label":"Add",
|
||||||
|
"url":self.cmdurl('add')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id":"edit",
|
||||||
|
"label":"Edit",
|
||||||
|
"url":self.cmdurl('edit')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id":"delete",
|
||||||
|
"label":"delete",
|
||||||
|
"url":self.cmdurl('delete')
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async def build_crud(self):
|
||||||
|
kw = {
|
||||||
|
"bar_size":2,
|
||||||
|
"bar_at":"top",
|
||||||
|
"singlepage":False
|
||||||
|
}
|
||||||
|
kw.update(self.crud_dic.get('options', {}))
|
||||||
|
# kw['left_menu'] = self.build_menu_dic()
|
||||||
|
crud_id = self.crud_dic.get('crud_id', '')
|
||||||
|
return {
|
||||||
|
"id":self.cmdid(''),
|
||||||
|
"widgettype":"PagePanel",
|
||||||
|
"options":kw,
|
||||||
|
"subwidgets":[
|
||||||
|
{
|
||||||
|
"widgettype":"urlwidget",
|
||||||
|
"options":{
|
||||||
|
"params":{
|
||||||
|
},
|
||||||
|
"url":self.cmdurl('browser')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
def guess_uitype(self, field):
|
||||||
|
if 'date' in field['name']:
|
||||||
|
return 'date'
|
||||||
|
length = field.get('len', 0)
|
||||||
|
datatype = field['type']
|
||||||
|
if datatype in ['int', 'long', 'integer']:
|
||||||
|
return 'int'
|
||||||
|
if datatype in ['float', 'double']:
|
||||||
|
return 'float'
|
||||||
|
if datatype == 'text':
|
||||||
|
return 'text'
|
||||||
|
if datatype == 'str' and length > 80:
|
||||||
|
return 'text'
|
||||||
|
return 'str'
|
||||||
|
|
||||||
|
def set_field_width(self, field):
|
||||||
|
factor = 0.7
|
||||||
|
length = field.get('len', 15)
|
||||||
|
datatype = field['type']
|
||||||
|
if length > 80:
|
||||||
|
length = 80
|
||||||
|
if 'date' in field['name']:
|
||||||
|
return 10 * factor
|
||||||
|
return length * factor
|
||||||
|
|
||||||
|
async def get_fields(self):
|
||||||
|
fields = []
|
||||||
|
async with self.db.sqlorContext(self.database) as sor:
|
||||||
|
info = await sor.I(self.table)
|
||||||
|
if not info:
|
||||||
|
return []
|
||||||
|
self.crud_title = info['summary'][0]['title']
|
||||||
|
fields = [{
|
||||||
|
"name":i['name'],
|
||||||
|
"uitype":self.guess_uitype(i),
|
||||||
|
"datatype":i['type'],
|
||||||
|
"label":i.get('title'),
|
||||||
|
"length":i['len'],
|
||||||
|
"width":self.set_field_width(i),
|
||||||
|
"dec":i['dec']
|
||||||
|
} for i in info['fields'] ]
|
||||||
|
for f in fields:
|
||||||
|
ui = await self.get_uiattr(f['name'])
|
||||||
|
if ui:
|
||||||
|
f.update(ui)
|
||||||
|
|
||||||
|
return fields
|
||||||
|
|
||||||
|
async def build_new_data_form(self) -> dict:
|
||||||
|
"""
|
||||||
|
build new data form dic client application needed to
|
||||||
|
constructs a add data form widget
|
||||||
|
"""
|
||||||
|
fields = await self.get_fields()
|
||||||
|
opts = {
|
||||||
|
"cols":1,
|
||||||
|
"labelwidth":0.3,
|
||||||
|
"inputheight":1.5,
|
||||||
|
"toolbar_at":"top",
|
||||||
|
"fields":fields
|
||||||
|
}
|
||||||
|
print('crud_dic add=', self.crud_dic['actions']['add'])
|
||||||
|
nopts = self.crud_dic['actions']['add'].get('options',{})
|
||||||
|
opts.update(nopts)
|
||||||
|
print(nopts, opts)
|
||||||
|
d = {
|
||||||
|
"widgettype":"Form",
|
||||||
|
"options":opts,
|
||||||
|
"title_widget":{
|
||||||
|
"widgettype":"Text",
|
||||||
|
"options":{
|
||||||
|
"otext":self.crud_title,
|
||||||
|
"i18n":True
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"binds":[
|
||||||
|
{
|
||||||
|
"wid":"self",
|
||||||
|
"event":"on_submit",
|
||||||
|
"actiontype":"multiple",
|
||||||
|
"actions":[
|
||||||
|
{
|
||||||
|
"actiontype":"urlwidget",
|
||||||
|
"datawidget":"self",
|
||||||
|
"options":{
|
||||||
|
"url":self.cmdurl('new_data')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"actiontype":"script",
|
||||||
|
"target":'root.' + self.cmdid(''),
|
||||||
|
"script":"self.pop(None)"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
d = self.replace_crud_binds(d, 'add')
|
||||||
|
return d
|
||||||
|
|
||||||
|
async def build_new_data(self, *args):
|
||||||
|
ns = await self._env('request2ns')()
|
||||||
|
db = DBPools()
|
||||||
|
async with db.sqlorContext(self.database) as sor:
|
||||||
|
await sor.C(self.table, ns)
|
||||||
|
return self.message('data added')
|
||||||
|
|
||||||
|
async def build_edit_data(self, *args):
|
||||||
|
ns = await self._env('request2ns')()
|
||||||
|
db = DBPools()
|
||||||
|
async with db.sqlorContext(self.database) as sor:
|
||||||
|
await sor.U(self.table, ns)
|
||||||
|
return self.message('date modified')
|
||||||
|
|
||||||
|
async def build_edit_data_form(self) ->dict:
|
||||||
|
"""
|
||||||
|
build modify data form dic client application needed to
|
||||||
|
constructs a modify data form widget
|
||||||
|
"""
|
||||||
|
fields = await self.get_fields()
|
||||||
|
ns = await self._env('request2ns')()
|
||||||
|
db = DBPools()
|
||||||
|
async with db.sqlorContext(self.database) as sor:
|
||||||
|
info = await sor.I(self.table)
|
||||||
|
primary = info['summary'][0]['primary']
|
||||||
|
ns1 = {k:v for k, v in ns.items() if k in primary}
|
||||||
|
d = await sor.R(self.table, ns1)
|
||||||
|
if len(d) > 0:
|
||||||
|
for f in fields:
|
||||||
|
f['value'] = d[0][f['name']]
|
||||||
|
|
||||||
|
opts = {
|
||||||
|
"cols":1,
|
||||||
|
"labelwidth":0.3,
|
||||||
|
"inputheight":1.5,
|
||||||
|
"toolbar_at":"top",
|
||||||
|
"fields":fields
|
||||||
|
}
|
||||||
|
opts.update(self.crud_dic['actions']['edit'].get('options',{}))
|
||||||
|
d = {
|
||||||
|
"widgettype":"Form",
|
||||||
|
"options":opts,
|
||||||
|
"binds":[
|
||||||
|
{
|
||||||
|
"wid":"self",
|
||||||
|
"event":"on_submit",
|
||||||
|
"actiontype":"multiple",
|
||||||
|
"actions":[
|
||||||
|
{
|
||||||
|
"actiontype":"urlwidget",
|
||||||
|
"datawidget":"self",
|
||||||
|
"target":'root.' + self.cmdid(''),
|
||||||
|
"options":{
|
||||||
|
"url":self.cmdurl('edit_data')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
d = self.replace_crud_binds(d, 'edit')
|
||||||
|
return d
|
||||||
|
|
||||||
|
async def build_delete_data(self) -> dict:
|
||||||
|
ns = await self._env('request2ns')()
|
||||||
|
db = DBPools()
|
||||||
|
async with db.sqlorContext(self.database) as sor:
|
||||||
|
await sor.D(self.table, ns)
|
||||||
|
return self.message('data deleted')
|
||||||
|
|
||||||
|
async def build_filter_form(self) -> dict:
|
||||||
|
"""
|
||||||
|
build filter form dic client application needed
|
||||||
|
to construct filter form widget
|
||||||
|
"""
|
||||||
|
table_fields = await self.get_fields()
|
||||||
|
name_fields = {i['name']:i for i in table_fields }
|
||||||
|
tf_names = name_fields.keys()
|
||||||
|
filters = self.crud_dic['actions']['filter'].get('filters', None)
|
||||||
|
alter_fields = self.crud_dic['actions']['filter'].get('alter_fields', [])
|
||||||
|
dbf = DBFilter(filters)
|
||||||
|
variables = dbf.get_variables()
|
||||||
|
fields = []
|
||||||
|
for v, f in variables.items():
|
||||||
|
field = name_fields[f]
|
||||||
|
field['name'] = v
|
||||||
|
fields.append(field)
|
||||||
|
|
||||||
|
for af in alter_fields:
|
||||||
|
for f in fields:
|
||||||
|
if f['name'] == af['name']:
|
||||||
|
f.update(af)
|
||||||
|
|
||||||
|
opts = {
|
||||||
|
"cols":1,
|
||||||
|
"labelwidth":0.3,
|
||||||
|
"inputheight":1.5,
|
||||||
|
"toolbar_at":"top",
|
||||||
|
"fields":fields
|
||||||
|
}
|
||||||
|
opts.update(self.crud_dic['actions']['filter'].get('options',{}))
|
||||||
|
d = {
|
||||||
|
"widgettype":"Form",
|
||||||
|
"options":opts,
|
||||||
|
"title_widget":{
|
||||||
|
"widgettype":"Text",
|
||||||
|
"options":{
|
||||||
|
"otext":self.crud_title,
|
||||||
|
"i18n":True
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"binds":[
|
||||||
|
{
|
||||||
|
"wid":"self",
|
||||||
|
"event":"on_submit",
|
||||||
|
"actiontype":"multiple",
|
||||||
|
"actions":[
|
||||||
|
{
|
||||||
|
"actiontype":"script",
|
||||||
|
"datawidget":"self",
|
||||||
|
"target":'root.' + self.cmdid('browser'),
|
||||||
|
"script":"self.loadData(**kwargs)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"actiontype":"script",
|
||||||
|
"target":'root.' + self.cmdid(''),
|
||||||
|
"script":"self.pop(None)"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
d = self.replace_crud_binds(d, 'filter')
|
||||||
|
return d
|
||||||
|
|
||||||
|
def _env(self, fname):
|
||||||
|
return self.processor.run_ns.get(fname, None)
|
||||||
|
|
||||||
|
def user_params_path(self, user):
|
||||||
|
request = self._env('request')
|
||||||
|
fname = request.path.split('/')[-1]
|
||||||
|
fparts = fname.split('.')
|
||||||
|
fparts.insert(-1, user)
|
||||||
|
path = f'{fname}.{user}'
|
||||||
|
fnames = request.path.split('/')
|
||||||
|
fnames[-1] = path
|
||||||
|
path = '/'.join(fnames)
|
||||||
|
return path
|
||||||
|
|
||||||
|
def save_default_params(self, ns):
|
||||||
|
user = self._env('user')
|
||||||
|
if not user:
|
||||||
|
return
|
||||||
|
path = self.user_params_path(user)
|
||||||
|
url = self._env('entire_url')(path)
|
||||||
|
path = self.processor.resource.url2file(url)
|
||||||
|
print('realpath=', path)
|
||||||
|
with codecs.open(path, 'w', 'utf-8') as f:
|
||||||
|
json.dump(ns,f)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
async def get_default_params(self, user):
|
||||||
|
try:
|
||||||
|
path = self.user_params_path(user)
|
||||||
|
url = self._env('entire_url')(path)
|
||||||
|
path = self.processor.resource.url2file(url)
|
||||||
|
print('realpath=', path, 'url=', url)
|
||||||
|
with codecs.open(path, 'r', 'utf-8') as f:
|
||||||
|
dic = json.load(f)
|
||||||
|
return dic
|
||||||
|
except Exception as e:
|
||||||
|
print_exc()
|
||||||
|
print('get_default_params():', e)
|
||||||
|
return {}
|
||||||
|
|
||||||
|
async def build_browser_data(self):
|
||||||
|
ns = await self._env('request2ns')()
|
||||||
|
filters = self.crud_dic['actions']['filter'].get('filters',None)
|
||||||
|
async with self.db.sqlorContext(self.database) as sor:
|
||||||
|
r = await sor.R(self.table, ns, filters=filters)
|
||||||
|
d = [ i.to_dict() for i in r['rows'] ]
|
||||||
|
r['rows'] = d
|
||||||
|
return r
|
||||||
|
|
||||||
|
async def build_browser(self) -> dict:
|
||||||
|
"""
|
||||||
|
build data browser dic client application needed to
|
||||||
|
constructs a browser widget
|
||||||
|
"""
|
||||||
|
entire_url = self._env('entire_url')
|
||||||
|
crud_id = self.crud_dic.get('crud_id','')
|
||||||
|
table_fields = await self.get_fields()
|
||||||
|
actions = self.crud_dic.get('actions')
|
||||||
|
exclude_fields = actions['browser'].get('exclude_fields', [])
|
||||||
|
extend_fields = actions['browser'].get('extend_fields', [])
|
||||||
|
fields = [ f for f in table_fields if f['name'] not in exclude_fields ]
|
||||||
|
fields += extend_fields
|
||||||
|
if actions['browser'].get('checkbox'):
|
||||||
|
checkbox = {
|
||||||
|
'name':'_checkbox',
|
||||||
|
'uitype':'checkbox',
|
||||||
|
'width':1,
|
||||||
|
'freeze':True,
|
||||||
|
'label':''
|
||||||
|
}
|
||||||
|
fields.insert(0, checkbox)
|
||||||
|
|
||||||
|
user = self._env('user')
|
||||||
|
params = {}
|
||||||
|
dic = {}
|
||||||
|
if user:
|
||||||
|
dic = await self.get_default_params(user)
|
||||||
|
if dic == {}:
|
||||||
|
dic = await self.get_default_params('default')
|
||||||
|
params.update(dic)
|
||||||
|
ns = await self._env('request2ns')()
|
||||||
|
if ns != {}:
|
||||||
|
self.save_user_params(ns)
|
||||||
|
|
||||||
|
params.update(ns)
|
||||||
|
d = {
|
||||||
|
"id":self.cmdid('browser'),
|
||||||
|
"widgettype":"DataGrid",
|
||||||
|
"options":{
|
||||||
|
"row_height":2,
|
||||||
|
"header_css":"header_css",
|
||||||
|
"toolbar":{
|
||||||
|
"target":f"root.{self.cmdid('')}",
|
||||||
|
"executable":True,
|
||||||
|
"img_size_c":1,
|
||||||
|
"text_size_c":1,
|
||||||
|
"toolbar_orient":"H",
|
||||||
|
"tool_orient":"horizontal",
|
||||||
|
"css_on":"selected",
|
||||||
|
"css_off":"normal",
|
||||||
|
"tools":[
|
||||||
|
{
|
||||||
|
"name":"filter",
|
||||||
|
"source_on":entire_url('/imgs/crud_filter.png'),
|
||||||
|
"source_off":entire_url('/imgs/crud_filter.png'),
|
||||||
|
"label":"filter",
|
||||||
|
"url":self.cmdurl('filter')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name":"add",
|
||||||
|
"source_on":entire_url('/imgs/crud_add.png'),
|
||||||
|
"source_off":entire_url('/imgs/crud_add.png'),
|
||||||
|
"label":"add",
|
||||||
|
"url":self.cmdurl('add')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name":"edit",
|
||||||
|
"source_on":entire_url('/imgs/crud_edit.png'),
|
||||||
|
"source_off":entire_url('/imgs/crud_edit.png'),
|
||||||
|
"label":"edit",
|
||||||
|
"datawidget":f"root.{self.cmdid('browser')}",
|
||||||
|
"datamethod":'get_selected_data',
|
||||||
|
"url":self.cmdurl('edit')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name":"delete",
|
||||||
|
"source_on":entire_url('/imgs/crud_del.png'),
|
||||||
|
"source_off":entire_url('/imgs/crud_del.png'),
|
||||||
|
"label":"delete",
|
||||||
|
"datawidget":f"root.{self.cmdid('browser')}",
|
||||||
|
"datamethod":'get_selected_data',
|
||||||
|
"conform":{
|
||||||
|
"size_hint":[0.6,0.6],
|
||||||
|
"title":"删除记录",
|
||||||
|
"message":"要删除选中的记录码?"
|
||||||
|
},
|
||||||
|
"url":self.cmdurl('delete')
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"tailer":{
|
||||||
|
"info":[
|
||||||
|
"total_cnt",
|
||||||
|
"curpage"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"body_css":"body_css",
|
||||||
|
"row_normal_css":"normal_css",
|
||||||
|
"row_selected_css":"selected_css",
|
||||||
|
"dataloader":{
|
||||||
|
"page_rows":60,
|
||||||
|
"dataurl":self.cmdurl('browser_data'),
|
||||||
|
"params":params
|
||||||
|
},
|
||||||
|
"fields":fields
|
||||||
|
},
|
||||||
|
"title_widget":{
|
||||||
|
"widgettype":"Text",
|
||||||
|
"options":{
|
||||||
|
"otext":self.crud_title,
|
||||||
|
"i18n":True
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
d = self.replace_crud_binds(d, 'browser')
|
||||||
|
print('browser():return d=', d)
|
||||||
|
return d
|
||||||
|
|
||||||
|
def replace_crud_binds(self, dic, cmd):
|
||||||
|
binds = self.crud_dic['actions'][cmd].get('binds')
|
||||||
|
if binds:
|
||||||
|
dic['binds'] = binds
|
||||||
|
return dic
|
||||||
|
|
||||||
|
def error(self, errmsg, title=None):
|
||||||
|
return {
|
||||||
|
'widgettype':'Error',
|
||||||
|
'options':{
|
||||||
|
'title':title,
|
||||||
|
'message':errmsg
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def message(self, msg, title=None):
|
||||||
|
return {
|
||||||
|
'widgettype':'Message',
|
||||||
|
'options':{
|
||||||
|
'title':title,
|
||||||
|
'message':msg
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class CRUDProcessor(TemplateProcessor):
|
||||||
|
"""
|
||||||
|
accept json data from frontend with such attributes:
|
||||||
|
{
|
||||||
|
database:
|
||||||
|
table
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
@classmethod
|
||||||
|
def isMe(self, name):
|
||||||
|
return name == 'crud'
|
||||||
|
|
||||||
|
async def path_call(self, request, params={}):
|
||||||
|
txt = await super(CRUDProcessor, self).path_call(request, params=params)
|
||||||
|
dic = json.loads(txt)
|
||||||
|
database = dic['database']
|
||||||
|
table = dic['table']
|
||||||
|
crud = CRUDEngine(self, dic)
|
||||||
|
return await crud.render()
|
||||||
|
|
||||||
|
async def datahandle(self, request):
|
||||||
|
self.content = await self.path_call(request)
|
||||||
|
|
65
crud/metadata.py
Normal file
65
crud/metadata.py
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
|
||||||
|
import ujson as json
|
||||||
|
from appPublic.jsonConfig import getConfig
|
||||||
|
from sqlor.dbpools import DBPools
|
||||||
|
from ahserver.serverenv import ServerEnv
|
||||||
|
|
||||||
|
def getMetaDatabase():
|
||||||
|
config = getConfig()
|
||||||
|
if config.metadatabase:
|
||||||
|
return config.databases[config.metadatabase].get('dbname')
|
||||||
|
return None
|
||||||
|
|
||||||
|
env = ServerEnv()
|
||||||
|
env.getMetaDatabase = getMetaDatabase
|
||||||
|
|
||||||
|
class Metadata:
|
||||||
|
"""
|
||||||
|
store meta data in a md_metadata table
|
||||||
|
metadata table info:
|
||||||
|
md_metadata {
|
||||||
|
id,
|
||||||
|
ancestor,
|
||||||
|
name,
|
||||||
|
title,
|
||||||
|
type,
|
||||||
|
length,
|
||||||
|
dec,
|
||||||
|
nullable,
|
||||||
|
default,
|
||||||
|
uiattr
|
||||||
|
|
||||||
|
|
||||||
|
"""
|
||||||
|
def __init__(self):
|
||||||
|
self.database = getMetaDatabase()
|
||||||
|
self.table = 'md_metadata'
|
||||||
|
self.db = DBPools()
|
||||||
|
|
||||||
|
async def get_metadata(self, id:str):
|
||||||
|
async with self.db.sqlorContext(self.database) as sor:
|
||||||
|
x = await sor.R(self.table, {'id':id})
|
||||||
|
if len(x) > 0:
|
||||||
|
b = {k:v for k,v in x[0].items() \
|
||||||
|
if v is not None and k!='ancestor' }
|
||||||
|
if len(b.keys()) == len(x[0].keys()) - 1:
|
||||||
|
return b
|
||||||
|
if x[0].get('ancestor') is None:
|
||||||
|
return b
|
||||||
|
x = await self.get_metadata(self,x[0].get('ancestor'))
|
||||||
|
return x.update(b)
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def get_metadata_uiattr(self, id:str):
|
||||||
|
async with self.db.sqlorContext(self.database) as sor:
|
||||||
|
x = await sor.R(self.table, {'id':id})
|
||||||
|
if len(x) > 0:
|
||||||
|
ui_attr = x[0].get('ui_attr', None)
|
||||||
|
if not ui_attr:
|
||||||
|
id = x[0].get('ancestor', None)
|
||||||
|
if not id:
|
||||||
|
return None
|
||||||
|
return await self.get_metadata_uiattr(id)
|
||||||
|
return json.loads(ui_attr)
|
||||||
|
return None
|
||||||
|
|
BIN
dataui/.DS_Store
vendored
Normal file
BIN
dataui/.DS_Store
vendored
Normal file
Binary file not shown.
0
dataui/__init__.py
Normal file
0
dataui/__init__.py
Normal file
225
dataui/crud_parser.py
Normal file
225
dataui/crud_parser.py
Normal file
@ -0,0 +1,225 @@
|
|||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import json
|
||||||
|
import asyncio
|
||||||
|
from appPublic.jsonConfig import getConfig
|
||||||
|
from appPublic.folderUtils import ProgramPath
|
||||||
|
from sqlor.dbpools import DBPools
|
||||||
|
from appPublic.myTE import MyTemplateEngine
|
||||||
|
from ahserver.baseProcessor import TemplateProcessor
|
||||||
|
|
||||||
|
uitypes = [
|
||||||
|
"tel",
|
||||||
|
"email",
|
||||||
|
"file",
|
||||||
|
"password",
|
||||||
|
"str",
|
||||||
|
"int",
|
||||||
|
"float",
|
||||||
|
"date",
|
||||||
|
"check",
|
||||||
|
"checkbox",
|
||||||
|
"code",
|
||||||
|
"text",
|
||||||
|
"audio",
|
||||||
|
"video"
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class CrudParser:
|
||||||
|
def __init__(self, frontend, metadb=None):
|
||||||
|
self.frontend = frontend
|
||||||
|
self.metadb = metadb
|
||||||
|
basepath = os.path.join(os.path.dirname(__file__), 'tmpl');
|
||||||
|
paths = [basepath, os.path.join(basepath, self.frontend)]
|
||||||
|
self.te = MyTemplateEngine(paths)
|
||||||
|
|
||||||
|
def guess_uitype(self, field):
|
||||||
|
if 'date' in field['name']:
|
||||||
|
return 'date'
|
||||||
|
length = field.get('len', 0)
|
||||||
|
datatype = field['type']
|
||||||
|
if datatype in ['int', 'long', 'integer']:
|
||||||
|
return 'int'
|
||||||
|
if datatype in ['float', 'double']:
|
||||||
|
return 'float'
|
||||||
|
if datatype == 'text':
|
||||||
|
return 'text'
|
||||||
|
if datatype == 'str' and length > 80:
|
||||||
|
return 'text'
|
||||||
|
return 'str'
|
||||||
|
|
||||||
|
def set_field_width(self, field):
|
||||||
|
factor = 20 * 0.7
|
||||||
|
length = field.get('len', 15)
|
||||||
|
datatype = field['type']
|
||||||
|
if length > 80:
|
||||||
|
length = 80
|
||||||
|
if 'date' in field['name']:
|
||||||
|
return 10 * factor
|
||||||
|
return length * factor
|
||||||
|
|
||||||
|
async def get_uiparams(self, metadata, name):
|
||||||
|
if not metadata:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
id = f'{self.database}.{self.table}.{field}'
|
||||||
|
ui = await metadata.get_metadata_uiattr(id)
|
||||||
|
if ui:
|
||||||
|
return ui
|
||||||
|
id = field
|
||||||
|
ui = await metadata.get_metadata_uiattr(id)
|
||||||
|
return ui
|
||||||
|
|
||||||
|
async def render(self, cmd, opts):
|
||||||
|
database = opts['database']
|
||||||
|
table = opts['table']
|
||||||
|
self.database = database
|
||||||
|
self.table = table
|
||||||
|
db = DBPools()
|
||||||
|
metadata = None
|
||||||
|
async with db.sqlorContext(database) as sor:
|
||||||
|
info = await sor.I(table)
|
||||||
|
dic = {
|
||||||
|
"database":database,
|
||||||
|
"table":table,
|
||||||
|
"baseurl":opts['baseurl'],
|
||||||
|
"request":opts.get('request'),
|
||||||
|
"title":info['summary'][0]['title'],
|
||||||
|
"dataurl":"test_url",
|
||||||
|
"fields":await self.get_fields(metadata, info['fields'])
|
||||||
|
}
|
||||||
|
cmd_dic = opts.get(cmd)
|
||||||
|
if cmd_dic:
|
||||||
|
if cmd_dic.get('alter'):
|
||||||
|
self.alter_fields(dic, cmd_dic['alter'])
|
||||||
|
if cmd_dic.get('exclude'):
|
||||||
|
self.exclude_fields(dic, cmd_dic['exclude'])
|
||||||
|
if cmd_dic.get('include'):
|
||||||
|
self.include_fields(dic, cmd_dic['include'])
|
||||||
|
dic.update({k:v for k,v in cmd_dic.items() if k not in ['include', 'alter', 'exclude' ] })
|
||||||
|
|
||||||
|
tmpl = f'crud_{cmd}.tmpl'
|
||||||
|
s = self.te.render(tmpl, dic)
|
||||||
|
return s
|
||||||
|
|
||||||
|
def alter_fields(self, dic, alter):
|
||||||
|
for f in dic['fields']:
|
||||||
|
d = alter.get(f['name'])
|
||||||
|
if d:
|
||||||
|
f.update(d)
|
||||||
|
|
||||||
|
def exclude_fields(self, dic, exclude):
|
||||||
|
fields = dic['fields'].copy()
|
||||||
|
dic['fields'] = [ f for f in fields if f['name'] not in exclude ]
|
||||||
|
|
||||||
|
def include_fields(self, dic, include):
|
||||||
|
dic['fields'] += include
|
||||||
|
|
||||||
|
async def get_fields(self, metadata, fields, mode='view'):
|
||||||
|
"""
|
||||||
|
this is fowr bricks frontend framework
|
||||||
|
|
||||||
|
argument opts has the following attributes:
|
||||||
|
opts = {
|
||||||
|
database:
|
||||||
|
table:
|
||||||
|
filters:
|
||||||
|
extend:
|
||||||
|
}
|
||||||
|
mode is one of ['view', 'input']
|
||||||
|
|
||||||
|
return:
|
||||||
|
a json object cntains the fields info
|
||||||
|
"""
|
||||||
|
|
||||||
|
fields = [{
|
||||||
|
"name":i['name'],
|
||||||
|
"uitype":self.guess_uitype(i),
|
||||||
|
"datatype":i['type'],
|
||||||
|
"label":i.get('title'),
|
||||||
|
"length":i.get('len'),
|
||||||
|
"width":self.set_field_width(i),
|
||||||
|
"dec":i.get('dec', None)
|
||||||
|
} for i in fields ]
|
||||||
|
|
||||||
|
for f in fields:
|
||||||
|
ui = await self.get_uiparams(metadata, f)
|
||||||
|
if ui:
|
||||||
|
f.update(ui)
|
||||||
|
|
||||||
|
if mode == 'view':
|
||||||
|
for f in fields:
|
||||||
|
f['readonly'] = True
|
||||||
|
return fields
|
||||||
|
|
||||||
|
async def crud_render(cmd, crud_json_file):
|
||||||
|
with codecs.open(crud_json_file) as f:
|
||||||
|
d = json.load(f)
|
||||||
|
cp = CrudParser(frontend='bricks')
|
||||||
|
s = await cp.render(cmd, d)
|
||||||
|
return s
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
cmd = sys.argv[2]
|
||||||
|
jsonfile = sys.argv[1]
|
||||||
|
s = await crud_render(cmd, jsonfile)
|
||||||
|
print(s)
|
||||||
|
|
||||||
|
class BricksCRUDProcessor(TemplateProcessor):
|
||||||
|
"""
|
||||||
|
accept json data from frontend with such attributes:
|
||||||
|
{
|
||||||
|
database:
|
||||||
|
table
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
@classmethod
|
||||||
|
def isMe(self, name):
|
||||||
|
return name == 'bricks_crud'
|
||||||
|
|
||||||
|
async def run_pyscript(self, txt, request):
|
||||||
|
lines = [ '\t' + l for l in txt.split('\n') ]
|
||||||
|
txt = "async def myfunc(request, **ns):\n" + '\n'.join(lines)
|
||||||
|
lenv = self.run_ns.copy()
|
||||||
|
exec(txt, lenv, lenv)
|
||||||
|
f = lenv['myfunc']
|
||||||
|
del lenv['request']
|
||||||
|
return await f(request, **lenv)
|
||||||
|
|
||||||
|
async def path_call(self, request, params={}):
|
||||||
|
self.run_ns.update(params)
|
||||||
|
params_kw = await self.run_ns.get('request2ns')()
|
||||||
|
txt = await super(BricksCRUDProcessor, self).path_call(request,
|
||||||
|
params=self.run_ns)
|
||||||
|
cmd = params_kw.get('command', 'browser')
|
||||||
|
dic = json.loads(txt)
|
||||||
|
dic['request'] = request
|
||||||
|
baseurl = request.path.split('/')[-1]
|
||||||
|
dic['baseurl'] = baseurl
|
||||||
|
cp = CrudParser(frontend='bricks')
|
||||||
|
s = await cp.render(cmd, dic)
|
||||||
|
if cmd in [ "select", "insert", "update", "delete" ]:
|
||||||
|
s = await self.run_pyscript(s, request)
|
||||||
|
return s
|
||||||
|
|
||||||
|
async def datahandle(self, request):
|
||||||
|
self.content = await self.path_call(request)
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import json
|
||||||
|
import codecs
|
||||||
|
|
||||||
|
if len(sys.argv) > 3:
|
||||||
|
print(f'{sys.argv[0]} jsonfile cmd')
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
p = os.getcwd()
|
||||||
|
config = getConfig(p, {
|
||||||
|
'workdir':p,
|
||||||
|
'programPath':ProgramPath()})
|
||||||
|
DBPools(config.databases)
|
||||||
|
loop = asyncio.get_event_loop()
|
||||||
|
loop.run_until_complete(main())
|
4
dataui/iptvchannels.json
Normal file
4
dataui/iptvchannels.json
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"table":"iptvchannels",
|
||||||
|
"database":"iptvdb"
|
||||||
|
}
|
3
dataui/t.sh
Normal file
3
dataui/t.sh
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
|
||||||
|
cd ~/www
|
||||||
|
python ~/py/dataui/dataui/get_fields.py ~/py/dataui/dataui/iptvchannels.json
|
BIN
dataui/tmpl/.DS_Store
vendored
Normal file
BIN
dataui/tmpl/.DS_Store
vendored
Normal file
Binary file not shown.
17
dataui/tmpl/bricks/crud.json
Normal file
17
dataui/tmpl/bricks/crud.json
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"database":"iptvdb",
|
||||||
|
"table":"iptvchannels",
|
||||||
|
"browser":{
|
||||||
|
"field_alter":{
|
||||||
|
"name1":{
|
||||||
|
},
|
||||||
|
"name2":{
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"field_exclude":[],
|
||||||
|
"field_include":[]
|
||||||
|
},
|
||||||
|
"filter":{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
25
dataui/tmpl/bricks/crud_add.tmpl
Normal file
25
dataui/tmpl/bricks/crud_add.tmpl
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"widgettype":"PopupForm",
|
||||||
|
"options":{
|
||||||
|
"submit_url":"{{baseurl}}?command=insert",
|
||||||
|
"width":"{{width}}",
|
||||||
|
"height":"{{height}},
|
||||||
|
"title":"{{title}}",
|
||||||
|
"description":"{{description}}",
|
||||||
|
"fields":[
|
||||||
|
{% for f in fields %}
|
||||||
|
{
|
||||||
|
{% if node == 'view' %}
|
||||||
|
"readonly":true,
|
||||||
|
{% endif %}
|
||||||
|
"name":"{{f.name}}",
|
||||||
|
"label":"{{f.label}}",
|
||||||
|
"datatype":"{{f.datatype}}",
|
||||||
|
"uitype":"{{f.uitype}}",
|
||||||
|
"width":"{{f.width}}"
|
||||||
|
}
|
||||||
|
{% if not loop.last %},{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
17
dataui/tmpl/bricks/crud_browser.tmpl
Normal file
17
dataui/tmpl/bricks/crud_browser.tmpl
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"widgettype":"DataGrid",
|
||||||
|
"options":{
|
||||||
|
{% if admin %}
|
||||||
|
"admin":true,
|
||||||
|
{% endif %}
|
||||||
|
{% if lineno %}
|
||||||
|
"lineno":true,
|
||||||
|
{% endif %}
|
||||||
|
{% if check %}
|
||||||
|
"check":true,
|
||||||
|
{% endif %}
|
||||||
|
"dataurl":"{{baseurl}}?command=select",
|
||||||
|
"title":"{{title}}",
|
||||||
|
"fields":{{json.dumps(fields)}}
|
||||||
|
}
|
||||||
|
}
|
57
dataui/tmpl/bricks/crud_choose.tmpl
Normal file
57
dataui/tmpl/bricks/crud_choose.tmpl
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
{
|
||||||
|
"widgettype":"VBox",
|
||||||
|
"options":{
|
||||||
|
"height":"auto",
|
||||||
|
"width":"100%"
|
||||||
|
},
|
||||||
|
"subwidgets":[
|
||||||
|
{
|
||||||
|
widgettype":"DataGrid",
|
||||||
|
"options":{
|
||||||
|
"check":true,
|
||||||
|
"dataurl":"{{baseurl}}?command=select",
|
||||||
|
"title":"{{title}}",
|
||||||
|
"fields":[
|
||||||
|
{% for f in fields %}
|
||||||
|
{
|
||||||
|
{% if node == 'view' %}
|
||||||
|
"readonly":true,
|
||||||
|
{% endif %}
|
||||||
|
"name":"{{f.name}}",
|
||||||
|
"label":"{{f.label}}",
|
||||||
|
"datatype":"{{f.datatype}}",
|
||||||
|
"uitype":"{{f.uitype}}",
|
||||||
|
"width":"{{f.width}}"
|
||||||
|
}
|
||||||
|
{% if not loop.last %},{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"widgettype":"HBox",
|
||||||
|
"options":{
|
||||||
|
"height":"auto",
|
||||||
|
"width":"100%"
|
||||||
|
},
|
||||||
|
"subwidgets":[
|
||||||
|
{
|
||||||
|
"widgettype":"Button",
|
||||||
|
"options":{
|
||||||
|
"icon":"/imgs/select.png",
|
||||||
|
"name":"select",
|
||||||
|
"Label":"Select"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"widgettype":"Button",
|
||||||
|
"options":{
|
||||||
|
"icon":"/imgs/discard.png",
|
||||||
|
"name":"discard",
|
||||||
|
"Label":"Discard"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
9
dataui/tmpl/bricks/crud_delete.tmpl
Normal file
9
dataui/tmpl/bricks/crud_delete.tmpl
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
db = DBPools()
|
||||||
|
async with db.sqlorContext('{{database}}') as sor:
|
||||||
|
await sor.D("{{table}}", params_kw)
|
||||||
|
return {
|
||||||
|
"state":"ok",
|
||||||
|
"data":{
|
||||||
|
k:id
|
||||||
|
}
|
||||||
|
}
|
25
dataui/tmpl/bricks/crud_edit.tmpl
Normal file
25
dataui/tmpl/bricks/crud_edit.tmpl
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"widgettype":"ModalForm",
|
||||||
|
"options":{
|
||||||
|
"width":"{{width}}",
|
||||||
|
"height":"{{height}},
|
||||||
|
"submit_url":"{{baseurl}}?command=update",
|
||||||
|
"title":"{{title}}",
|
||||||
|
"description":"{{description}}",
|
||||||
|
"fields":[
|
||||||
|
{% for f in fields %}
|
||||||
|
{
|
||||||
|
{% if node == 'view' %}
|
||||||
|
"readonly":true,
|
||||||
|
{% endif %}
|
||||||
|
"name":"{{f.name}}",
|
||||||
|
"label":"{{f.label}}",
|
||||||
|
"datatype":"{{f.datatype}}",
|
||||||
|
"uitype":"{{f.uitype}}",
|
||||||
|
"width":"{{f.width}}"
|
||||||
|
}
|
||||||
|
{% if not loop.last %},{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
27
dataui/tmpl/bricks/crud_filter.tmpl
Normal file
27
dataui/tmpl/bricks/crud_filter.tmpl
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"widgettype":"ModalForm",
|
||||||
|
"options":{
|
||||||
|
"fields":[
|
||||||
|
{% for f in fileds %}
|
||||||
|
{
|
||||||
|
"name":"{{f.name}}",
|
||||||
|
"label":"{{f.title}}",
|
||||||
|
"uitype":"{{f.uitype",
|
||||||
|
"uiparams":{{f.uiparanms}}"
|
||||||
|
}
|
||||||
|
{% if not loop.last %},{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"binds":[
|
||||||
|
{
|
||||||
|
"wid":"self",
|
||||||
|
"event":"submit",
|
||||||
|
"actiontype":"script",
|
||||||
|
"target":"{{params_kw.target}}",
|
||||||
|
"datawidget":"self",
|
||||||
|
"script":"self.loadData(kwargs);"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
12
dataui/tmpl/bricks/crud_insert.tmpl
Normal file
12
dataui/tmpl/bricks/crud_insert.tmpl
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
db = DBPools()
|
||||||
|
id = uuid()
|
||||||
|
async with db.sqlorContext('{{database}}') as sor:
|
||||||
|
k = info.summary[0].primary[0]
|
||||||
|
params_kw.update({k:id})
|
||||||
|
await sor.C("{{table}}", params_kw)
|
||||||
|
return {
|
||||||
|
"state":"ok",
|
||||||
|
"data":{
|
||||||
|
k:id
|
||||||
|
}
|
||||||
|
}
|
16
dataui/tmpl/bricks/crud_remove.tmpl
Normal file
16
dataui/tmpl/bricks/crud_remove.tmpl
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"widgettype":"Conform",
|
||||||
|
"options":{
|
||||||
|
"title":"Delete record",
|
||||||
|
"message":"delete record, are you sure?"
|
||||||
|
},
|
||||||
|
"binds":[
|
||||||
|
{
|
||||||
|
"wid":"self",
|
||||||
|
"event":"conform",
|
||||||
|
"actiontype":"script",
|
||||||
|
"target":"{{params_kw.target}}",
|
||||||
|
"script":"self.del_row()"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
9
dataui/tmpl/bricks/crud_select.tmpl
Normal file
9
dataui/tmpl/bricks/crud_select.tmpl
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
if not params_kw.get('page'):
|
||||||
|
params_kw['page'] = 1
|
||||||
|
if not params_kw.get('rows'):
|
||||||
|
params_kw['rows'] = 80
|
||||||
|
print('params_kw=', params_kw)
|
||||||
|
db = DBPools()
|
||||||
|
async with db.sqlorContext('{{database}}') as sor:
|
||||||
|
d = await sor.R("{{table}}", params_kw)
|
||||||
|
return d
|
7
dataui/tmpl/bricks/crud_update.tmpl
Normal file
7
dataui/tmpl/bricks/crud_update.tmpl
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
db = DBPools()
|
||||||
|
async with db.sqlorContext('{{database}}') as sor:
|
||||||
|
await sor.U(params_kw)
|
||||||
|
return {
|
||||||
|
"state":"ok",
|
||||||
|
"data"{}
|
||||||
|
}
|
20
dataui/tmpl/bricks/tree.json
Normal file
20
dataui/tmpl/bricks/tree.json
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"root":["node1", "node2" ...];
|
||||||
|
"nodetypes":{
|
||||||
|
"nodetype1";{
|
||||||
|
"icon":"ttt",
|
||||||
|
"database":"ttt",
|
||||||
|
"table":"rrt",
|
||||||
|
"idField",
|
||||||
|
"textField",
|
||||||
|
"pidField""
|
||||||
|
"rootpid":"1",
|
||||||
|
"contextmenu":
|
||||||
|
"children":[
|
||||||
|
"nodetyp1", "nodetype2", ...
|
||||||
|
]
|
||||||
|
},
|
||||||
|
...
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
17
dataui/uitypes.py
Normal file
17
dataui/uitypes.py
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
uitypes = [
|
||||||
|
"tel",
|
||||||
|
"email",
|
||||||
|
"file",
|
||||||
|
"password",
|
||||||
|
"str",
|
||||||
|
"int",
|
||||||
|
"float",
|
||||||
|
"date",
|
||||||
|
"check",
|
||||||
|
"checkbox",
|
||||||
|
"code",
|
||||||
|
"text",
|
||||||
|
"audio",
|
||||||
|
"video"
|
||||||
|
]
|
||||||
|
|
91
docs/codes.md
Normal file
91
docs/codes.md
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
# 代码
|
||||||
|
|
||||||
|
## 实现方法
|
||||||
|
代码以一组数据表形式存储在数据库中, 按照配置,代码表可以存在独立的数据库中也可以存储在业务数据库中
|
||||||
|
多级代码,代码键值以“.“字符分割,可形成多级代码
|
||||||
|
|
||||||
|
其中
|
||||||
|
代码定义表定义每个代码的名称
|
||||||
|
代码码值表存储各个代码键值和码值
|
||||||
|
|
||||||
|
## 数据结构
|
||||||
|
代码定义表
|
||||||
|
code_definition
|
||||||
|
{
|
||||||
|
code_name,
|
||||||
|
code_desc
|
||||||
|
}
|
||||||
|
|
||||||
|
代码码值表
|
||||||
|
code_keyvalue
|
||||||
|
{
|
||||||
|
code_name,
|
||||||
|
code_key,
|
||||||
|
level,
|
||||||
|
code_value
|
||||||
|
}
|
||||||
|
|
||||||
|
## API
|
||||||
|
class CodeKeyValue
|
||||||
|
def __init__(self, database):
|
||||||
|
self.database = database
|
||||||
|
|
||||||
|
def new_code(self, code_name:str) -> None:
|
||||||
|
"""
|
||||||
|
新建一个代码
|
||||||
|
"""
|
||||||
|
def add_kv(self, code_name:str, code_key:str, code_value:str)->None:
|
||||||
|
"""
|
||||||
|
新增一个代码
|
||||||
|
code_name:代码名称
|
||||||
|
code_key:代码键值
|
||||||
|
code_value:代码键值
|
||||||
|
"""
|
||||||
|
|
||||||
|
def delete_kv(self, code_name:str, code_key:str) -> None:
|
||||||
|
"""
|
||||||
|
删除一个代码
|
||||||
|
code_name:
|
||||||
|
code_key:
|
||||||
|
"""
|
||||||
|
|
||||||
|
def modify_kv(self, code_name:str, code_key:str, code_value:str) -> None
|
||||||
|
"""
|
||||||
|
修改一个代码值
|
||||||
|
code_name:代码名称
|
||||||
|
code_key:代码键值
|
||||||
|
code_value:新代码键值
|
||||||
|
"""
|
||||||
|
|
||||||
|
def get_code_value(self, code_name:str, code_key:str) -> str:
|
||||||
|
"""
|
||||||
|
得到代码码值
|
||||||
|
code_name:代码名称
|
||||||
|
code_key:代码键值
|
||||||
|
返回值:字符串,如果没找到返回None
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
def get_code_items(self, code_name:str, code_key_part:str=Nkne) -> dict
|
||||||
|
"""
|
||||||
|
得到代码名称的代码字典,如果code_key_part不为空,则获得级联的下级代码的字典
|
||||||
|
code_name:代码名称
|
||||||
|
code_key:代码键值
|
||||||
|
返回值:字符串,如果没找到返回None
|
||||||
|
"""
|
||||||
|
|
||||||
|
def sql_completion(self, sql, codes):
|
||||||
|
"""
|
||||||
|
sql代码补全,补全每个代码的码值
|
||||||
|
sql:原始sql代码
|
||||||
|
codes:需要补全的代码数组,每个codes中的code有以下格式:
|
||||||
|
{
|
||||||
|
code_keyfield, sql中键值字段
|
||||||
|
code_valuefield, 补充的键值字段
|
||||||
|
code_name, 代码名称
|
||||||
|
}
|
||||||
|
返回值:补全的SQL代码
|
||||||
|
局限:补全的sql没有做有效性分析,不能保证能够执行。比如code_keyfield在原始sql的结果字段中不存在,代码库和原始sql的目标数据库不是同一个数据库。
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
BIN
models/md_code_definition.xlsx
Normal file
BIN
models/md_code_definition.xlsx
Normal file
Binary file not shown.
BIN
models/md_code_keyvalue.xlsx
Normal file
BIN
models/md_code_keyvalue.xlsx
Normal file
Binary file not shown.
BIN
models/md_metadata.xlsx
Normal file
BIN
models/md_metadata.xlsx
Normal file
Binary file not shown.
73
models/t.sql
Normal file
73
models/t.sql
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
drop table if exists md_code_definition;
|
||||||
|
CREATE TABLE md_code_definition
|
||||||
|
(
|
||||||
|
|
||||||
|
`code_name` VARCHAR(100) comment '代码名称',
|
||||||
|
`code_desc` VARCHAR(400) comment '代码描述'
|
||||||
|
|
||||||
|
|
||||||
|
,primary key(code_name)
|
||||||
|
|
||||||
|
|
||||||
|
)
|
||||||
|
engine=innodb
|
||||||
|
default charset=utf8
|
||||||
|
comment '代码定义表'
|
||||||
|
;
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX md_code_definition_idx1 ON md_code_definition(code_name);
|
||||||
|
|
||||||
|
|
||||||
|
drop table if exists md_metadata;
|
||||||
|
CREATE TABLE md_metadata
|
||||||
|
(
|
||||||
|
|
||||||
|
`id` VARCHAR(500) comment '数据id',
|
||||||
|
`ancestor` VARCHAR(500) comment '祖先',
|
||||||
|
`name` VARCHAR(100) comment '数据名字',
|
||||||
|
`title` VARCHAR(100) comment '标题',
|
||||||
|
`type` VARCHAR(30) comment '数据类型',
|
||||||
|
`length` int comment '数据长度',
|
||||||
|
`dec` int comment '小数位',
|
||||||
|
`nullable` CHAR(1) comment '可否为空',
|
||||||
|
`defaultvalue` CHAR(1) comment '缺省值',
|
||||||
|
`uiattr` VARCHAR(3000) comment '界面属性'
|
||||||
|
|
||||||
|
|
||||||
|
,primary key(id)
|
||||||
|
|
||||||
|
|
||||||
|
)
|
||||||
|
engine=innodb
|
||||||
|
default charset=utf8
|
||||||
|
comment '元数据'
|
||||||
|
;
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX md_metadata_idx1 ON md_metadata(id);
|
||||||
|
|
||||||
|
|
||||||
|
drop table if exists md_code_keyvalue;
|
||||||
|
CREATE TABLE md_code_keyvalue
|
||||||
|
(
|
||||||
|
|
||||||
|
`code_name` VARCHAR(100) comment '代码名称',
|
||||||
|
`code_key` VARCHAR(500) comment '代码键',
|
||||||
|
`level` int comment '级别',
|
||||||
|
`code_value` VARCHAR(200) comment '代码值'
|
||||||
|
|
||||||
|
|
||||||
|
,primary key(code_name,code_key)
|
||||||
|
|
||||||
|
|
||||||
|
)
|
||||||
|
engine=innodb
|
||||||
|
default charset=utf8
|
||||||
|
comment '代码码值表'
|
||||||
|
;
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX md_code_keyvalue_idx1 ON md_code_keyvalue(code_name,code_key);
|
||||||
|
CREATE INDEX md_code_keyvalue_idx2 ON md_code_keyvalue(code_name);
|
||||||
|
CREATE INDEX md_code_keyvalue_idx3 ON md_code_keyvalue(code_name,code_key,level);
|
0
requirements.txt
Normal file
0
requirements.txt
Normal file
52
setup.py
Executable file
52
setup.py
Executable file
@ -0,0 +1,52 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
|
||||||
|
from distutils.core import setup
|
||||||
|
from setuptools import setup, find_packages
|
||||||
|
|
||||||
|
# usage:
|
||||||
|
# python setup.py bdist_wininst generate a window executable file
|
||||||
|
# python setup.py bdist_egg generate a egg file
|
||||||
|
# Release information about eway
|
||||||
|
|
||||||
|
version = "0.1.1"
|
||||||
|
name = "dataui"
|
||||||
|
description = "A crud engine for ahserver http server and kivyblocks front-end GUI"
|
||||||
|
author = "yumoqing"
|
||||||
|
email = "yumoqing@gmail.com"
|
||||||
|
|
||||||
|
packages=find_packages()
|
||||||
|
package_data = {
|
||||||
|
"dataui":[
|
||||||
|
"tmpl/*",
|
||||||
|
"tmpl/*/*"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
with open("README.md", "r") as fh:
|
||||||
|
long_description = fh.read()
|
||||||
|
|
||||||
|
setup(
|
||||||
|
name="dataui",
|
||||||
|
version=version,
|
||||||
|
|
||||||
|
# uncomment the following lines if you fill them out in release.py
|
||||||
|
description=description,
|
||||||
|
author=author,
|
||||||
|
author_email=email,
|
||||||
|
install_requires=[
|
||||||
|
"ahserver"
|
||||||
|
],
|
||||||
|
packages=packages,
|
||||||
|
package_data=package_data,
|
||||||
|
keywords = [
|
||||||
|
],
|
||||||
|
url="https://github.com/yumoqing/dataui",
|
||||||
|
long_description=long_description,
|
||||||
|
long_description_content_type="text/markdown",
|
||||||
|
classifiers = [
|
||||||
|
'Operating System :: OS Independent',
|
||||||
|
'Programming Language :: Python :: 3',
|
||||||
|
'License :: OSI Approved :: MIT License',
|
||||||
|
],
|
||||||
|
)
|
Loading…
Reference in New Issue
Block a user