This commit is contained in:
yumoqing 2023-11-20 07:14:53 +08:00
commit 68da1a1aea
32 changed files with 1582 additions and 0 deletions

21
README.md Normal file
View 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
View File

15
crud/build_filter_sql.py Normal file
View 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
View 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
View 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
View 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

Binary file not shown.

0
dataui/__init__.py Normal file
View File

225
dataui/crud_parser.py Normal file
View 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
View File

@ -0,0 +1,4 @@
{
"table":"iptvchannels",
"database":"iptvdb"
}

3
dataui/t.sh Normal file
View 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

Binary file not shown.

View File

@ -0,0 +1,17 @@
{
"database":"iptvdb",
"table":"iptvchannels",
"browser":{
"field_alter":{
"name1":{
},
"name2":{
}
},
"field_exclude":[],
"field_include":[]
},
"filter":{
}
}

View 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 %}
]
}
}

View 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)}}
}
}

View 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"
}
}
]
}
]
}

View 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
}
}

View 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 %}
]
}
}

View 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);"
}
]
}

View 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
}
}

View 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()"
}
]
}

View 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

View File

@ -0,0 +1,7 @@
db = DBPools()
async with db.sqlorContext('{{database}}') as sor:
await sor.U(params_kw)
return {
"state":"ok",
"data"{}
}

View 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
View 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
View 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的目标数据库不是同一个数据库。
"""

Binary file not shown.

Binary file not shown.

BIN
models/md_metadata.xlsx Normal file

Binary file not shown.

73
models/t.sql Normal file
View 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
View File

52
setup.py Executable file
View 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',
],
)