From cf375504a8bf32f11f13ad5fff435a3c737d47ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=96=B9=20=E6=9F=8F?= Date: Fri, 14 Jul 2023 07:06:39 +0000 Subject: [PATCH 1/5] code refactoring&kintone app update --- backend/app/api/api_v1/routers/kintone.py | 263 ++++++++++++++++------ 1 file changed, 191 insertions(+), 72 deletions(-) diff --git a/backend/app/api/api_v1/routers/kintone.py b/backend/app/api/api_v1/routers/kintone.py index 1203c0a..c7e9ad4 100644 --- a/backend/app/api/api_v1/routers/kintone.py +++ b/backend/app/api/api_v1/routers/kintone.py @@ -8,6 +8,141 @@ import app.core.config as c kinton_router = r = APIRouter() +def getfieldsfromexcel(df): + appname = df.iloc[0,2] + col=[] + for row in range(5,len(df)): + if not df.iloc[row,3] in c.KINTONE_FIELD_TYPE: + continue + p=[] + for column in range(1,7): + if(not pd.isna(df.iloc[row,column])): + if(property[column-1]=="options"): + o=[] + for v in df.iloc[row,column].split(','): + o.append(f"\"{v.split('|')[0]}\":{{\"label\":\"{v.split('|')[0]}\",\"index\":\"{v.split('|')[1]}\"}}") + p.append(f"\"{property[column-1]}\":{{{','.join(o)}}}") + elif(property[column-1]=="required"): + p.append(f"\"{property[column-1]}\":{df.iloc[row,column]}") + else: + p.append(f"\"{property[column-1]}\":\"{df.iloc[row,column]}\"") + col.append(f"\"{df.iloc[row,2]}\":{{{','.join(p)}}}") + fields = ",".join(col).replace("False","false").replace("True","true") + return json.loads(f"{{{fields}}}") + +def getsettingfromexcel(df): + appname = df.iloc[0,2] + des = df.iloc[2,2] + return {"name":appname,"description":des} + +def getsettingfromkintone(app:str): + headers={c.API_V1_AUTH_KEY:c.API_V1_AUTH_VALUE} + params = {"app":app} + url = f"{c.BASE_URL}{c.API_V1_STR}/app/settings.json" + r = httpx.get(url,headers=headers,params=params) + return r.json() + + +def analysesettings(excel,kintone): + updatesettings={} + updates = excel.keys() & kintone.keys() + for key in updates: + if excel[key] != kintone[key]: + updatesettings[key] = excel[key] + return updatesettings + +def createkintoneapp(name:str): + headers={c.API_V1_AUTH_KEY:c.API_V1_AUTH_VALUE,"Content-Type": "application/json"} + data = {"name":name} + url = f"{c.BASE_URL}{c.API_V1_STR}/preview/app.json" + r = httpx.post(url,headers=headers,data=json.dumps(data)) + return r.json() + +def updateappsettingstokintone(app:str,updates:dict): + headers={c.API_V1_AUTH_KEY:c.API_V1_AUTH_VALUE,"Content-Type": "application/json"} + url = f"{c.BASE_URL}{c.API_V1_STR}/preview/app/settings.json" + data = {"app":app} + data.update(updates) + r = httpx.put(url,headers=headers,data=json.dumps(data)) + return r.json() + +def addfieldstokintone(app:str,fields:dict,revision:str = None): + headers={c.API_V1_AUTH_KEY:c.API_V1_AUTH_VALUE,"Content-Type": "application/json"} + url = f"{c.BASE_URL}{c.API_V1_STR}/preview/app/form/fields.json" + if revision != None: + data = {"app":app,"revision":revision,"properties":fields} + else: + data = {"app":app,"properties":fields} + r = httpx.post(url,headers=headers,data=json.dumps(data)) + return r.json() + +def updatefieldstokintone(app:str,revision:str,fields:dict): + headers={c.API_V1_AUTH_KEY:c.API_V1_AUTH_VALUE,"Content-Type": "application/json"} + url = f"{c.BASE_URL}{c.API_V1_STR}/preview/app/form/fields.json" + data = {"app":app,"properties":fields} + r = httpx.put(url,headers=headers,data=json.dumps(data)) + return r.json() + +def deletefieldsfromkintone(app:str,revision:str,fields:dict): + headers={c.API_V1_AUTH_KEY:c.API_V1_AUTH_VALUE,"Content-Type": "application/json"} + url = f"{c.BASE_URL}{c.API_V1_STR}/preview/app/form/fields.json" + params = {"app":app,"revision":revision,"fields":fields} + #r = httpx.delete(url,headers=headers,content=json.dumps(params)) + r = httpx.request(method="DELETE",url=url,headers=headers,content=json.dumps(params)) + return r.json() + +def deoployappfromkintone(app:str,revision:str): + headers={c.API_V1_AUTH_KEY:c.API_V1_AUTH_VALUE,"Content-Type": "application/json"} + url = f"{c.BASE_URL}{c.API_V1_STR}/preview/app/deploy.json" + data = {"apps":[{"app":app,"revision":revision}],"revert": False} + r = httpx.post(url,headers=headers,data=json.dumps(data)) + return r.json + +def getfieldsfromkintone(app): + headers={c.API_V1_AUTH_KEY:c.API_V1_AUTH_VALUE} + params = {"app":app} + url = f"{c.BASE_URL}{c.API_V1_STR}/app/form/fields.json" + r = httpx.get(url,headers=headers,params=params) + return r.json() + +def analysefields(excel,kintone): + updatefields={} + addfields={} + delfields=[] + updates = excel.keys() & kintone.keys() + adds = excel.keys() - kintone.keys() + dels = kintone.keys() - excel.keys() + for key in updates: + for p in property: + if excel[key].get(p) != None and kintone[key][p] != excel[key][p]: + updatefields[key] = excel[key] + break + for key in adds: + addfields[key] = excel[key] + for key in dels: + if kintone[key]["type"] in c.KINTONE_FIELD_TYPE: + delfields.append(key) + + return {"update":updatefields,"add":addfields,"del":delfields} + + +@r.post("/test",) +async def test(file:UploadFile= File(...),app:str=None): + if file.filename.endswith('.xlsx'): + try: + content = await file.read() + df = pd.read_excel(BytesIO(content)) + excel = getfieldsfromexcel(df) + if(app != None): + kintone = getfieldsfromkintone(app) + fields = analysefields(excel,kintone["properties"]) + except Exception as e: + raise HTTPException(status_code=400, detail=f"Error occurred while parsing file {file.filename}: {str(e)}") + else: + raise HTTPException(status_code=400, detail=f"File {file.filename} is not an Excel file") + + return fields + @r.post("/upload",) async def upload(files:t.List[UploadFile] = File(...)): @@ -33,6 +168,18 @@ async def allapps(): r = httpx.get(url,headers=headers) return r.json() +@r.get("/appfields") +async def appfields(app:str): + return getfieldsfromkintone(app) + + +@r.get("/alljscs") +async def alljscs(app:str): + headers={c.API_V1_AUTH_KEY:c.API_V1_AUTH_VALUE} + url = f"{c.BASE_URL}{c.API_V1_STR}/app/customize.json" + params = {"app":app} + r = httpx.get(url,headers=headers,params=params) + return r.json() @r.post("/createapp",) async def createapp(name:str): @@ -55,97 +202,69 @@ async def createappfromexcel(files:t.List[UploadFile] = File(...)): if file.filename.endswith('.xlsx'): try: content = await file.read() - #アプリ名 df = pd.read_excel(BytesIO(content)) - # print(df) + #アプリ名 appname = df.iloc[0,2] - col=[] - for row in range(5,len(df)): - if not df.iloc[row,3] in c.KINTONE_FIELD_TYPE: - continue - p=[] - for column in range(1,7): - if(not pd.isna(df.iloc[row,column])): - if(property[column-1]=="options"): - o=[] - for v in df.iloc[row,column].split(','): - o.append(f"\"{v.split('|')[0]}\":{{\"label\":\"{v.split('|')[0]}\",\"index\":\"{v.split('|')[1]}\"}}") - p.append(f"\"{property[column-1]}\":{{{','.join(o)}}}") - elif(property[column-1]=="required"): - p.append(f"\"{property[column-1]}\":{df.iloc[row,column]}") - else: - p.append(f"\"{property[column-1]}\":\"{df.iloc[row,column]}\"") - col.append(f"\"{df.iloc[row,2]}\":{{{','.join(p)}}}") - - headers={c.API_V1_AUTH_KEY:c.API_V1_AUTH_VALUE,"Content-Type": "application/json"} - data = {"name":appname} - url = f"{c.BASE_URL}{c.API_V1_STR}/preview/app.json" - r = httpx.post(url,headers=headers,data=json.dumps(data)) - result1 = r.json() - if result1.get("app") != None: - url = f"{c.BASE_URL}{c.API_V1_STR}/preview/app/form/fields.json" - form = f"{{\"app\":{result1['app']},\"properties\":{{{','.join(col)}}}}}".replace("False","false").replace("True","true") - print(form) - data = json.loads(form) - r = httpx.post(url,headers=headers,data=json.dumps(data)) - result2 = r.json() - if result2.get("revision") != None: - url = f"{c.BASE_URL}{c.API_V1_STR}/preview/app/deploy.json" - data = {"apps":[{"app":result1["app"],"revision":result2["revision"]}],"revert": False} - r = httpx.post(url,headers=headers,data=json.dumps(data)) + desc = df.iloc[2,2] + result = {"app":0,"revision":0,"msg":""} + fields = getfieldsfromexcel(df) + app = createkintoneapp(appname) + if app.get("app") != None: + result["app"] = app["app"] + app = updateappsettingstokintone(result["app"],{"description":desc}) + if app.get("revision") != None: + result["revision"] = app["revision"] + app = addfieldstokintone(result["app"],fields) + if app.get("revision") != None: + result["revision"] = app["revision"] + deoployappfromkintone(result["app"],result["revision"]) except Exception as e: raise HTTPException(status_code=400, detail=f"Error occurred while parsing file {file.filename}: {str(e)}") else: raise HTTPException(status_code=400, detail=f"File {file.filename} is not an Excel file") - return {"app":result1["app"],"revision":result2["revision"]} + return result @r.post("/updateappfromexcel",) -async def updateappfromexcel(app:str,revision:str,files:t.List[UploadFile] = File(...)): +async def updateappfromexcel(app:str,files:t.List[UploadFile] = File(...)): for file in files: if file.filename.endswith('.xlsx'): try: content = await file.read() - #アプリ名 df = pd.read_excel(BytesIO(content)) - # print(df) - appname = df.iloc[0,2] - col=[] - for row in range(5,len(df)): - if not df.iloc[row,3] in c.KINTONE_FIELD_TYPE: - continue - p=[] - for column in range(1,7): - if(not pd.isna(df.iloc[row,column])): - if(property[column-1]=="options"): - o=[] - for v in df.iloc[row,column].split(','): - o.append(f"\"{v.split('|')[0]}\":{{\"label\":\"{v.split('|')[0]}\",\"index\":\"{v.split('|')[1]}\"}}") - p.append(f"\"{property[column-1]}\":{{{','.join(o)}}}") - elif(property[column-1]=="required"): - p.append(f"\"{property[column-1]}\":{df.iloc[row,column]}") - else: - p.append(f"\"{property[column-1]}\":\"{df.iloc[row,column]}\"") - col.append(f"\"{df.iloc[row,2]}\":{{{','.join(p)}}}") - - headers={c.API_V1_AUTH_KEY:c.API_V1_AUTH_VALUE,"Content-Type": "application/json"} - - url = f"{c.BASE_URL}{c.API_V1_STR}/preview/app/form/fields.json" - form = f"{{\"app\":{app},\"revision\": {revision},\"properties\":{{{','.join(col)}}}}}".replace("False","false").replace("True","true") - print(form) - data = json.loads(form) - r = httpx.put(url,headers=headers,data=json.dumps(data)) - result = r.json() - if result.get("revision") != None: - url = f"{c.BASE_URL}{c.API_V1_STR}/preview/app/deploy.json" - data = {"apps":[{"app":app,"revision":result["revision"]}],"revert": False} - r = httpx.post(url,headers=headers,data=json.dumps(data)) + excel = getsettingfromexcel(df) + kintone= getsettingfromkintone(app) + settings = analysesettings(excel,kintone) + excel = getfieldsfromexcel(df) + kintone = getfieldsfromkintone(app) + revision = kintone["revision"] + fields = analysefields(excel,kintone["properties"]) + result = {"app":app,"revision":revision,"msg":"No Update"} + deploy = False + if len(fields["update"]) > 0: + result = updatefieldstokintone(app,revision,fields["update"]) + revision = result["revision"] + deploy = True + if len(fields["add"]) > 0: + result = addfieldstokintone(app,fields["add"],revision) + revision = result["revision"] + deploy = True + if len(fields["del"]) > 0: + result = deletefieldsfromkintone(app,revision,fields["del"]) + revision = result["revision"] + deploy = True + if len(settings) > 0: + result = updateappsettingstokintone(app,settings) + revision = result["revision"] + deploy = True + if deploy: + result = deoployappfromkintone(app,revision) except Exception as e: raise HTTPException(status_code=400, detail=f"Error occurred while parsing file {file.filename}: {str(e)}") else: raise HTTPException(status_code=400, detail=f"File {file.filename} is not an Excel file") - return r.json() + return result From b97e8067aeaff89bf89469b4d82baeaf8acd4e2c Mon Sep 17 00:00:00 2001 From: "maxiaozhe@alicorns.co.jp" Date: Fri, 14 Jul 2023 16:19:05 +0900 Subject: [PATCH 2/5] =?UTF-8?q?Rulr=20Editor=20=E4=BD=9C=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/layouts/MainLayout.vue | 13 +++++++++ frontend/src/pages/IndexPage.vue | 6 ++++ frontend/src/pages/RuleEditor.vue | 44 +++++++++++++++++++++++++---- frontend/src/router/routes.ts | 5 ++++ 4 files changed, 62 insertions(+), 6 deletions(-) diff --git a/frontend/src/layouts/MainLayout.vue b/frontend/src/layouts/MainLayout.vue index fc69d4d..f1c3334 100644 --- a/frontend/src/layouts/MainLayout.vue +++ b/frontend/src/layouts/MainLayout.vue @@ -50,6 +50,19 @@ import { ref } from 'vue'; import EssentialLink, { EssentialLinkProps } from 'components/EssentialLink.vue'; const essentialLinks: EssentialLinkProps[] = [ +{ + title: 'ホーム', + caption: 'home', + icon: 'home', + link: '/' + }, + { + title: 'ルールエディター', + caption: 'rule', + icon: 'rule', + link: '/#/ruleEditor' + } + , { title: 'Docs', caption: 'quasar.dev', diff --git a/frontend/src/pages/IndexPage.vue b/frontend/src/pages/IndexPage.vue index cff76f0..36bc2ed 100644 --- a/frontend/src/pages/IndexPage.vue +++ b/frontend/src/pages/IndexPage.vue @@ -1,6 +1,12 @@ diff --git a/frontend/src/router/routes.ts b/frontend/src/router/routes.ts index 2d34fc1..0b5f767 100644 --- a/frontend/src/router/routes.ts +++ b/frontend/src/router/routes.ts @@ -6,6 +6,11 @@ const routes: RouteRecordRaw[] = [ component: () => import('layouts/MainLayout.vue'), children: [{ path: '', component: () => import('pages/IndexPage.vue') }], }, + { + path: '/ruleEditor/', + component: () => import('layouts/MainLayout.vue'), + children: [{ path: '', component: () => import('pages/RuleEditor.vue') }], + }, // Always leave this as last one, // but you can also remove it From 9ab593661da7bfc8ced1a753c1aa31b7dbd9fb12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=96=B9=20=E6=9F=8F?= Date: Sat, 15 Jul 2023 02:09:06 +0000 Subject: [PATCH 3/5] bugfix:add field type --- backend/app/core/config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/app/core/config.py b/backend/app/core/config.py index fc24cce..ea605bc 100644 --- a/backend/app/core/config.py +++ b/backend/app/core/config.py @@ -12,4 +12,4 @@ API_V1_AUTH_KEY = "X-Cybozu-Authorization" API_V1_AUTH_VALUE = "TVhaOm1heHoxMjA1" -KINTONE_FIELD_TYPE=["GROUP","GROUP_SELECT","CHECK_BOX","SUBTABLE","RICH_TEXT","RICH_TEXT","LINK","REFERENCE_TABLE","CALC","TIME","NUMBER","ORGANIZATION_SELECT","FILE","DATETIME","DATE","MULTI_SELECT","SINGLE_LINE_TEXT","MULTI_LINE_TEXT"] +KINTONE_FIELD_TYPE=["GROUP","GROUP_SELECT","CHECK_BOX","SUBTABLE","DROP_DOWN","USER_SELECT","RADIO_BUTTON","RICH_TEXT","LINK","REFERENCE_TABLE","CALC","TIME","NUMBER","ORGANIZATION_SELECT","FILE","DATETIME","DATE","MULTI_SELECT","SINGLE_LINE_TEXT","MULTI_LINE_TEXT"] \ No newline at end of file From 350d3b53cc749acac71bcde59f23e4dcf993899a Mon Sep 17 00:00:00 2001 From: "maxiaozhe@alicorns.co.jp" Date: Sat, 15 Jul 2023 11:55:16 +0900 Subject: [PATCH 4/5] =?UTF-8?q?Backend=20=E6=9B=B4=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/pages/RuleEditor.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/pages/RuleEditor.vue b/frontend/src/pages/RuleEditor.vue index 962845f..8f6ff1b 100644 --- a/frontend/src/pages/RuleEditor.vue +++ b/frontend/src/pages/RuleEditor.vue @@ -44,6 +44,6 @@ const props = withDefaults(defineProps(), { actions:()=>["フィールド制御","一覧画面","その他"] }); function onItemClick(evt: Event){ - return; + return; } From 4d43b3c976ec115a643e597b5531ef79c95f7f15 Mon Sep 17 00:00:00 2001 From: "maxiaozhe@alicorns.co.jp" Date: Mon, 17 Jul 2023 11:20:56 +0900 Subject: [PATCH 5/5] =?UTF-8?q?=E3=83=86=E3=83=B3=E3=83=97=E3=83=AC?= =?UTF-8?q?=E3=83=BC=E3=83=88=E6=9B=B4=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/Temp/日報設計書.xlsx | Bin 11254 -> 11846 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/backend/Temp/日報設計書.xlsx b/backend/Temp/日報設計書.xlsx index 1ea901f6c0731eebd1ce1d0801aac0d5a80f8d66..b9f01e522ab38f60866344b7e44ea6a0d5f96d71 100644 GIT binary patch delta 5357 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 delta 4834 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