From f1f480d4299fa0863fc190c1b035b97fa1181dbf Mon Sep 17 00:00:00 2001 From: yumoqing Date: Tue, 24 Jun 2025 11:50:40 +0800 Subject: [PATCH] first commit --- README.md | 59 ++++++ examples/audit_log.json | 14 ++ examples/organization.json | 32 +++ examples/orgtypes.json | 18 ++ examples/permission.json | 21 ++ examples/role.json | 31 +++ examples/rolepermission.json | 26 +++ examples/userapp.json | 17 ++ examples/userdepartment.json | 17 ++ examples/userrole.json | 19 ++ examples/users.json | 23 +++ model.xltx | Bin 0 -> 14840 bytes requirements.txt | 4 + singletree.py | 136 +++++++++++++ tmpls.py | 381 +++++++++++++++++++++++++++++++++++ xls2crud.py | 304 ++++++++++++++++++++++++++++ xls2ddl.py | 94 +++++++++ xls2ui.py | 46 +++++ xlsxData.py | 330 ++++++++++++++++++++++++++++++ 19 files changed, 1572 insertions(+) create mode 100755 README.md create mode 100644 examples/audit_log.json create mode 100644 examples/organization.json create mode 100644 examples/orgtypes.json create mode 100644 examples/permission.json create mode 100644 examples/role.json create mode 100644 examples/rolepermission.json create mode 100644 examples/userapp.json create mode 100644 examples/userdepartment.json create mode 100644 examples/userrole.json create mode 100644 examples/users.json create mode 100644 model.xltx create mode 100755 requirements.txt create mode 100644 singletree.py create mode 100644 tmpls.py create mode 100644 xls2crud.py create mode 100755 xls2ddl.py create mode 100644 xls2ui.py create mode 100755 xlsxData.py diff --git a/README.md b/README.md new file mode 100755 index 0000000..e1b4575 --- /dev/null +++ b/README.md @@ -0,0 +1,59 @@ +# xls2ddl + +a tool to create related database schema from xlsx file + +there is a xlsx template file called "model.xlst" in this repo + +## How to use it + +1 double click "model.xlst" +2 in "sammary" datasheet, wirte the table name, table label, primary key +3 in "fields" datasheet, create all the field in the table +4 in "validation" datashhet identifies the index + first column is index name + second column choose the "idx" + thrid column for duplicatable index use "index:f1,f2 ..." or "unique:f1, f2, ..." for unique index + +5 save it with the tablenme as xlsx file's name +6 repeat 1 to 5 for all the table. +7 translates all xlsx file to ddl sql using +in the folder hold all the xlsx file + +for mysql +``` +python path/to/xls2ddl.py mysql . +``` +for sqlite3 +``` +python path/to/xls2ddl.py sqlite3 . +``` +for oracle +``` +python path/to/xls2ddl.py oracle . +``` +for postgresql +``` +python path/to/xls2ddl.py postgresql . +``` +for sqlserver +``` +python path/to/xls2ddl.py sqlserver . +``` +## examples xlsx file +open the [examples.xlsx](./examples.xlsx) to find out how to write a table schema infomation + +write your table schema info file: + +1 copy examples.xlsx file to 'your table name'.xlsx +2 open 'your table name'.xlsx and change information in summary, fields, validation codes +3 save it +4 use xls2ddl translates xlsx file to ddl file +5 use the ddl file to create you database tables + +# xls2crud +xls2crud is a tool to create table data administration website user interface. +it is only support [ahserver](https://git.kaiyuancloud.cn/yumoqing/ahserver) backend server and [bricks](https://git.kaiyuancloud.cn/yumoqing/bricks) front end + + + + diff --git a/examples/audit_log.json b/examples/audit_log.json new file mode 100644 index 0000000..96d42f5 --- /dev/null +++ b/examples/audit_log.json @@ -0,0 +1,14 @@ +{ + "tblname": "audit_log", + "title":"审计日志", + "params": { + "sortby":"userid", + "browserfields": { + "exclouded": ["id"], + "cwidth": {} + }, + "editexclouded": [ + "id" + ] + } +} diff --git a/examples/organization.json b/examples/organization.json new file mode 100644 index 0000000..6e6f649 --- /dev/null +++ b/examples/organization.json @@ -0,0 +1,32 @@ +{ + "models_dir": "${HOME}$/py/rbac/models", + "output_dir": "${HOME}$/py/sage/wwwroot/_a/organization", + "dbname": "sage", + "tblname": "organization", + "title":"Organization", + "params": { + "sortby":"orgname", + "browserfields": { + "exclouded": ["id"], + "cwidth": {} + }, + "editexclouded": [ + "id" + ], + "subtables":[ + { + "field":"orgid", + "title":"Org. type", + "url":"../orgtypes", + "subtable":"orgtypes" + }, + { + "field":"orgid", + "title":"Users", + "url":"../users", + "subtable":"users" + } + ], + "record_toolbar": null + } +} diff --git a/examples/orgtypes.json b/examples/orgtypes.json new file mode 100644 index 0000000..e6778da --- /dev/null +++ b/examples/orgtypes.json @@ -0,0 +1,18 @@ +{ + "models_dir": "${HOME}$/py/rbac/models", + "output_dir": "${HOME}$/py/sage/wwwroot/_a/orgtypes", + "dbname": "sage", + "tblname": "orgtypes", + "title":"Org. type", + "params": { + "browserfields": { + "exclouded": ["id", "orgid"], + "cwidth": {} + }, + "editexclouded": [ + "id", + "orgid" + ], + "record_toolbar": null + } +} diff --git a/examples/permission.json b/examples/permission.json new file mode 100644 index 0000000..cb4d36b --- /dev/null +++ b/examples/permission.json @@ -0,0 +1,21 @@ +{ + "tblname": "permission", + "uitype":"tree", + "title":"权限", + "params":{ + "idField":"id", + "textField":"path", + "sortby":"path", + "editable":true, + "browserfields":{ + "alters":{} + }, + "edit_exclouded_fields":[], + "parentField":"parentid", + "toolbar":{ + }, + "binds":[ + ] + } + +} diff --git a/examples/role.json b/examples/role.json new file mode 100644 index 0000000..3c52962 --- /dev/null +++ b/examples/role.json @@ -0,0 +1,31 @@ +{ + "models_dir": "${HOME}$/py/rbac/models", + "output_dir": "${HOME}$/py/sage/wwwroot/_a/role", + "dbname": "sage", + "tblname": "role", + "title":"角色", + "params": { + "sortby":"name", + "browserfields": { + "exclouded": ["id"], + "cwidth": {} + }, + "editexclouded": [ + "id" + ], + "subtables":[ + { + "field":"roleid", + "title":"角色权限", + "url":"../rolepermission", + "subtable":"rolepermission" + }, + { + "field":"roleid", + "title":"用户", + "url":"../users", + "subtable":"users" + } + ] + } +} diff --git a/examples/rolepermission.json b/examples/rolepermission.json new file mode 100644 index 0000000..49bf3fd --- /dev/null +++ b/examples/rolepermission.json @@ -0,0 +1,26 @@ +{ + "models_dir": "${HOME}$/py/rbac/models", + "output_dir": "${HOME}$/py/sage/wwwroot/_a/rolepermission", + "dbname": "sage", + "tblname": "rolepermission", + "title":"用户", + "params": { + "relation":{ + "outter_field":"permid", + "param_field":"roleid" + }, + "noedit":true, + "browserfields": { + "exclouded": ["id", "roleid"], + "alters":{ + "permid":{ + "cwidth":60 + } + } + }, + "editexclouded": [ + "id", "roleid" + ], + "record_toolbar": null + } +} diff --git a/examples/userapp.json b/examples/userapp.json new file mode 100644 index 0000000..82d65f6 --- /dev/null +++ b/examples/userapp.json @@ -0,0 +1,17 @@ +{ + "models_dir": "${HOME}$/py/rbac/models", + "output_dir": "${HOME}$/py/sage/wwwroot/_a/userapp", + "dbname": "sage", + "tblname": "userapp", + "title":"用户", + "params": { + "browserfields": { + "exclouded": ["id", "userid"], + "cwidth": {} + }, + "editexclouded": [ + "id", "userid" + ], + "record_toolbar": null + } +} diff --git a/examples/userdepartment.json b/examples/userdepartment.json new file mode 100644 index 0000000..91f409e --- /dev/null +++ b/examples/userdepartment.json @@ -0,0 +1,17 @@ +{ + "models_dir": "${HOME}$/py/rbac/models", + "output_dir": "${HOME}$/py/sage/wwwroot/_a/userdepartment", + "dbname": "sage", + "tblname": "userdepartment", + "title":"用户", + "params": { + "browserfields": { + "exclouded": ["id", "userid"], + "cwidth": {} + }, + "editexclouded": [ + "id", "userid" + ], + "record_toolbar": null + } +} diff --git a/examples/userrole.json b/examples/userrole.json new file mode 100644 index 0000000..b258926 --- /dev/null +++ b/examples/userrole.json @@ -0,0 +1,19 @@ +{ + "tblname": "userrole", + "title":"用户角色", + "params": { + "relation":{ + "param_field":"userid", + "outter_field":"roleid" + }, + "browserfields": { + "exclouded": ["id", "userid"], + "alters": {} + }, + "editexclouded": [ + "id", + "userid" + ], + "record_toolbar": null + } +} diff --git a/examples/users.json b/examples/users.json new file mode 100644 index 0000000..ed1b2d3 --- /dev/null +++ b/examples/users.json @@ -0,0 +1,23 @@ +{ + "tblname": "users", + "title":"用户", + "params": { + "sortby":"username", + "confidential_fields":["password"], + "logined_userorgid":"orgid", + "browserfields": { + "exclouded": ["id", "password", "orgid", "nick_name" ], + "cwidth": {} + }, + "editexclouded": [ + "id", "nick_name", "orgid" + ], + "subtables": [ + { + "field":"userid", + "title":"用户角色", + "subtable":"userrole" + } + ] + } +} diff --git a/model.xltx b/model.xltx new file mode 100644 index 0000000000000000000000000000000000000000..6c3d6125906cdc4b9233ea3c37e65e453bc7dd26 GIT binary patch literal 14840 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 32: + id = uuid() +ns['id'] = id +{% for f in confidential_fields or [] %} +if params_kw.get('{{f}}'): + ns['{{f}}'] = password_encode(params_kw.get('{{f}}')) +{% endfor %} +{% if logined_userid %} +userid = await get_user() +if not userid: + return { + "widgettype":"Error", + "options":{ + "title":"Authorization Error", + "timeout":3, + "cwidth":16, + "cheight":9, + "message":"Please login" + } + } +ns['{{logined_userid}}'] = userid +{% endif %} +{% if logined_userorgid %} +userorgid = await get_userorgid() +if not userorgid: + return { + "widgettype":"Error", + "options":{ + "title":"Authorization Error", + "timeout":3, + "cwidth":16, + "cheight":9, + "message":"Please login" + } + } +ns['{{logined_userorgid}}'] = userorgid +{% endif %} +db = DBPools() +dbname = get_module_dbname('{{modulename}}') +async with db.sqlorContext(dbname) as sor: + r = await sor.C('{{summary[0].name}}', ns.copy()) + return { + "widgettype":"Message", + "options":{ + "user_data":ns, + "cwidth":16, + "cheight":9, + "title":"Add Success", + "timeout":3, + "message":"ok" + } + } + +return { + "widgettype":"Error", + "options":{ + "title":"Add Error", + "cwidth":16, + "cheight":9, + "timeout":3, + "message":"failed" + } +} +""" +data_update_tmpl = """ +ns = params_kw.copy() +{% if logined_userid %} +userid = await get_user() +if not userid: + return { + "widgettype":"Error", + "options":{ + "title":"Authorization Error", + "timeout":3, + "cwidth":16, + "cheight":9, + "message":"Please login" + } + } +ns['{{logined_userid}}'] = userid +{% endif %} +{% if logined_userorgid %} +userorgid = await get_userorgid() +if not userorgid: + return { + "widgettype":"Error", + "options":{ + "title":"Authorization Error", + "timeout":3, + "cwidth":16, + "cheight":9, + "message":"Please login" + } + } +ns['{{logined_userorgid}}'] = userorgid +{% endif %} +{% for f in confidential_fields or [] %} +if params_kw.get('{{f}}'): + ns['{{f}}'] = password_encode(params_kw.get('{{f}}')) +{% endfor %} + +db = DBPools() +dbname = get_module_dbname('{{modulename}}') +async with db.sqlorContext(dbname) as sor: + r = await sor.U('{{summary[0].name}}', ns) + debug('update success'); + return { + "widgettype":"Message", + "options":{ + "title":"Update Success", + "cwidth":16, + "cheight":9, + "timeout":3, + "message":"ok" + } + } + +return { + "widgettype":"Error", + "options":{ + "title":"Update Error", + "cwidth":16, + "cheight":9, + "timeout":3, + "message":"failed" + } +} +""" +data_delete_tmpl = """ +ns = { + 'id':params_kw['id'], +} +{% if logined_userid %} +userid = await get_user() +if not userid: + return { + "widgettype":"Error", + "options":{ + "title":"Authorization Error", + "timeout":3, + "cwidth":16, + "cheight":9, + "message":"Please login" + } + } +ns['{{logined_userid}}'] = userid +{% endif %} +{% if logined_userorgid %} +userorgid = await get_userorgid() +if not userorgid: + return { + "widgettype":"Error", + "options":{ + "title":"Authorization Error", + "timeout":3, + "cwidth":16, + "cheight":9, + "message":"Please login" + } + } +ns['{{logined_userorgid}}'] = userorgid +{% endif %} +db = DBPools() +dbname = get_module_dbname('{{modulename}}') +async with db.sqlorContext(dbname) as sor: + r = await sor.D('{{summary[0].name}}', ns) + debug('delete success'); + return { + "widgettype":"Message", + "options":{ + "title":"Delete Success", + "timeout":3, + "cwidth":16, + "cheight":9, + "message":"ok" + } + } + +debug('Delete failed'); +return { + "widgettype":"Error", + "options":{ + "title":"Delete Error", + "timeout":3, + "cwidth":16, + "cheight":9, + "message":"failed" + } +} +""" + +check_changed_tmpls = """ +is_checked = params_kw.get('has_{{relation.param_field}}') +debug(f'{params_kw=}, {is_checked=}') +dbname = get_module_dbname('{{modulename}}') +if is_checked == 'true': + ns = { + "id":uuid(), + "{{relation.param_field}}":params_kw.{{relation.param_field}}, + "{{relation.outter_field}}":params_kw.{{relation.outter_field}} + } + db = DBPools(); + async with db.sqlorContext(dbname) as sor: + await sor.C('{{tblname}}', ns) + + return { + "widgettype":"Message", + "options":{ + "title":"Success", + "message":"record add success", + "timeout":2 + } + } +else: + ns = { + "{{relation.param_field}}":params_kw.{{relation.param_field}}, + "{{relation.outter_field}}":params_kw.{{relation.outter_field}} + } + sql = "delete from {{tblname}} where {{relation.param_field}}=" + "${" + "{{relation.param_field}}" + "}$" + " and {{relation.outter_field}}=" + "${" + "{{relation.outter_field}}" + "}$" + db = DBPools() + async with db.sqlorContext(dbname) as sor: + await sor.sqlExe(sql, ns) + + return { + "widgettype":"Message", + "options":{ + "title":"Success", + "message":"delete record success", + "timeout":3 + } + } +""" diff --git a/xls2crud.py b/xls2crud.py new file mode 100644 index 0000000..d3180c9 --- /dev/null +++ b/xls2crud.py @@ -0,0 +1,304 @@ +import os +import sys +import codecs +import json +import argparse + +from appPublic.dictObject import DictObject +from xlsxData import xlsxFactory +from appPublic.folderUtils import listFile, _mkdir +from appPublic.myTE import MyTemplateEngine +from tmpls import data_browser_tmpl, get_data_tmpl, data_new_tmpl, data_update_tmpl, data_delete_tmpl, check_changed_tmpls +from appPublic.argsConvert import ArgsConvert + +""" +usage: +xls2crud.py dbname models_dir uidir +""" + +def build_dbdesc(models_dir: str) -> dict: + db_desc = {} + for f in listFile(models_dir, suffixs=['.xlsx']): + print(f'{f} handle ...') + x = xlsxFactory(f) + d = x.get_data() + tbname = d.summary[0].name + db_desc.update({tbname:d}) + return db_desc + +def build_subtable(subtable): + t = subtable + url = f"../{t.subtable}" + if t.url: + url = t.url + params = t.params or {} + params[t.field] = "${id}" + return { + "widgettype":"urlwidget", + "options":{ + "params":params, + "url":"{{entire_url('" + url + "')}}" + } + } + +def build_crud_ui(crud_data: dict, dbdesc: dict): + uidir = crud_data.output_dir + tables = [ k for k in dbdesc.keys() ] + desc = dbdesc[crud_data.tblname] + desc.update(crud_data.params) + binds = desc.binds or [] + if desc.relation: + desc.checkField = 'has_' + desc.relation.param_field + binds.append({ + "wid":"self", + "event":"row_check_changed", + "actiontype":"urlwidget", + "target":"self", + "options":{ + "params":{}, + "url":"{{entire_url('check_changed.dspy')}}" + } + }) + desc.bindsstr = json.dumps(binds, indent=4, ensure_ascii=False) + if desc.subtables: + if len(desc.subtables) == 1: + t = desc.subtables[0] + d = build_subtable(t) + content_view = DictObject(**d) + else: + items = [] + for t in desc.subtables: + d = build_subtable(t) + item = { + "name":t.subtable, + "label":t.title or t.subtable, + "content":d + } + items.append(item) + content_view = DictObject(**{ + "widgettype":"TabPanel", + "options":{ + "tab_wide":"auto", + "height":"100%", + "width":"100%", + "tab_pos":"top", + "items":items + } + }) + desc.content_view = content_view + + desc.update({ + "tblname":crud_data.tblname, + "dbname":crud_data.dbname + }) + build_table_crud_ui(uidir, desc) + +def build_table_crud_ui(uidir: str, desc: dict) -> None: + _mkdir(uidir) + build_data_browser(uidir, desc) + if desc.relation: + build_check_changed(uidir, desc) + else: + build_data_new(uidir, desc) + build_data_update(uidir, desc) + build_data_delete(uidir, desc) + build_get_data(uidir, desc) + +def alter_field(field:dict, desc:DictObject) -> dict: + name = field['name'] + ret = field.copy() + alters = desc.browserfields.alters + if alters: + [ ret.update(alters[k]) for k in alters.keys() if k == name ] + return ret + +def field_list(desc: dict) -> list: + fs = [] + for f in desc.fields: + if desc.codes and f.name in [c.field for c in desc.codes]: + d = get_code_desc(f, desc) + else: + d = setup_ui_info(f, confidential_fields=desc.confidential_fields or []) + """ + use alters to modify fields + """ + d = alter_field(d, desc) + fs.append(d) + return fs + +def get_code_desc(field: dict, desc: dict) -> dict: + d = DictObject(**field.copy()) + if not desc.codes: + return None + for c in desc.codes: + if d.name == c.field: + d.label = d.title or d.name + d.uitype = 'code' + d.valueField = d.name + d.textField = d.name + '_text' + d.params = { + 'dbname':"{{get_module_dbname('" + desc.modulename + "')}}", + 'table':c.table, + 'tblvalue':c.valuefield, + 'tbltext':c.textfield, + 'valueField':d.valueField, + 'textField':d.textField + } + if c.cond: + d.params['cond'] = c.cond + ac = ArgsConvert('[[', ']]') + vars = ac.findAllVariables(c.cond) + for v in vars: + d.params[v] = '{{params_kw.' + v + '}}' + d.dataurl = "{{entire_url('/appbase/get_code.dspy')}}" + return d + return None + +def setup_ui_info(field:dict, confidential_fields=[]) ->dict: + d = DictObject(**field.copy()) + if d.length: + d.cwidth = d.length if d.length < 18 else 18 + if d.cwidth < 4: + d.cwidth = 4; + else: + d.length = 0 + + if d.type == 'date': + d.uitype = 'date' + d.length = 0 + elif d.type == 'time': + d.uitype = 'time' + d.length = 0 + elif d.type in ['int', 'short', 'long', 'longlong']: + d.uitype = 'int' + d.length = 0 + elif d.type == 'text': + d.uitype = 'text' + elif d.type in ['float', 'double', 'decimal']: + d.uitype = 'float' + else: + if d.name in confidential_fields: + d.uitype = 'password' + elif d.name.endswith('_date') or d.name.endswith('_dat'): + d.uitype = 'date' + d.length = 0 + else: + d.uitype = 'str' + d.datatype = d.type + d.label = d.title or d.name + return d + +def construct_get_data_sql(desc: dict) -> str: + shortnames = [c for c in 'bcdefghjklmnopqrstuvwxyz'] + infos = [] + if desc.relation and desc.codes: + param_field = "${" + desc.relation.param_field + "}$" + for code in desc.codes: + if code.field == desc.relation.outter_field: + return f"""select '$[{desc.relation.param_field}]$' as {desc.relation.param_field}, +case when b.{desc.relation.param_field} is NULL then 0 else 1 end has_{desc.relation.param_field}, +a.{code.valuefield} as {code.field}, +a.{code.textfield} as {code.field}_text +from {code.table} a left join +(select * from {desc.tblsql or desc.tblname} where {desc.relation.param_field} ={param_field}) b + on a.{code.valuefield} = b.{code.field} +""" + if not desc.codes or len(desc.codes) == 0: + return f"select * from {desc.tblsql or desc.tblname} where 1=1 " + ' [[filterstr]]' + + for i, c in enumerate(desc.codes): + shortname = shortnames[i] + cond = '1 = 1' + if c.cond: + cond = c.cond + csql = f"""(select {c.valuefield} as {c.field}, + {c.textfield} as {c.field}_text from {c.table} where {cond})""" + infos.append([f'{shortname}.{c.field}_text', f"{csql} {shortname} on a.{c.field} = {shortname}.{c.field}"]) + bt = f'(select * from {desc.summary[0].name} where 1=1' + " [[filterstr]]) a" + infos.insert(0, ['a.*', bt]) + fields = ', '.join([i[0] for i in infos]) + tables = ' left join '.join([i[1] for i in infos]) + return f"""select {fields} +from {tables}""" + + +def build_data_browser(pat: str, desc: dict): + desc = desc.copy() + desc.fieldliststr = json.dumps(field_list(desc), ensure_ascii=False, indent=4) + e = MyTemplateEngine([]) + s = e.renders(data_browser_tmpl, desc) + with open(os.path.join(pat, f'index.ui'), 'w') as f: + f.write(s) + +def build_data_new(pat: str, desc: dict): + e = MyTemplateEngine([]) + desc = desc.copy() + s = e.renders(data_new_tmpl, desc) + with open(os.path.join(pat, f'add_{desc.tblname}.dspy'), 'w') as f: + f.write(s) + +def build_data_update(pat: str, desc: dict): + e = MyTemplateEngine([]) + desc = desc.copy() + s = e.renders(data_update_tmpl, desc) + with open(os.path.join(pat, f'update_{desc.tblname}.dspy'), 'w') as f: + f.write(s) + +def build_data_delete(pat: str, desc: dict): + e = MyTemplateEngine([]) + desc = desc.copy() + s = e.renders(data_delete_tmpl, desc) + with open(os.path.join(pat, f'delete_{desc.tblname}.dspy'), 'w') as f: + f.write(s) + +def build_get_data(pat: str, desc: dict): + e = MyTemplateEngine([]) + desc = desc.copy() + desc.sql = construct_get_data_sql(desc) + s = e.renders(get_data_tmpl, desc) + with open(os.path.join(pat, f'get_{desc.tblname}.dspy'), 'w') as f: + f.write(s) + +def build_check_changed(pat:str, desc:dict): + e = MyTemplateEngine([]) + desc = desc.copy() + s = e.renders(check_changed_tmpls, desc) + with open(os.path.join(pat, 'check_changed.dspy'), 'w') as f: + f.write(s) + +if __name__ == '__main__': + """ + crud_json has following format + { + "tblname", + "params" + } + """ + parser = argparse.ArgumentParser('xls2crud') + parser.add_argument('-m', '--models_dir') + parser.add_argument('-o', '--output_dir') + parser.add_argument('modulename') + parser.add_argument('files', nargs='*') + args = parser.parse_args() + if len(args.files) < 1: + print(f'Usage:\n{sys.argv[0]} [-m models_dir] [-o output_dir] json_file ....\n') + sys.exit(1) + ns = {k:v for k, v in os.environ.items()} + for fn in args.files: + print(f'handle {fn}') + crud_data = {} + with codecs.open(fn, 'r', 'utf-8') as f: + a = json.load(f) + ac = ArgsConvert('${','}$') + a = ac.convert(a,ns) + crud_data = DictObject(**a) + if args.models_dir: + crud_data.models_dir = args.models_dir + models_dir = crud_data.models_dir + if args.output_dir: + tblname = crud_data.alias or crud_data.tblname + crud_data.output_dir = os.path.join(args.output_dir, tblname) + crud_data.params.modulename = args.modulename + dbdesc = build_dbdesc(models_dir) + build_crud_ui(crud_data, dbdesc) + diff --git a/xls2ddl.py b/xls2ddl.py new file mode 100755 index 0000000..a4af1df --- /dev/null +++ b/xls2ddl.py @@ -0,0 +1,94 @@ +# -*- coding:utf-8 -*- +import io +import sys +from traceback import print_exc +from xlsxData import CRUDData, xlsxFactory + +import codecs +import json +from sqlor.ddl_template_sqlserver import sqlserver_ddl_tmpl +from sqlor.ddl_template_mysql import mysql_ddl_tmpl +from sqlor.ddl_template_oracle import oracle_ddl_tmpl +from sqlor.ddl_template_postgresql import postgresql_ddl_tmpl + +from appPublic.myTE import MyTemplateEngine +from appPublic.folderUtils import listFile + +tmpls = { + "sqlserver":sqlserver_ddl_tmpl, + "mysql":mysql_ddl_tmpl, + "oracle":oracle_ddl_tmpl, + "postgresql":postgresql_ddl_tmpl +} + +def xls2ddl(xlsfile,dbtype): + data = None + if xlsfile.endswith('json'): + with codecs.open(xlsfile,'r','utf-8') as f: + data = json.load(f) + else: + d = xlsxFactory(xlsfile) + if d is None: + print(xlsfile, 'can not read data') + return + data = d.get_data() + if data is None: + print(xlsfile, 'not data return from XLSX file') + return + tmpl = tmpls.get(dbtype.lower()) + if tmpl is None: + raise Exception('%s database not implemented' % dbtype) + e = MyTemplateEngine([]) + s = e.renders(tmpl,data) + # print(data.data) + if data.data: + ins = gen_insert(data) + s = f"{s}\n{ins}\n" + return s + +def gen_insert(xls): + tbl = xls.summary[0].name + lines = [] + for d in xls.data: + ks = [] + vs = [] + for k,v in d.items(): + ks.append(k) + if isinstance(v, str): + vs.append(f"'{v}'") + else: + vs.append(str(v)) + + line = f"insert into {tbl} ({','.join(ks)}) values ({','.join(vs)});" + lines.append(line) + return "\n".join(lines) + +def model2ddl(folder,dbtype): + ddl_str = '' + for f in listFile(folder, suffixs=['xlsx','json']): + try: + ddl_str += f'\n-- {f}\n' + s = xls2ddl(f,dbtype) + ddl_str = f"{ddl_str}\n{s}\n" + + except Exception as e: + print('Exception:',e,'f=',f) + print_exc() + return ddl_str + +if __name__ == '__main__': + import sys + ##解决windows 终端中输出中文出现 + # UnicodeEncodeError: 'gbk' codec can't encode character '\xa0' in position 20249 + # 错误 + # BEGIN + sys.stdout = io.TextIOWrapper(sys.stdout.buffer,encoding='utf8') + # + # END + if len(sys.argv) < 3: + print('Usage:%s dbtype folder' % sys.argv[0]) + sys.exit(1) + + s = model2ddl(sys.argv[2], sys.argv[1]) + print(s) + diff --git a/xls2ui.py b/xls2ui.py new file mode 100644 index 0000000..b690b24 --- /dev/null +++ b/xls2ui.py @@ -0,0 +1,46 @@ +import os +import codecs +import json + +import sys +import argparse +from appPublic.argsConvert import ArgsConvert +from appPublic.dictObject import DictObject +from xls2crud import build_dbdesc, build_crud_ui +from singletree import build_tree_ui +if __name__ == '__main__': + """ + crud_json has following format + { + "tblname", + "params" + } + """ + parser = argparse.ArgumentParser('xls2crud') + parser.add_argument('-m', '--models_dir') + parser.add_argument('-o', '--output_dir') + parser.add_argument('modulename') + parser.add_argument('files', nargs='*') + args = parser.parse_args() + if len(args.files) < 1: + print(f'Usage:\n{sys.argv[0]} [-m models_dir] [-o output_dir] json_file ....\n') + sys.exit(1) + print(args) + ns = {k:v for k, v in os.environ.items()} + dbdesc = build_dbdesc(args.models_dir) + for fn in args.files: + print(f'handle {fn}') + crud_data = {} + with codecs.open(fn, 'r', 'utf-8') as f: + a = json.load(f) + ac = ArgsConvert('${','}$') + a = ac.convert(a,ns) + crud_data = DictObject(**a) + tblname = crud_data.alias or crud_data.tblname + crud_data.output_dir = os.path.join(args.output_dir, tblname) + crud_data.params.modulename = args.modulename + crud_data.params.tblname = crud_data.tblname + if crud_data.uitype == 'tree': + build_tree_ui(crud_data, dbdesc) + continue + build_crud_ui(crud_data, dbdesc) diff --git a/xlsxData.py b/xlsxData.py new file mode 100755 index 0000000..04154fb --- /dev/null +++ b/xlsxData.py @@ -0,0 +1,330 @@ +import os +import sys +from traceback import print_exc +from openpyxl import load_workbook +from appPublic.myjson import loadf,dumpf,dumps +from appPublic.dictObject import DictObject + +class TypeConvert: + def conv(self,typ,v): + if typ is None: + return v + f = getattr(self,'to_'+typ,None) + if f is None: + return v + return f(v) + + def to_int(self,v): + try: + return int(v) + except: + return 0 + + def to_float(self,v): + try: + return float(v) + except: + return 0.0 + + def to_str(self,v): + try: + return str(v) + except: + return '' + def to_json(self,v): + if v == '': + return v + try: + return loads(v) + except: + return v + + def to_date(self,v): + return v + + def to_time(self,v): + return v + + def to_timestamp(self,v): + return v + + def to_cruddata(self,v): + vs = v.split('"',3) + if vs < 3: + return v + fn = vs[1] + d = CRUDData(fn) + try: + data = d.get_data() + except Exception as e: + print_exc() + print(e) + return v + if vs[2] is None: + return data + cmd = "d%s" % vs[2] + ret=eval(cmd,{'d':data}) + return ret + + def to_xlsxdata(self,v): + vs = v.split('"',3) + if vs < 3: + return v + fn = vs[1] + d = XLSXData(fn) + try: + data = d.get_data() + except Exception as e: + print_exc() + print(e) + return v + if vs[2] is None: + return data + cmd = "d%s" % vs[2] + ret=eval(cmd,{'d':data}) + return ret + +class CRUDException(Exception): + def __init__(self,xlsfile,errmsg,*args,**argv): + Exception.__init__(self,*args,**argv) + self.xlsfile = xlsfile + self.errmsg = errmsg + + def __str__(self): + return 'filename:' + self.xlsfile+' error:' + self.errmsg + +class XLSXData(object): + def __init__(self,xlsxfile, book=None): + self.xlsxfile = xlsxfile + if book is None: + self.book = load_workbook(filename=xlsxfile) + else: + self.book = book + self._read() + + def get_data(self): + return self.data + + def readRecords(self,name,sheet): + i = 1 + recs = [] + fields = [] + tc = TypeConvert() + + for i,row in enumerate(sheet.values): + if i==0: + fields = self.getFieldNames(row) + continue + rec = {} + for j, a in enumerate(row): + if a is None: + continue + k = fields[j][0] + v = tc.conv(fields[j][1],a) + rec[k] = v + if rec == {}: + continue + o = DictObject(**rec) + recs.append(o) + return {name:recs} + + def _read(self): + ret = {} + for i,s in enumerate(self.book.worksheets): + ret.update(self.readRecords(self.book.sheetnames[i], s)) + self.data = DictObject(**ret) + + def getFieldNames(self,row): + fs = [] + for i,f in enumerate(row): + if f is None: + f = 'F_' + str(i) + else: + if type(f) != type(""): + f = 'F_' + str(f) + """ + else: + f = f.encode('utf-8') + """ + b=f.split(':') + if len(b) < 2: + b.append(None) + fs.append(b) + i+= 1 + return fs + +class CRUDData(XLSXData): + @classmethod + def isMe(self,book): + if book is None: + return False + + names = book.sheetnames + if 'summary' not in names: + return False + if 'fields' not in names: + return False + if 'validation' not in names: + return False + return True + + def _read(self): + super()._read() + d = self.data + if not 'summary' in d.keys(): + raise CRUDException(self.xlsxfile,'summary sheet missing') + if not 'fields' in d.keys(): + raise CRUDException(self.xlsxfile,'fields sheet missing') + if not 'validation' in d.keys(): + raise CRUDException(self.xlsxfile,'validation sheet missing') + if len(d['summary']) != 1: + raise CRUDException(self.xlsxfile, 'Not summary or more than one summary') + self.convPrimary() + self.convForeignkey() + self.convIndex() + self.check_codes_fields() + + def convPrimary(self): + d = self.data + v = d['summary'][0]['primary'] + v = v.split(',') + self.data['summary'][0]['primary'] = v + self.check_primary_fields() + + def check_primary_fields(self): + primarys = self.data['summary'][0]['primary'] + if primarys is None: + raise CRUDException(self.xlsxfile, 'primary is None') + for p in primarys: + r = self.check_field(p) + if not r: + raise CRUDException( + self.xlsxfile, f'primary error({p})') + + def check_codes_fields(self): + if 'codes' not in self.data.keys(): + return + for f in self.data['codes']: + r = self.check_field(f['field']) + if not r: + raise CRUDException( + self.xlsxfile, + f'code definintion error({f["field"]})') + + + def check_field(self, fieldname): + return fieldname in [f['name'] for f in self.data['fields']] + + def convForeignkey(self): + data = self.data + vs = data['validation'] + nvs = [] + for v in vs: + if v['oper'] == 'fk': + m = v['value'] + des= m.split(':') + if len(des) != 3: + raise CRUDException(self.xlsxfile,'fk value error:%s' % m) + v['value'] = {'table':des[0],'value':des[1],'title':des[2]} + nvs.append(v) + data['validation'] = nvs + self.data = data + + def getFieldByNmae(self,fields,name): + for f in fields: + if f['name'] == name: + return f + + def getFKs(self,validation): + fks = [] + for v in validation: + if v['oepr'] == 'fk': + fks.append(v) + return fks + + def getIDXs(self,validation): + idxs = [] + for v in validation: + if v['oper'] == 'idx': + idxs.append(v) + return idxs + + def convIndex(self): + data = self.data + vs = data['validation'] + nvs = [] + for v in vs: + if v['oper'] == 'idx': + idx = {} + idx['name'] = v['name'] + m = v['value'] + des= m.split(':') + if len(des) != 2: + raise CRUDException(self.xlsxfile,'idx value format:idx_type:keylist:%s' % m) + idx['idxtype'] = des[0] + idx['idxfields'] = des[1].split(',') + self.check_field(f for f in idx['idxfields']) + nvs.append(idx) + data['indexes'] = nvs + self.data = data + +def xlsxFactory(xlsxfilename): + def findSubclass(name,klass): + for k in klass.__subclasses__(): + if k.isMe(name): + return k + k1 = findSubclass(name,k) + if k1 is not None: + return k1 + return None + try: + book = load_workbook(filename=xlsxfilename) + if book is None: + print(f'{xlsxfilename} read error') + return None + k = findSubclass(book, XLSXData) + if k is not None: + xlsx = k(xlsxfilename, book=book) + return xlsx + return XLSXData(xlsxfilename, book=book) + + except Exception as e: + print_exc() + print(xlsxfilename, 'new class failed\n%s' % str(e)) + print_exc() + return None + +def ValueConvert(s): + if s[:9] == 'xlsfile::': + d = xlsxFactory(s[9:]) + return d.get_data() + if s[:10] == 'jsonfile::': + return loadf(s[10:]) + return s + +def paramentHandle(ns): + for k,v in ns.items(): + ns[k] = ValueConvert(v) + return ns + +if __name__ == '__main__': + retData = {} + ns = {} + datafiles = [] + for a in sys.argv[1:]: + m = a.split('=',1) + if len(m)>1: + ns[m[0]] = m[1] + else: + datafiles.append(a) + + ns = paramentHandle(ns) + for f in datafiles: + ext = os.path.splitext(f)[-1] + if ext in ['.xlsx','.xls' ]: + d = xlsxFactory(f) + data = d.get_data() + retData.update(data) + retData.update(ns) + print( dumps(retData)) +