From 68da1a1aeac98e49b2497e2dc1e83e537c1364c0 Mon Sep 17 00:00:00 2001 From: yumoqing Date: Mon, 20 Nov 2023 07:14:53 +0800 Subject: [PATCH] bugfix --- README.md | 21 + crud/__init__.py | 0 crud/build_filter_sql.py | 15 + crud/code_keyvalue.py | 150 +++++++ crud/crudProcessor.py | 625 +++++++++++++++++++++++++++ crud/metadata.py | 65 +++ dataui/.DS_Store | Bin 0 -> 6148 bytes dataui/__init__.py | 0 dataui/crud_parser.py | 225 ++++++++++ dataui/iptvchannels.json | 4 + dataui/t.sh | 3 + dataui/tmpl/.DS_Store | Bin 0 -> 6148 bytes dataui/tmpl/bricks/crud.json | 17 + dataui/tmpl/bricks/crud_add.tmpl | 25 ++ dataui/tmpl/bricks/crud_browser.tmpl | 17 + dataui/tmpl/bricks/crud_choose.tmpl | 57 +++ dataui/tmpl/bricks/crud_delete.tmpl | 9 + dataui/tmpl/bricks/crud_edit.tmpl | 25 ++ dataui/tmpl/bricks/crud_filter.tmpl | 27 ++ dataui/tmpl/bricks/crud_insert.tmpl | 12 + dataui/tmpl/bricks/crud_remove.tmpl | 16 + dataui/tmpl/bricks/crud_select.tmpl | 9 + dataui/tmpl/bricks/crud_update.tmpl | 7 + dataui/tmpl/bricks/tree.json | 20 + dataui/uitypes.py | 17 + docs/codes.md | 91 ++++ models/md_code_definition.xlsx | Bin 0 -> 14666 bytes models/md_code_keyvalue.xlsx | Bin 0 -> 14863 bytes models/md_metadata.xlsx | Bin 0 -> 15233 bytes models/t.sql | 73 ++++ requirements.txt | 0 setup.py | 52 +++ 32 files changed, 1582 insertions(+) create mode 100644 README.md create mode 100644 crud/__init__.py create mode 100644 crud/build_filter_sql.py create mode 100644 crud/code_keyvalue.py create mode 100644 crud/crudProcessor.py create mode 100644 crud/metadata.py create mode 100644 dataui/.DS_Store create mode 100644 dataui/__init__.py create mode 100644 dataui/crud_parser.py create mode 100644 dataui/iptvchannels.json create mode 100644 dataui/t.sh create mode 100644 dataui/tmpl/.DS_Store create mode 100644 dataui/tmpl/bricks/crud.json create mode 100644 dataui/tmpl/bricks/crud_add.tmpl create mode 100644 dataui/tmpl/bricks/crud_browser.tmpl create mode 100644 dataui/tmpl/bricks/crud_choose.tmpl create mode 100644 dataui/tmpl/bricks/crud_delete.tmpl create mode 100644 dataui/tmpl/bricks/crud_edit.tmpl create mode 100644 dataui/tmpl/bricks/crud_filter.tmpl create mode 100644 dataui/tmpl/bricks/crud_insert.tmpl create mode 100644 dataui/tmpl/bricks/crud_remove.tmpl create mode 100644 dataui/tmpl/bricks/crud_select.tmpl create mode 100644 dataui/tmpl/bricks/crud_update.tmpl create mode 100644 dataui/tmpl/bricks/tree.json create mode 100644 dataui/uitypes.py create mode 100644 docs/codes.md create mode 100644 models/md_code_definition.xlsx create mode 100644 models/md_code_keyvalue.xlsx create mode 100644 models/md_metadata.xlsx create mode 100644 models/t.sql create mode 100644 requirements.txt create mode 100755 setup.py diff --git a/README.md b/README.md new file mode 100644 index 0000000..1c1d216 --- /dev/null +++ b/README.md @@ -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 diff --git a/crud/__init__.py b/crud/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/crud/build_filter_sql.py b/crud/build_filter_sql.py new file mode 100644 index 0000000..5bd6dea --- /dev/null +++ b/crud/build_filter_sql.py @@ -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 + diff --git a/crud/code_keyvalue.py b/crud/code_keyvalue.py new file mode 100644 index 0000000..804b085 --- /dev/null +++ b/crud/code_keyvalue.py @@ -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 diff --git a/crud/crudProcessor.py b/crud/crudProcessor.py new file mode 100644 index 0000000..5189d86 --- /dev/null +++ b/crud/crudProcessor.py @@ -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) + diff --git a/crud/metadata.py b/crud/metadata.py new file mode 100644 index 0000000..a9ce0ea --- /dev/null +++ b/crud/metadata.py @@ -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 + diff --git a/dataui/.DS_Store b/dataui/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..77545714bff58f7fb697661daa479c07d68cbf44 GIT binary patch literal 6148 zcmeHKL2uJA6n^dsO_@MN0%;c{OI)kcu`xhgLKz3H1i=ANNtTEPGP_FBRZ~?dXZSH( z`6c`pPVhb3Luu%aZE&FG)s4hdPEeUFt)m=E-)VFvSBUf=>Ub9 zV@y+usicas4Yqguiwbb=ZqN~OHlviz=dYvzvU&QV&__w36Y%&Ud5SXlWehE%o0Nl9 z(+TF7AomKtj0!vf1NwwolUkeM+u96mL=V86tn$R23wX!4RpfuI|7$Gx4!V)xO5zTDe?xa`ZtvNw~v@a90-Ygd)t z=a--VSA%@dE8rFQuM`mN!{~5?CAqzIV{yFKN|X;MY|Lvc{s}?DY{lU6R=k5^41UQC WVCb;12oKDE2pAcB 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()) diff --git a/dataui/iptvchannels.json b/dataui/iptvchannels.json new file mode 100644 index 0000000..72dadb3 --- /dev/null +++ b/dataui/iptvchannels.json @@ -0,0 +1,4 @@ +{ + "table":"iptvchannels", + "database":"iptvdb" +} diff --git a/dataui/t.sh b/dataui/t.sh new file mode 100644 index 0000000..0c358ab --- /dev/null +++ b/dataui/t.sh @@ -0,0 +1,3 @@ + +cd ~/www +python ~/py/dataui/dataui/get_fields.py ~/py/dataui/dataui/iptvchannels.json diff --git a/dataui/tmpl/.DS_Store b/dataui/tmpl/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..0102a8f8880d3fdabf18e596156948325f927da7 GIT binary patch literal 6148 zcmeHK%}&BV5S|5u5@NzZ6ONmBB?^Ln@lp{FUairC8l*t9p>_!cIS`Vb^@V&AUq@$l z7ZZZsj4@;;nfbP}Gi~SFX14tF_$fnQ^Q_6Lbd=vqt+ z>a7EfZUGQ8=+=T``fa2fY0k2l^<+SUQSJs9Ml3X@KZjObnt2g?|Jz4Q!Z!KV{$(^&40l literal 0 HcmV?d00001 diff --git a/dataui/tmpl/bricks/crud.json b/dataui/tmpl/bricks/crud.json new file mode 100644 index 0000000..04d61bc --- /dev/null +++ b/dataui/tmpl/bricks/crud.json @@ -0,0 +1,17 @@ +{ + "database":"iptvdb", + "table":"iptvchannels", + "browser":{ + "field_alter":{ + "name1":{ + }, + "name2":{ + } + }, + "field_exclude":[], + "field_include":[] + }, + "filter":{ + } +} + diff --git a/dataui/tmpl/bricks/crud_add.tmpl b/dataui/tmpl/bricks/crud_add.tmpl new file mode 100644 index 0000000..8e614e0 --- /dev/null +++ b/dataui/tmpl/bricks/crud_add.tmpl @@ -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 %} + ] + } +} diff --git a/dataui/tmpl/bricks/crud_browser.tmpl b/dataui/tmpl/bricks/crud_browser.tmpl new file mode 100644 index 0000000..ebbe8da --- /dev/null +++ b/dataui/tmpl/bricks/crud_browser.tmpl @@ -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)}} + } +} diff --git a/dataui/tmpl/bricks/crud_choose.tmpl b/dataui/tmpl/bricks/crud_choose.tmpl new file mode 100644 index 0000000..c8808e9 --- /dev/null +++ b/dataui/tmpl/bricks/crud_choose.tmpl @@ -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" + } + } + ] +} + ] +} diff --git a/dataui/tmpl/bricks/crud_delete.tmpl b/dataui/tmpl/bricks/crud_delete.tmpl new file mode 100644 index 0000000..8994314 --- /dev/null +++ b/dataui/tmpl/bricks/crud_delete.tmpl @@ -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 + } + } diff --git a/dataui/tmpl/bricks/crud_edit.tmpl b/dataui/tmpl/bricks/crud_edit.tmpl new file mode 100644 index 0000000..979250c --- /dev/null +++ b/dataui/tmpl/bricks/crud_edit.tmpl @@ -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 %} + ] + } +} diff --git a/dataui/tmpl/bricks/crud_filter.tmpl b/dataui/tmpl/bricks/crud_filter.tmpl new file mode 100644 index 0000000..7cf97d0 --- /dev/null +++ b/dataui/tmpl/bricks/crud_filter.tmpl @@ -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);" + } + ] +} + diff --git a/dataui/tmpl/bricks/crud_insert.tmpl b/dataui/tmpl/bricks/crud_insert.tmpl new file mode 100644 index 0000000..119d070 --- /dev/null +++ b/dataui/tmpl/bricks/crud_insert.tmpl @@ -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 + } + } diff --git a/dataui/tmpl/bricks/crud_remove.tmpl b/dataui/tmpl/bricks/crud_remove.tmpl new file mode 100644 index 0000000..57653ed --- /dev/null +++ b/dataui/tmpl/bricks/crud_remove.tmpl @@ -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()" + } + ] +} diff --git a/dataui/tmpl/bricks/crud_select.tmpl b/dataui/tmpl/bricks/crud_select.tmpl new file mode 100644 index 0000000..d49dd2f --- /dev/null +++ b/dataui/tmpl/bricks/crud_select.tmpl @@ -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 diff --git a/dataui/tmpl/bricks/crud_update.tmpl b/dataui/tmpl/bricks/crud_update.tmpl new file mode 100644 index 0000000..dafe679 --- /dev/null +++ b/dataui/tmpl/bricks/crud_update.tmpl @@ -0,0 +1,7 @@ +db = DBPools() +async with db.sqlorContext('{{database}}') as sor: + await sor.U(params_kw) + return { + "state":"ok", + "data"{} + } diff --git a/dataui/tmpl/bricks/tree.json b/dataui/tmpl/bricks/tree.json new file mode 100644 index 0000000..9e3a014 --- /dev/null +++ b/dataui/tmpl/bricks/tree.json @@ -0,0 +1,20 @@ +{ + "root":["node1", "node2" ...]; + "nodetypes":{ + "nodetype1";{ + "icon":"ttt", + "database":"ttt", + "table":"rrt", + "idField", + "textField", + "pidField"" + "rootpid":"1", + "contextmenu": + "children":[ + "nodetyp1", "nodetype2", ... + ] + }, + ... + } +} + diff --git a/dataui/uitypes.py b/dataui/uitypes.py new file mode 100644 index 0000000..cef0d01 --- /dev/null +++ b/dataui/uitypes.py @@ -0,0 +1,17 @@ +uitypes = [ + "tel", + "email", + "file", + "password", + "str", + "int", + "float", + "date", + "check", + "checkbox", + "code", + "text", + "audio", + "video" +] + diff --git a/docs/codes.md b/docs/codes.md new file mode 100644 index 0000000..aefb91f --- /dev/null +++ b/docs/codes.md @@ -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的目标数据库不是同一个数据库。 + + """ + diff --git a/models/md_code_definition.xlsx b/models/md_code_definition.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..72adbe15944c2390fa46e3029f5d1b54540fd357 GIT binary patch literal 14666 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 literal 0 HcmV?d00001 diff --git a/models/md_code_keyvalue.xlsx b/models/md_code_keyvalue.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..79069c62e27fdfa5fd13541179f768308e6e65e2 GIT binary patch literal 14863 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 literal 0 HcmV?d00001 diff --git a/models/md_metadata.xlsx b/models/md_metadata.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..f7b75bbf2f0e4e903c620e54647a9ece7beccfa0 GIT binary patch literal 15233 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