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