commit 68da1a1aeac98e49b2497e2dc1e83e537c1364c0 Author: yumoqing Date: Mon Nov 20 07:14:53 2023 +0800 bugfix 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 0000000..7754571 Binary files /dev/null and b/dataui/.DS_Store differ diff --git a/dataui/__init__.py b/dataui/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/dataui/crud_parser.py b/dataui/crud_parser.py new file mode 100644 index 0000000..b8437ce --- /dev/null +++ b/dataui/crud_parser.py @@ -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()) 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 0000000..0102a8f Binary files /dev/null and b/dataui/tmpl/.DS_Store differ 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 0000000..72adbe1 Binary files /dev/null and b/models/md_code_definition.xlsx differ diff --git a/models/md_code_keyvalue.xlsx b/models/md_code_keyvalue.xlsx new file mode 100644 index 0000000..79069c6 Binary files /dev/null and b/models/md_code_keyvalue.xlsx differ diff --git a/models/md_metadata.xlsx b/models/md_metadata.xlsx new file mode 100644 index 0000000..f7b75bb Binary files /dev/null and b/models/md_metadata.xlsx differ diff --git a/models/t.sql b/models/t.sql new file mode 100644 index 0000000..eb3b077 --- /dev/null +++ b/models/t.sql @@ -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); diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..e69de29 diff --git a/setup.py b/setup.py new file mode 100755 index 0000000..830fee2 --- /dev/null +++ b/setup.py @@ -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', + ], +)