From f0e7aec6d30931a89998e83818e8107f52986c5f Mon Sep 17 00:00:00 2001 From: "shimoda.m@nds-tyo.co.jp" Date: Thu, 30 Mar 2023 14:16:36 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=AE=9F=E6=B6=88=E5=8C=96&=E3=82=A2?= =?UTF-8?q?=E3=83=AB=E3=83=88=E3=83=9E=E3=83=BC=E3=82=AF=20Web=E3=82=A2?= =?UTF-8?q?=E3=83=97=E3=83=AA=E3=82=B1=E3=83=BC=E3=82=B7=E3=83=A7=E3=83=B3?= =?UTF-8?q?=E3=81=AE=E8=A9=A6=E4=BD=9C=E5=93=81=E3=82=92=E7=99=BB=E9=8C=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ecs/jskult-webapp/.dockerignore | 12 + ecs/jskult-webapp/.env.example | 24 + ecs/jskult-webapp/.gitignore | 6 + ecs/jskult-webapp/.vscode/launch.json | 17 + .../.vscode/recommended_settings.json | 18 + ecs/jskult-webapp/Dockerfile | 19 + ecs/jskult-webapp/Pipfile | 34 + ecs/jskult-webapp/Pipfile.lock | 765 ++++++++++++++++++ ecs/jskult-webapp/README.md | 192 +++++ ecs/jskult-webapp/src/__init__.py | 0 ecs/jskult-webapp/src/aws/__init__.py | 0 ecs/jskult-webapp/src/aws/aws_api_client.py | 5 + ecs/jskult-webapp/src/aws/cognito.py | 25 + ecs/jskult-webapp/src/aws/s3.py | 32 + ecs/jskult-webapp/src/controller/__init__.py | 0 ecs/jskult-webapp/src/controller/bio.py | 87 ++ .../src/controller/bio_download.py | 121 +++ .../src/controller/healthcheck.py | 10 + ecs/jskult-webapp/src/controller/login.py | 150 ++++ ecs/jskult-webapp/src/controller/logout.py | 41 + ecs/jskult-webapp/src/controller/menu.py | 55 ++ ecs/jskult-webapp/src/core/__init__.py | 0 ecs/jskult-webapp/src/core/tasks.py | 21 + ecs/jskult-webapp/src/db/__init__.py | 0 ecs/jskult-webapp/src/db/database.py | 71 ++ ecs/jskult-webapp/src/db/sql_condition.py | 35 + ecs/jskult-webapp/src/db/tasks.py | 22 + ecs/jskult-webapp/src/depends/__init__.py | 0 ecs/jskult-webapp/src/depends/auth.py | 48 ++ ecs/jskult-webapp/src/depends/database.py | 17 + ecs/jskult-webapp/src/depends/services.py | 16 + ecs/jskult-webapp/src/error/__init__.py | 0 .../src/error/exception_handler.py | 15 + ecs/jskult-webapp/src/error/exceptions.py | 33 + ecs/jskult-webapp/src/main.py | 40 + ecs/jskult-webapp/src/model/__init__.py | 0 ecs/jskult-webapp/src/model/db/__init__.py | 0 .../src/model/db/base_db_model.py | 5 + .../src/model/db/bio_sales_view.py | 74 ++ ecs/jskult-webapp/src/model/db/hdke_tbl.py | 8 + .../src/model/db/pharmacy_product_master.py | 5 + ecs/jskult-webapp/src/model/db/user_master.py | 36 + .../src/model/db/wholesaler_master.py | 8 + .../src/model/internal/jwt_token.py | 152 ++++ .../src/model/internal/session.py | 53 ++ .../src/model/request/__init__.py | 0 ecs/jskult-webapp/src/model/request/bio.py | 137 ++++ .../src/model/request/bio_download.py | 20 + ecs/jskult-webapp/src/model/request/login.py | 15 + ecs/jskult-webapp/src/model/view/__init__.py | 0 .../src/model/view/bio_disp_model.py | 19 + .../src/model/view/bio_view_model.py | 137 ++++ .../src/model/view/logout_view_model.py | 10 + .../src/model/view/mainte_login_view_model.py | 5 + .../src/model/view/menu_view_model.py | 26 + .../src/model/view/user_view_model.py | 26 + .../src/repositories/__init__.py | 0 .../src/repositories/base_repository.py | 43 + .../repositories/bio_sales_view_repository.py | 116 +++ .../src/repositories/hdke_tbl_repository.py | 18 + .../pharmacy_product_master_repository.py | 36 + .../repositories/user_master_repository.py | 27 + .../wholesaler_master_repository.py | 35 + ecs/jskult-webapp/src/router/__init__.py | 0 .../src/router/session_router.py | 125 +++ ecs/jskult-webapp/src/services/__init__.py | 0 .../src/services/base_service.py | 13 + .../src/services/batch_status_service.py | 41 + .../src/services/bio_view_service.py | 119 +++ .../src/services/login_service.py | 57 ++ .../src/services/session_service.py | 15 + ecs/jskult-webapp/src/static/__init__.py | 0 ecs/jskult-webapp/src/static/css/bioStyle.css | 291 +++++++ .../src/static/css/datepicker.css | 11 + .../src/static/css/menuStyle.css | 49 ++ .../src/static/css/pagenation.css | 65 ++ .../static/function/businessLogicScript.js | 213 +++++ .../src/static/img/icon_modal_confirm.png | Bin 0 -> 13691 bytes .../src/static/img/icon_modal_error.png | Bin 0 -> 17229 bytes .../src/static/lib/fixed_midashi.js | 165 ++++ ecs/jskult-webapp/src/system_var/__init__.py | 0 ecs/jskult-webapp/src/system_var/constants.py | 129 +++ .../src/system_var/environment.py | 22 + ecs/jskult-webapp/src/templates/__init__.py | 6 + ecs/jskult-webapp/src/templates/_header.html | 14 + ecs/jskult-webapp/src/templates/_modal.html | 40 + .../src/templates/bioSearchList.html | 462 +++++++++++ ecs/jskult-webapp/src/templates/logout.html | 75 ++ .../src/templates/maintlogin.html | 27 + ecs/jskult-webapp/src/templates/menu.html | 36 + ecs/jskult-webapp/src/util/sanitize.py | 14 + ecs/jskult-webapp/src/util/string_util.py | 10 + 92 files changed, 4941 insertions(+) create mode 100644 ecs/jskult-webapp/.dockerignore create mode 100644 ecs/jskult-webapp/.env.example create mode 100644 ecs/jskult-webapp/.gitignore create mode 100644 ecs/jskult-webapp/.vscode/launch.json create mode 100644 ecs/jskult-webapp/.vscode/recommended_settings.json create mode 100644 ecs/jskult-webapp/Dockerfile create mode 100644 ecs/jskult-webapp/Pipfile create mode 100644 ecs/jskult-webapp/Pipfile.lock create mode 100644 ecs/jskult-webapp/README.md create mode 100644 ecs/jskult-webapp/src/__init__.py create mode 100644 ecs/jskult-webapp/src/aws/__init__.py create mode 100644 ecs/jskult-webapp/src/aws/aws_api_client.py create mode 100644 ecs/jskult-webapp/src/aws/cognito.py create mode 100644 ecs/jskult-webapp/src/aws/s3.py create mode 100644 ecs/jskult-webapp/src/controller/__init__.py create mode 100644 ecs/jskult-webapp/src/controller/bio.py create mode 100644 ecs/jskult-webapp/src/controller/bio_download.py create mode 100644 ecs/jskult-webapp/src/controller/healthcheck.py create mode 100644 ecs/jskult-webapp/src/controller/login.py create mode 100644 ecs/jskult-webapp/src/controller/logout.py create mode 100644 ecs/jskult-webapp/src/controller/menu.py create mode 100644 ecs/jskult-webapp/src/core/__init__.py create mode 100644 ecs/jskult-webapp/src/core/tasks.py create mode 100644 ecs/jskult-webapp/src/db/__init__.py create mode 100644 ecs/jskult-webapp/src/db/database.py create mode 100644 ecs/jskult-webapp/src/db/sql_condition.py create mode 100644 ecs/jskult-webapp/src/db/tasks.py create mode 100644 ecs/jskult-webapp/src/depends/__init__.py create mode 100644 ecs/jskult-webapp/src/depends/auth.py create mode 100644 ecs/jskult-webapp/src/depends/database.py create mode 100644 ecs/jskult-webapp/src/depends/services.py create mode 100644 ecs/jskult-webapp/src/error/__init__.py create mode 100644 ecs/jskult-webapp/src/error/exception_handler.py create mode 100644 ecs/jskult-webapp/src/error/exceptions.py create mode 100644 ecs/jskult-webapp/src/main.py create mode 100644 ecs/jskult-webapp/src/model/__init__.py create mode 100644 ecs/jskult-webapp/src/model/db/__init__.py create mode 100644 ecs/jskult-webapp/src/model/db/base_db_model.py create mode 100644 ecs/jskult-webapp/src/model/db/bio_sales_view.py create mode 100644 ecs/jskult-webapp/src/model/db/hdke_tbl.py create mode 100644 ecs/jskult-webapp/src/model/db/pharmacy_product_master.py create mode 100644 ecs/jskult-webapp/src/model/db/user_master.py create mode 100644 ecs/jskult-webapp/src/model/db/wholesaler_master.py create mode 100644 ecs/jskult-webapp/src/model/internal/jwt_token.py create mode 100644 ecs/jskult-webapp/src/model/internal/session.py create mode 100644 ecs/jskult-webapp/src/model/request/__init__.py create mode 100644 ecs/jskult-webapp/src/model/request/bio.py create mode 100644 ecs/jskult-webapp/src/model/request/bio_download.py create mode 100644 ecs/jskult-webapp/src/model/request/login.py create mode 100644 ecs/jskult-webapp/src/model/view/__init__.py create mode 100644 ecs/jskult-webapp/src/model/view/bio_disp_model.py create mode 100644 ecs/jskult-webapp/src/model/view/bio_view_model.py create mode 100644 ecs/jskult-webapp/src/model/view/logout_view_model.py create mode 100644 ecs/jskult-webapp/src/model/view/mainte_login_view_model.py create mode 100644 ecs/jskult-webapp/src/model/view/menu_view_model.py create mode 100644 ecs/jskult-webapp/src/model/view/user_view_model.py create mode 100644 ecs/jskult-webapp/src/repositories/__init__.py create mode 100644 ecs/jskult-webapp/src/repositories/base_repository.py create mode 100644 ecs/jskult-webapp/src/repositories/bio_sales_view_repository.py create mode 100644 ecs/jskult-webapp/src/repositories/hdke_tbl_repository.py create mode 100644 ecs/jskult-webapp/src/repositories/pharmacy_product_master_repository.py create mode 100644 ecs/jskult-webapp/src/repositories/user_master_repository.py create mode 100644 ecs/jskult-webapp/src/repositories/wholesaler_master_repository.py create mode 100644 ecs/jskult-webapp/src/router/__init__.py create mode 100644 ecs/jskult-webapp/src/router/session_router.py create mode 100644 ecs/jskult-webapp/src/services/__init__.py create mode 100644 ecs/jskult-webapp/src/services/base_service.py create mode 100644 ecs/jskult-webapp/src/services/batch_status_service.py create mode 100644 ecs/jskult-webapp/src/services/bio_view_service.py create mode 100644 ecs/jskult-webapp/src/services/login_service.py create mode 100644 ecs/jskult-webapp/src/services/session_service.py create mode 100644 ecs/jskult-webapp/src/static/__init__.py create mode 100644 ecs/jskult-webapp/src/static/css/bioStyle.css create mode 100644 ecs/jskult-webapp/src/static/css/datepicker.css create mode 100644 ecs/jskult-webapp/src/static/css/menuStyle.css create mode 100644 ecs/jskult-webapp/src/static/css/pagenation.css create mode 100644 ecs/jskult-webapp/src/static/function/businessLogicScript.js create mode 100644 ecs/jskult-webapp/src/static/img/icon_modal_confirm.png create mode 100644 ecs/jskult-webapp/src/static/img/icon_modal_error.png create mode 100644 ecs/jskult-webapp/src/static/lib/fixed_midashi.js create mode 100644 ecs/jskult-webapp/src/system_var/__init__.py create mode 100644 ecs/jskult-webapp/src/system_var/constants.py create mode 100644 ecs/jskult-webapp/src/system_var/environment.py create mode 100644 ecs/jskult-webapp/src/templates/__init__.py create mode 100644 ecs/jskult-webapp/src/templates/_header.html create mode 100644 ecs/jskult-webapp/src/templates/_modal.html create mode 100644 ecs/jskult-webapp/src/templates/bioSearchList.html create mode 100644 ecs/jskult-webapp/src/templates/logout.html create mode 100644 ecs/jskult-webapp/src/templates/maintlogin.html create mode 100644 ecs/jskult-webapp/src/templates/menu.html create mode 100644 ecs/jskult-webapp/src/util/sanitize.py create mode 100644 ecs/jskult-webapp/src/util/string_util.py diff --git a/ecs/jskult-webapp/.dockerignore b/ecs/jskult-webapp/.dockerignore new file mode 100644 index 00000000..8b9da402 --- /dev/null +++ b/ecs/jskult-webapp/.dockerignore @@ -0,0 +1,12 @@ +tests/* +.coverage +.env +.env.example +.report/* +.vscode/* +.pytest_cache/* +*/__pychache__/* +Dockerfile +pytest.ini +README.md +*.sql diff --git a/ecs/jskult-webapp/.env.example b/ecs/jskult-webapp/.env.example new file mode 100644 index 00000000..73f72119 --- /dev/null +++ b/ecs/jskult-webapp/.env.example @@ -0,0 +1,24 @@ +#AWS +##Cognito +COGNITO_AUTH_DOMAIN=******* +AUTHORIZE_ENDPOINT=********* +TOKEN_ENDPOINT=*************** +COGNITO_IDENTITY_PROVIDER=********* +COGNITO_REDIRECT_URI=***************** +COGNITO_USER_POOL_ID=******************** +COGNITO_CLIENT_ID=********************** +COGNITO_CLIENT_SECRET=****************************** +##DynamoDB +AWS_REGION=************** +SESSION_TABLE_NAME=*********************** +##S3 +BIO_ACCESS_LOG_BUCKET=******************* +#MySQL +DB_HOST=************ +DB_PORT=************ +DB_USERNAME=************ +DB_PASSWORD=************ +DB_SCHEMA=************ +#実装の設定 +BIO_SEARCH_RESULT_MAX_COUNT=35000 +SESSION_EXPIRE_MINUTE=20 \ No newline at end of file diff --git a/ecs/jskult-webapp/.gitignore b/ecs/jskult-webapp/.gitignore new file mode 100644 index 00000000..547e4960 --- /dev/null +++ b/ecs/jskult-webapp/.gitignore @@ -0,0 +1,6 @@ +__pycache__ + +.vscode/settings.json +.env +!src/data/BioData_template.xlsx +src/data/* diff --git a/ecs/jskult-webapp/.vscode/launch.json b/ecs/jskult-webapp/.vscode/launch.json new file mode 100644 index 00000000..a3a5a0b8 --- /dev/null +++ b/ecs/jskult-webapp/.vscode/launch.json @@ -0,0 +1,17 @@ +{ + // IntelliSense を使用して利用可能な属性を学べます。 + // 既存の属性の説明をホバーして表示します。 + // 詳細情報は次を確認してください: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "Launch AP Server(FastAPI)", + "type": "python", + "request": "launch", + "module": "uvicorn", + "args": ["src.main:app","--reload", "--no-server-header"], + "justMyCode": true, + "envFile": "${workspaceFolder}/.env" + } + ] +} \ No newline at end of file diff --git a/ecs/jskult-webapp/.vscode/recommended_settings.json b/ecs/jskult-webapp/.vscode/recommended_settings.json new file mode 100644 index 00000000..d5ce3e07 --- /dev/null +++ b/ecs/jskult-webapp/.vscode/recommended_settings.json @@ -0,0 +1,18 @@ +{ + "[python]": { + "editor.defaultFormatter": null, + "editor.formatOnSave": true, + "editor.codeActionsOnSave": { + "source.organizeImports": true + } + }, + // 自身の環境に合わせて変えてください + "python.defaultInterpreterPath": "", + "python.linting.lintOnSave": true, + "python.linting.enabled": true, + "python.linting.pylintEnabled": false, + "python.linting.flake8Enabled": true, + "python.linting.flake8Args": ["--max-line-length=120"], + "python.formatting.provider": "autopep8", + "python.formatting.autopep8Args": ["--max-line-length", "120"] +} diff --git a/ecs/jskult-webapp/Dockerfile b/ecs/jskult-webapp/Dockerfile new file mode 100644 index 00000000..ef14f718 --- /dev/null +++ b/ecs/jskult-webapp/Dockerfile @@ -0,0 +1,19 @@ +FROM python:3.9 + +ENV TZ="Asia/Tokyo" + +WORKDIR /usr/src/app +COPY Pipfile Pipfile.lock ./ +RUN \ + apt update -y && \ + # パッケージのセキュリティアップデートのみを適用するコマンド + apt install -y unattended-upgrades && \ + unattended-upgrades && \ + pip install --upgrade pip wheel setuptools && \ + pip install pipenv --no-cache-dir && \ + pipenv install --system --deploy && \ + pip uninstall -y pipenv virtualenv-clone virtualenv + +COPY src ./src + +CMD ["gunicorn", "src.main:app", "-w", "4", "-k" ,"uvicorn.workers.UvicornWorker", "-b", "0.0.0.0:80"] diff --git a/ecs/jskult-webapp/Pipfile b/ecs/jskult-webapp/Pipfile new file mode 100644 index 00000000..7512a1dc --- /dev/null +++ b/ecs/jskult-webapp/Pipfile @@ -0,0 +1,34 @@ +[[source]] +url = "https://pypi.org/simple" +verify_ssl = true +name = "pypi" + +[scripts] +app = "uvicorn src.main:app --reload --no-server-header" + +[packages] +fastapi = "*" +uvicorn = "*" +gunicorn = "*" +boto3 = "*" +jinja2 = "*" +pyjwt = "*" +"pyjwt[crypto]" = "*" +requests = "*" +python-multipart = "*" +pynamodb = "*" +PyMySQL = "*" +pandas = "*" +openpyxl = "*" +xlrd = "*" +sqlalchemy = "*" + +[dev-packages] +autopep8 = "*" +flake8 = "*" + +[requires] +python_version = "3.9" + +[pipenv] +allow_prereleases = true diff --git a/ecs/jskult-webapp/Pipfile.lock b/ecs/jskult-webapp/Pipfile.lock new file mode 100644 index 00000000..85633582 --- /dev/null +++ b/ecs/jskult-webapp/Pipfile.lock @@ -0,0 +1,765 @@ +{ + "_meta": { + "hash": { + "sha256": "d78a6bf1a96aa14c45431185961cae6d54ca1da8ea0319e1976bad4c2bebd673" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.9" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "anyio": { + "hashes": [ + "sha256:25ea0d673ae30af41a0c442f81cf3b38c7e79fdc7b60335a4c14e05eb0947421", + "sha256:fbbe32bd270d2a2ef3ed1c5d45041250284e31fc0a4df4a5a6071842051a51e3" + ], + "markers": "python_full_version >= '3.6.2'", + "version": "==3.6.2" + }, + "boto3": { + "hashes": [ + "sha256:278d896e9090a976f41ec68da5c572bc4e5b7cb1e515f1898fee8cb2fadfb50d", + "sha256:3ce2225a61832d69831d669d912424ea3863268ca1cfa2a82203bb90952acefa" + ], + "index": "pypi", + "version": "==1.26.91" + }, + "botocore": { + "hashes": [ + "sha256:4ed6a488aee1b42367eace71f7d0993dda05b02eebd7dcdd78db5c9ce3d80da5", + "sha256:a8a800a2a945da807758cace539fc5b5ec1d5082ce363799d3a3870c2c4ed6fc" + ], + "markers": "python_version >= '3.7'", + "version": "==1.29.91" + }, + "certifi": { + "hashes": [ + "sha256:35824b4c3a97115964b408844d64aa14db1cc518f6562e8d7261699d1350a9e3", + "sha256:4ad3232f5e926d6718ec31cfc1fcadfde020920e278684144551c91769c7bc18" + ], + "markers": "python_version >= '3.6'", + "version": "==2022.12.7" + }, + "cffi": { + "hashes": [ + "sha256:00a9ed42e88df81ffae7a8ab6d9356b371399b91dbdf0c3cb1e84c03a13aceb5", + "sha256:03425bdae262c76aad70202debd780501fabeaca237cdfddc008987c0e0f59ef", + "sha256:04ed324bda3cda42b9b695d51bb7d54b680b9719cfab04227cdd1e04e5de3104", + "sha256:0e2642fe3142e4cc4af0799748233ad6da94c62a8bec3a6648bf8ee68b1c7426", + "sha256:173379135477dc8cac4bc58f45db08ab45d228b3363adb7af79436135d028405", + "sha256:198caafb44239b60e252492445da556afafc7d1e3ab7a1fb3f0584ef6d742375", + "sha256:1e74c6b51a9ed6589199c787bf5f9875612ca4a8a0785fb2d4a84429badaf22a", + "sha256:2012c72d854c2d03e45d06ae57f40d78e5770d252f195b93f581acf3ba44496e", + "sha256:21157295583fe8943475029ed5abdcf71eb3911894724e360acff1d61c1d54bc", + "sha256:2470043b93ff09bf8fb1d46d1cb756ce6132c54826661a32d4e4d132e1977adf", + "sha256:285d29981935eb726a4399badae8f0ffdff4f5050eaa6d0cfc3f64b857b77185", + "sha256:30d78fbc8ebf9c92c9b7823ee18eb92f2e6ef79b45ac84db507f52fbe3ec4497", + "sha256:320dab6e7cb2eacdf0e658569d2575c4dad258c0fcc794f46215e1e39f90f2c3", + "sha256:33ab79603146aace82c2427da5ca6e58f2b3f2fb5da893ceac0c42218a40be35", + "sha256:3548db281cd7d2561c9ad9984681c95f7b0e38881201e157833a2342c30d5e8c", + "sha256:3799aecf2e17cf585d977b780ce79ff0dc9b78d799fc694221ce814c2c19db83", + "sha256:39d39875251ca8f612b6f33e6b1195af86d1b3e60086068be9cc053aa4376e21", + "sha256:3b926aa83d1edb5aa5b427b4053dc420ec295a08e40911296b9eb1b6170f6cca", + "sha256:3bcde07039e586f91b45c88f8583ea7cf7a0770df3a1649627bf598332cb6984", + "sha256:3d08afd128ddaa624a48cf2b859afef385b720bb4b43df214f85616922e6a5ac", + "sha256:3eb6971dcff08619f8d91607cfc726518b6fa2a9eba42856be181c6d0d9515fd", + "sha256:40f4774f5a9d4f5e344f31a32b5096977b5d48560c5592e2f3d2c4374bd543ee", + "sha256:4289fc34b2f5316fbb762d75362931e351941fa95fa18789191b33fc4cf9504a", + "sha256:470c103ae716238bbe698d67ad020e1db9d9dba34fa5a899b5e21577e6d52ed2", + "sha256:4f2c9f67e9821cad2e5f480bc8d83b8742896f1242dba247911072d4fa94c192", + "sha256:50a74364d85fd319352182ef59c5c790484a336f6db772c1a9231f1c3ed0cbd7", + "sha256:54a2db7b78338edd780e7ef7f9f6c442500fb0d41a5a4ea24fff1c929d5af585", + "sha256:5635bd9cb9731e6d4a1132a498dd34f764034a8ce60cef4f5319c0541159392f", + "sha256:59c0b02d0a6c384d453fece7566d1c7e6b7bae4fc5874ef2ef46d56776d61c9e", + "sha256:5d598b938678ebf3c67377cdd45e09d431369c3b1a5b331058c338e201f12b27", + "sha256:5df2768244d19ab7f60546d0c7c63ce1581f7af8b5de3eb3004b9b6fc8a9f84b", + "sha256:5ef34d190326c3b1f822a5b7a45f6c4535e2f47ed06fec77d3d799c450b2651e", + "sha256:6975a3fac6bc83c4a65c9f9fcab9e47019a11d3d2cf7f3c0d03431bf145a941e", + "sha256:6c9a799e985904922a4d207a94eae35c78ebae90e128f0c4e521ce339396be9d", + "sha256:70df4e3b545a17496c9b3f41f5115e69a4f2e77e94e1d2a8e1070bc0c38c8a3c", + "sha256:7473e861101c9e72452f9bf8acb984947aa1661a7704553a9f6e4baa5ba64415", + "sha256:8102eaf27e1e448db915d08afa8b41d6c7ca7a04b7d73af6514df10a3e74bd82", + "sha256:87c450779d0914f2861b8526e035c5e6da0a3199d8f1add1a665e1cbc6fc6d02", + "sha256:8b7ee99e510d7b66cdb6c593f21c043c248537a32e0bedf02e01e9553a172314", + "sha256:91fc98adde3d7881af9b59ed0294046f3806221863722ba7d8d120c575314325", + "sha256:94411f22c3985acaec6f83c6df553f2dbe17b698cc7f8ae751ff2237d96b9e3c", + "sha256:98d85c6a2bef81588d9227dde12db8a7f47f639f4a17c9ae08e773aa9c697bf3", + "sha256:9ad5db27f9cabae298d151c85cf2bad1d359a1b9c686a275df03385758e2f914", + "sha256:a0b71b1b8fbf2b96e41c4d990244165e2c9be83d54962a9a1d118fd8657d2045", + "sha256:a0f100c8912c114ff53e1202d0078b425bee3649ae34d7b070e9697f93c5d52d", + "sha256:a591fe9e525846e4d154205572a029f653ada1a78b93697f3b5a8f1f2bc055b9", + "sha256:a5c84c68147988265e60416b57fc83425a78058853509c1b0629c180094904a5", + "sha256:a66d3508133af6e8548451b25058d5812812ec3798c886bf38ed24a98216fab2", + "sha256:a8c4917bd7ad33e8eb21e9a5bbba979b49d9a97acb3a803092cbc1133e20343c", + "sha256:b3bbeb01c2b273cca1e1e0c5df57f12dce9a4dd331b4fa1635b8bec26350bde3", + "sha256:cba9d6b9a7d64d4bd46167096fc9d2f835e25d7e4c121fb2ddfc6528fb0413b2", + "sha256:cc4d65aeeaa04136a12677d3dd0b1c0c94dc43abac5860ab33cceb42b801c1e8", + "sha256:ce4bcc037df4fc5e3d184794f27bdaab018943698f4ca31630bc7f84a7b69c6d", + "sha256:cec7d9412a9102bdc577382c3929b337320c4c4c4849f2c5cdd14d7368c5562d", + "sha256:d400bfb9a37b1351253cb402671cea7e89bdecc294e8016a707f6d1d8ac934f9", + "sha256:d61f4695e6c866a23a21acab0509af1cdfd2c013cf256bbf5b6b5e2695827162", + "sha256:db0fbb9c62743ce59a9ff687eb5f4afbe77e5e8403d6697f7446e5f609976f76", + "sha256:dd86c085fae2efd48ac91dd7ccffcfc0571387fe1193d33b6394db7ef31fe2a4", + "sha256:e00b098126fd45523dd056d2efba6c5a63b71ffe9f2bbe1a4fe1716e1d0c331e", + "sha256:e229a521186c75c8ad9490854fd8bbdd9a0c9aa3a524326b55be83b54d4e0ad9", + "sha256:e263d77ee3dd201c3a142934a086a4450861778baaeeb45db4591ef65550b0a6", + "sha256:ed9cb427ba5504c1dc15ede7d516b84757c3e3d7868ccc85121d9310d27eed0b", + "sha256:fa6693661a4c91757f4412306191b6dc88c1703f780c8234035eac011922bc01", + "sha256:fcd131dd944808b5bdb38e6f5b53013c5aa4f334c5cad0c72742f6eba4b73db0" + ], + "version": "==1.15.1" + }, + "charset-normalizer": { + "hashes": [ + "sha256:04afa6387e2b282cf78ff3dbce20f0cc071c12dc8f685bd40960cc68644cfea6", + "sha256:04eefcee095f58eaabe6dc3cc2262f3bcd776d2c67005880894f447b3f2cb9c1", + "sha256:0be65ccf618c1e7ac9b849c315cc2e8a8751d9cfdaa43027d4f6624bd587ab7e", + "sha256:0c95f12b74681e9ae127728f7e5409cbbef9cd914d5896ef238cc779b8152373", + "sha256:0ca564606d2caafb0abe6d1b5311c2649e8071eb241b2d64e75a0d0065107e62", + "sha256:10c93628d7497c81686e8e5e557aafa78f230cd9e77dd0c40032ef90c18f2230", + "sha256:11d117e6c63e8f495412d37e7dc2e2fff09c34b2d09dbe2bee3c6229577818be", + "sha256:11d3bcb7be35e7b1bba2c23beedac81ee893ac9871d0ba79effc7fc01167db6c", + "sha256:12a2b561af122e3d94cdb97fe6fb2bb2b82cef0cdca131646fdb940a1eda04f0", + "sha256:12d1a39aa6b8c6f6248bb54550efcc1c38ce0d8096a146638fd4738e42284448", + "sha256:1435ae15108b1cb6fffbcea2af3d468683b7afed0169ad718451f8db5d1aff6f", + "sha256:1c60b9c202d00052183c9be85e5eaf18a4ada0a47d188a83c8f5c5b23252f649", + "sha256:1e8fcdd8f672a1c4fc8d0bd3a2b576b152d2a349782d1eb0f6b8e52e9954731d", + "sha256:20064ead0717cf9a73a6d1e779b23d149b53daf971169289ed2ed43a71e8d3b0", + "sha256:21fa558996782fc226b529fdd2ed7866c2c6ec91cee82735c98a197fae39f706", + "sha256:22908891a380d50738e1f978667536f6c6b526a2064156203d418f4856d6e86a", + "sha256:3160a0fd9754aab7d47f95a6b63ab355388d890163eb03b2d2b87ab0a30cfa59", + "sha256:322102cdf1ab682ecc7d9b1c5eed4ec59657a65e1c146a0da342b78f4112db23", + "sha256:34e0a2f9c370eb95597aae63bf85eb5e96826d81e3dcf88b8886012906f509b5", + "sha256:3573d376454d956553c356df45bb824262c397c6e26ce43e8203c4c540ee0acb", + "sha256:3747443b6a904001473370d7810aa19c3a180ccd52a7157aacc264a5ac79265e", + "sha256:38e812a197bf8e71a59fe55b757a84c1f946d0ac114acafaafaf21667a7e169e", + "sha256:3a06f32c9634a8705f4ca9946d667609f52cf130d5548881401f1eb2c39b1e2c", + "sha256:3a5fc78f9e3f501a1614a98f7c54d3969f3ad9bba8ba3d9b438c3bc5d047dd28", + "sha256:3d9098b479e78c85080c98e1e35ff40b4a31d8953102bb0fd7d1b6f8a2111a3d", + "sha256:3dc5b6a8ecfdc5748a7e429782598e4f17ef378e3e272eeb1340ea57c9109f41", + "sha256:4155b51ae05ed47199dc5b2a4e62abccb274cee6b01da5b895099b61b1982974", + "sha256:49919f8400b5e49e961f320c735388ee686a62327e773fa5b3ce6721f7e785ce", + "sha256:53d0a3fa5f8af98a1e261de6a3943ca631c526635eb5817a87a59d9a57ebf48f", + "sha256:5f008525e02908b20e04707a4f704cd286d94718f48bb33edddc7d7b584dddc1", + "sha256:628c985afb2c7d27a4800bfb609e03985aaecb42f955049957814e0491d4006d", + "sha256:65ed923f84a6844de5fd29726b888e58c62820e0769b76565480e1fdc3d062f8", + "sha256:6734e606355834f13445b6adc38b53c0fd45f1a56a9ba06c2058f86893ae8017", + "sha256:6baf0baf0d5d265fa7944feb9f7451cc316bfe30e8df1a61b1bb08577c554f31", + "sha256:6f4f4668e1831850ebcc2fd0b1cd11721947b6dc7c00bf1c6bd3c929ae14f2c7", + "sha256:6f5c2e7bc8a4bf7c426599765b1bd33217ec84023033672c1e9a8b35eaeaaaf8", + "sha256:6f6c7a8a57e9405cad7485f4c9d3172ae486cfef1344b5ddd8e5239582d7355e", + "sha256:7381c66e0561c5757ffe616af869b916c8b4e42b367ab29fedc98481d1e74e14", + "sha256:73dc03a6a7e30b7edc5b01b601e53e7fc924b04e1835e8e407c12c037e81adbd", + "sha256:74db0052d985cf37fa111828d0dd230776ac99c740e1a758ad99094be4f1803d", + "sha256:75f2568b4189dda1c567339b48cba4ac7384accb9c2a7ed655cd86b04055c795", + "sha256:78cacd03e79d009d95635e7d6ff12c21eb89b894c354bd2b2ed0b4763373693b", + "sha256:80d1543d58bd3d6c271b66abf454d437a438dff01c3e62fdbcd68f2a11310d4b", + "sha256:830d2948a5ec37c386d3170c483063798d7879037492540f10a475e3fd6f244b", + "sha256:891cf9b48776b5c61c700b55a598621fdb7b1e301a550365571e9624f270c203", + "sha256:8f25e17ab3039b05f762b0a55ae0b3632b2e073d9c8fc88e89aca31a6198e88f", + "sha256:9a3267620866c9d17b959a84dd0bd2d45719b817245e49371ead79ed4f710d19", + "sha256:a04f86f41a8916fe45ac5024ec477f41f886b3c435da2d4e3d2709b22ab02af1", + "sha256:aaf53a6cebad0eae578f062c7d462155eada9c172bd8c4d250b8c1d8eb7f916a", + "sha256:abc1185d79f47c0a7aaf7e2412a0eb2c03b724581139193d2d82b3ad8cbb00ac", + "sha256:ac0aa6cd53ab9a31d397f8303f92c42f534693528fafbdb997c82bae6e477ad9", + "sha256:ac3775e3311661d4adace3697a52ac0bab17edd166087d493b52d4f4f553f9f0", + "sha256:b06f0d3bf045158d2fb8837c5785fe9ff9b8c93358be64461a1089f5da983137", + "sha256:b116502087ce8a6b7a5f1814568ccbd0e9f6cfd99948aa59b0e241dc57cf739f", + "sha256:b82fab78e0b1329e183a65260581de4375f619167478dddab510c6c6fb04d9b6", + "sha256:bd7163182133c0c7701b25e604cf1611c0d87712e56e88e7ee5d72deab3e76b5", + "sha256:c36bcbc0d5174a80d6cccf43a0ecaca44e81d25be4b7f90f0ed7bcfbb5a00909", + "sha256:c3af8e0f07399d3176b179f2e2634c3ce9c1301379a6b8c9c9aeecd481da494f", + "sha256:c84132a54c750fda57729d1e2599bb598f5fa0344085dbde5003ba429a4798c0", + "sha256:cb7b2ab0188829593b9de646545175547a70d9a6e2b63bf2cd87a0a391599324", + "sha256:cca4def576f47a09a943666b8f829606bcb17e2bc2d5911a46c8f8da45f56755", + "sha256:cf6511efa4801b9b38dc5546d7547d5b5c6ef4b081c60b23e4d941d0eba9cbeb", + "sha256:d16fd5252f883eb074ca55cb622bc0bee49b979ae4e8639fff6ca3ff44f9f854", + "sha256:d2686f91611f9e17f4548dbf050e75b079bbc2a82be565832bc8ea9047b61c8c", + "sha256:d7fc3fca01da18fbabe4625d64bb612b533533ed10045a2ac3dd194bfa656b60", + "sha256:dd5653e67b149503c68c4018bf07e42eeed6b4e956b24c00ccdf93ac79cdff84", + "sha256:de5695a6f1d8340b12a5d6d4484290ee74d61e467c39ff03b39e30df62cf83a0", + "sha256:e0ac8959c929593fee38da1c2b64ee9778733cdf03c482c9ff1d508b6b593b2b", + "sha256:e1b25e3ad6c909f398df8921780d6a3d120d8c09466720226fc621605b6f92b1", + "sha256:e633940f28c1e913615fd624fcdd72fdba807bf53ea6925d6a588e84e1151531", + "sha256:e89df2958e5159b811af9ff0f92614dabf4ff617c03a4c1c6ff53bf1c399e0e1", + "sha256:ea9f9c6034ea2d93d9147818f17c2a0860d41b71c38b9ce4d55f21b6f9165a11", + "sha256:f645caaf0008bacf349875a974220f1f1da349c5dbe7c4ec93048cdc785a3326", + "sha256:f8303414c7b03f794347ad062c0516cee0e15f7a612abd0ce1e25caf6ceb47df", + "sha256:fca62a8301b605b954ad2e9c3666f9d97f63872aa4efcae5492baca2056b74ab" + ], + "markers": "python_version >= '3.7'", + "version": "==3.1.0" + }, + "click": { + "hashes": [ + "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e", + "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48" + ], + "markers": "python_version >= '3.7'", + "version": "==8.1.3" + }, + "cryptography": { + "hashes": [ + "sha256:103e8f7155f3ce2ffa0049fe60169878d47a4364b277906386f8de21c9234aa1", + "sha256:23df8ca3f24699167daf3e23e51f7ba7334d504af63a94af468f468b975b7dd7", + "sha256:2725672bb53bb92dc7b4150d233cd4b8c59615cd8288d495eaa86db00d4e5c06", + "sha256:30b1d1bfd00f6fc80d11300a29f1d8ab2b8d9febb6ed4a38a76880ec564fae84", + "sha256:35d658536b0a4117c885728d1a7032bdc9a5974722ae298d6c533755a6ee3915", + "sha256:50cadb9b2f961757e712a9737ef33d89b8190c3ea34d0fb6675e00edbe35d074", + "sha256:5f8c682e736513db7d04349b4f6693690170f95aac449c56f97415c6980edef5", + "sha256:6236a9610c912b129610eb1a274bdc1350b5df834d124fa84729ebeaf7da42c3", + "sha256:788b3921d763ee35dfdb04248d0e3de11e3ca8eb22e2e48fef880c42e1f3c8f9", + "sha256:8bc0008ef798231fac03fe7d26e82d601d15bd16f3afaad1c6113771566570f3", + "sha256:8f35c17bd4faed2bc7797d2a66cbb4f986242ce2e30340ab832e5d99ae60e011", + "sha256:b49a88ff802e1993b7f749b1eeb31134f03c8d5c956e3c125c75558955cda536", + "sha256:bc0521cce2c1d541634b19f3ac661d7a64f9555135e9d8af3980965be717fd4a", + "sha256:bc5b871e977c8ee5a1bbc42fa8d19bcc08baf0c51cbf1586b0e87a2694dde42f", + "sha256:c43ac224aabcbf83a947eeb8b17eaf1547bce3767ee2d70093b461f31729a480", + "sha256:d15809e0dbdad486f4ad0979753518f47980020b7a34e9fc56e8be4f60702fac", + "sha256:d7d84a512a59f4412ca8549b01f94be4161c94efc598bf09d027d67826beddc0", + "sha256:e029b844c21116564b8b61216befabca4b500e6816fa9f0ba49527653cae2108", + "sha256:e8a0772016feeb106efd28d4a328e77dc2edae84dfbac06061319fdb669ff828", + "sha256:e944fe07b6f229f4c1a06a7ef906a19652bdd9fd54c761b0ff87e83ae7a30354", + "sha256:eb40fe69cfc6f5cdab9a5ebd022131ba21453cf7b8a7fd3631f45bbf52bed612", + "sha256:fa507318e427169ade4e9eccef39e9011cdc19534f55ca2f36ec3f388c1f70f3", + "sha256:ffd394c7896ed7821a6d13b24657c6a34b6e2650bd84ae063cf11ccffa4f1a97" + ], + "version": "==39.0.2" + }, + "et-xmlfile": { + "hashes": [ + "sha256:8eb9e2bc2f8c97e37a2dc85a09ecdcdec9d8a396530a6d5a33b30b9a92da0c5c", + "sha256:a2ba85d1d6a74ef63837eed693bcb89c3f752169b0e3e7ae5b16ca5e1b3deada" + ], + "markers": "python_version >= '3.6'", + "version": "==1.1.0" + }, + "fastapi": { + "hashes": [ + "sha256:451387550c2d25a972193f22e408a82e75a8e7867c834a03076704fe20df3256", + "sha256:4a75936dbf9eb74be5eb0d41a793adefe9f3fc6ba66dbdabd160120fd3c2d9cd" + ], + "index": "pypi", + "version": "==0.94.1" + }, + "greenlet": { + "hashes": [ + "sha256:03a8f4f3430c3b3ff8d10a2a86028c660355ab637cee9333d63d66b56f09d52a", + "sha256:0bf60faf0bc2468089bdc5edd10555bab6e85152191df713e2ab1fcc86382b5a", + "sha256:18a7f18b82b52ee85322d7a7874e676f34ab319b9f8cce5de06067384aa8ff43", + "sha256:18e98fb3de7dba1c0a852731c3070cf022d14f0d68b4c87a19cc1016f3bb8b33", + "sha256:1a819eef4b0e0b96bb0d98d797bef17dc1b4a10e8d7446be32d1da33e095dbb8", + "sha256:26fbfce90728d82bc9e6c38ea4d038cba20b7faf8a0ca53a9c07b67318d46088", + "sha256:2780572ec463d44c1d3ae850239508dbeb9fed38e294c68d19a24d925d9223ca", + "sha256:283737e0da3f08bd637b5ad058507e578dd462db259f7f6e4c5c365ba4ee9343", + "sha256:2d4686f195e32d36b4d7cf2d166857dbd0ee9f3d20ae349b6bf8afc8485b3645", + "sha256:2dd11f291565a81d71dab10b7033395b7a3a5456e637cf997a6f33ebdf06f8db", + "sha256:30bcf80dda7f15ac77ba5af2b961bdd9dbc77fd4ac6105cee85b0d0a5fcf74df", + "sha256:32e5b64b148966d9cccc2c8d35a671409e45f195864560829f395a54226408d3", + "sha256:36abbf031e1c0f79dd5d596bfaf8e921c41df2bdf54ee1eed921ce1f52999a86", + "sha256:3a06ad5312349fec0ab944664b01d26f8d1f05009566339ac6f63f56589bc1a2", + "sha256:3a51c9751078733d88e013587b108f1b7a1fb106d402fb390740f002b6f6551a", + "sha256:3c9b12575734155d0c09d6c3e10dbd81665d5c18e1a7c6597df72fd05990c8cf", + "sha256:3f6ea9bd35eb450837a3d80e77b517ea5bc56b4647f5502cd28de13675ee12f7", + "sha256:4b58adb399c4d61d912c4c331984d60eb66565175cdf4a34792cd9600f21b394", + "sha256:4d2e11331fc0c02b6e84b0d28ece3a36e0548ee1a1ce9ddde03752d9b79bba40", + "sha256:5454276c07d27a740c5892f4907c86327b632127dd9abec42ee62e12427ff7e3", + "sha256:561091a7be172ab497a3527602d467e2b3fbe75f9e783d8b8ce403fa414f71a6", + "sha256:6c3acb79b0bfd4fe733dff8bc62695283b57949ebcca05ae5c129eb606ff2d74", + "sha256:703f18f3fda276b9a916f0934d2fb6d989bf0b4fb5a64825260eb9bfd52d78f0", + "sha256:7492e2b7bd7c9b9916388d9df23fa49d9b88ac0640db0a5b4ecc2b653bf451e3", + "sha256:76ae285c8104046b3a7f06b42f29c7b73f77683df18c49ab5af7983994c2dd91", + "sha256:7cafd1208fdbe93b67c7086876f061f660cfddc44f404279c1585bbf3cdc64c5", + "sha256:7efde645ca1cc441d6dc4b48c0f7101e8d86b54c8530141b09fd31cef5149ec9", + "sha256:88d9ab96491d38a5ab7c56dd7a3cc37d83336ecc564e4e8816dbed12e5aaefc8", + "sha256:8eab883b3b2a38cc1e050819ef06a7e6344d4a990d24d45bc6f2cf959045a45b", + "sha256:910841381caba4f744a44bf81bfd573c94e10b3045ee00de0cbf436fe50673a6", + "sha256:9190f09060ea4debddd24665d6804b995a9c122ef5917ab26e1566dcc712ceeb", + "sha256:937e9020b514ceedb9c830c55d5c9872abc90f4b5862f89c0887033ae33c6f73", + "sha256:94c817e84245513926588caf1152e3b559ff794d505555211ca041f032abbb6b", + "sha256:971ce5e14dc5e73715755d0ca2975ac88cfdaefcaab078a284fea6cfabf866df", + "sha256:9d14b83fab60d5e8abe587d51c75b252bcc21683f24699ada8fb275d7712f5a9", + "sha256:9f35ec95538f50292f6d8f2c9c9f8a3c6540bbfec21c9e5b4b751e0a7c20864f", + "sha256:a1846f1b999e78e13837c93c778dcfc3365902cfb8d1bdb7dd73ead37059f0d0", + "sha256:acd2162a36d3de67ee896c43effcd5ee3de247eb00354db411feb025aa319857", + "sha256:b0ef99cdbe2b682b9ccbb964743a6aca37905fda5e0452e5ee239b1654d37f2a", + "sha256:b80f600eddddce72320dbbc8e3784d16bd3fb7b517e82476d8da921f27d4b249", + "sha256:b864ba53912b6c3ab6bcb2beb19f19edd01a6bfcbdfe1f37ddd1778abfe75a30", + "sha256:b9ec052b06a0524f0e35bd8790686a1da006bd911dd1ef7d50b77bfbad74e292", + "sha256:ba2956617f1c42598a308a84c6cf021a90ff3862eddafd20c3333d50f0edb45b", + "sha256:bdfea8c661e80d3c1c99ad7c3ff74e6e87184895bbaca6ee8cc61209f8b9b85d", + "sha256:be4ed120b52ae4d974aa40215fcdfde9194d63541c7ded40ee12eb4dda57b76b", + "sha256:c4302695ad8027363e96311df24ee28978162cdcdd2006476c43970b384a244c", + "sha256:c48f54ef8e05f04d6eff74b8233f6063cb1ed960243eacc474ee73a2ea8573ca", + "sha256:c9c59a2120b55788e800d82dfa99b9e156ff8f2227f07c5e3012a45a399620b7", + "sha256:cd021c754b162c0fb55ad5d6b9d960db667faad0fa2ff25bb6e1301b0b6e6a75", + "sha256:d27ec7509b9c18b6d73f2f5ede2622441de812e7b1a80bbd446cb0633bd3d5ae", + "sha256:d5508f0b173e6aa47273bdc0a0b5ba055b59662ba7c7ee5119528f466585526b", + "sha256:d75209eed723105f9596807495d58d10b3470fa6732dd6756595e89925ce2470", + "sha256:db1a39669102a1d8d12b57de2bb7e2ec9066a6f2b3da35ae511ff93b01b5d564", + "sha256:dbfcfc0218093a19c252ca8eb9aee3d29cfdcb586df21049b9d777fd32c14fd9", + "sha256:e0f72c9ddb8cd28532185f54cc1453f2c16fb417a08b53a855c4e6a418edd099", + "sha256:e7c8dc13af7db097bed64a051d2dd49e9f0af495c26995c00a9ee842690d34c0", + "sha256:ea9872c80c132f4663822dd2a08d404073a5a9b5ba6155bea72fb2a79d1093b5", + "sha256:eff4eb9b7eb3e4d0cae3d28c283dc16d9bed6b193c2e1ace3ed86ce48ea8df19", + "sha256:f82d4d717d8ef19188687aa32b8363e96062911e63ba22a0cff7802a8e58e5f1", + "sha256:fc3a569657468b6f3fb60587e48356fe512c1754ca05a564f11366ac9e306526" + ], + "markers": "platform_machine == 'aarch64' or (platform_machine == 'ppc64le' or (platform_machine == 'x86_64' or (platform_machine == 'amd64' or (platform_machine == 'AMD64' or (platform_machine == 'win32' or platform_machine == 'WIN32')))))", + "version": "==2.0.2" + }, + "gunicorn": { + "hashes": [ + "sha256:9dcc4547dbb1cb284accfb15ab5667a0e5d1881cc443e0677b4882a4067a807e", + "sha256:e0a968b5ba15f8a328fdfd7ab1fcb5af4470c28aaf7e55df02a99bc13138e6e8" + ], + "index": "pypi", + "version": "==20.1.0" + }, + "h11": { + "hashes": [ + "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d", + "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761" + ], + "markers": "python_version >= '3.7'", + "version": "==0.14.0" + }, + "idna": { + "hashes": [ + "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4", + "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2" + ], + "markers": "python_version >= '3.5'", + "version": "==3.4" + }, + "jinja2": { + "hashes": [ + "sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852", + "sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61" + ], + "index": "pypi", + "version": "==3.1.2" + }, + "jmespath": { + "hashes": [ + "sha256:02e2e4cc71b5bcab88332eebf907519190dd9e6e82107fa7f83b1003a6252980", + "sha256:90261b206d6defd58fdd5e85f478bf633a2901798906be2ad389150c5c60edbe" + ], + "markers": "python_version >= '3.7'", + "version": "==1.0.1" + }, + "markupsafe": { + "hashes": [ + "sha256:0576fe974b40a400449768941d5d0858cc624e3249dfd1e0c33674e5c7ca7aed", + "sha256:085fd3201e7b12809f9e6e9bc1e5c96a368c8523fad5afb02afe3c051ae4afcc", + "sha256:090376d812fb6ac5f171e5938e82e7f2d7adc2b629101cec0db8b267815c85e2", + "sha256:0b462104ba25f1ac006fdab8b6a01ebbfbce9ed37fd37fd4acd70c67c973e460", + "sha256:137678c63c977754abe9086a3ec011e8fd985ab90631145dfb9294ad09c102a7", + "sha256:1bea30e9bf331f3fef67e0a3877b2288593c98a21ccb2cf29b74c581a4eb3af0", + "sha256:22152d00bf4a9c7c83960521fc558f55a1adbc0631fbb00a9471e097b19d72e1", + "sha256:22731d79ed2eb25059ae3df1dfc9cb1546691cc41f4e3130fe6bfbc3ecbbecfa", + "sha256:2298c859cfc5463f1b64bd55cb3e602528db6fa0f3cfd568d3605c50678f8f03", + "sha256:28057e985dace2f478e042eaa15606c7efccb700797660629da387eb289b9323", + "sha256:2e7821bffe00aa6bd07a23913b7f4e01328c3d5cc0b40b36c0bd81d362faeb65", + "sha256:2ec4f2d48ae59bbb9d1f9d7efb9236ab81429a764dedca114f5fdabbc3788013", + "sha256:340bea174e9761308703ae988e982005aedf427de816d1afe98147668cc03036", + "sha256:40627dcf047dadb22cd25ea7ecfe9cbf3bbbad0482ee5920b582f3809c97654f", + "sha256:40dfd3fefbef579ee058f139733ac336312663c6706d1163b82b3003fb1925c4", + "sha256:4cf06cdc1dda95223e9d2d3c58d3b178aa5dacb35ee7e3bbac10e4e1faacb419", + "sha256:50c42830a633fa0cf9e7d27664637532791bfc31c731a87b202d2d8ac40c3ea2", + "sha256:55f44b440d491028addb3b88f72207d71eeebfb7b5dbf0643f7c023ae1fba619", + "sha256:608e7073dfa9e38a85d38474c082d4281f4ce276ac0010224eaba11e929dd53a", + "sha256:63ba06c9941e46fa389d389644e2d8225e0e3e5ebcc4ff1ea8506dce646f8c8a", + "sha256:65608c35bfb8a76763f37036547f7adfd09270fbdbf96608be2bead319728fcd", + "sha256:665a36ae6f8f20a4676b53224e33d456a6f5a72657d9c83c2aa00765072f31f7", + "sha256:6d6607f98fcf17e534162f0709aaad3ab7a96032723d8ac8750ffe17ae5a0666", + "sha256:7313ce6a199651c4ed9d7e4cfb4aa56fe923b1adf9af3b420ee14e6d9a73df65", + "sha256:7668b52e102d0ed87cb082380a7e2e1e78737ddecdde129acadb0eccc5423859", + "sha256:7df70907e00c970c60b9ef2938d894a9381f38e6b9db73c5be35e59d92e06625", + "sha256:7e007132af78ea9df29495dbf7b5824cb71648d7133cf7848a2a5dd00d36f9ff", + "sha256:835fb5e38fd89328e9c81067fd642b3593c33e1e17e2fdbf77f5676abb14a156", + "sha256:8bca7e26c1dd751236cfb0c6c72d4ad61d986e9a41bbf76cb445f69488b2a2bd", + "sha256:8db032bf0ce9022a8e41a22598eefc802314e81b879ae093f36ce9ddf39ab1ba", + "sha256:99625a92da8229df6d44335e6fcc558a5037dd0a760e11d84be2260e6f37002f", + "sha256:9cad97ab29dfc3f0249b483412c85c8ef4766d96cdf9dcf5a1e3caa3f3661cf1", + "sha256:a4abaec6ca3ad8660690236d11bfe28dfd707778e2442b45addd2f086d6ef094", + "sha256:a6e40afa7f45939ca356f348c8e23048e02cb109ced1eb8420961b2f40fb373a", + "sha256:a6f2fcca746e8d5910e18782f976489939d54a91f9411c32051b4aab2bd7c513", + "sha256:a806db027852538d2ad7555b203300173dd1b77ba116de92da9afbc3a3be3eed", + "sha256:abcabc8c2b26036d62d4c746381a6f7cf60aafcc653198ad678306986b09450d", + "sha256:b8526c6d437855442cdd3d87eede9c425c4445ea011ca38d937db299382e6fa3", + "sha256:bb06feb762bade6bf3c8b844462274db0c76acc95c52abe8dbed28ae3d44a147", + "sha256:c0a33bc9f02c2b17c3ea382f91b4db0e6cde90b63b296422a939886a7a80de1c", + "sha256:c4a549890a45f57f1ebf99c067a4ad0cb423a05544accaf2b065246827ed9603", + "sha256:ca244fa73f50a800cf8c3ebf7fd93149ec37f5cb9596aa8873ae2c1d23498601", + "sha256:cf877ab4ed6e302ec1d04952ca358b381a882fbd9d1b07cccbfd61783561f98a", + "sha256:d9d971ec1e79906046aa3ca266de79eac42f1dbf3612a05dc9368125952bd1a1", + "sha256:da25303d91526aac3672ee6d49a2f3db2d9502a4a60b55519feb1a4c7714e07d", + "sha256:e55e40ff0cc8cc5c07996915ad367fa47da6b3fc091fdadca7f5403239c5fec3", + "sha256:f03a532d7dee1bed20bc4884194a16160a2de9ffc6354b3878ec9682bb623c54", + "sha256:f1cd098434e83e656abf198f103a8207a8187c0fc110306691a2e94a78d0abb2", + "sha256:f2bfb563d0211ce16b63c7cb9395d2c682a23187f54c3d79bfec33e6705473c6", + "sha256:f8ffb705ffcf5ddd0e80b65ddf7bed7ee4f5a441ea7d3419e861a12eaf41af58" + ], + "markers": "python_version >= '3.7'", + "version": "==2.1.2" + }, + "numpy": { + "hashes": [ + "sha256:003a9f530e880cb2cd177cba1af7220b9aa42def9c4afc2a2fc3ee6be7eb2b22", + "sha256:150947adbdfeceec4e5926d956a06865c1c690f2fd902efede4ca6fe2e657c3f", + "sha256:2620e8592136e073bd12ee4536149380695fbe9ebeae845b81237f986479ffc9", + "sha256:2eabd64ddb96a1239791da78fa5f4e1693ae2dadc82a76bc76a14cbb2b966e96", + "sha256:4173bde9fa2a005c2c6e2ea8ac1618e2ed2c1c6ec8a7657237854d42094123a0", + "sha256:4199e7cfc307a778f72d293372736223e39ec9ac096ff0a2e64853b866a8e18a", + "sha256:4cecaed30dc14123020f77b03601559fff3e6cd0c048f8b5289f4eeabb0eb281", + "sha256:557d42778a6869c2162deb40ad82612645e21d79e11c1dc62c6e82a2220ffb04", + "sha256:63e45511ee4d9d976637d11e6c9864eae50e12dc9598f531c035265991910468", + "sha256:6524630f71631be2dabe0c541e7675db82651eb998496bbe16bc4f77f0772253", + "sha256:76807b4063f0002c8532cfeac47a3068a69561e9c8715efdad3c642eb27c0756", + "sha256:7de8fdde0003f4294655aa5d5f0a89c26b9f22c0a58790c38fae1ed392d44a5a", + "sha256:889b2cc88b837d86eda1b17008ebeb679d82875022200c6e8e4ce6cf549b7acb", + "sha256:92011118955724465fb6853def593cf397b4a1367495e0b59a7e69d40c4eb71d", + "sha256:97cf27e51fa078078c649a51d7ade3c92d9e709ba2bfb97493007103c741f1d0", + "sha256:9a23f8440561a633204a67fb44617ce2a299beecf3295f0d13c495518908e910", + "sha256:a51725a815a6188c662fb66fb32077709a9ca38053f0274640293a14fdd22978", + "sha256:a77d3e1163a7770164404607b7ba3967fb49b24782a6ef85d9b5f54126cc39e5", + "sha256:adbdce121896fd3a17a77ab0b0b5eedf05a9834a18699db6829a64e1dfccca7f", + "sha256:c29e6bd0ec49a44d7690ecb623a8eac5ab8a923bce0bea6293953992edf3a76a", + "sha256:c72a6b2f4af1adfe193f7beb91ddf708ff867a3f977ef2ec53c0ffb8283ab9f5", + "sha256:d0a2db9d20117bf523dde15858398e7c0858aadca7c0f088ac0d6edd360e9ad2", + "sha256:e3ab5d32784e843fc0dd3ab6dcafc67ef806e6b6828dc6af2f689be0eb4d781d", + "sha256:e428c4fbfa085f947b536706a2fc349245d7baa8334f0c5723c56a10595f9b95", + "sha256:e8d2859428712785e8a8b7d2b3ef0a1d1565892367b32f915c4a4df44d0e64f5", + "sha256:eef70b4fc1e872ebddc38cddacc87c19a3709c0e3e5d20bf3954c147b1dd941d", + "sha256:f64bb98ac59b3ea3bf74b02f13836eb2e24e48e0ab0145bbda646295769bd780", + "sha256:f9006288bcf4895917d02583cf3411f98631275bc67cce355a7f39f8c14338fa" + ], + "markers": "python_version < '3.10'", + "version": "==1.24.2" + }, + "openpyxl": { + "hashes": [ + "sha256:a6f5977418eff3b2d5500d54d9db50c8277a368436f4e4f8ddb1be3422870184", + "sha256:f91456ead12ab3c6c2e9491cf33ba6d08357d802192379bb482f1033ade496f5" + ], + "index": "pypi", + "version": "==3.1.2" + }, + "pandas": { + "hashes": [ + "sha256:008aa9843e92753d1345353e643c51017d8a9e303041db3165b683fc16a4d380", + "sha256:1f060ae468cb24e1ab42c6344b097375b24a902d3cefb5524f93ef0cd0db5f4b", + "sha256:2379d66055592480aab24cda5b1543539302e0f85e9a33538e9e4fd309b3063e", + "sha256:26a507e14dc9a5ef29239b85d0ef5f01a7e308b88781b451a415d9d15e2d1a61", + "sha256:314bc00a0575151d3ec3124af23bf2ef7533b0e160fb138007a4ef1b3c6a0e63", + "sha256:3935c394e1b10d5c311bd9378018a468283adfe8469dc8084e21d55ca06be979", + "sha256:47f116fcb3aa533ab6661ca391136a643e25d1387dae989ed3e5b9248b98e2e9", + "sha256:4e99adf0a3b4e040fad8823567b52eacfd48db50d11024244a60197430ec74b8", + "sha256:67a5251a821b5af1c5aefe5a610a7758fae04693434fb98b2ebad10349cd727a", + "sha256:7bb2d670c1f7de9bcef0986ae9f832fbd99acc43db1d5fe22f2f06bda8a67d43", + "sha256:7fc7c85fcf27726633751d064f4d115dbccb202b0b6ea2909b6d89ca071115e3", + "sha256:8010e4c988c2c2ed1f5763a6e579448a13a7c87b810400124bb872121c9ca3f9", + "sha256:867fd5c3325c302e8feaaa7ec2d99c224be38551d8a9e1ae5d15be7e04424172", + "sha256:8cb4789c8b1f361d7b07a25002e871546b108519af9c176f8a5ca66316c09d90", + "sha256:8ce8603f8cf07044458914b81bb7445b6cc31d381657e0fac21b3eee40f404d0", + "sha256:adc1e91f282426d37830837f108747f0628e7635b1e83b2401b4f7e2a0068a82", + "sha256:b72ba4e9553645c0bfd688a4e89efe9694fb2936adb5c6295d31626233cb674a", + "sha256:c3c3be69e186d12a94004b0c76bb390e26b48e4b444f3adc86d2cf6506c71d99", + "sha256:cf960fc1f2545114b9ed1a0f025d6de63c891df31640e454e333e3b38504d36b", + "sha256:dc45eb7f23c92e0aa5278bb210fb30136e6e0b760636cf18874cdf2d6448df0f", + "sha256:e5ebb19a66d8c4a4563e6cb628a23ee6898dc50e5dfe8b73c692cd7ea81def0a", + "sha256:e817d97597be5c21b1a66cbecadd0d0242482b72f6f5b60129fce5cec329e274", + "sha256:e829b927b156f85432390580d8799dfee59db0be3954235cf5f5df8a42eaaacd", + "sha256:ebc301fb34185275d9ad57838f533d5413a02b434174d1be89785141f785b226", + "sha256:f082e075aeac904db0e69d8b8acc1d610362e3d823ace3af029622b24b105900" + ], + "index": "pypi", + "version": "==2.0.0rc0" + }, + "pycparser": { + "hashes": [ + "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9", + "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206" + ], + "version": "==2.21" + }, + "pydantic": { + "hashes": [ + "sha256:012c99a9c0d18cfde7469aa1ebff922e24b0c706d03ead96940f5465f2c9cf62", + "sha256:0abd9c60eee6201b853b6c4be104edfba4f8f6c5f3623f8e1dba90634d63eb35", + "sha256:12e837fd320dd30bd625be1b101e3b62edc096a49835392dcf418f1a5ac2b832", + "sha256:163e79386c3547c49366e959d01e37fc30252285a70619ffc1b10ede4758250a", + "sha256:189318051c3d57821f7233ecc94708767dd67687a614a4e8f92b4a020d4ffd06", + "sha256:1c84583b9df62522829cbc46e2b22e0ec11445625b5acd70c5681ce09c9b11c4", + "sha256:3091d2eaeda25391405e36c2fc2ed102b48bac4b384d42b2267310abae350ca6", + "sha256:32937835e525d92c98a1512218db4eed9ddc8f4ee2a78382d77f54341972c0e7", + "sha256:3a2be0a0f32c83265fd71a45027201e1278beaa82ea88ea5b345eea6afa9ac7f", + "sha256:3ac1cd4deed871dfe0c5f63721e29debf03e2deefa41b3ed5eb5f5df287c7b70", + "sha256:3ce13a558b484c9ae48a6a7c184b1ba0e5588c5525482681db418268e5f86186", + "sha256:415a3f719ce518e95a92effc7ee30118a25c3d032455d13e121e3840985f2efd", + "sha256:43cdeca8d30de9a897440e3fb8866f827c4c31f6c73838e3a01a14b03b067b1d", + "sha256:476f6674303ae7965730a382a8e8d7fae18b8004b7b69a56c3d8fa93968aa21c", + "sha256:4c19eb5163167489cb1e0161ae9220dadd4fc609a42649e7e84a8fa8fff7a80f", + "sha256:4ca83739c1263a044ec8b79df4eefc34bbac87191f0a513d00dd47d46e307a65", + "sha256:528dcf7ec49fb5a84bf6fe346c1cc3c55b0e7603c2123881996ca3ad79db5bfc", + "sha256:53de12b4608290992a943801d7756f18a37b7aee284b9ffa794ee8ea8153f8e2", + "sha256:587d92831d0115874d766b1f5fddcdde0c5b6c60f8c6111a394078ec227fca6d", + "sha256:60184e80aac3b56933c71c48d6181e630b0fbc61ae455a63322a66a23c14731a", + "sha256:6195ca908045054dd2d57eb9c39a5fe86409968b8040de8c2240186da0769da7", + "sha256:61f1f08adfaa9cc02e0cbc94f478140385cbd52d5b3c5a657c2fceb15de8d1fb", + "sha256:72cb30894a34d3a7ab6d959b45a70abac8a2a93b6480fc5a7bfbd9c935bdc4fb", + "sha256:751f008cd2afe812a781fd6aa2fb66c620ca2e1a13b6a2152b1ad51553cb4b77", + "sha256:89f15277d720aa57e173954d237628a8d304896364b9de745dcb722f584812c7", + "sha256:8c32b6bba301490d9bb2bf5f631907803135e8085b6aa3e5fe5a770d46dd0160", + "sha256:acc6783751ac9c9bc4680379edd6d286468a1dc8d7d9906cd6f1186ed682b2b0", + "sha256:b1eb6610330a1dfba9ce142ada792f26bbef1255b75f538196a39e9e90388bf4", + "sha256:b243b564cea2576725e77aeeda54e3e0229a168bc587d536cd69941e6797543d", + "sha256:b41822064585fea56d0116aa431fbd5137ce69dfe837b599e310034171996084", + "sha256:bbd5c531b22928e63d0cb1868dee76123456e1de2f1cb45879e9e7a3f3f1779b", + "sha256:cf95adb0d1671fc38d8c43dd921ad5814a735e7d9b4d9e437c088002863854fd", + "sha256:e277bd18339177daa62a294256869bbe84df1fb592be2716ec62627bb8d7c81d", + "sha256:ea4e2a7cb409951988e79a469f609bba998a576e6d7b9791ae5d1e0619e1c0f2", + "sha256:f9289065611c48147c1dd1fd344e9d57ab45f1d99b0fb26c51f1cf72cd9bcd31", + "sha256:fd9b9e98068fa1068edfc9eabde70a7132017bdd4f362f8b4fd0abed79c33083" + ], + "markers": "python_version >= '3.7'", + "version": "==1.10.6" + }, + "pyjwt": { + "extras": [ + "crypto" + ], + "hashes": [ + "sha256:69285c7e31fc44f68a1feb309e948e0df53259d579295e6cfe2b1792329f05fd", + "sha256:d83c3d892a77bbb74d3e1a2cfa90afaadb60945205d1095d9221f04466f64c14" + ], + "index": "pypi", + "version": "==2.6.0" + }, + "pymysql": { + "hashes": [ + "sha256:41fc3a0c5013d5f039639442321185532e3e2c8924687abe6537de157d403641", + "sha256:816927a350f38d56072aeca5dfb10221fe1dc653745853d30a216637f5d7ad36" + ], + "index": "pypi", + "version": "==1.0.2" + }, + "pynamodb": { + "hashes": [ + "sha256:3c4d10867d59e6d7a2b54ee4ae213f1021d6f50ff93145e3909784bfc2b7560e", + "sha256:e09c39880560e10251778185b3d0c7a97ee8f42ab363a940c674e9330b61bf9d" + ], + "index": "pypi", + "version": "==5.4.1" + }, + "python-dateutil": { + "hashes": [ + "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86", + "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==2.8.2" + }, + "python-multipart": { + "hashes": [ + "sha256:e9925a80bb668529f1b67c7fdb0a5dacdd7cbfc6fb0bff3ea443fe22bdd62132", + "sha256:ee698bab5ef148b0a760751c261902cd096e57e10558e11aca17646b74ee1c18" + ], + "index": "pypi", + "version": "==0.0.6" + }, + "pytz": { + "hashes": [ + "sha256:01a0681c4b9684a28304615eba55d1ab31ae00bf68ec157ec3708a8182dbbcd0", + "sha256:78f4f37d8198e0627c5f1143240bb0206b8691d8d7ac6d78fee88b78733f8c4a" + ], + "version": "==2022.7.1" + }, + "requests": { + "hashes": [ + "sha256:64299f4909223da747622c030b781c0d7811e359c37124b4bd368fb8c6518baa", + "sha256:98b1b2782e3c6c4904938b84c0eb932721069dfdb9134313beff7c83c2df24bf" + ], + "index": "pypi", + "version": "==2.28.2" + }, + "s3transfer": { + "hashes": [ + "sha256:06176b74f3a15f61f1b4f25a1fc29a4429040b7647133a463da8fa5bd28d5ecd", + "sha256:2ed07d3866f523cc561bf4a00fc5535827981b117dd7876f036b0c1aca42c947" + ], + "markers": "python_version >= '3.7'", + "version": "==0.6.0" + }, + "setuptools": { + "hashes": [ + "sha256:2ee892cd5f29f3373097f5a814697e397cf3ce313616df0af11231e2ad118077", + "sha256:b78aaa36f6b90a074c1fa651168723acbf45d14cb1196b6f02c0fd07f17623b2" + ], + "markers": "python_version >= '3.7'", + "version": "==67.6.0" + }, + "six": { + "hashes": [ + "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", + "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==1.16.0" + }, + "sniffio": { + "hashes": [ + "sha256:e60305c5e5d314f5389259b7f22aaa33d8f7dee49763119234af3755c55b9101", + "sha256:eecefdce1e5bbfb7ad2eeaabf7c1eeb404d7757c379bd1f7e5cce9d8bf425384" + ], + "markers": "python_version >= '3.7'", + "version": "==1.3.0" + }, + "sqlalchemy": { + "hashes": [ + "sha256:1df00f280fcf7628379c6838d47ac6abd2319848cb02984af313de9243994db8", + "sha256:1fd154847f2c77128e16757e3fd2028151aa8208dd3b9a5978918ea786a15312", + "sha256:20f36bff3b6c9fa94e40114fda4dc5048d40fd665390f5547b456a28e8059ee8", + "sha256:224c817e880359d344a462fc4dd94a233804f371aa290b024b6b976a2f5ade36", + "sha256:2ad44f45526411bebbf427cf858955a35f3a6bfd7db8f4314b12da4c0d1a4fd2", + "sha256:2c4c64f321080c83a3f0eed11cc9b73fe2a574f6b8339c402861274165c24cf6", + "sha256:3625a52fae744cff6f9beb6ed0775468b9eb7e6e8f6730676dfc49aa77d98b4e", + "sha256:3be54b3825512b3de5698ae04bf4aad6ea60442ac0f6b91ee4b8fa4db5c2dccd", + "sha256:4100c80070a66b042f1010b29b29a88d1d151c27a5e522c95ec07518b361a7a3", + "sha256:47e96be3e8c9c0f2c71ec87599be4bb8409d61841b66964a36b2447bec510b3b", + "sha256:483712fce53e2f7ec95ed7d106cd463f9fc122c28a7df4aaf2bc873d0d2a901f", + "sha256:48824b989a0e4340cd099dd4539702ddb1a5ce449f8a7355124e40a4935a95fa", + "sha256:4d653962da384a1d99795dbd8aac4a7516071b2f2984ed2aa25545fae670b808", + "sha256:5b067b2eaf3d97a49f3f6217981efa7b45d5726c2142f103712b020dd250fd98", + "sha256:5c35175b74cbcfe9af077bd13e87cfab13239e075c0e1e920095082f9377f0ed", + "sha256:61abff42e44e5daf17372cb8baa90e970dc647fc5f747e2caa9f9768acf17be8", + "sha256:6987f658389ad8bb6257db91551e7fde3e904974eef6f323856260907ef311d7", + "sha256:709f1ecb5dcea59f36fa0f485e09e41ff313b2d62c83a6f99b36870b0d6e42fa", + "sha256:7635cd38e3ea8522729b14451157104fce2117c44e7ba6a14684ed153d71b567", + "sha256:778db814cc21eff200c8bd42b4ffe976fa3378d10fb84d2c164d3c6a30bb38ee", + "sha256:81d4fc8f5c966677a3a2f39eb8e496442269d8c7d285b28145f7745fcc089d63", + "sha256:82691d3539023c3cee5ae055c47bf873728cd6b33bfaa7b916bea5a99b92f700", + "sha256:8ef7c56c74f4420b2c4a148d2531ba7f99b946cbf438a2bbcb2435fb4938a08d", + "sha256:9310666251385e4374c6f0bae6d69e62bc422021298ceb8669bf6ff56957ff37", + "sha256:ac6274dd530b684cca8cbb774e348afac6846f15d1694a56954413be6e2e8dcd", + "sha256:b7be0e6a4061d28b66ca4b4eb24558dd8c6386d3bcd2d6d7ef247be27cf1281b", + "sha256:bea2c1341abe9bc6f30071b8ada1a3c44f24ec0fe1b9418e9c1112ed32057c9e", + "sha256:bfcadfb8f0a9d26a76a5e2488cedd2e7cf8e70fe76d58aeb1c85eb83b33cbc5c", + "sha256:bfce790746d059af6d0bc68b578ba20d50a63c71a3db16edce7aa8eccdd73796", + "sha256:bfde1d7cf8b9aa6bbd0d53946cd508d76db7689afd442e2289642cdc8908b7b7", + "sha256:c343f0b546495f5d7a239c70bf50a99a48d7321c165b82afafa8483b9ebebf6e", + "sha256:c5d754665edea1ecdc79e3023659cb5594372e10776f3b3734d75c2c3ce95013", + "sha256:c76caced0c8e9129810895f71954c72f478e30bea7d0bba7130bade396be5048", + "sha256:ca147d9cde38b481085408e1d4277ee834cb88bcc31bc01933bc6513340071bc", + "sha256:d7bd001a40997f0c9a9ac10a57663a9397959966a5a365bb24a4d1a17aa60175", + "sha256:db91fe985f2264ab49b3450ab7e2a59c34f7eaf3bf283d6b9e2f9ee02b29e533", + "sha256:e0e270a4f5b42c67362d9c6af648cb86f6a00b20767553cfd734c914e1e2a5e0", + "sha256:ed714b864349704a7a719ec7199eec3f9cd15c190ecf6e10c34b5a0c549c5c18", + "sha256:edc16c8e24605d0a7925afaf99dbcbdc3f98a2cdda4622f1ea34482cb3b91940", + "sha256:f47709c98544384d390aed34046f0573df5725d22861c0cd0a5c151bc22eedff", + "sha256:ff10ad2d74a9a79c2984a2c709943e5362a1c898d8f3414815ea57515ae80c84" + ], + "index": "pypi", + "version": "==2.0.6" + }, + "starlette": { + "hashes": [ + "sha256:41da799057ea8620e4667a3e69a5b1923ebd32b1819c8fa75634bbe8d8bea9bd", + "sha256:e87fce5d7cbdde34b76f0ac69013fd9d190d581d80681493016666e6f96c6d5e" + ], + "markers": "python_version >= '3.7'", + "version": "==0.26.1" + }, + "typing-extensions": { + "hashes": [ + "sha256:5cb5f4a79139d699607b3ef622a1dedafa84e115ab0024e0d9c044a9479ca7cb", + "sha256:fb33085c39dd998ac16d1431ebc293a8b3eedd00fd4a32de0ff79002c19511b4" + ], + "markers": "python_version >= '3.7'", + "version": "==4.5.0" + }, + "urllib3": { + "hashes": [ + "sha256:8a388717b9476f934a21484e8c8e61875ab60644d29b9b39e11e4b9dc1c6b305", + "sha256:aa751d169e23c7479ce47a0cb0da579e3ede798f994f5816a74e4f4500dcea42" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'", + "version": "==1.26.15" + }, + "uvicorn": { + "hashes": [ + "sha256:8635a388062222082f4b06225b867b74a7e4ef942124453d4d1d1a5cb3750932", + "sha256:e69e955cb621ae7b75f5590a814a4fcbfb14cb8f44a36dfe3c5c75ab8aee3ad5" + ], + "index": "pypi", + "version": "==0.21.0" + }, + "xlrd": { + "hashes": [ + "sha256:6a33ee89877bd9abc1158129f6e94be74e2679636b8a205b43b85206c3f0bbdd", + "sha256:f72f148f54442c6b056bf931dbc34f986fd0c3b0b6b5a58d013c9aef274d0c88" + ], + "index": "pypi", + "version": "==2.0.1" + } + }, + "develop": { + "autopep8": { + "hashes": [ + "sha256:86e9303b5e5c8160872b2f5ef611161b2893e9bfe8ccc7e2f76385947d57a2f1", + "sha256:f9849cdd62108cb739dbcdbfb7fdcc9a30d1b63c4cc3e1c1f893b5360941b61c" + ], + "index": "pypi", + "version": "==2.0.2" + }, + "flake8": { + "hashes": [ + "sha256:3833794e27ff64ea4e9cf5d410082a8b97ff1a06c16aa3d2027339cd0f1195c7", + "sha256:c61007e76655af75e6785a931f452915b371dc48f56efd765247c8fe68f2b181" + ], + "index": "pypi", + "version": "==6.0.0" + }, + "mccabe": { + "hashes": [ + "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325", + "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e" + ], + "markers": "python_version >= '3.6'", + "version": "==0.7.0" + }, + "pycodestyle": { + "hashes": [ + "sha256:347187bdb476329d98f695c213d7295a846d1152ff4fe9bacb8a9590b8ee7053", + "sha256:8a4eaf0d0495c7395bdab3589ac2db602797d76207242c17d470186815706610" + ], + "markers": "python_version >= '3.6'", + "version": "==2.10.0" + }, + "pyflakes": { + "hashes": [ + "sha256:ec55bf7fe21fff7f1ad2f7da62363d749e2a470500eab1b555334b67aa1ef8cf", + "sha256:ec8b276a6b60bd80defed25add7e439881c19e64850afd9b346283d4165fd0fd" + ], + "markers": "python_version >= '3.6'", + "version": "==3.0.1" + }, + "tomli": { + "hashes": [ + "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc", + "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f" + ], + "markers": "python_version < '3.11'", + "version": "==2.0.1" + } + } +} diff --git a/ecs/jskult-webapp/README.md b/ecs/jskult-webapp/README.md new file mode 100644 index 00000000..23a796e6 --- /dev/null +++ b/ecs/jskult-webapp/README.md @@ -0,0 +1,192 @@ +# FastAPI 試作プロジェクト + +## 概要 + +PythonのWebフレームワーク「FastAPI」を利用して、MINE(旧称)のWebアプリケーションを開発してみるサンプルのリポジトリです。 +新名称はMeDaCA(Merck Database for Commercial Application)を表示する。 + +## 環境情報 + +- Python 3.9 +- MySQL 8.x +- VSCode +- その他ライブラリ Pipfileを参照 + +## 環境構築 + +- Pythonの構築 + - Merck_NewDWH開発2021のWiki、[Python環境構築](https://nds-tyo.backlog.com/alias/wiki/1874930)を参照 + - 「Pipenvの導入」までを行っておくこと + - 構築完了後、プロジェクト配下で以下のコマンドを実行し、Pythonの仮想環境を作成する + - `pipenv install` + - この手順で出力される仮想環境のパスは、後述するVSCodeの設定手順で使用するため、控えておく + +- MySQLの環境構築 + - Windowsの場合、以下のリンクからダウンロードする + - + - Dockerを利用する場合、「newsdwh-tools」リポジトリのMySQL設定を使用すると便利 + - 「crm-table-to-ddl」フォルダ内で以下のコマンドを実行すると + - `docker-compose up -d` + - Dockerの構築手順は、[Dockerのセットアップ手順](https://nds-tyo.backlog.com/alias/wiki/1754332)を参照のこと + - データを投入する + - 立ち上げたデータベースに「src05」スキーマを作成する + - [ローカル開発用データ](https://ndstokyo.sharepoint.com/:f:/r/sites/merck-new-dwh-team/Shared%20Documents/03.NewDWH%E6%A7%8B%E7%AF%89%E3%83%95%E3%82%A7%E3%83%BC%E3%82%BA3/02.%E9%96%8B%E7%99%BA/90.%E9%96%8B%E7%99%BA%E5%85%B1%E6%9C%89/%E3%83%AD%E3%83%BC%E3%82%AB%E3%83%AB%E9%96%8B%E7%99%BA%E7%94%A8%E3%83%87%E3%83%BC%E3%82%BF?csf=1&web=1&e=VVcRUs)をダウンロードし、mysqlコマンドを使用して復元する + - `mysql -h <ホスト名> -P <ポート> -u <ユーザー名> -p src05 < src05_dump.sql` +- 環境変数の設定 + - 「.env.example」ファイルをコピーし、「.env」ファイルを作成する + - 環境変数を設定する。設定内容はPRJメンバーより共有を受けてください +- VSCodeの設定 + - 「.vscode/recommended_settings.json」ファイルをコピーし、「settings.json」ファイルを作成する + - 「python.defaultInterpreterPath」を、Pythonの構築手順で作成した仮想環境のパスに変更する + +## 実行 + +- VSCode上で「F5」キーを押下すると、Webアプリケーションのサーバーが起動する +- 「」にアクセスし、ログイン画面が表示されていれば成功 + +## フォルダ構成 + +```text +. +├── Pipfile -- Pythonモジュールの依存関係を管理するファイル +├── Pipfile.lock -- Pythonモジュールの依存関係バージョン固定用ファイル +├── README.md +├── auth_flow.drawio -- 認証フローの説明図 +└── src -- ソースコードの保管場所 + ├── aws -- AWSリソース操作用のコード + │   ├── aws_api_client.py + │   ├── cognito.py + │   └── s3.py + ├── controller -- ルーティング層。基本的に1画面1つ + │   ├── bio.py + │   ├── bio_download.py + │   ├── login.py + │   ├── logout.py + │   └── menu.py + ├── core -- APサーバーのコア設定。 + │   └── tasks.py -- 起動・終了時に実行するタスクを設定。 + ├── data -- 生物由来照会のエクセルファイルテンプレート。これはS3に持っていくかも。 + │   └── BioData_template.xlsx + ├── db -- データベース関連処理。 + │   ├── database.py -- データベース接続、クエリ発行の共通モジュール。 + │   ├── sql_condition.py -- SQLの条件式を組み立てるためのモジュール + │   └── tasks.py -- coreに渡すタスク。サーバー起動時にDBとの接続モジュールの初期化、終了時にインスタンス破棄を行っている。 + ├── depends -- FastAPIの依存性注入(DI)使用するモジュールの置き場。Dependsで利用想定。 + │   ├── auth.py -- セッション等の認証関連 + │   ├── database.py -- リポジトリ層をコントローラーにDIするためのもの + │   └── services.py -- サービス層をコントローラーにDIするためのもの + ├── error -- エラー処理関連のモジュール置き場 + │   ├── exception_handler.py -- FastAPI内部でエラー発生時のハンドリング + │   └── exceptions.py -- カスタム例外クラス + ├── main.py -- APサーバーのエントリーポイント。ここでルーターやハンドラーの登録を行う + ├── model -- モデル層(MVCのM) + │   ├── db -- リポジトリから返されるDBレコードのモデル + │   │   ├── base_db_model.py + │   │   ├── bio_sales_view.py + │   │   ├── hdke_tbl.py + │   │   ├── pharmacy_product_master.py + │   │   ├── user_master.py + │   │   └── wholesaler_master.py + │   ├── jwt_token.py -- 認証用JWTトークンのモデル + │   ├── request -- 画面からのリクエストを受け付けるモデル + │   │   ├── bio.py + │   │   ├── bio_download.py + │   │   └── login.py + │   ├── session.py -- セッションデータのモデル + │   └── view -- ビューモデル。画面に対応したモデル。 + │   ├── bio_disp_model.py + │   ├── bio_view_model.py + │   ├── logout_view_model.py + │   ├── mainte_login_view_model.py + │   ├── menu_view_model.py + │   └── user_view_model.py + ├── repositories -- リポジトリ層。DB操作モジュール置き場。 + │   ├── base_repository.py + │   ├── bio_sales_view_repository.py + │   ├── hdke_tbl_repository.py + │   ├── pharmacy_product_master_repository.py + │   ├── user_master_repository.py + │   └── wholesaler_master_repository.py + ├── router -- コントローラー層の共通ルーティングの定義 + │   └── session_router.py + ├── services -- サービス層。ビジネスロジックはできる限りここに押し込む + │   ├── base_service.py + │   ├── batch_status_service.py + │   ├── bio_view_service.py + │   ├── login_service.py + │   └── session_service.py + ├── static -- 静的ファイルの配信ルートディレクトリ + │   ├── css + │   │   ├── bioStyle.css + │   │   ├── datepicker.css + │   │   ├── menuStyle.css + │   │   └── pagenation.css + │   ├── function + │   │   └── businessLogicScript.js + │   ├── img + │   │   ├── icon_modal_confirm.png + │   │   └── icon_modal_error.png + │   ├── lib + │   │   └── fixed_midashi.js + │   ├── sample.css + │   └── sample.js + ├── system_var -- システム変数 + │   ├── constants.py -- 定数 + │   └── environment.py -- 環境変数 + ├── templates -- ビューテンプレートエンジンの格納場所(Jinja2) + │   ├── _header.html -- 共通ヘッダー + │   ├── _modal.html -- モーダルの部品 + │   ├── bioSearchList.html + │   ├── logout.html + │   ├── maintlogin.html + │   ├── menu.html + └── util -- ユーティリティ関数置き場 + ├── sanitize.py -- モデルクラスのサニタイズを行うデコレータ + └── string_util.py -- 文字列操作関連のユーティリティ +``` + +## (参考)ファイルの追いかけ方 + +- APサーバーそのものは「src/main」にある。 + - ルーター、例外処理ハンドラ、開始終了タスクの設定、静的ファイルディレクトリのマウントを行っている +- URLパスに対する操作の実装は、「controller」フォルダを見る + - `@router.xxx`の`xxx`の部分がHTTPメソッドに相当する。このデコレータが付与された関数が、HTTPメソッドを処理するパスオペレーション関数となる + - パスオペレーション関数の引数には、リクエストで受け取るパラメータと、その関数内で使用できる依存関係を注入できる + - Request型と、Response型の引数は、その名の通り。 + - str型, int型などの引数を指定した場合、その引数はクエリストリングを意味する + - Dependsで初期値が設定される引数は、Depends関数に渡した関数が処理されてから代入される + - たとえば、`get_service`関数にサービスクラスの型を渡すと、get_service関数でサービスクラスのインスタンスを作成して返してくれる + - サービスクラスはリポジトリクラスに依存しているので、自分でインスタンスを組み立てる手間が省ける + - formやリクエストボディのJSONを受け取る場合、リクエスト用のモデルクラスに「as_form」や「as_body」などの関数を実装し、リクエストを受け取れるようにする + - ビジネスロジックは基本的にサービスクラスに押し込む。コントローラーではそのサービスクラスをDependsで依存して利用すること +- ビジネスロジックに相当するサービスクラスは「services」フォルダに格納する。共通実装は以下。 + - REPOSITORIES定数に、依存するリポジトリクラスを辞書形式で指定する + - CLIENTS定数に、依存するAWS APIクライアントクラスを辞書形式で指定する + - `__init__`コンストラクタ内で、2つの定数に指定したキーに紐付いたインスタンスが渡ってくるため、インスタンス変数として登録する + - あとは、ビジネスロジックにあたる関数を生やしていくだけ +- DBへのアクセスを行うリポジトリクラスは「repositories」フォルダに格納する。 + - SQL文を用意し、`_db`インスタンス変数のメソッドを利用してクエリを実行する。 + - 必要に応じて条件設定をする。条件設定には`SQLCondition`クラスを使用する。 + - `BioSalesViewRepository`のやり方が参考になる + - リポジトリクラスは、サービスクラスで利用するようにする(そのために、サービスクラス側に依存関係を書いている) +- モデルクラスは、「models」フォルダに格納する + - HTTPリクエスト用のモデルクラスは「request」フォルダへ + - ビュー表示用のモデルクラス(View Model)は「view」フォルダへ + - 画面への項目埋め込みや、非表示の制御を行うため、View Modelに値を詰めて、テンプレート側で操作するようにする + - DBから取得したレコードのモデルクラスは「db」フォルダへ + - 内部処理に利用するモデルクラスは「internal」フォルダへ +- 「static」フォルダは、静的ファイルの置き場所 + - CSS, JS、画像ファイルを置く。 + - 画面の細かな制御は「BusinessLogicScript.js」で行っている。現行からちょこちょこ変える必要がある。 + - CSSも、現行は画面ごとに分かれているが、一つのベーススタイルにまとめたい +- 「templates」フォルダは、テンプレートエンジンを格納する + - 各画面1つのテンプレートエンジンを用意する + - 内部で使う変数は、コントローラーで「templates.TemplateResponse」」に詰めて渡す + - テンプレート内でincludeする用途のテンプレートは先頭に「_」をつける + - `_header.html`は、``タグ内に記載する部品。共通的に読み込むCSS等のファイルを指定する。 + - PHPでは分岐などをベタ書きしているが、View Modelに宣言的な関数を用意して、可読性を向上させている。 +- コントローラーの共通処理は、「router」フォルダ内のモジュールで実装している + - コントローラーのrouter変数が、`router.route_class = AfterSetCookieSessionRoute`となっている場合、レスポンス時、クッキーにセッションキーを登録する動きをする + - コントローラーのrouter変数が、`router.route_class = Authenticate`となっている場合、以下の動きをする + - リクエスト到達時にセッションの有無をチェックする + - レスポンス時、クッキーにセッションキーを登録する diff --git a/ecs/jskult-webapp/src/__init__.py b/ecs/jskult-webapp/src/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/ecs/jskult-webapp/src/aws/__init__.py b/ecs/jskult-webapp/src/aws/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/ecs/jskult-webapp/src/aws/aws_api_client.py b/ecs/jskult-webapp/src/aws/aws_api_client.py new file mode 100644 index 00000000..06143bcf --- /dev/null +++ b/ecs/jskult-webapp/src/aws/aws_api_client.py @@ -0,0 +1,5 @@ +from abc import ABCMeta + + +class AWSAPIClient(metaclass=ABCMeta): + pass diff --git a/ecs/jskult-webapp/src/aws/cognito.py b/ecs/jskult-webapp/src/aws/cognito.py new file mode 100644 index 00000000..c80f5bac --- /dev/null +++ b/ecs/jskult-webapp/src/aws/cognito.py @@ -0,0 +1,25 @@ +import boto3 + +from src.aws.aws_api_client import AWSAPIClient +from src.system_var import environment + + +class CognitoClient(AWSAPIClient): + def __init__(self) -> None: + self.__client = boto3.client('cognito-idp') + + def login_by_user_password_flow(self, username: str, password: str, secret_hash: str): + + auth_response = self.__client.admin_initiate_auth( + UserPoolId=environment.COGNITO_USER_POOL_ID, + ClientId=environment.COGNITO_CLIENT_ID, + AuthFlow='ADMIN_USER_PASSWORD_AUTH', + AuthParameters={ + 'USERNAME': username, + 'PASSWORD': password, + 'SECRET_HASH': secret_hash + }, + ) + authentication_result = auth_response['AuthenticationResult'] + + return authentication_result['IdToken'], authentication_result['RefreshToken'], diff --git a/ecs/jskult-webapp/src/aws/s3.py b/ecs/jskult-webapp/src/aws/s3.py new file mode 100644 index 00000000..852c7ceb --- /dev/null +++ b/ecs/jskult-webapp/src/aws/s3.py @@ -0,0 +1,32 @@ +from urllib.parse import quote + +import boto3 + +from src.aws.aws_api_client import AWSAPIClient + + +class S3Client(AWSAPIClient): + __s3_client = boto3.client('s3') + + def upload_file(self, local_file_path: str, bucket_name: str, file_key: str): + self.__s3_client.upload_file( + local_file_path, + Bucket=bucket_name, + Key=file_key + ) + + def generate_presigned_url(self, bucket_name: str, file_key: str, download_filename: str=''): + # presigned_urlを生成 + presigned_url = self.__s3_client.generate_presigned_url( + 'get_object', + Params={ + 'Bucket': bucket_name, + 'Key': file_key, + # 別ファイル名に変更するための仕掛け。Unicode文字はquoteでエスケープが必要 + 'ResponseContentDisposition': f'attachment; filename="{quote(download_filename)}"' + }, + # 有効期限20分 + ExpiresIn=1200 + ) + + return presigned_url diff --git a/ecs/jskult-webapp/src/controller/__init__.py b/ecs/jskult-webapp/src/controller/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/ecs/jskult-webapp/src/controller/bio.py b/ecs/jskult-webapp/src/controller/bio.py new file mode 100644 index 00000000..cf191331 --- /dev/null +++ b/ecs/jskult-webapp/src/controller/bio.py @@ -0,0 +1,87 @@ +from typing import Optional + +from fastapi import APIRouter, Depends, HTTPException, Request +from fastapi.exceptions import HTTPException +from starlette import status + +from src.depends.services import get_service +from src.model.internal.session import UserSession +from src.model.request.bio import BioModel +from src.model.view.bio_view_model import BioViewModel +from src.router.session_router import AuthenticatedRoute +from src.services.batch_status_service import BatchStatusService +from src.services.bio_view_service import BioViewService +from src.services.session_service import set_session +from src.system_var import constants +from src.templates import templates + +router = APIRouter() +router.route_class = AuthenticatedRoute + +######################### +# Views # +######################### +@router.get('/bio/BioSearchList') +def bio_view( + request: Request, + batch_status_service:BatchStatusService=Depends(get_service(BatchStatusService)), + bio_service: BioViewService=Depends(get_service(BioViewService)) +): + session: UserSession = request.session + # バッチ処理中の場合、機能を利用させない + if batch_status_service.is_batch_processing(): + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=constants.LOGOUT_REASON_BATCH_PROCESSING) + # 検索項目の取得 + bio = bio_service.prepare_bio_view(session) + # セッション書き換え + session.update( + actions=[ + UserSession.last_access_time.set(UserSession.new_last_access_time()), + UserSession.record_expiration_time.set(UserSession.new_record_expiration_time()), + ] + ) + session_key = set_session(session) + templates_response = templates.TemplateResponse( + 'bioSearchList.html', { + 'request': request, + 'bio': bio, + }, + headers={'session_key': session_key} + ) + return templates_response + +@router.post('/bio/BioSearchList') +def search_bio( + request: Request, + bio_form: Optional[BioModel] = Depends(BioModel.as_form), + bio_service: BioViewService=Depends(get_service(BioViewService)), + batch_status_service:BatchStatusService=Depends(get_service(BatchStatusService)) +): + # error_log(date("Y/m/d H:i:s") . " [INFO] UserId:" . $UserId . "\r\n", 3, "$execLog"); + session: UserSession = request.session + # バッチ処理中の場合、機能を利用させない + if batch_status_service.is_batch_processing(): + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=constants.LOGOUT_REASON_BATCH_PROCESSING) + + # 生物由来データを検索 + bio_sales_view_data = bio_service.search_bio_data(bio_form) + # 検索項目などのデータを取得 + bio: BioViewModel = bio_service.prepare_bio_view(session) + bio.bio_data = bio_sales_view_data + bio.form_data = bio_form + # セッション書き換え + session.update( + actions=[ + UserSession.last_access_time.set(UserSession.new_last_access_time()), + UserSession.record_expiration_time.set(UserSession.new_record_expiration_time()), + ] + ) + session_key = set_session(session) + templates_response = templates.TemplateResponse( + 'bioSearchList.html', { + 'request': request, + 'bio': bio + }, + headers={'session_key': session_key} + ) + return templates_response diff --git a/ecs/jskult-webapp/src/controller/bio_download.py b/ecs/jskult-webapp/src/controller/bio_download.py new file mode 100644 index 00000000..576e7a83 --- /dev/null +++ b/ecs/jskult-webapp/src/controller/bio_download.py @@ -0,0 +1,121 @@ +"""生物由来ファイルダウンロード APIRoute""" +from datetime import datetime +from typing import Union + +from fastapi import APIRouter, Depends, HTTPException +from fastapi.exceptions import HTTPException +from fastapi.responses import JSONResponse +from starlette import status + +from src.depends.auth import verify_session +from src.depends.services import get_service +from src.error.exceptions import DBException +from src.model.internal.session import UserSession +from src.model.request.bio import BioModel +from src.model.request.bio_download import BioDownloadModel +from src.services.batch_status_service import BatchStatusService +from src.services.bio_view_service import BioViewService +from src.services.session_service import set_session +from src.system_var import constants + +router = APIRouter() + +######################### +# APIs # +######################### +@router.post('/api/bio/download') +async def download_bio_data( + search_param: BioModel=Depends(BioModel.as_body), + download_param: BioDownloadModel=Depends(BioDownloadModel.as_body), + bio_service: BioViewService = Depends(get_service(BioViewService)), + batch_status_service: BatchStatusService = Depends(get_service(BatchStatusService)), + session: Union[UserSession, None]=Depends(verify_session) + ): + # 通常のビューとはルーティングの扱いを変えるために、個別のルーターで登録する + # error_log(date("Y/m/d H:i:s") . " [INFO] getBioData start" . "\r\n", 3, "$execLog"); + # 改修後のパラメータを打ち出すようにする + # いらない error_log(date("Y/m/d H:i:s") . " [INFO] param:szConditions=" . htmlspecialchars($_POST["szConditions"], ENT_QUOTES) . "\r\n", 3, "$execLog"); + # いらない error_log(date("Y/m/d H:i:s") . " [INFO] param:pageNum=" . htmlspecialchars($_POST["pageNum"], ENT_QUOTES) . "\r\n", 3, "$execLog"); + # いらない error_log(date("Y/m/d H:i:s") . " [INFO] szUser=" . htmlspecialchars($_POST["szUser"], ENT_QUOTES) . "\r\n", 3, "$execLog"); + # いらない error_log(date("Y/m/d H:i:s") . " [INFO] szfilename=" . htmlspecialchars($_POST["szfilename"], ENT_QUOTES) . "\r\n", 3, "$execLog"); + # いらない error_log(date("Y/m/d H:i:s") . " [INFO] extension=" . htmlspecialchars($_POST["extension"], ENT_QUOTES) . "\r\n", 3, "$execLog"); + # いらない error_log(date("Y/m/d H:i:s") . " [INFO] sql=" . htmlspecialchars($_POST["sql"], ENT_QUOTES) . "\r\n", 3, "$execLog"); + # いらない error_log(date("Y/m/d H:i:s") . " [INFO] arrayPrepare=" . $_POST["arrayPrepare"] . "\r\n", 3, "$execLog"); + # ファイル名に使用するタイムスタンプを初期化しておく + now = datetime.now() + if session is None: + return {'status': 'session_expired'} + # バッチ処理中の場合、機能を利用させない + if batch_status_service.is_batch_processing(): + return {'status': 'batch_processing'} + try: + # 生物由来データを検索 + search_result_df = bio_service.search_download_bio_data(search_param) + except DBException as e: + # error_log(date("Y/m/d H:i:s") . " [ERROR] " . "\r\n", 3, "$execLog"); + print('DB Error', e.args) + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail={'error': 'db_error', 'message': e.args} + ) + if search_result_df.size < 1: + # 検索結果が0件の場合、download_urlを返さない + print('Bio data not found') + return {'status': 'ok', 'download_url': None} + + # ファイルに打ち出すカラムを抽出 + extract_df = search_result_df[constants.BIO_EXTRACT_COLUMNS] + + # 値を変換 + # データ種別の正式名を設定 + extract_df.loc[:, 'slip_org_kbn'] = extract_df['slip_org_kbn'].apply(lambda key: constants.SLIP_ORG_KBN_FULL_NAME.get(key)) + # データ区分の区分の日本語名を設定 + extract_df.loc[:, 'data_kbn'] = extract_df['data_kbn'].apply(lambda key: constants.DATA_KBN_JP_NAME.get(key)) + # ロット番号エラーフラグの日本語名を設定 + extract_df.loc[:, 'lot_no_err_flg'] = extract_df['lot_no_err_flg'].apply(lambda key: constants.LOT_NO_ERR_FLG_JP_NAME.get(key)) + # 訂正前伝票管理番号がセットされているときのみ修正日時、修正者、エラー詳細種別をセット + extract_df.loc[:, 'ins_dt'] = extract_df['bef_slip_mgt_no'].apply(lambda bef_slip_mgt_no:extract_df['ins_dt'] if bef_slip_mgt_no is not None else '') + extract_df.loc[:, 'ins_usr'] = extract_df['bef_slip_mgt_no'].apply(lambda bef_slip_mgt_no:extract_df['ins_usr'] if bef_slip_mgt_no is not None else '') + + # 種別によって出力を変える + local_file_path = '' + if download_param.kind == 'xlsx': + # error_log(date("Y/m/d H:i:s") . " [INFO] 今回はExcelファイルに出力する" . "\r\n", 3, "$execLog"); + local_file_path = bio_service.write_excel_file(extract_df, download_param.user_id, timestamp=now) + elif download_param.kind == 'csv': + # error_log(date("Y/m/d H:i:s") . " [INFO] 今回はCSVファイルに出力する" . "\r\n", 3, "$execLog"); + local_file_path = bio_service.write_csv_file(extract_df, download_param.user_id, header=constants.BIO_CSV_HEADER, timestamp=now) + + # ローカルファイルからS3にアップロードし、ダウンロード用URLを取得する + try: + bio_service.upload_bio_data_file(local_file_path) + download_file_url = bio_service.generate_download_file_url(local_file_path, download_param.user_id, download_param.kind) + except Exception as e: + print('S3 access error', e.args) + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail={'error': 'aws_error', 'message': e.args} + ) + + # セッション書き換え + session.update( + actions=[ + UserSession.last_access_time.set(UserSession.new_last_access_time()), + UserSession.record_expiration_time.set(UserSession.new_record_expiration_time()), + ] + ) + set_session(session) + + # クッキーも書き換え + json_response = JSONResponse(content={ + 'status': 'ok', + 'download_url': download_file_url + }) + json_response.set_cookie( + key='session', + value=session.session_key, + max_age=20*60, + secure=True, + httponly=True + ) + return json_response diff --git a/ecs/jskult-webapp/src/controller/healthcheck.py b/ecs/jskult-webapp/src/controller/healthcheck.py new file mode 100644 index 00000000..b36abc8d --- /dev/null +++ b/ecs/jskult-webapp/src/controller/healthcheck.py @@ -0,0 +1,10 @@ +from fastapi import APIRouter + +router = APIRouter() + +######################### +# Views # +######################### +@router.get('/healthcheck') +def healthcheck(): + return {'status': 'OK'} diff --git a/ecs/jskult-webapp/src/controller/login.py b/ecs/jskult-webapp/src/controller/login.py new file mode 100644 index 00000000..35550850 --- /dev/null +++ b/ecs/jskult-webapp/src/controller/login.py @@ -0,0 +1,150 @@ +import os.path as path +import secrets +import urllib.parse as parse +from typing import Union + +from fastapi import APIRouter, Depends, HTTPException, Request, Response +from fastapi.responses import RedirectResponse +from starlette import status + +from src.depends.auth import code_security +from src.depends.services import get_service +from src.error.exceptions import JWTTokenVerifyException, NotAuthorizeException +from src.model.internal.session import UserSession +from src.model.request.login import LoginModel +from src.model.view.mainte_login_view_model import MainteLoginViewModel +from src.router.session_router import AfterSetCookieSessionRoute +from src.services.login_service import LoginService +from src.services.session_service import set_session +from src.system_var import constants, environment +from src.templates import templates + +router = APIRouter() +router.route_class = AfterSetCookieSessionRoute + +######################### +# Views # +######################### +@router.get('/userlogin') +def login_user_redirect_view(): + auth_query_string = parse.urlencode( + { + 'response_type': 'code', + 'identity_provider': environment.COGNITO_IDENTITY_PROVIDER, + 'client_id': environment.COGNITO_CLIENT_ID, + 'redirect_uri': environment.COGNITO_REDIRECT_URI + } + ) + authorize_endpoint_url = f'{environment.COGNITO_AUTH_DOMAIN}/{environment.AUTHORIZE_ENDPOINT}?{auth_query_string}' + + return RedirectResponse(url=authorize_endpoint_url, status_code=status.HTTP_303_SEE_OTHER) + +@router.get('/maintlogin') +def login_maintenance_view(request: Request): + mainte_login = MainteLoginViewModel() + return templates.TemplateResponse( + 'maintlogin.html', + { + 'request': request, + 'mainte_login': mainte_login + } + ) + +######################### +# APIs # +######################### +@router.post('/login') +def sso_authorize( + response: Response, + request: LoginModel = Depends(LoginModel.as_form), + login_service: LoginService = Depends(get_service(LoginService)) + ): + try: + jwt_token = login_service.login(request.username, request.password) + except NotAuthorizeException as e: + print(e) + raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail=constants.LOGOUT_REASON_LOGIN_ERROR) + except JWTTokenVerifyException as e: + raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail=constants.LOGOUT_REASON_SESSION_EXPIRED) + + verified_token = jwt_token.verify_token() + # 普通の認証だと、`cognito:username`に入る。 + user_id = verified_token.user_id + user_record = login_service.logged_in_user(user_id) + # ユーザーが有効ではない場合、ログアウトにリダイレクトする + if not user_record.is_enable_user(): + raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail=constants.LOGOUT_REASON_LOGIN_ERROR) + # メンテユーザーではない場合、ログアウトにリダイレクトする + if user_record is None or not user_record.is_maintenance_user(): + raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail=constants.LOGOUT_REASON_LOGIN_ERROR) + # CSRFトークンを生成 + csrf_token = secrets.token_urlsafe(32) + # DynamoDBにトークンIDを設定する + session_model: UserSession = UserSession.new( + user_id=user_id, + id_token=verified_token.id_token, + refresh_token=verified_token.refresh_token, + csrf_token=csrf_token, + bio_flg=user_record.auth_flg1, + doc_flg=user_record.auth_flg2, + inst_flg=user_record.auth_flg3, + master_mainte_flg=user_record.auth_flg4, + user_flg=user_record.mntuser_flg + ) + session_key = set_session(session_model) + + response = RedirectResponse( + url='/menu', + status_code=status.HTTP_303_SEE_OTHER, + headers={'session_key': session_key} + ) + return response + + +@router.get('/authorize') +def sso_authorize( + code:Union[str, None]=Depends(code_security), + login_service: LoginService=Depends(get_service(LoginService)) + ) -> Response: + if not code: + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=constants.LOGOUT_REASON_NOT_LOGIN) + + # トークン取得 + jwt_token = login_service.login_with_security_code(code) + try: + # トークン検証 + verified_token = jwt_token.verify_token() + except JWTTokenVerifyException as e: + raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail=constants.LOGOUT_REASON_SESSION_EXPIRED) + + # トークンからユーザーIDを取得 + user_id = verified_token.user_id + user_record = login_service.logged_in_user(user_id) + # ユーザーが有効ではない場合、ログアウトにリダイレクトする + if not user_record.is_enable_user(): + raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail=constants.LOGOUT_REASON_LOGIN_ERROR) + # Merckユーザーではない場合、ログアウトにリダイレクトする + if user_record is None or not user_record.is_groupware_user(): + raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail=constants.LOGOUT_REASON_LOGIN_ERROR) + + # CSRFトークンを生成 + csrf_token = secrets.token_urlsafe(32) + # DynamoDBにトークンIDを設定する + session_model: UserSession = UserSession.new( + user_id=user_id, + id_token=verified_token.id_token, + refresh_token=verified_token.refresh_token, + csrf_token=csrf_token, + bio_flg=user_record.auth_flg1, + doc_flg=user_record.auth_flg2, + inst_flg=user_record.auth_flg3, + master_mainte_flg=user_record.auth_flg4, + user_flg=user_record.mntuser_flg + ) + session_key = set_session(session_model) + response = RedirectResponse( + url='/menu', + status_code=status.HTTP_303_SEE_OTHER, + headers={'session_key': session_key} + ) + return response diff --git a/ecs/jskult-webapp/src/controller/logout.py b/ecs/jskult-webapp/src/controller/logout.py new file mode 100644 index 00000000..252e2a01 --- /dev/null +++ b/ecs/jskult-webapp/src/controller/logout.py @@ -0,0 +1,41 @@ +from typing import Optional, Union + +from fastapi import APIRouter, Depends, Request +from fastapi.responses import HTMLResponse + +from src.depends.auth import verify_session +from src.model.internal.session import UserSession +from src.model.view.logout_view_model import LogoutViewModel +from src.system_var import constants +from src.templates import templates + +router = APIRouter() + +######################### +# Views # +######################### +@router.get('/logout', response_class=HTMLResponse) +def logout_view( + request: Request, + reason: Optional[str] = None, + session: Union[UserSession, None]=Depends(verify_session) + ): + redirect_to = '/userlogin' + link_text = 'MeDaCA機能メニューへ' + if session is not None and session.user_flg == '1': + redirect_to = '/maintlogin' + link_text = 'Login画面に戻る' + logout = LogoutViewModel() + logout.redirect_to = redirect_to + logout.reason = constants.LOGOUT_REASON_MESSAGE_MAP.get(reason, '') + logout.link_text = link_text + template_response = templates.TemplateResponse( + 'logout.html', + { + 'request': request, + 'logout': logout, + } + ) + # クッキーを削除 + template_response.delete_cookie('session') + return template_response diff --git a/ecs/jskult-webapp/src/controller/menu.py b/ecs/jskult-webapp/src/controller/menu.py new file mode 100644 index 00000000..26cf9482 --- /dev/null +++ b/ecs/jskult-webapp/src/controller/menu.py @@ -0,0 +1,55 @@ +from fastapi import APIRouter, Depends, Request +from fastapi.responses import HTMLResponse + +from src.depends.services import get_service +from src.model.internal.session import UserSession +from src.model.view.menu_view_model import MenuViewModel +from src.model.view.user_view_model import UserViewModel +from src.router.session_router import AuthenticatedRoute +from src.services.batch_status_service import BatchStatusService +from src.services.session_service import set_session +from src.templates import templates + +router = APIRouter() +router.route_class = AuthenticatedRoute + +######################### +# Views # +######################### +@router.get('/menu', response_class=HTMLResponse) +def menu_view( + request: Request, + batch_status_service:BatchStatusService=Depends(get_service(BatchStatusService)) + ): + session: UserSession = request.session + # 日付マスターからバッチ情報を取得する + hdke_tbl_record = batch_status_service.hdke_table_record + + batch_status = hdke_tbl_record.bch_actf + user = UserViewModel( + doc_flg=session.doc_flg, + inst_flg=session.inst_flg, + bio_flg=session.bio_flg, + master_mainte_flg=session.master_mainte_flg + ) + menu = MenuViewModel( + batch_status=batch_status, + user_model=user + ) + # セッション書き換え + session.update( + actions=[ + UserSession.last_access_time.set(UserSession.new_last_access_time()), + UserSession.record_expiration_time.set(UserSession.new_record_expiration_time()), + ] + ) + set_session(session) + templates_response = templates.TemplateResponse( + 'menu.html', + { + 'request': request, + 'menu': menu + }, + headers={'session_key': session.session_key} + ) + return templates_response diff --git a/ecs/jskult-webapp/src/core/__init__.py b/ecs/jskult-webapp/src/core/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/ecs/jskult-webapp/src/core/tasks.py b/ecs/jskult-webapp/src/core/tasks.py new file mode 100644 index 00000000..42352465 --- /dev/null +++ b/ecs/jskult-webapp/src/core/tasks.py @@ -0,0 +1,21 @@ +"""FastAPIサーバーの起動・終了イベントのラッパー""" + +from typing import Callable + +from fastapi import FastAPI + +from src.db.tasks import close_db, init_db + + +def create_start_app_handler(app: FastAPI) -> Callable: + def start_app() -> None: + init_db(app) + + return start_app + + +def create_stop_app_handler(app: FastAPI) -> Callable: + def stop_app() -> None: + close_db(app) + + return stop_app \ No newline at end of file diff --git a/ecs/jskult-webapp/src/db/__init__.py b/ecs/jskult-webapp/src/db/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/ecs/jskult-webapp/src/db/database.py b/ecs/jskult-webapp/src/db/database.py new file mode 100644 index 00000000..2f941663 --- /dev/null +++ b/ecs/jskult-webapp/src/db/database.py @@ -0,0 +1,71 @@ +from sqlalchemy import Engine, create_engine, text +from sqlalchemy.engine.url import URL + +from src.error.exceptions import DBException + + +class Database: + + __engine: Engine = None + __host: str = None + __port: str = None + __user: str = None + __password: str = None + __schema: str = None + __connection_string:str = None + + def __init__(self, host: str, port: int, user: str, password: str, schema: str) -> None: + self.__host = host + self.__port = int(port) + self.__user = user + self.__password = password + self.__schema = schema + + self.__connection_string = URL.create( + drivername='mysql+pymysql', + username=self.__user, + password=self.__password, + host=self.__host, + port=self.__port, + database=self.__schema, + query={"charset": "utf8mb4"} + ) + + @property + def connection(self): + return self.__engine + + def engine_init(self): + engine = create_engine( + self.__connection_string, + pool_timeout=5 + ) + self.__engine = engine + + def execute_query(self, select_query: str, parameters=None) -> list[dict]: + if self.__engine is None: + raise DBException('データベースが初期化されていません') + + with self.__engine.begin() as trx: + try: + result = trx.execute(text(select_query), parameters=parameters) + except Exception as e: + trx.rollback() + raise DBException(e) + return result.mappings().all() + + def execute(self, query: str, parameters=None) -> None: + if self.__engine is None: + raise DBException('データベースが初期化されていません') + + with self.__engine.begin() as trx: + try: + trx.execute(text(query), parameters=parameters) + except Exception as e: + trx.rollback() + raise DBException(e) + + def close(self): + if self.__engine is not None: + self.__engine.dispose(close=True) + self.__engine = None diff --git a/ecs/jskult-webapp/src/db/sql_condition.py b/ecs/jskult-webapp/src/db/sql_condition.py new file mode 100644 index 00000000..dfd73858 --- /dev/null +++ b/ecs/jskult-webapp/src/db/sql_condition.py @@ -0,0 +1,35 @@ +class SQLCondition: + column: str + operator: str + param: str + literal: bool + + def __init__(self, column: str, operator: str, param: str, literal=False) -> None: + """ + Args: + column (str): カラム名 + operator (str): 比較演算子 + param (str): パラメータ(プレースホルダーかリテラル値か) + literal (bool, optional): リテラル値を埋め込むかどうか。 + 画面から渡ってきた値を使うとSQLインジェクションの危険性があるため、固定値で使用すること。 + """ + self.column = column + self.operator = operator + self.param = param + self.literal=literal + + def apply(self): + # literalがFalseならプレースホルダー。Trueだったならは固定値。 + param = f':{self.param}' if self.literal is False else self.param + return f' {self.column} {self.operator} {param}' + +# 定数 +EQ = '=' +NE = '<>' +GT = '>' +LT = '<' +GE = '>=' +LE = '<=' +LIKE = 'LIKE' +IS = 'IS' +IS_NOT = 'IS NOT' \ No newline at end of file diff --git a/ecs/jskult-webapp/src/db/tasks.py b/ecs/jskult-webapp/src/db/tasks.py new file mode 100644 index 00000000..9da8d300 --- /dev/null +++ b/ecs/jskult-webapp/src/db/tasks.py @@ -0,0 +1,22 @@ +from fastapi import FastAPI + +from src.db.database import Database +from src.system_var import environment + + +def init_db(app: FastAPI) -> None: + # DB接続モジュールを初期化 + database = Database( + host=environment.DB_HOST, + port=environment.DB_PORT, + user=environment.DB_USERNAME, + password=environment.DB_PASSWORD, + schema=environment.DB_SCHEMA + ) + database.engine_init() + # FastAPI App内で使える変数として追加 + app.state._db = database + + +def close_db(app: FastAPI) -> None: + app.state._db.close() diff --git a/ecs/jskult-webapp/src/depends/__init__.py b/ecs/jskult-webapp/src/depends/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/ecs/jskult-webapp/src/depends/auth.py b/ecs/jskult-webapp/src/depends/auth.py new file mode 100644 index 00000000..3310df6f --- /dev/null +++ b/ecs/jskult-webapp/src/depends/auth.py @@ -0,0 +1,48 @@ +import datetime +from typing import Union + +from fastapi import Depends +from fastapi.security import APIKeyCookie, APIKeyQuery + +from src.error.exceptions import JWTTokenVerifyException +from src.model.internal.jwt_token import JWTToken +from src.model.internal.session import UserSession +from src.services.session_service import get_session +from src.system_var import environment + +cookie_security = APIKeyCookie(name='session', auto_error=False) +code_security = APIKeyQuery(name='code', auto_error=False) + + +def get_current_session(session_key=Depends(cookie_security)): + if session_key is None: + return None + + session = get_session(session_key) + + # sessionが存在しない場合はNoneが返る + return session + + +def check_session_expired(session:Union[UserSession, None]=Depends(get_current_session)): + """セッションの最後にアクセスした時間が、セッション有効期限切れであるかどうかをチェックする""" + if session is None: + return None + + last_access_time = session.last_access_time + session_expired_period = datetime.datetime.fromtimestamp(last_access_time) + datetime.timedelta(minutes=environment.SESSION_EXPIRE_MINUTE) + if session_expired_period < datetime.datetime.now(): + return None + + return session + +def verify_session(session:Union[UserSession, None]=Depends(check_session_expired)): + if session is None: + return None + jwt_token = JWTToken(session.id_token, session.refresh_token) + try: + jwt_token.verify_token() + except JWTTokenVerifyException as e: + print(e) + return None + return session diff --git a/ecs/jskult-webapp/src/depends/database.py b/ecs/jskult-webapp/src/depends/database.py new file mode 100644 index 00000000..f764d5c5 --- /dev/null +++ b/ecs/jskult-webapp/src/depends/database.py @@ -0,0 +1,17 @@ +from typing import Callable, Type + +from fastapi import Depends +from starlette.requests import Request + +from src.db.database import Database +from src.repositories.base_repository import BaseRepository + + +def get_database(request: Request) -> Database: + return request.app.state._db + + +def get_repository(Repo_type: Type[BaseRepository]) -> Callable: + def get_repo(db: Database = Depends(get_database)) -> Type[BaseRepository]: + return Repo_type(db) + return get_repo \ No newline at end of file diff --git a/ecs/jskult-webapp/src/depends/services.py b/ecs/jskult-webapp/src/depends/services.py new file mode 100644 index 00000000..d93a0c97 --- /dev/null +++ b/ecs/jskult-webapp/src/depends/services.py @@ -0,0 +1,16 @@ +from typing import Callable, Type + +from fastapi import Depends +from starlette.requests import Request + +from src.db.database import Database +from src.depends.database import get_database +from src.services.base_service import BaseService + + +def get_service(Service_type: Type[BaseService]) -> Callable: + def get_service(db: Database=Depends(get_database)) -> Type[BaseService]: + repositories = {key: repository(db) for key, repository in Service_type.REPOSITORIES.items()} + clients = {key: client() for key, client in Service_type.CLIENTS.items()} + return Service_type(repositories=repositories, clients=clients) + return get_service \ No newline at end of file diff --git a/ecs/jskult-webapp/src/error/__init__.py b/ecs/jskult-webapp/src/error/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/ecs/jskult-webapp/src/error/exception_handler.py b/ecs/jskult-webapp/src/error/exception_handler.py new file mode 100644 index 00000000..679f7ebd --- /dev/null +++ b/ecs/jskult-webapp/src/error/exception_handler.py @@ -0,0 +1,15 @@ +from urllib import parse + +from fastapi import Request +from fastapi.exceptions import HTTPException +from fastapi.responses import RedirectResponse +from starlette import status + + +def http_exception_handler(request: Request, exc: HTTPException): + # 非同期API呼び出しの場合、detailにdictが入ってくるため、そのまま流す + if hasattr(exc, 'detail') is True and type(exc.detail) == dict: + raise exc + error_detail = exc.detail if hasattr(exc, 'detail') else '' + reason = parse.quote(error_detail) + return RedirectResponse(f'/logout?reason={reason}', status_code=status.HTTP_303_SEE_OTHER) diff --git a/ecs/jskult-webapp/src/error/exceptions.py b/ecs/jskult-webapp/src/error/exceptions.py new file mode 100644 index 00000000..53b299db --- /dev/null +++ b/ecs/jskult-webapp/src/error/exceptions.py @@ -0,0 +1,33 @@ +from typing import Union + +from starlette import status + + +class MeDaCaException(Exception): + """Webアプリの共通例外""" + pass + +class NotAuthorizeException(MeDaCaException): + """認証失敗の例外""" + pass + +class JWTTokenVerifyException(MeDaCaException): + """トークン検証失敗の例外""" + pass + +class DBException(MeDaCaException): + """DB関連の例外""" + pass + +class UnexpectedException(MeDaCaException): + """予期しない例外""" + + # デフォルトを500エラーとする + default_status_code = status.HTTP_500_INTERNAL_SERVER_ERROR + + def __init__( + self, + detail: Union[str, dict], + ) -> None: + self.status_code = status.HTTP_500_INTERNAL_SERVER_ERROR + self.detail = detail diff --git a/ecs/jskult-webapp/src/main.py b/ecs/jskult-webapp/src/main.py new file mode 100644 index 00000000..fbc990cf --- /dev/null +++ b/ecs/jskult-webapp/src/main.py @@ -0,0 +1,40 @@ +import os.path as path + +from fastapi import FastAPI +from fastapi.staticfiles import StaticFiles +from starlette import status + +import src.static as static +from src.controller import bio, bio_download, healthcheck, login, logout, menu +from src.core import tasks +from src.error.exception_handler import http_exception_handler +from src.error.exceptions import UnexpectedException + +app = FastAPI() + +# 静的ファイルをマウント +app.mount('/static', StaticFiles(directory=path.dirname(static.__file__)), name='static') +# ログイン関連のルーター +app.include_router(login.router) +# ログアウト関連のルーター +app.include_router(logout.router) +# メニュー画面関連のルーター +app.include_router(menu.router) +# 生物由来関連のルーター +app.include_router(bio.router) +# 生物由来のダウンロード用APIルーター。 +# クライアントから非同期呼出しされるため、共通ルーターとは異なる扱いとする。 +app.include_router(bio_download.router) +# ヘルスチェック用のルーター +app.include_router(healthcheck.router) + +# エラー発生時にログアウト画面に遷移させるハンドラー +app.add_exception_handler(status.HTTP_401_UNAUTHORIZED, http_exception_handler) +app.add_exception_handler(status.HTTP_403_FORBIDDEN, http_exception_handler) + +# サーバーエラーが発生した場合のハンドラー。HTTPExceptionではハンドリングできないため、個別に設定 +app.add_exception_handler(UnexpectedException, http_exception_handler) + +# サーバー起動・終了イベントを登録 +app.add_event_handler('startup', tasks.create_start_app_handler(app)) +app.add_event_handler('shutdown', tasks.create_stop_app_handler(app)) diff --git a/ecs/jskult-webapp/src/model/__init__.py b/ecs/jskult-webapp/src/model/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/ecs/jskult-webapp/src/model/db/__init__.py b/ecs/jskult-webapp/src/model/db/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/ecs/jskult-webapp/src/model/db/base_db_model.py b/ecs/jskult-webapp/src/model/db/base_db_model.py new file mode 100644 index 00000000..0f817d91 --- /dev/null +++ b/ecs/jskult-webapp/src/model/db/base_db_model.py @@ -0,0 +1,5 @@ +from pydantic import BaseModel + + +class BaseDBModel(BaseModel): + pass diff --git a/ecs/jskult-webapp/src/model/db/bio_sales_view.py b/ecs/jskult-webapp/src/model/db/bio_sales_view.py new file mode 100644 index 00000000..92288443 --- /dev/null +++ b/ecs/jskult-webapp/src/model/db/bio_sales_view.py @@ -0,0 +1,74 @@ +from datetime import date, datetime +from typing import Optional + +from src.model.db.base_db_model import BaseDBModel + + +class BioSalesViewModel(BaseDBModel): + conv_cd: Optional[int] + rec_data: Optional[str] + rec_whs_cd: Optional[str] + rec_whs_sub_cd: Optional[str] + rec_whs_org_cd: Optional[str] + rec_cust_cd: Optional[str] + rec_comm_cd: Optional[str] + rec_tran_kbn: Optional[str] + rev_hsdnymd_wrk: Optional[str] + rev_hsdnymd_srk: Optional[str] + rec_urag_no: Optional[str] + rec_comm_nm: Optional[str] + rec_nnskfcl_nm: Optional[str] + rec_nnsk_fcl_addr: Optional[str] + rec_lot_num: Optional[str] + rec_amt: Optional[str] + rec_ymd: Optional[str] + sale_data_cat: Optional[str] + slip_file_nm: Optional[str] + slip_mgt_no: Optional[str] + row_num: Optional[int] + hsdn_ymd: Optional[str] + exec_dt: Optional[str] + v_tran_cd: Optional[int] + tran_kbn_nm: Optional[str] + whs_org_cd: Optional[str] + v_whsorg_cd: Optional[str] + whs_org_nm: Optional[str] + whs_org_kn: Optional[str] + v_whs_cd: Optional[int] + whs_nm: Optional[str] + nnsk_cd: Optional[str] + v_inst_cd: Optional[str] + v_inst_kn: Optional[str] + v_inst_nm: Optional[str] + v_inst_addr: Optional[str] + comm_cd: Optional[str] + comm_nm: Optional[str] + whs_rep_comm_nm: Optional[str] + whs_rep_nnskfcl_nm: Optional[str] + whs_rep_nnsk_fcl_addr: Optional[str] + mkr_inf_1: Optional[str] + mkr_cd: Optional[str] + htdnymd_err_kbn: Optional[str] + prd_exis_kbn: Optional[str] + fcl_exis_kbn: Optional[str] + amt: Optional[int] + slip_org_kbn: Optional[str] + bef_slip_mgt_no: Optional[str] + lot_no_err_flg: Optional[str] + iko_flg: Optional[str] + kjyo_ym: Optional[str] + tksnbk_kbn: Optional[str] + fcl_exec_kbn: Optional[str] + rec_sts_kbn: Optional[str] + ins_dt: Optional[datetime] + ins_usr: Optional[str] + dcf_inst_cd: Optional[str] + inst_cd: Optional[str] + inst_name_form: Optional[str] + address: Optional[str] + tel_no: Optional[str] + data_kbn: Optional[str] + ser_no: Optional[str] + lot_num: Optional[str] + expr_dt: Optional[date] + amt_fugo: Optional[str] diff --git a/ecs/jskult-webapp/src/model/db/hdke_tbl.py b/ecs/jskult-webapp/src/model/db/hdke_tbl.py new file mode 100644 index 00000000..920cb2e3 --- /dev/null +++ b/ecs/jskult-webapp/src/model/db/hdke_tbl.py @@ -0,0 +1,8 @@ +from datetime import datetime +from typing import Optional + +from src.model.db.base_db_model import BaseDBModel + + +class HdkeTblModel(BaseDBModel): + bch_actf: Optional[str] diff --git a/ecs/jskult-webapp/src/model/db/pharmacy_product_master.py b/ecs/jskult-webapp/src/model/db/pharmacy_product_master.py new file mode 100644 index 00000000..e26f4381 --- /dev/null +++ b/ecs/jskult-webapp/src/model/db/pharmacy_product_master.py @@ -0,0 +1,5 @@ +from src.model.db.base_db_model import BaseDBModel + + +class PharmacyProductMasterModel(BaseDBModel): + mkr_cd_nm: str diff --git a/ecs/jskult-webapp/src/model/db/user_master.py b/ecs/jskult-webapp/src/model/db/user_master.py new file mode 100644 index 00000000..5b55c014 --- /dev/null +++ b/ecs/jskult-webapp/src/model/db/user_master.py @@ -0,0 +1,36 @@ +from datetime import datetime +from typing import Optional + +from src.model.db.base_db_model import BaseDBModel + + +class UserMasterModel(BaseDBModel): + user_id: Optional[str] + mail_adr: Optional[str] + user_nm: Optional[str] + auth_flg1: Optional[str] + auth_flg2: Optional[str] + auth_flg3: Optional[str] + auth_flg4: Optional[str] + auth_flg5: Optional[str] + auth_flg6: Optional[str] + auth_flg7: Optional[str] + auth_flg8: Optional[str] + auth_flg9: Optional[str] + auth_flg10: Optional[str] + pwd: Optional[str] + enabled_flg: Optional[str] + creater: Optional[str] + create_date: Optional[datetime] + updater: Optional[str] + update_date: Optional[datetime] + mntuser_flg: Optional[str] + + def is_enable_user(self): + return self.enabled_flg == 'Y' + + def is_maintenance_user(self): + return self.mntuser_flg == '1' + + def is_groupware_user(self): + return self.mntuser_flg == '0' diff --git a/ecs/jskult-webapp/src/model/db/wholesaler_master.py b/ecs/jskult-webapp/src/model/db/wholesaler_master.py new file mode 100644 index 00000000..90cccc3f --- /dev/null +++ b/ecs/jskult-webapp/src/model/db/wholesaler_master.py @@ -0,0 +1,8 @@ +from src.model.db.base_db_model import BaseDBModel + + +class WholesalerMasterModel(BaseDBModel): + rec_whs_cd: str + rec_whs_sub_cd: str + nm: str + whs_nm: str diff --git a/ecs/jskult-webapp/src/model/internal/jwt_token.py b/ecs/jskult-webapp/src/model/internal/jwt_token.py new file mode 100644 index 00000000..a10635ad --- /dev/null +++ b/ecs/jskult-webapp/src/model/internal/jwt_token.py @@ -0,0 +1,152 @@ +import base64 +import json +from typing import Optional + +import jwt +import requests +from starlette import status + +from src.error.exceptions import JWTTokenVerifyException +from src.system_var import environment + + +class JWTToken: + id_token: str + refresh_token: str + verified_jwt: Optional[dict] + + def __init__(self, id_token: str, refresh_token: str, verified_jwt: dict=None) -> None: + self.id_token = id_token + self.refresh_token = refresh_token + self.verified_jwt = verified_jwt + + @property + def verified_token(self): + if self.verified_jwt is None: + raise JWTTokenVerifyException('検証されていないトークン') + return self.verified_jwt + + @property + def user_id(self): + verified_token = self.verified_token + + user_id: str = None + identities: dict = verified_token.get('identities') + if identities is not None: + # 一般ユーザーログインによる(SSO経由) + user_id = identities[0]['userId'] + # <社員番号>@<ドメイン名>となっているため、@で分割 + user_id = user_id.split('@')[0] + else: + # メンテユーザーによるログイン + user_id = verified_token.get('cognito:username') + + return user_id + + @classmethod + def request(cls, code: str): + """JWTをリクエストし、新たなインスタンスを返す + + Args: + code (str): セキュリティコード + + Raises: + JWTTokenVerifyException: 認証失敗 + + Returns: + JWTToken: JWTTokenのモデルクラス + """ + token_url = f'{environment.COGNITO_AUTH_DOMAIN}/{environment.TOKEN_ENDPOINT}' + request_params = { + 'grant_type': 'authorization_code', + 'client_id': environment.COGNITO_CLIENT_ID, + 'code': code, + 'redirect_uri': environment.COGNITO_REDIRECT_URI + } + + message = bytes(f'{environment.COGNITO_CLIENT_ID}:{environment.COGNITO_CLIENT_SECRET}', 'utf8') + auth_header_value = base64.b64encode(message).decode() + request_headers = { + 'Content-Type': 'application/x-www-form-urlencoded', + 'Authorization': f'Basic {auth_header_value}' + } + + res = requests.post(token_url, params=request_params, headers=request_headers) + if res.status_code != status.HTTP_200_OK: + raise JWTTokenVerifyException(res.text) + + token_response = json.loads(res.text) + + return cls(id_token=token_response['id_token'], refresh_token=token_response['refresh_token']) + + @classmethod + def refresh(cls, refresh_token: str): + """JWTをリフレッシュし、新たなインスタンスを返す + + Args: + refresh_token (str): リフレッシュトークン + + Raises: + JWTTokenVerifyException: 認証失敗 + + Returns: + JWTToken: JWTTokenのモデルクラス + """ + token_url = f'{environment.COGNITO_AUTH_DOMAIN}/{environment.TOKEN_ENDPOINT}' + request_params = { + 'grant_type': 'refresh_token', + 'client_id': environment.COGNITO_CLIENT_ID, + 'refresh_token': refresh_token, + 'redirect_uri': environment.COGNITO_REDIRECT_URI + } + + message = bytes(f'{environment.COGNITO_CLIENT_ID}:{environment.COGNITO_CLIENT_SECRET}', 'utf8') + auth_header_value = base64.b64encode(message).decode() + request_headers = { + 'Content-Type': 'application/x-www-form-urlencoded', + 'Authorization': f'Basic {auth_header_value}' + } + + res = requests.post(token_url, params=request_params, headers=request_headers) + if res.status_code != status.HTTP_200_OK: + raise JWTTokenVerifyException(res.text) + + token_response = json.loads(res.text) + return cls(id_token=token_response['id_token'], refresh_token=refresh_token) + + def verify_token(self): + if self.id_token is None: + raise Exception('アクセストークンがない') + + issuer = f'https://cognito-idp.{environment.AWS_REGION}.amazonaws.com/{environment.COGNITO_USER_POOL_ID}' + jwks_url = f'{issuer}/.well-known/jwks.json' + + jwks_client = jwt.PyJWKClient(jwks_url) + signing_key = jwks_client.get_signing_key_from_jwt(self.id_token) + + try: + verified_jwt = jwt.decode( + self.id_token, + signing_key.key, + algorithms=['RS256'], + audience=environment.COGNITO_CLIENT_ID, + issuer=issuer, + # Cognitoのサーバー時間とのズレにより、Issued atクレームの検証に失敗するパターンに対処する + options={'verify_iat': False} + ) + # 有効期限(exp)が切れた場合、トークンをリフレッシュする + except jwt.ExpiredSignatureError: + refreshed_jwt_token = JWTToken.refresh(self.refresh_token) + return refreshed_jwt_token.verified_token() + # 有効期限以外の検証に失敗した場合は例外とする + except jwt.InvalidTokenError as e: + raise JWTTokenVerifyException('Invalid token', e) + # IDトークンを使用していることを検証する + if verified_jwt['token_use'] != 'id': + raise JWTTokenVerifyException('Invalid `token_use` claim, should be `id`.') + + return JWTToken( + id_token=self.id_token, + refresh_token=self.refresh_token, + verified_jwt=verified_jwt + ) diff --git a/ecs/jskult-webapp/src/model/internal/session.py b/ecs/jskult-webapp/src/model/internal/session.py new file mode 100644 index 00000000..5235d0f3 --- /dev/null +++ b/ecs/jskult-webapp/src/model/internal/session.py @@ -0,0 +1,53 @@ +import datetime +import uuid + +from pynamodb.attributes import NumberAttribute, UnicodeAttribute +from pynamodb.models import Model as DynamoDBTableModel + +from src.system_var import environment + + +class UserSession(DynamoDBTableModel): + class Meta: + table_name = environment.SESSION_TABLE_NAME + region = environment.AWS_REGION + session_key = UnicodeAttribute(hash_key=True) + user_id = UnicodeAttribute() + id_token = UnicodeAttribute() + doc_flg = UnicodeAttribute() + inst_flg = UnicodeAttribute() + bio_flg = UnicodeAttribute() + master_mainte_flg = UnicodeAttribute() + user_flg = UnicodeAttribute() + refresh_token = UnicodeAttribute() + csrf_token = UnicodeAttribute() + last_access_time = NumberAttribute() + record_expiration_time = NumberAttribute() + + @classmethod + def new_last_access_time(cls): + return datetime.datetime.now().timestamp() + + @classmethod + def new_record_expiration_time(cls, expire=environment.SESSION_EXPIRE_MINUTE): + last_access_time = datetime.datetime.fromtimestamp(cls.new_last_access_time()) + return (last_access_time + datetime.timedelta(minutes=expire)).timestamp() + + @classmethod + def new( + cls, user_id, id_token, refresh_token, csrf_token, doc_flg, inst_flg, bio_flg, master_mainte_flg, user_flg + ): + return cls( + session_key=str(uuid.uuid4()), + user_id=user_id, + id_token=id_token, + refresh_token=refresh_token, + csrf_token=csrf_token, + doc_flg=doc_flg, + inst_flg=inst_flg, + bio_flg=bio_flg, + master_mainte_flg=master_mainte_flg, + user_flg=user_flg, + last_access_time=cls.new_last_access_time(), + record_expiration_time=cls.new_record_expiration_time() + ) diff --git a/ecs/jskult-webapp/src/model/request/__init__.py b/ecs/jskult-webapp/src/model/request/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/ecs/jskult-webapp/src/model/request/bio.py b/ecs/jskult-webapp/src/model/request/bio.py new file mode 100644 index 00000000..a32bb2f2 --- /dev/null +++ b/ecs/jskult-webapp/src/model/request/bio.py @@ -0,0 +1,137 @@ +from typing import Optional + +from fastapi import Body, Form +from pydantic import BaseModel + +from src.util.sanitize import sanitize +from src.util.string_util import is_not_empty + + +@sanitize +class BioModel(BaseModel): + wholesaler_code: Optional[str] + wholesaler_sub_code: Optional[str] + wholesaler_name: Optional[str] + org_kbn: Optional[str] + rec_ymd_from: Optional[str] + rec_ymd_to: Optional[str] + rec_lot_num: Optional[str] + data_kbn: Optional[str] + maker_cd: Optional[str] + rev_hsdnymd_srk_from: Optional[str] + rev_hsdnymd_srk_to: Optional[str] + ikoFlg: Optional[str] + + @classmethod + def as_form( + cls, + ctrl_wholesaler: str = Form(None), + ctrl_org_kbn: str = Form(None), + ctrl_rec_ymd_from: str = Form(None), + ctrl_rec_ymd_to: str = Form(None), + ctrl_rec_lot_num: str = Form(None), + ctrl_data_kbn: str = Form(None), + ctrl_maker_cd: str = Form(None), + ctrl_rev_hsdnymd_srk_from: str = Form(None), + ctrl_rev_hsdnymd_srk_to: str = Form(None), + ikoFlg: str = Form(None) + ): + + return cls.__convert_request_param( + cls, + ctrl_wholesaler, + ctrl_org_kbn, + ctrl_rec_ymd_from, + ctrl_rec_ymd_to, + ctrl_rec_lot_num, + ctrl_data_kbn, + ctrl_maker_cd, + ctrl_rev_hsdnymd_srk_from, + ctrl_rev_hsdnymd_srk_to, + ikoFlg + ) + + @classmethod + def as_body( + cls, + ctrl_wholesaler: str = Body(None), + ctrl_org_kbn: str = Body(None), + ctrl_rec_ymd_from: str = Body(None), + ctrl_rec_ymd_to: str = Body(None), + ctrl_rec_lot_num: str = Body(None), + ctrl_data_kbn: str = Body(None), + ctrl_maker_cd: str = Body(None), + ctrl_rev_hsdnymd_srk_from: str = Body(None), + ctrl_rev_hsdnymd_srk_to: str = Body(None), + ikoFlg: str = Body(None) + ): + + return cls.__convert_request_param( + cls, + ctrl_wholesaler, + ctrl_org_kbn, + ctrl_rec_ymd_from, + ctrl_rec_ymd_to, + ctrl_rec_lot_num, + ctrl_data_kbn, + ctrl_maker_cd, + ctrl_rev_hsdnymd_srk_from, + ctrl_rev_hsdnymd_srk_to, + ikoFlg + ) + + + def __convert_request_param( + cls, + ctrl_wholesaler: str, + ctrl_org_kbn: str, + ctrl_rec_ymd_from: str, + ctrl_rec_ymd_to: str, + ctrl_rec_lot_num: str, + ctrl_data_kbn: str, + ctrl_maker_cd: str, + ctrl_rev_hsdnymd_srk_from: str, + ctrl_rev_hsdnymd_srk_to: str, + ikoFlg: str + ): + wholesaler_code = None + wholesaler_sub_code = None + wholesaler_name = None + # 卸コード・卸サブコード + if is_not_empty(ctrl_wholesaler): + # 卸コードは`020-01:卸名`という感じのデータで来るので、分割 + wholesaler_without_name = ctrl_wholesaler.split(':')[0] + wholesaler_name = ctrl_wholesaler.split(':')[1] + wholesaler_code = wholesaler_without_name.split('-')[0] + wholesaler_sub_code = wholesaler_without_name.split('-')[1] + + # 処理日 + rec_ymd_from = None + rec_ymd_to = None + if is_not_empty(ctrl_rec_ymd_from): + rec_ymd_from = ctrl_rec_ymd_from.replace('/', '') + if is_not_empty(ctrl_rec_ymd_to): + rec_ymd_to = ctrl_rec_ymd_to.replace('/', '') + + # 発伝年月日 + rev_hsdnymd_srk_from = None + rev_hsdnymd_srk_to = None + if is_not_empty(ctrl_rev_hsdnymd_srk_from): + rev_hsdnymd_srk_from = ctrl_rev_hsdnymd_srk_from.replace('/', '') + if is_not_empty(ctrl_rev_hsdnymd_srk_to): + rev_hsdnymd_srk_to = ctrl_rev_hsdnymd_srk_to.replace('/', '') + + return cls( + wholesaler_code=wholesaler_code, + wholesaler_sub_code=wholesaler_sub_code, + wholesaler_name=wholesaler_name, + org_kbn=ctrl_org_kbn, + rec_ymd_from=rec_ymd_from, + rec_ymd_to=rec_ymd_to, + rec_lot_num=ctrl_rec_lot_num, + data_kbn=ctrl_data_kbn, + maker_cd=ctrl_maker_cd, + rev_hsdnymd_srk_from=rev_hsdnymd_srk_from, + rev_hsdnymd_srk_to=rev_hsdnymd_srk_to, + ikoFlg=ikoFlg + ) \ No newline at end of file diff --git a/ecs/jskult-webapp/src/model/request/bio_download.py b/ecs/jskult-webapp/src/model/request/bio_download.py new file mode 100644 index 00000000..8b4b5ae9 --- /dev/null +++ b/ecs/jskult-webapp/src/model/request/bio_download.py @@ -0,0 +1,20 @@ +from typing import Optional + +from fastapi import Body +from pydantic import BaseModel + + +class BioDownloadModel(BaseModel): + user_id: str + kind: str + + @classmethod + def as_body( + cls, + user_id: str = Body(), + kind: str = Body() + ): + return cls( + user_id=user_id, + kind=kind + ) diff --git a/ecs/jskult-webapp/src/model/request/login.py b/ecs/jskult-webapp/src/model/request/login.py new file mode 100644 index 00000000..f0c431ab --- /dev/null +++ b/ecs/jskult-webapp/src/model/request/login.py @@ -0,0 +1,15 @@ +from fastapi import Form +from pydantic import BaseModel + + +class LoginModel(BaseModel): + username: str + password: str + + @classmethod + def as_form( + cls, + ctrl_username: str = Form(), + ctrl_password: str = Form() + ): + return cls(username=ctrl_username, password=ctrl_password) diff --git a/ecs/jskult-webapp/src/model/view/__init__.py b/ecs/jskult-webapp/src/model/view/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/ecs/jskult-webapp/src/model/view/bio_disp_model.py b/ecs/jskult-webapp/src/model/view/bio_disp_model.py new file mode 100644 index 00000000..0f1a11fd --- /dev/null +++ b/ecs/jskult-webapp/src/model/view/bio_disp_model.py @@ -0,0 +1,19 @@ +from src.model.db.bio_sales_view import BioSalesViewModel +from src.system_var import constants +from src.util.sanitize import sanitize + + +@sanitize +class BisDisplayModel(BioSalesViewModel): + def __init__(self, param: BioSalesViewModel) -> None: + super().__init__(**param.dict()) + + # 区分・フラグの正式名称を設定 + self.slip_org_kbn = constants.SLIP_ORG_KBN_FULL_NAME.get(self.slip_org_kbn) + self.data_kbn = constants.DATA_KBN_JP_NAME.get(self.data_kbn) + self.lot_no_err_flg = constants.LOT_NO_ERR_FLG_JP_NAME.get(self.lot_no_err_flg) + + # 訂正前伝票管理番号がセットされているときのみ修正日時、修正者、エラー詳細種別をセット + if (self.bef_slip_mgt_no is None): + self.ins_dt = "" + self.ins_usr = "" diff --git a/ecs/jskult-webapp/src/model/view/bio_view_model.py b/ecs/jskult-webapp/src/model/view/bio_view_model.py new file mode 100644 index 00000000..0657bd3c --- /dev/null +++ b/ecs/jskult-webapp/src/model/view/bio_view_model.py @@ -0,0 +1,137 @@ +import json +from collections import OrderedDict +from datetime import datetime +from typing import Optional + +from pydantic import BaseModel + +from src.model.db.pharmacy_product_master import PharmacyProductMasterModel +from src.model.db.wholesaler_master import WholesalerMasterModel +from src.model.request.bio import BioModel +from src.model.view.bio_disp_model import BisDisplayModel +from src.system_var import environment + + +class BioViewModel(BaseModel): + subtitle: str = '生物由来検索一覧' + user_id: Optional[str] + batch_status: Optional[str] + whs_models: list[WholesalerMasterModel] + phm_models: list[PharmacyProductMasterModel] + bio_data: Optional[list[BisDisplayModel]] = [] + form_data: Optional[BioModel] + + def display_wholesaler_names(self): + display_names = [ + f'{whs_model.rec_whs_cd}-{whs_model.rec_whs_sub_cd}:{whs_model.nm}' + for whs_model in self.whs_models + ] + return display_names + + def display_org_kbn(self): + return OrderedDict( + { + '': '', + 'J': 'JD-NET', + 'N': 'NHI', + 'H': '手入力' + } + ) + + def display_data_kbn(self): + return OrderedDict( + { + '' : '', + '0': '正常', + '1': 'ロットエラー', + '3': 'ロット不明', + '9': 'エラー(解消済)', + '2': '除外' + } + ) + + def bio_data_json_str(self): + def date_handler(obj): + return obj.isoformat() if hasattr(obj, 'isoformat') else obj + return json.dumps([model.dict() for model in self.bio_data], ensure_ascii=False, default=date_handler) + + def is_selected_whs_name(self, selected_wholesaler): + if not self.is_form_submitted(): + return '' + + form_wholesaler_full_name = f'{self.form_data.wholesaler_code}-{self.form_data.wholesaler_sub_code}:{self.form_data.wholesaler_name}' + return self._selected_value(form_wholesaler_full_name, selected_wholesaler) + + def is_selected_org_kbn(self, selected_org_kbn): + if not self.is_form_submitted(): + return '' + return self._selected_value(self.form_data.org_kbn, selected_org_kbn) + + def is_input_rec_ymd_from(self): + if not self.is_form_submitted(): + return '' + + return self._format_date_string(self.form_data.rec_ymd_from) + + def is_input_rec_ymd_to(self): + if not self.is_form_submitted(): + return '' + + return self._format_date_string(self.form_data.rec_ymd_to) + + def is_input_lot_num(self): + if not self.is_form_submitted(): + return '' + + return self.form_data.rec_lot_num or '' + + def is_selected_data_kbn(self, selected_data_kbn): + if not self.is_form_submitted(): + return '' + + return self._selected_value(self.form_data.data_kbn, selected_data_kbn) + + def is_selected_maker_cd(self, selected_maker_cd): + if not self.is_form_submitted(): + return '' + + return self._selected_value(self.form_data.maker_cd, selected_maker_cd) + + def is_input_rev_hsdnymd_srk_from(self): + if not self.is_form_submitted(): + return '' + + return self._format_date_string(self.form_data.rev_hsdnymd_srk_from) + + def is_input_rev_hsdnymd_srk_to(self): + if not self.is_form_submitted(): + return '' + + return self._format_date_string(self.form_data.rev_hsdnymd_srk_to) + + def is_checked_iko_flg(self): + if not self.is_form_submitted(): + return '' + + return 'checked' if self.form_data.ikoFlg else '' + + def disabled_button(self): + return 'disabled' if self.is_data_empty() or self.is_data_overflow_max_length() else '' + + def is_form_submitted(self): + return self.form_data is not None + + def is_data_empty(self): + return len(self.bio_data) == 0 + + def is_data_overflow_max_length(self): + return len(self.bio_data) >= environment.BIO_SEARCH_RESULT_MAX_COUNT + + def _format_date_string(self, date_string): + if date_string is None: + return '' + date = datetime.strptime(date_string, '%Y%m%d') + return date.strftime('%Y/%m/%d') + + def _selected_value(self, form_value: str, current_value: str): + return 'selected' if form_value == current_value else '' diff --git a/ecs/jskult-webapp/src/model/view/logout_view_model.py b/ecs/jskult-webapp/src/model/view/logout_view_model.py new file mode 100644 index 00000000..b219918a --- /dev/null +++ b/ecs/jskult-webapp/src/model/view/logout_view_model.py @@ -0,0 +1,10 @@ +from typing import Optional + +from pydantic import BaseModel + + +class LogoutViewModel(BaseModel): + subtitle: str = 'MeDaCA Logout' + redirect_to: Optional[str] + reason: Optional[str] + link_text:Optional[str] diff --git a/ecs/jskult-webapp/src/model/view/mainte_login_view_model.py b/ecs/jskult-webapp/src/model/view/mainte_login_view_model.py new file mode 100644 index 00000000..6d4f9108 --- /dev/null +++ b/ecs/jskult-webapp/src/model/view/mainte_login_view_model.py @@ -0,0 +1,5 @@ +from pydantic import BaseModel + + +class MainteLoginViewModel(BaseModel): + subtitle: str = 'MeDaCA Mainte Login' diff --git a/ecs/jskult-webapp/src/model/view/menu_view_model.py b/ecs/jskult-webapp/src/model/view/menu_view_model.py new file mode 100644 index 00000000..647bdec9 --- /dev/null +++ b/ecs/jskult-webapp/src/model/view/menu_view_model.py @@ -0,0 +1,26 @@ +from typing import Optional + +from pydantic import BaseModel + +from src.model.view.user_view_model import UserViewModel + + +class MenuViewModel(BaseModel): + subtitle: str = 'MeDaCA 機能メニュー' + batch_status: Optional[str] + user_model: UserViewModel + + def is_batch_processing(self): + return self.batch_status == '1' + + def is_available_ult_doctor_menu(self): + return self.user_model.has_ult_doctor_permission() + + def is_available_ult_inst_menu(self): + return self.user_model.has_ult_inst_permission() + + def is_available_bio_menu(self): + return self.user_model.has_bio_permission() + + def is_available_master_maintenance_menu(self): + return self.user_model.has_master_maintenance_permission() diff --git a/ecs/jskult-webapp/src/model/view/user_view_model.py b/ecs/jskult-webapp/src/model/view/user_view_model.py new file mode 100644 index 00000000..7d36fba2 --- /dev/null +++ b/ecs/jskult-webapp/src/model/view/user_view_model.py @@ -0,0 +1,26 @@ +from typing import Optional + +from pydantic import BaseModel + + +class UserViewModel(BaseModel): + bio_flg: str # AUTH_FLG1 + doc_flg: str # AUTH_FLG2 + inst_flg: str # AUTH_FLG3 + master_mainte_flg: str # AUTH_FLG4 + user_flg: Optional[str] # MNTUSER_FLG + + def has_ult_doctor_permission(self): + return self.doc_flg == '1' + + def has_ult_inst_permission(self): + return self.inst_flg == '1' + + def has_bio_permission(self): + return self.bio_flg == '1' + + def has_master_maintenance_permission(self): + return self.master_mainte_flg == '1' + + def is_maintenance_user(self): + return self.user_flg == '1' diff --git a/ecs/jskult-webapp/src/repositories/__init__.py b/ecs/jskult-webapp/src/repositories/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/ecs/jskult-webapp/src/repositories/base_repository.py b/ecs/jskult-webapp/src/repositories/base_repository.py new file mode 100644 index 00000000..bb15148f --- /dev/null +++ b/ecs/jskult-webapp/src/repositories/base_repository.py @@ -0,0 +1,43 @@ +from abc import ABCMeta + +import pandas as pd +from sqlalchemy import text + +from src.db.database import Database +from src.model.db.base_db_model import BaseDBModel + + +class BaseRepository(metaclass=ABCMeta): + + _database: Database + def __init__(self, db: Database) -> None: + self._database = db + + def fetch_all(self) -> list[BaseDBModel]: + """データ全件取得メソッド""" + pass + + def fetch_one(self, parameter: dict) -> BaseDBModel: + """データ1件取得メソッド""" + pass + + def fetch_many(self, parameter: dict) -> list[BaseDBModel]: + """条件付きデータ取得メソッド""" + pass + + def fetch_as_data_frame(self, parameter: dict) -> pd.DataFrame: + pass + + + def _to_data_frame(self, query, parameter: BaseDBModel): + params = params=parameter.dict() + + sql_query = pd.read_sql( + text(query), + con=self._database.connection, + params=params) + df = pd.DataFrame( + sql_query, + index=None + ) + return df diff --git a/ecs/jskult-webapp/src/repositories/bio_sales_view_repository.py b/ecs/jskult-webapp/src/repositories/bio_sales_view_repository.py new file mode 100644 index 00000000..73667cc4 --- /dev/null +++ b/ecs/jskult-webapp/src/repositories/bio_sales_view_repository.py @@ -0,0 +1,116 @@ +from src.db import sql_condition as condition +from src.db.sql_condition import SQLCondition +from src.model.db.bio_sales_view import BioSalesViewModel +from src.model.request.bio import BioModel +from src.repositories.base_repository import BaseRepository +from src.util.string_util import is_not_empty + + +class BioSalesViewRepository(BaseRepository): + FETCH_SQL = """\ + SELECT + ( + CASE + WHEN LEFT(bs.v_tran_cd, 1) = 2 + AND bs.amt >= 1 THEN CONCAT('-', bs.amt) + ELSE bs.amt + END + ) AS amt_fugo, + bs.*, + ln.ser_no, + ln.lot_num, + ln.expr_dt + FROM + src05.bio_sales_view bs + LEFT OUTER JOIN + src05.lot_num_mst ln + ON bs.mkr_cd = ln.ser_no + AND bs.rec_lot_num = ln.lot_num + WHERE + {where_clause} + ORDER BY + bs.rec_whs_cd, + bs.rec_whs_sub_cd, + bs.rev_hsdnymd_srk, + bs.slip_mgt_no + ASC\ + """ + + def fetch_many(self, parameter: BioModel) -> list[BioSalesViewModel]: + try: + where_clause = self.__build_condition(parameter) + # error_log(date("Y/m/d H:i:s") . " [INFO] DB Return=" . $result . "\r\n", 3, "$execLog"); + # error_log(date("Y/m/d H:i:s") . " [INFO] DB参照実行" . "\r\n", 3, "$execLog"); + query = self.FETCH_SQL.format(where_clause=where_clause) + # error_log(date("Y/m/d H:i:s") . " [INFO] SQL: " . $query . "\r\n", 3, "$execLog"); + result = self._database.execute_query(query, parameter.dict()) + models = [BioSalesViewModel(**r) for r in result] + # error_log(date("Y/m/d H:i:s") . " [INFO] count=" . $count . "\r\n", 3, "$execLog"); + return models + except Exception as e: + # TODO: ファイルへの書き出しはloggerでやる + print(f"[ERROR] DB Error : Exception={e.args}") + raise e + + def fetch_as_data_frame(self, parameter: BioModel): + try: + where_clause = self.__build_condition(parameter) + # error_log(date("Y/m/d H:i:s") . " [INFO] DB Return=" . $result . "\r\n", 3, "$execLog"); + # error_log(date("Y/m/d H:i:s") . " [INFO] DB参照実行" . "\r\n", 3, "$execLog"); + query = self.FETCH_SQL.format(where_clause=where_clause) + # error_log(date("Y/m/d H:i:s") . " [INFO] SQL: " . $query . "\r\n", 3, "$execLog"); + # result = self._database.execute_query(query, parameter.dict()) + # models = [BioSalesViewModel(**r) for r in result] + # error_log(date("Y/m/d H:i:s") . " [INFO] count=" . $count . "\r\n", 3, "$execLog"); + df = self._to_data_frame(query, parameter) + return df + except Exception as e: + # TODO: ファイルへの書き出しはloggerでやる + print(f"[ERROR] DB Error : Exception={e.args}") + raise e + + def __build_condition(self, parameter: BioModel): + where_clauses: list[SQLCondition] = [] + + # 卸(コード/サブコード) + if is_not_empty(parameter.wholesaler_code) and is_not_empty(parameter.wholesaler_sub_code): + where_clauses.append(SQLCondition('rec_whs_cd', condition.EQ, 'wholesaler_code')) + where_clauses.append(SQLCondition('rec_whs_sub_cd', condition.EQ, 'wholesaler_sub_code')) + # データ種別 + if is_not_empty(parameter.org_kbn): + where_clauses.append(SQLCondition('slip_org_kbn', condition.EQ, 'org_kbn')) + # 処理日 開始日 + if is_not_empty(parameter.rec_ymd_from): + where_clauses.append(SQLCondition('rec_ymd', condition.GE, 'rec_ymd_from')) + # 処理日 終了日 + if is_not_empty(parameter.rec_ymd_to): + where_clauses.append(SQLCondition('rec_ymd', condition.LE, 'rec_ymd_to')) + # ロット番号 + if is_not_empty(parameter.rec_lot_num): + rec_lot_num = parameter.rec_lot_num + # あいまい検索文字列('%')が含まれる場合は'LIKE'、でなければ'='で検索 + rec_lot_num_comparator = condition.LIKE if rec_lot_num in '%' else condition.EQ + where_clauses.append(SQLCondition('rec_lot_num', rec_lot_num_comparator, 'rec_lot_num')) + # データ区分 + if is_not_empty(parameter.data_kbn): + where_clauses.append(SQLCondition('data_kbn', condition.EQ, 'data_kbn')) + # 製品 + if is_not_empty(parameter.maker_cd): + where_clauses.append(SQLCondition('mkr_cd', condition.EQ, 'maker_cd')) + # 発伝年月日 開始日 + if is_not_empty(parameter.rev_hsdnymd_srk_from): + where_clauses.append(SQLCondition('rev_hsdnymd_srk', condition.GE, 'rev_hsdnymd_srk_from')) + # 発伝年月日 終了日 + if is_not_empty(parameter.rev_hsdnymd_srk_to): + where_clauses.append(SQLCondition('rev_hsdnymd_srk', condition.LE, 'rev_hsdnymd_srk_to')) + # 移行フラグ + # チェックが入っていない場合、移行対象(IKO_FLG = '*')を省く + if parameter.ikoFlg is None: + where_clauses.append(SQLCondition('iko_flg', condition.IS, 'NULL', literal=True)) + # 固定条件 + # Viewで返されるロット番号9件をNull以外で抽出 + where_clauses.append(SQLCondition('LENGTH(TRIM(rec_lot_num))', condition.GT, '0', literal=True)) + + where_clauses_str = ' AND '.join([condition.apply() for condition in where_clauses]) + # error_log(date("Y/m/d H:i:s") . " [INFO] 条件設定終了:" . $szConditions . "\r\n", 3, "$execLog"); + return where_clauses_str diff --git a/ecs/jskult-webapp/src/repositories/hdke_tbl_repository.py b/ecs/jskult-webapp/src/repositories/hdke_tbl_repository.py new file mode 100644 index 00000000..586000e6 --- /dev/null +++ b/ecs/jskult-webapp/src/repositories/hdke_tbl_repository.py @@ -0,0 +1,18 @@ +from src.model.db.hdke_tbl import HdkeTblModel +from src.model.request.bio import BioModel +from src.repositories.base_repository import BaseRepository + + +class HdkeTblRepository(BaseRepository): + FETCH_SQL = "SELECT bch_actf FROM src05.hdke_tbl" + + def fetch_all(self) -> list[HdkeTblModel]: + try: + query = self.FETCH_SQL + result = self._database.execute_query(query) + models = [HdkeTblModel(**r) for r in result] + return models + except Exception as e: + # TODO: ファイルへの書き出しはloggerでやる + print(f"[ERROR] DB Error : Exception={e.args}") + raise e diff --git a/ecs/jskult-webapp/src/repositories/pharmacy_product_master_repository.py b/ecs/jskult-webapp/src/repositories/pharmacy_product_master_repository.py new file mode 100644 index 00000000..bfcbe286 --- /dev/null +++ b/ecs/jskult-webapp/src/repositories/pharmacy_product_master_repository.py @@ -0,0 +1,36 @@ +from src.model.db.pharmacy_product_master import PharmacyProductMasterModel +from src.repositories.base_repository import BaseRepository + + +class PharmacyProductMasterRepository(BaseRepository): + + FETCH_SQL = """\ + SELECT + CONCAT(IFNULL(mkr_cd, ''), ' ', IFNULL(mkr_inf_1, '')) AS mkr_cd_nm + FROM + src05.phm_prd_mst_v t1 + INNER JOIN + ( + SELECT + prd_cd,MAX(sub_no) AS sno + FROM + src05.phm_prd_mst_v + WHERE rec_sts_kbn <> '9' + GROUP BY prd_cd + ) fmv2 + ON t1.prd_cd = fmv2.prd_cd AND t1.sub_no = fmv2.sno + WHERE + mkr_cd IS NOT NULL + ORDER BY mkr_cd + """ + + def fetch_all(self) -> list[PharmacyProductMasterModel]: + try: + result = self._database.execute_query(self.FETCH_SQL) + models = [PharmacyProductMasterModel(**r) for r in result] + return models + except Exception as e: + # TODO: ファイルへの書き出しはloggerでやる + print(f"[ERROR] getOroshiData DB Error. ") + print(f"[ERROR] ErrorMessage: {e.args}") + raise e diff --git a/ecs/jskult-webapp/src/repositories/user_master_repository.py b/ecs/jskult-webapp/src/repositories/user_master_repository.py new file mode 100644 index 00000000..788a6172 --- /dev/null +++ b/ecs/jskult-webapp/src/repositories/user_master_repository.py @@ -0,0 +1,27 @@ +from src.model.db.user_master import UserMasterModel +from src.model.request.bio import BioModel +from src.repositories.base_repository import BaseRepository + + +class UserMasterRepository(BaseRepository): + FETCH_SQL = """\ + SELECT + * + FROM + src05.user_mst + WHERE + LOWER(user_id) = LOWER(:user_id)\ + """ + + def fetch_one(self, parameter: dict) -> UserMasterModel: + try: + query = self.FETCH_SQL + result = self._database.execute_query(query, parameter) + models = [UserMasterModel(**r) for r in result] + if len(models) == 0: + return None + return models[0] + except Exception as e: + # TODO: ファイルへの書き出しはloggerでやる + print(f"[ERROR] DB Error : Exception={e.args}") + raise e diff --git a/ecs/jskult-webapp/src/repositories/wholesaler_master_repository.py b/ecs/jskult-webapp/src/repositories/wholesaler_master_repository.py new file mode 100644 index 00000000..b472fac5 --- /dev/null +++ b/ecs/jskult-webapp/src/repositories/wholesaler_master_repository.py @@ -0,0 +1,35 @@ +from src.model.db.wholesaler_master import WholesalerMasterModel +from src.repositories.base_repository import BaseRepository + + +class WholesalerMasterRepository(BaseRepository): + + FETCH_SQL = """\ + SELECT DISTINCT + b.rec_whs_cd, + b.rec_whs_sub_cd, + v2.nm, + b.whs_nm + FROM src05.bio_sales b + LEFT OUTER JOIN + ( + SELECT sub_no, nm, v_whs_cd, rec_sts_kbn + FROM src05.whs_mst_v + WHERE (SELECT STR_TO_DATE(syor_date, '%Y%m%d') FROM src05.hdke_tbl) BETWEEN start_date AND end_date + ) v2 + ON b.v_whs_cd = v2.v_whs_cd + AND v2.rec_sts_kbn <> '9' + ORDER BY b.rec_whs_cd, b.rec_whs_sub_cd , b.whs_nm DESC + """ + + def fetch_all(self) -> list[WholesalerMasterModel]: + try: + result = self._database.execute_query(self.FETCH_SQL) + result_data = [res for res in result] + models = [WholesalerMasterModel(**r) for r in result_data] + return models + except Exception as e: + # TODO: ファイルへの書き出しはloggerでやる + print(f"[ERROR] getOroshiData DB Error. ") + print(f"[ERROR] ErrorMessage: {e.args}") + raise e diff --git a/ecs/jskult-webapp/src/router/__init__.py b/ecs/jskult-webapp/src/router/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/ecs/jskult-webapp/src/router/session_router.py b/ecs/jskult-webapp/src/router/session_router.py new file mode 100644 index 00000000..7a4ada09 --- /dev/null +++ b/ecs/jskult-webapp/src/router/session_router.py @@ -0,0 +1,125 @@ +import logging +from typing import Callable + +from fastapi import Request, Response +from fastapi.exceptions import HTTPException +from fastapi.routing import APIRoute +from starlette import status + +from src.depends.auth import (check_session_expired, get_current_session, + verify_session) +from src.error.exceptions import UnexpectedException +from src.system_var import constants, environment + +logger = logging.getLogger('uvicorn') + +class MeDaCaRoute(APIRoute): + """アプリケーションのカスタムルーター + + Args: + APIRoute (APIRoute): FastAPIの標準APIRoute + """ + def get_route_handler(self) -> Callable: + """前後処理を付加するルートハンドラーを返す + + Raises: + e: HTTPException + UnexpectedException: HTTPException以外の例外をカスタム例外にする + + Returns: + Callable: カスタムルートハンドラー + """ + original_route_handler = super().get_route_handler() + + # 返却するルートハンドラーを定義。必ず非同期関数にする必要がある。 + async def custom_route_handler(request: Request) -> Response: + try: + logger.info('pre routing process') + # 事前処理 + request = await self.pre_process_route(request) + # 本来のルーティング処理 + logger.info('routing process') + response = await original_route_handler(request) + # 事後処理 + logger.info('post routing process') + return await self.post_process_route(request, response) + except HTTPException as e: + raise e + except Exception as e: + logger.exception(e) + raise UnexpectedException(detail=constants.LOGOUT_REASON_UNEXPECTED) + return custom_route_handler + + async def pre_process_route(self, request: Request) -> Request: + """ルートハンドラーの事前処理 + + Args: + request (Request): FastAPIのリクエストクラス + + Returns: + Request: 加工後のRequestインスタンス + """ + return request + async def post_process_route(self, request: Request, response: Response) -> Response: + """ルートハンドラーの事後処理 + + Args: + request (Request): FastAPIのリクエストインスタンス + response (Response): FastAPIのレスポンスインスタンス(original_route_handlerのレスポンス) + Returns: + Response: 加工後のResponseインスタンス + """ + return response + +class BeforeCheckSessionRoute(MeDaCaRoute): + """事前処理として、セッションチェックを行うルートハンドラー + + Args: + MeDaCaRoute (MeDaCaRoute): 共通ルートハンドラー + """ + async def pre_process_route(self, request: Request): + request = await super().pre_process_route(request) + # セッションを取得 + session_key = request.cookies.get('session') + current_session = get_current_session(session_key) + checked_session = check_session_expired(current_session) + verified_session = verify_session(checked_session) + # セッションが有効でない場合、エラーにする + if verified_session is None: + raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail=constants.LOGOUT_REASON_SESSION_EXPIRED) + scope = request.scope + scope['session'] = verified_session + session_request = Request(receive=request.receive, scope=scope) + return session_request + +class AfterSetCookieSessionRoute(MeDaCaRoute): + """事後処理として、セッションキーをcookieに設定するカスタムルートハンドラー + + Args: + MeDaCaRoute (MeDaCaRoute): 共通ルートハンドラー + """ + async def post_process_route(self, request: Request, response: Response): + response = await super().post_process_route(request, response) + session_key = response.headers.get('session_key', None) + # セッションキーがない場合はセットせずに返す + if session_key is None: + return response + + del response.headers['session_key'] + # クッキーにセッションを設定 + response.set_cookie( + key='session', + value=session_key, + max_age=environment.SESSION_EXPIRE_MINUTE * 60, # cookieの有効期限は秒数指定なので、60秒をかける + secure=True, + httponly=True + ) + return response + +class AuthenticatedRoute(BeforeCheckSessionRoute, AfterSetCookieSessionRoute): + async def pre_process_route(self, request: Request): + request = await super().pre_process_route(request) + return request + async def post_process_route(self, request: Request, response: Response): + response = await super().post_process_route(request, response) + return response diff --git a/ecs/jskult-webapp/src/services/__init__.py b/ecs/jskult-webapp/src/services/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/ecs/jskult-webapp/src/services/base_service.py b/ecs/jskult-webapp/src/services/base_service.py new file mode 100644 index 00000000..a2669225 --- /dev/null +++ b/ecs/jskult-webapp/src/services/base_service.py @@ -0,0 +1,13 @@ +from abc import ABCMeta + +from src.aws.aws_api_client import AWSAPIClient +from src.repositories.base_repository import BaseRepository + + +class BaseService(metaclass=ABCMeta): + # 各サービスが依存するrepositoryクラスのマップ + REPOSITORIES: dict[str, BaseRepository] = {} + # 各サービスが依存するAWS APIクライアントクラスのマップ + CLIENTS: dict[str, AWSAPIClient] = {} + def __init__(self, repositories: dict[str, BaseRepository], clients: dict[str, AWSAPIClient]) -> None: + pass diff --git a/ecs/jskult-webapp/src/services/batch_status_service.py b/ecs/jskult-webapp/src/services/batch_status_service.py new file mode 100644 index 00000000..e30f0bed --- /dev/null +++ b/ecs/jskult-webapp/src/services/batch_status_service.py @@ -0,0 +1,41 @@ +from src.aws.aws_api_client import AWSAPIClient +from src.error.exceptions import DBException +from src.model.db.hdke_tbl import HdkeTblModel +from src.repositories.base_repository import BaseRepository +from src.repositories.hdke_tbl_repository import HdkeTblRepository +from src.services.base_service import BaseService + + +class BatchStatusService(BaseService): + + REPOSITORIES = { + 'hdke_table_repository': HdkeTblRepository + } + hdke_table_repository: HdkeTblRepository + __hdke_table_record: list[HdkeTblModel] = [] + def __init__(self, repositories: dict[str, BaseRepository], clients: dict[str, AWSAPIClient]) -> None: + super().__init__(repositories, clients) + self.hdke_table_repository = repositories['hdke_table_repository'] + # サービスインスタンス生成時に日付テーブルを取得する。取得できない場合は例外とする + try: + self.__hdke_table_record = self.hdke_table_repository.fetch_all() + except Exception as e: + raise DBException(e) + + @property + def hdke_table_record(self) -> HdkeTblModel: + # 日付マスタのレコードがあることを確認 + self.__assert_record_exists() + # 日付テーブルのレコードは必ず1件 + return self.__hdke_table_record[0] + + def is_batch_processing(self): + # 日付マスタのレコードがあることを確認 + self.__assert_record_exists() + return self.hdke_table_record.bch_actf == '1' # TODO: 定数化する + + def __assert_record_exists(self): + # 日付マスタのレコードがない場合は例外とする + if len(self.__hdke_table_record) == 0: + raise DBException('日付テーブルのレコードが存在しません') + diff --git a/ecs/jskult-webapp/src/services/bio_view_service.py b/ecs/jskult-webapp/src/services/bio_view_service.py new file mode 100644 index 00000000..c1c2f5c1 --- /dev/null +++ b/ecs/jskult-webapp/src/services/bio_view_service.py @@ -0,0 +1,119 @@ +import os.path as path +import shutil +from datetime import datetime + +import pandas as pd + +from src.aws.aws_api_client import AWSAPIClient +from src.aws.s3 import S3Client +from src.model.internal.session import UserSession +from src.model.request.bio import BioModel +from src.model.view.bio_disp_model import BisDisplayModel +from src.model.view.bio_view_model import BioViewModel +from src.repositories.base_repository import BaseRepository +from src.repositories.bio_sales_view_repository import BioSalesViewRepository +from src.repositories.pharmacy_product_master_repository import \ + PharmacyProductMasterRepository +from src.repositories.wholesaler_master_repository import \ + WholesalerMasterRepository +from src.services.base_service import BaseService +from src.system_var import constants, environment + + +class BioViewService(BaseService): + REPOSITORIES = { + 'whs_repository': WholesalerMasterRepository, + 'phm_repository': PharmacyProductMasterRepository, + 'bio_sales_repository': BioSalesViewRepository + } + + CLIENTS = { + 's3_client': S3Client + } + + whs_repository: WholesalerMasterRepository + phm_repository: PharmacyProductMasterRepository + bio_sales_repository: BioSalesViewRepository + s3_client: S3Client + def __init__(self, repositories: dict[str, BaseRepository], clients: dict[str, AWSAPIClient]) -> None: + super().__init__(repositories, clients) + self.whs_repository = repositories['whs_repository'] + self.phm_repository = repositories['phm_repository'] + self.bio_sales_repository = repositories['bio_sales_repository'] + self.s3_client = clients['s3_client'] + + def prepare_bio_view( + self, + session: UserSession + ) ->BioViewModel: + # 卸リストを取得 + wholesalers = self.whs_repository.fetch_all() + # 製品リストを取得 + products = self.phm_repository.fetch_all() + bio = BioViewModel( + whs_models=wholesalers, + phm_models=products, + user_id=session.user_id + ) + return bio + + def search_bio_data(self, search_params: BioModel): + # 生物由来データを検索 + bio_sales_view_data = self.bio_sales_repository.fetch_many(parameter=search_params) + # 画面表示用に加工 + display_bio_data: list[BisDisplayModel] = [BisDisplayModel(data) for data in bio_sales_view_data] + + return display_bio_data + + def search_download_bio_data(self, search_params: BioModel): + # 生物由来データをダウンロードするために、DBから検索した結果をデータフレームに変換 + bio_sales_data_frame = self.bio_sales_repository.fetch_as_data_frame(parameter=search_params) + return bio_sales_data_frame + + def write_excel_file(self, data_frame: pd.DataFrame, user_id: str, timestamp: datetime): + # Excelに書き込み + output_file_path = path.join(constants.BIO_TEMPORARY_FILE_DIR_PATH, f'Result_{user_id.upper()}_{timestamp:%Y%m%d%H%M%S%f}.xlsx') + + # テンプレートファイルをコピーして出力ファイルの枠だけを作る + shutil.copyfile( + src=constants.BIO_EXCEL_TEMPLATE_FILE_PATH, + dst=output_file_path + ) + # ExcelWriterの追記モード(`mode`='a')でファイルを開く + # `engine``='openpyxlは、追記モードでExcelを開くためのおまじない(xlsxしか動作しないが、こちらが出すものなので問題ナシ) + # 既存シートへの書き込みは、`if_sheet_exists='overlay'を指定する + with pd.ExcelWriter(output_file_path, engine='openpyxl', mode='a', if_sheet_exists='overlay') as writer: + # `sheet_name`引数を省略した場合は、「Sheet1」に書き込む。 + # DF内のヘッダと連番を書き込みたくない場合、`header`と`index`をFalseに指定する。 + # `startrow`と`startcol`で、Excelの書き込み位置を決定する。省略した場合はA1セルから書く。 + data_frame.to_excel(writer, header=False, index=False, startrow=1, startcol=0) + + return output_file_path + + def write_csv_file(self, data_frame: pd.DataFrame, user_id: str, header: list[str], timestamp: datetime): + # csvに書き込み + output_file_path = path.join(constants.BIO_TEMPORARY_FILE_DIR_PATH, f'Result_{user_id.upper()}_{timestamp:%Y%m%d%H%M%S%f}.csv') + # 横長のDataFrameとするため、ヘッダーの加工処理 + header_data = {} + for df_column, header_column in zip(data_frame.columns, header): + header_data[df_column] = header_column + + header_df = pd.DataFrame([header_data], index=None) + output_df = pd.concat([header_df, data_frame]) + # ヘッダー行としてではなく、1レコードとして出力する + output_df.to_csv(output_file_path, index=False, header=False) + + return output_file_path + + def upload_bio_data_file(self, local_file_path: str) -> None: + bucket_name = environment.BIO_ACCESS_LOG_BUCKET + # TODO: フォルダを変える + file_key =f'bio/{path.basename(local_file_path)}' + self.s3_client.upload_file(local_file_path, bucket_name, file_key) + + def generate_download_file_url(self, local_file_path:str, user_id: str, kind: str) -> str: + bucket_name = environment.BIO_ACCESS_LOG_BUCKET + # TODO: フォルダを変える + file_key = f'bio/{path.basename(local_file_path)}' + download_filename = f'{user_id.upper()}_生物由来卸販売データ.{kind}' + return self.s3_client.generate_presigned_url(bucket_name, file_key, download_filename) diff --git a/ecs/jskult-webapp/src/services/login_service.py b/ecs/jskult-webapp/src/services/login_service.py new file mode 100644 index 00000000..6442c82a --- /dev/null +++ b/ecs/jskult-webapp/src/services/login_service.py @@ -0,0 +1,57 @@ +import base64 +import hashlib +import hmac + +from src.aws.aws_api_client import AWSAPIClient +from src.aws.cognito import CognitoClient +from src.error.exceptions import NotAuthorizeException +from src.model.db.user_master import UserMasterModel +from src.model.internal.jwt_token import JWTToken +from src.repositories.base_repository import BaseRepository +from src.repositories.user_master_repository import UserMasterRepository +from src.services.base_service import BaseService +from src.system_var import environment + + +class LoginService(BaseService): + REPOSITORIES = { + 'user_repository': UserMasterRepository + } + + CLIENTS = { + 'cognito_client': CognitoClient + } + + def __init__(self, repositories: dict[str, BaseRepository], clients: dict[str, AWSAPIClient]) -> None: + super().__init__(repositories, clients) + self.user_repository = repositories['user_repository'] + self.cognito_client = clients['cognito_client'] + + def login(self, username: str, password: str) -> JWTToken: + try: + id_token, refresh_token = self.cognito_client.login_by_user_password_flow( + username, + password, + self.__secret_hash(username) + ) + except Exception as e: + if e.response['Error']['Code'] == 'NotAuthorizedException': + raise NotAuthorizeException(e) + else: + raise e + + return JWTToken(id_token, refresh_token) + + def login_with_security_code(self, code: str) -> JWTToken: + return JWTToken.request(code) + + def logged_in_user(self, user_id): + user_record: UserMasterModel = self.user_repository.fetch_one({'user_id': user_id}) + return user_record + + def __secret_hash(self, username: str): + # see - https://aws.amazon.com/jp/premiumsupport/knowledge-center/cognito-unable-to-verify-secret-hash/ # noqa + message = bytes(username + environment.COGNITO_CLIENT_ID, 'utf-8') + key = bytes(environment.COGNITO_CLIENT_SECRET, 'utf-8') + digest = hmac.new(key, message, digestmod=hashlib.sha256).digest() + return base64.b64encode(digest).decode() diff --git a/ecs/jskult-webapp/src/services/session_service.py b/ecs/jskult-webapp/src/services/session_service.py new file mode 100644 index 00000000..92a7a193 --- /dev/null +++ b/ecs/jskult-webapp/src/services/session_service.py @@ -0,0 +1,15 @@ + +from src.model.internal.session import UserSession + + +def set_session(session: UserSession) -> str: + session.save() + return session.session_key + +def get_session(key: str) -> UserSession: + try: + session = UserSession.get(hash_key=key, consistent_read=True) + return session + except UserSession.DoesNotExist as e: + print(e) + return None diff --git a/ecs/jskult-webapp/src/static/__init__.py b/ecs/jskult-webapp/src/static/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/ecs/jskult-webapp/src/static/css/bioStyle.css b/ecs/jskult-webapp/src/static/css/bioStyle.css new file mode 100644 index 00000000..8bc72999 --- /dev/null +++ b/ecs/jskult-webapp/src/static/css/bioStyle.css @@ -0,0 +1,291 @@ +body { + white-space: nowrap; + background-color: LightCyan; + font-family: "ヒラギノ角ゴ Pro W3", "Hiragino Kaku Gothic Pro", "メイリオ", Meiryo, Osaka, "MS Pゴシック", "MS PGothic", sans-serif; +} + +h1 { + font-size: 155%; + margin-left: 2%; + margin-top: 0%; + margin-bottom: 0%; +} + +.title { + width: 800px; +} + +table{ + border-collapse : collapse; +} + +.search_table { + margin-bottom: 30px; + padding-bottom: 15px; + border-bottom: solid 1px gray; + width: 1132px; +} + +._form { + width: 1132px; + margin-left: 10px; + margin-right: 20px; +} + +.back_bt { + padding-bottom: 10px; +} + +._form input[type=text] { + width: 193px; + height: 25px; +} +._form input[type=checkbox] { + width: 13px; + height: 13px; +} + +._form select { + width: 193px; + height: 25px; +} + +.result_info { + text-align: right; +} + + +.search_tb { + padding-right: 25px; +} + +.search_bt { + /* width: 60px; */ + margin-left: 10px; +} + +.clear_bt{ + margin-left: 120px; + /* width: 60px */ +} + +.search_dropdown { + width: 175px; +} + +.bioScroll_div { + overflow: auto; + padding-top: 10px; + height: 250px; + width: 1132px; +} + +.noLine{ + text-decoration: none; +} + +.resultAreaMsg { + margin-top: 5%; + text-align: center; + font-size: 150%; +} + +.search_btTd { + text-align: right; +} + +.selection { + display: none; +} + +#page-1 { + display: block; +} + +.search_middleTd { + padding-right: 25px; + width : 450px; +} + +.docSearchScroll_div { + overflow: auto; + height: 200px; + width: 1132px; +} + +.transition{ + text-align: right; + margin-right: 60px; +} + +.transition_bt{ + width: 110px; + height: 40px; + margin-left: 15px; + margin-right: 15px; +} + +.instutionInfo_table{ + width: 1132px; + margin-bottom: 50px; +} + +.institution_column { + width : 160px; + background : rgb(225, 233, 250); + border : solid 1px; +} + +.institution_data { + background : rgb(244, 244, 244); + border : solid 1px; + padding-left : 0.5em; + padding-right : 0.5em; +} + +.data_width_long { + width : 500px; +} + +.data_width_middle { + width : 300px; +} + +.data_width_short { + width : 100px; +} + +.checkbox_margin { + margin-left : 20px; +} + +.border_top_none { + border-top-style:none; +} + +.border_bottom_none { + border-bottom-style:none; +} + +.textbox_margin { + margin-left : 20px; +} + +.textbox_margin_short { + margin-left : 5px; +} + +.label_margin { + margin-left: 10px; + margin-right: 10px; +} + +.trt_course{ + width: 70px; +} + +.small_tb{ + width: 100px; +} + +.docBelongScroll_div { + overflow: auto; + height: 100px; + width: 500px; + margin: 0px 30px 0px 30px; +} + +.rightPadding_table{ + padding-right: 50px; +} + +.verticalBar_td{ + width: 1px; + height: 150px; + background-color: gray; +} + +.docPlaceScroll_div { + overflow: auto; + height: 150px; + width: 700px; + margin: 0px 30px 0px 30px; +} + +.result_tr{ + overflow-y: scroll; + overflow-x: scroll; + +} + +.result_data{ + overflow-y: scroll; + overflow-x: scroll; + width: 50px; +} + +/* tablesoter */ +table.tablesorter { + font-family:arial; + background-color: #CDCDCD; + font-size: 12pt; + text-align: left; +} + +table.tablesorter thead tr th, table.tablesorter tfoot tr th { + background-color: #e6EEEE; + border: 0.1px solid silver; + font-size: 12pt; + padding: 4px; + padding-right: 20px; +} +table.tablesorter thead tr .header { + background-image: url(bg.gif); + background-repeat: no-repeat; + background-position: center right; + cursor: pointer; +} +table.tablesorter tbody td { + color: #3D3D3D; + padding: 4px; + background-color: #FFF; + border: 0.1px solid silver; + vertical-align: top; +} +table.tablesorter tbody td div{ + float: right; +} +table.tablesorter tbody tr.odd td { + background-color:#F0F0F6; +} +table.tablesorter thead tr .headerSortUp { + background-image: url(asc.gif); +} +table.tablesorter thead tr .headerSortDown { + background-image: url(desc.gif); +} +table.tablesorter thead tr .headerSortDown, table.tablesorter thead tr .headerSortUp { + background-color: #8dbdd8; +} + +#loading { + z-index: 10000; + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: #FFF; + overflow-x: hidden; + overflow-y: auto; + outline: 0; + text-align: center; + display: none; + opacity: 0.7; +} + +#loading_content { + position: absolute; + top: 50%; + left: 50%; +} \ No newline at end of file diff --git a/ecs/jskult-webapp/src/static/css/datepicker.css b/ecs/jskult-webapp/src/static/css/datepicker.css new file mode 100644 index 00000000..fb2f5217 --- /dev/null +++ b/ecs/jskult-webapp/src/static/css/datepicker.css @@ -0,0 +1,11 @@ +/* 日曜日:赤 */ +.flatpickr-calendar .flatpickr-innerContainer .flatpickr-weekdays .flatpickr-weekday:nth-child(7n + 1), +.flatpickr-calendar .flatpickr-innerContainer .flatpickr-days .flatpickr-day:not(.flatpickr-disabled):not(.prevMonthDay):not(.nextMonthDay):nth-child(7n + 1) { + color: red; +} + +/* 土曜日:青 */ +.flatpickr-calendar .flatpickr-innerContainer .flatpickr-weekdays .flatpickr-weekday:nth-child(7), +.flatpickr-calendar .flatpickr-innerContainer .flatpickr-days .flatpickr-day:not(.flatpickr-disabled):not(.prevMonthDay):not(.nextMonthDay):nth-child(7n) { + color: blue; +} \ No newline at end of file diff --git a/ecs/jskult-webapp/src/static/css/menuStyle.css b/ecs/jskult-webapp/src/static/css/menuStyle.css new file mode 100644 index 00000000..b1920070 --- /dev/null +++ b/ecs/jskult-webapp/src/static/css/menuStyle.css @@ -0,0 +1,49 @@ +body{ + background-color: LightCyan; + background-size: 220%,220%; + font-family: "ヒラギノ角ゴ Pro W3", "Hiragino Kaku Gothic Pro", "メイリオ", Meiryo, Osaka, "MS Pゴシック", "MS PGothic", sans-serif; +} + +.background{ + margin-top: 5%; + padding: 2%; + background-color: white; + width: 40%; + border-radius: 25px; + box-shadow:5px 5px rgba(0,0,0,0.4);; +} + +.btn_width { + width: 80%; +} + +.form_login{ + width: 80%; + font-size: 180%; + margin: 1%; +} + +.form_login::-webkit-input-placeholder{ + color: gray; +} +.form_login:-ms-input-placeholder{ + color: gray; +} +.form_login::-moz-placeholder{ + color: gray; +} + +.logout_p{ + font-size: 160%; +} + +.notUseBioMsg{ + font-size: 143%; + color: red; +} + +.batchMsg{ + color: red; + font-size: 120%; + text-align: center; +} \ No newline at end of file diff --git a/ecs/jskult-webapp/src/static/css/pagenation.css b/ecs/jskult-webapp/src/static/css/pagenation.css new file mode 100644 index 00000000..3edbf3b2 --- /dev/null +++ b/ecs/jskult-webapp/src/static/css/pagenation.css @@ -0,0 +1,65 @@ +.pagination { + position: relative; + padding-top: 10px; +} + +/* .paginationjs { + width: 100%; +} */ + +.paginationjs > .paginationjs-nav.J-paginationjs-nav{ + position: absolute; + right: 0; +} + +div.paginationjs-pages ul { + list-style: none; + padding: 0; + margin: 0; +} + +.paginationjs-pages > ul > li > a { + padding: 6px 18px; + color: white; + background-color: gainsboro; + border: 1px solid; +} +.paginationjs-pages > ul > li > a:hover { + color: black; + background-color: white; + cursor: pointer; +} +.paginationjs-pages > ul > li.active > a { + color: white; + background-color: gray; +} +.paginationjs-pages > ul > li.active > a:hover { + color: white; + background-color: gray; + cursor: text; +} +.paginationjs-pages > ul > li.disabled > a { + color: white; + background-color: gray; + cursor: text; +} +.paginationjs-pages > ul > li.disabled > a:hover { + color: white; + background-color: gray; + cursor: text; +} + +.paginationjs-page { + margin: 0 4px; +} +.paginationjs-pages > ul { + display: flex; + align-items: baseline; +} +.paginationjs-pages > ul > li.paginationjs-ellipsis.disabled > a { + border: none; + background-color: LightCyan; + color: black; + margin: 0 4px; + padding: 0; +} diff --git a/ecs/jskult-webapp/src/static/function/businessLogicScript.js b/ecs/jskult-webapp/src/static/function/businessLogicScript.js new file mode 100644 index 00000000..90ecc18b --- /dev/null +++ b/ecs/jskult-webapp/src/static/function/businessLogicScript.js @@ -0,0 +1,213 @@ +// 検索フォーム + +// 戻るボタンの関数 +// 機能概要:メニュー画面に遷移する +function backToMenu(){ + location.href = "/menu"; +} + +// クリアボタンの関数 +// 利用条件1:form名がsearch +// 利用条件2:form入力値名がctrl_で始まる +function clr() { + const formElement = document.search + const formInputElements = Array.from(formElement.elements) + for (const formInput of formInputElements) { + if (formInput.name.startsWith('ctrl_')) { + formInput.value = ""; + } + } + + // 検索ボタンを再度非活性にする + formBtDisabled(); +} + +/** + * ボタンの活性非活性関数 + * @param {*} buttonId ボタンのID デフォルトは`search_bt` + * @param {*} formId フォームのname デフォルトは`search` + * @param {*} all 全インプットが入力されている場合に活性化するかどうか + */ +function formBtDisabled(buttonId='search_bt', formId='search', all=false) { + const formElement = document[formId] + const formInputElements = Array.from(formElement.elements) + // 検査対象を + const checkTargetValueLength = formInputElements + .filter((elem) => elem.name.startsWith('ctrl_')) + .map((elem) => elem.value.length) + + // 活性、非活性の判断 + let validFlg = false; + if (all) { + validFlg = checkTargetValueLength.every((num) => num !== 0) + } else { + validFlg = checkTargetValueLength.some((num) => num !== 0) + } + + if (validFlg == true) { + $(`#${buttonId}`).removeAttr('disabled'); + // クリアボタンはあれば活性化する + if ($('#clear')) { + $('#clear').removeAttr('disabled'); + } + } + else { + $(`#${buttonId}`).attr('disabled', 'disabled'); + // クリアボタンはあれば非活性化する + if ($('#clear')) { + $('#clear').attr('disabled', 'disabled'); + } + } +} + +// ドロップダウンリスト再選択の関数 +// 引数:ドロップダウンリストのID +// 引数:選択したいリスト選択表示文字列 +function selectDropDowList(id, selectedName){ + // 以前の検索条件再読み込み + var select = document.getElementById(id); + select = select.children[0]; + var options = select.options; + if (options != null) + { + for(var i = 0; i < options.length; i++){ + if(options[i].text === selectedName){ + options[i].selected = true; + } + }; + } +} + +/** + * DatePickerを設定 + */ +function enableDatePicker() { + // カレンダーの表示を日曜日始まりに変更 + flatpickr.l10ns.ja.firstDayOfWeek = 0; + $('.date_picker').flatpickr( + { + locale: 'ja', // 日本語カレンダー + allowInput: true, // 入力可能にする + dateFormat: 'Y/m/d' // 日付のフォーマットを修正 + } + ) +} + +// 日付入力チェック +// 引数:チェックするテキストボックスNo +function autoModifyDate($this){ + // 日付フォーマットチェック + + if($this.value === "" || + (!$this.value.match(/^\d{4}\/\d{2}\/\d{2}$/) && !$this.value.match(/^\d{4}\d{2}\d{2}$/))) + { + $this.value = ""; + return; + } + + /** @type { string }*/ + let strFormat = $this.value;; + // yyyyMMddの場合→yyyy/MM/dd + const datePatternMatches = strFormat.match(/^(\d{4})(\d{2})(\d{2})$/); + if (datePatternMatches){ + strFormat = `${datePatternMatches[1]}/${datePatternMatches[2]}/${datePatternMatches[3]}`; + } + // yyyy/00/00~yyyy/00/00の場合→yyyy/01/01~yyyy/12/31 + // yyyy/MM/00~yyyy/MM/01の場合→yyyy/MM/01~yyyy/MM/末日 + // 開始日の場合 + if ($this.name.includes('from')){ + strFormat = strFormat.replace("00/00", "01/01"); + strFormat = strFormat.replace("00", "01"); + } + // 終了日の場合 + else if ($this.name.includes('to')){ + strFormat = strFormat.replace("00/00", "12/31"); + const date = new Date(strFormat.slice(0, 4), strFormat.slice(5, 7), 0).getDate(); + strFormat = strFormat.replace("00", date.toString()); + } + $this.value = strFormat; +} + +// 他のページで共通化しよう +// ページが読み込まれたときにsendクラスのボタンを押せないようにする +// 初期値をdisabledにしときゃいい +$(function(){ + $(".send").prop('disabled',true); +}); + +// チェックボックス全選択関数 +// 条件:チェックボックスのクラス名に"selectedページ数"というのがついていること +// 条件:ボタンにクラス名 send がついていること +function allOn(){ + var selected = ".selected" + tableCurrentPage + " input.checkbox"; + $(selected).prop("checked", true); + $(".send").prop('disabled',false); +} + +// チェックボックス全解除関数 +// 条件:チェックボックスのクラス名に"selectedページ数"というのがついていること +// 条件:ボタンにクラス名 send がついていること +function allOff(pageCount){ + for (var i = 1; i <= pageCount; i++) { + var selected = ".selected" + i + " input.checkbox"; + $(selected).prop("checked", false); + } + $(".send").prop('disabled',true); +} + +// 検索結果のところのボタンをチェックが1個でも付いたら押せるようにして、チェックがなければ押せないようにする関数 +// 条件:チェックボックスのクラス名に"selectedページ数"というのがついていること +// 条件:ボタンにクラス名 send がついていること +function resultBtDisablead(){ + var selected = ".selected" + tableCurrentPage; + var cnt1 = $(selected + ' :checked').length; + selected += " input.checkbox"; + + if(cnt1 == 0) { + $(".send").prop('disabled',true); + } + else { + $(".send").prop('disabled',false); + } +} + +// 前のスペースを許さない入力チェック +function checkSpaceForm($this) +{ + var str=$this.value; + + while(str.match(/(^\s+)/g)) + { + str=str.replace(/(^\s+)/g, ""); + } + + $this.value=str; +} + +// あいまい検索用に半角の英数字と%以外を許さない入力チェック +function checkAimaiSearhForm($this) +{ + var str=$this.value; + while(str.match(/[^A-Z^a-z^0-9\%]/)) + { + str=str.replace(/[^A-Z^a-z^0-9\%]/,""); + } + $this.value=str; +} + +// 大文字小文字-以外を許さない入力チェック +function checkPassForm($this) +{ + var str=$this.value; + while(str.match(/[^ -\~]/)) + { + str=str.replace(/[^ -\~]/,""); + } + $this.value=str; +} + +// 廃止予定 +function DisplayErrorDialog(strMesssage) { + $("#errorTxt").html(strMesssage); + $("#error").dialog("open"); +} diff --git a/ecs/jskult-webapp/src/static/img/icon_modal_confirm.png b/ecs/jskult-webapp/src/static/img/icon_modal_confirm.png new file mode 100644 index 0000000000000000000000000000000000000000..e5c68f4d65ff568596586486b85c4589ebab310f GIT binary patch literal 13691 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 pOOTUE%$x?0|IecLkRy8c~Cw{{hI!7Ipvt literal 0 HcmV?d00001 diff --git a/ecs/jskult-webapp/src/static/img/icon_modal_error.png b/ecs/jskult-webapp/src/static/img/icon_modal_error.png new file mode 100644 index 0000000000000000000000000000000000000000..a4a0cbba34432b5b0e42090c25f8dc1cb1e56490 GIT binary patch literal 17229 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 literal 0 HcmV?d00001 diff --git a/ecs/jskult-webapp/src/static/lib/fixed_midashi.js b/ecs/jskult-webapp/src/static/lib/fixed_midashi.js new file mode 100644 index 00000000..f386d1b9 --- /dev/null +++ b/ecs/jskult-webapp/src/static/lib/fixed_midashi.js @@ -0,0 +1,165 @@ +/* + * FixedMidashi JavaScript Library, version 1.11 (2018/12/03) + * http://hp.vector.co.jp/authors/VA056612/fixed_midashi/ + * Copyright (C) 2012-2018 K.Koiso + */ + +/* Exsample */ +/******************************************************************* + + + + + + ... + + + + +
+
+ ... +*******************************************************************/ + +var FixedMidashi = new function() +{ + +var DISABLED = false; + +var ROWS = 1; +var COLS = 0; +var DIV_FULL_MODE = false; +var DIV_AUTO_SIZE = "both"; // both | width | height | none +var COPY_ID = true; +var BORDER_COLOR = null; +var BORDER_STYLE = null; +var BORDER_WIDTH = null; +var BOX_SHADOW = null; + +var DIV_MIN_WIDTH = 150; +var DIV_MIN_HEIGHT = 150; + +var DIV_BODY_SCROLL = 1; + +var RADIO_PREFIX = "_FIXED_HEADER_"; + +var POS_FIXED = 1; +var POS_ABSOLUTE = 2; +var POS_MIX = 3; +var _positionMode = -1; + +var h="checkbox",f="select-multiple",e="select-one",g="change",m="radio",l="INPUT",cc="fixed",bc="transparent",ac="0px",Zb="absolute",Yb="none",Xb="hidden",Wb="auto",Vb="",I="class",Ub="THEAD",Tb="scroll",n="resize",Sb=-1,Rb=null,Qb=true,Pb="both",Ob=false,Nb=this; + +var TIMER_WATCH_TABLESIZE=3e3,TID_HEADER="H",TID_NUMBER="N",TID_CORNER="C",PX="px",HEIGHT_MARGIN=10,MIN_SIZE=1,_isIE=Ob,_IEver=0,_isIE11=Ob,_isFirefox=Ob,_isOpera=Ob,_isSafari=Ob,_isChrome=Ob,_isMobile=Ob,_isBackCompat=Ob,_fixedHeaders=Rb,_fixedList=Rb,_body=Rb,_resizeTimerId=Rb,_execFlag=Ob,_IE_retryCount=0; + +Nb.create=function(){var f="_fixedhead";if(DISABLED)return Sb;if(!document.body.getBoundingClientRect)return -2;if(!window.addEventListener&&!window.attachEvent)return -3;var l=(new Date).getTime(),h=_fixedHeaders==Rb;if(!h)for(var b=0;b<_fixedHeaders.length;b++)_fixedHeaders[b].removeAllTables(Ob);for(var i=document.body.getElementsByTagName("TABLE"),e=[],b=0;b=0;if(_isIE){var j=a.indexOf("msie"),m=a.indexOf(";",j);_IEver=Number(a.substring(j+5,m))}if(!_isIE)_isIE11=a.indexOf("trident")>=0;_isFirefox=a.indexOf("firefox")>=0;_isOpera=a.indexOf("opera")>=0;_isSafari=a.indexOf("safari")>=0&&a.indexOf("chrome")<0;_isChrome=a.indexOf("chrome")>=0;_isMobile=a.indexOf("mobile")>=0;if(_isIE&&_rect(i[0]).right>=1e4&&_IE_retryCount<10){setTimeout(FixedMidashi.create,10);_IE_retryCount++;return -5}_isBackCompat=document.compatMode=="BackCompat";_body=_isBackCompat?document.body:document.documentElement;if(_isIE&&(_IEver<=7||_IEver<=9&&_isBackCompat))_positionMode=POS_ABSOLUTE;else if(_positionMode==Sb){_positionMode=POS_MIX;if(_isMobile)_positionMode=POS_FIXED}if(_isIE&&_IEver==8&&!_isBackCompat)_fixedList=new _FixedElementList;if(_isFirefox)g=_createObjectForFirefox();_isChrome&&_createObjectForChrome();_addEventListener(window,n,_onBodyResize);_isMobile&&_addEventListener(window,"orientationchange",_onBodyResize);(_isFirefox||_isSafari)&&!_isMobile&&TIMER_WATCH_TABLESIZE>=0&&setInterval(_checkZoom,TIMER_WATCH_TABLESIZE);_addEventListener(window,Tb,_onBodyScroll)}_fixedHeaders=[];for(var b=0;b=Math.min(B.length,11))d=0;if(j<0||j>=Math.min(r,11))j=0;if(!c||c<1||c>100)c=Sb;if(!b||b<1||b>100)b=Sb;if(F>1)s=Ob;if(m<0||m>1)m=1;var n=document.createElement("DIV");if(f!=Rb&&!_setStyle(n,"borderColor",f))f=Rb;if(g!=Rb&&!_setStyle(n,"borderStyle",g))g=Rb;if(h!=Rb&&!_setStyle(n,"borderWidth",h))h=Rb;if(i!=Rb&&!_setStyle(n,"boxShadow",i))i=Rb;if(l!=Rb&&!_setStyle(n,"backgroundColor",l))l=Rb;var w=Rb,x=Rb;if(p!=Rb&&!(_isIE&&(_IEver<=7||_IEver<=9&&_isBackCompat)))w=document.getElementById(p);if(q!=Rb&&!(_isIE&&(_IEver<=7||_IEver<=9&&_isBackCompat)))x=document.getElementById(q);var y=Rb;if(k.parentNode.tagName=="DIV"){var u=k.parentNode,o=u.currentStyle||document.defaultView.getComputedStyle(u,Vb);if(o.overflowX==Wb||o.overflowX==Tb||(o.overflowY==Wb||o.overflowY==Tb))y=u}return new _FixedHeader(y,k,r,d,j,c,b,s,v,z,C,f,g,h,i,l,m,w,x)}function + _onBodyScroll(){for(var a=0;a<_fixedHeaders.length;a++)_fixedHeaders[a].onBodyScroll()}function + _onBodyResize(){if(_execFlag)return;_resizeTimerId!=Rb&&clearTimeout(_resizeTimerId);var a=function(){_execute(n)}; +_resizeTimerId=setTimeout(a,500)}function + _execute(b){_execFlag=Qb;_resizeTimerId=Rb;for(var a=0;a<_fixedHeaders.length;a++)if(b==n)_fixedHeaders[a].initOnResize();else _fixedHeaders[a].init();_resizeSourceDiv();for(var a=0;a<_fixedHeaders.length;a++)_fixedHeaders[a].main();_execFlag=Ob}function + _resizeSourceDiv(){for(var b=Ob,a=0;a<_fixedHeaders.length;a++)if(_fixedHeaders[a].hideAllDivs(Qb))b=Qb;if(!b)return;for(var a=0;a<_fixedHeaders.length;a++)_fixedHeaders[a].resizeSourceDiv1();var d=document.body.style.overflowX,c=document.body.currentStyle||document.defaultView.getComputedStyle(document.body,Vb);if(_body.scrollWidth<=_body.clientWidth&&c.overflowX!=Tb)document.body.style.overflowX=Xb;for(var a=0;a<_fixedHeaders.length;a++)_fixedHeaders[a].resizeDivHeight();for(var a=0;a<_fixedHeaders.length;a++)_fixedHeaders[a].resizeDivWidth();document.body.style.overflowX=d;for(var a=0;a<_fixedHeaders.length;a++)_fixedHeaders[a].resizeSourceDiv2();for(var a=0;a<_fixedHeaders.length;a++)_fixedHeaders[a].hideAllDivs(Ob)}function + _FixedHeader(a,d,E,p,y,v,B,xb,Z,F,G,J,K,L,zb,Q,bb,M,N){var o="CLIENT",j="TBODY",g="1px",q=this,f=a!=Rb,b=Rb,c=Rb,e=Rb,rb=0,qb=0,hb=Vb,gb=Vb,U=Rb,W=Rb,u=[],yb=[],Y=0,fb=0,Cb=d.style.zIndex?d.style.zIndex:0,r=Ob,x=Ob,k=0,n=0,R=0,S=0,ib=0,mb=0,lb=0,ob=0,eb=0,db=0,X=0,Ib=d.style.display,jb=Sb,nb=Sb,m=Rb,z=Sb,A=Sb,i=0,l=0,V=Rb,T=Rb; +q.init=function(){if(Q==Rb){var e=f?a:d,b=_getBackgroundColor(e);if(b==Rb){var c=e.parentNode;while(c){b=_getBackgroundColor(c);if(b!=Rb)break;if(c.tagName=="HTML")break;c=c.parentNode}if(b==Rb)b="white"}Q=b}if(f){_addEventListener(a,Tb,ab);Jb();X=_offsetWidth(d)}if(M)i=Math.max(_rect(M).bottom,0);if(N)l=Math.max(_rect(N).right,0);wb()}; +q.initOnResize=function(){A=Sb;z=Sb;nb=Sb;jb=Sb;if(M)i=Math.max(_rect(M).bottom,0);if(N)l=Math.max(_rect(N).right,0);if(f){if(r||v>0)a.style.width=Vb;if(x||B>0)a.style.height=Vb;X=_offsetWidth(d)}wb()}; +q.removeAllTables=function(d){b&&w(b);c&&w(c);e&&w(e);b=Rb;c=Rb;e=Rb;if(f){_removeEventListener(a,Tb,ab);if(d){if(r||v>0)a.style.width=Vb;if(x||B>0)a.style.height=Vb}}}; +q.hideAllDivs=function(h){if(!f)return Ob;var d=b==Rb?Rb:b.parentNode,g=c==Rb?Rb:c.parentNode,j=e==Rb?Rb:e.parentNode;if(h){d&&I(d,DIV_MIN_WIDTH);g&&H(g,DIV_MIN_HEIGHT)}var i=h?Yb:Vb;if(d)d.style.display=i;if(g)g.style.display=i;if(j)j.style.display=i;(r||x||v>0||B>0)&&Fb(h);if(h){r&&t(a,DIV_MIN_WIDTH);x&&D(a,DIV_MIN_HEIGHT)}return Qb}; +q.resizeSourceDiv1=function(){if(!f)return;var e=m!=Rb?m:d;if(v>0){k=_body.clientWidth/100*v;k=Math.max(k,DIV_MIN_WIDTH);var c=_offsetWidth(e)+eb;c=Math.min(c,k);t(a,c)}if(B>0){n=_body.clientHeight/100*B;n=Math.max(n,DIV_MIN_HEIGHT);var b=_offsetHeight(e)+db;b=Math.min(b,n);D(a,b)}}; +q.resizeDivHeight=function(){if(!f)return;if(!x)return;if(_body.scrollHeight>_body.clientHeight){var e=Math.max(_body.scrollHeight-_body.clientHeight,0);a.style.height=Math.max(_body.clientHeight-30,MIN_SIZE)+PX;var g=Math.max(_body.scrollHeight-_body.clientHeight,0);n=_offsetHeight(a)-(g-e)}else{a.style.height=_body.clientHeight+PX;var h=Math.max(_body.scrollHeight-_body.clientHeight,0);n=_offsetHeight(a)-h}n--;n=Math.max(n,DIV_MIN_HEIGHT);var c=m!=Rb?m:d,b=_offsetHeight(c)+db;b=Math.min(b,n);if(xb)b=n;D(a,b)}; +q.resizeDivWidth=function(){if(!f)return;if(!r)return;if(_body.scrollWidth>_body.clientWidth){var e=Math.max(_body.scrollWidth-_body.clientWidth,0);a.style.width=Math.max(_body.clientWidth-16,MIN_SIZE)+PX;var g=Math.max(_body.scrollWidth-_body.clientWidth,0);k=_offsetWidth(a)-(g-e)}else{a.style.width=_body.clientWidth+PX;var h=Math.max(_body.scrollWidth-_body.clientWidth,0);k=_offsetWidth(a)-h}k--;k=Math.max(k,DIV_MIN_WIDTH);var c=m!=Rb?m:d,b=_offsetWidth(c)+eb;b=Math.min(b,k);if(xb)b=k;if(_isIE)b--;t(a,b)}; +q.resizeSourceDiv2=function(){if(!f)return;var b=m!=Rb?m:d;if((r||v>0)&&a.scrollWidth>a.clientWidth&&_offsetWidth(a)0)&&a.scrollHeight>a.clientHeight&&_offsetHeight(a)0)&&X>_offsetWidth(b)&&_offsetWidth(a)=1||Math.abs(_offsetHeight(d)-qb)>=1||_colsWidthList(_cells(d.rows[0]))!=hb||_rowsHeightList(d.rows)!=gb)return Qb}; +q.main=function(){var h=_offsetWidth(d),g=_offsetHeight(d),b=rb!=h,a=qb!=g;rb=h;qb=g;var c=_colsWidthList(_cells(d.rows[0]));if(hb!=c){hb=c;b=Qb}var e=_rowsHeightList(d.rows);if(gb!=e){gb=e;a=Qb}var i=Gb(b||a);(i||b||a)&&Hb();f&&Lb();if(f){ub();ab()}else kb(Qb)}; +function Gb(l){if(l){b!=Rb&&w(b);c!=Rb&&w(c);e!=Rb&&w(e);b=Rb;c=Rb;e=Rb}var i=Ob,j=d.rows,k=f?d.parentNode:_body,h=Ob,g=Ob;if(f){h=a.clientHeight0&&h)if(_rowsHeight(j,p)+30>=k.clientHeight)h=Ob;if(y>0&&g){var m=_cells(j[0]);if(_colsWidth(m,y)+30>=k.clientWidth)g=Ob}if(y>0&&g){if(c==Rb){c=pb(TID_NUMBER,j.length,y);if(c!=Rb)i=Qb}}else if(c!=Rb){w(c);c=Rb}if(p>0&&h){if(b==Rb){b=pb(TID_HEADER,p,E);if(b!=Rb)i=Qb}}else if(b!=Rb){w(b);b=Rb}if(b!=Rb&&c!=Rb){if(e==Rb){e=pb(TID_CORNER,p,y);i=Qb}}else if(e!=Rb){w(e);e=Rb}return i}function + Hb(){if(b!=Rb){t(b,_offsetWidth(d));cb(b)}if(c!=Rb){var g=_colsWidth(_cells(d.rows[0]),y),f=_colsWidth(_cells(c.rows[0]),y),a=f-g;a!=0&&t(c,_offsetWidth(c)-a);cb(c);D(c,_offsetHeight(d))}if(e!=Rb){t(e,_offsetWidth(c));cb(e);D(e,_offsetHeight(b))}}function + Lb(){var j=1,d=b==Rb?Rb:b.parentNode,f=c==Rb?Rb:c.parentNode,g=e==Rb?Rb:e.parentNode;d!=Rb&&s(d);f!=Rb&&s(f);g!=Rb&&s(g);if(d!=Rb){I(d,a.clientWidth);H(d,_offsetHeight(b));var i=_rect(b).bottom-_rect(d).bottom;i>0&&H(d,_offsetHeight(b)+i);d.clientHeight>=a.clientHeight&&H(d,a.clientHeight-j)}if(f!=Rb){H(f,a.clientHeight);I(f,_offsetWidth(c));var h=_rect(c).right-_rect(f).right;h>0&&I(f,_offsetWidth(c)+h);f.clientWidth>=a.clientWidth&&I(f,a.clientWidth-j)}if(g!=Rb){H(g,d.clientHeight);I(g,f.clientWidth)}}function + Jb(){var b=a.cloneNode(Ob),c=d.cloneNode(Ob);b.style.position=Zb;b.style.left=ac;b.style.top=ac;b.style.minWidth=g;b.style.minHeight=g;a.parentNode.appendChild(b);var e=document.createElement("DIV");e.style.position=Zb;e.style.left=ac;e.style.top=ac;a.parentNode.appendChild(e);var i=_rect(a),f=_rect(b);S=i.top-f.top;R=i.left-f.left;c.style.width="50px";c.style.height="50px";var k=document.createElement(j),m=document.createElement("TR"),l=document.createElement("TD");l.appendChild(document.createTextNode("x"));m.appendChild(l);k.appendChild(m);c.appendChild(k);var o=b.offsetWidth,n=b.offsetHeight;b.appendChild(c);r=b.offsetWidth!=o;x=b.offsetHeight!=n;if(r){if(v>0||Z!=Pb&&Z!="width")r=Ob}else v=Sb;if(x){if(B>0||Z!=Pb&&Z!="height")x=Ob}else B=Sb;eb=_offsetWidth(b)-_offsetWidth(c);db=_offsetHeight(b)-_offsetHeight(c);var h=_rect(e);mb=f.top-h.top;ib=f.left-h.left;ob=_rect(c).top-f.top;lb=_rect(c).left-f.left;b.parentNode.removeChild(b);e.parentNode.removeChild(e)}function + wb(){if(p==0&&y==0)return;var a=d.cloneNode(Ob);a.style.position=Zb;a.style.left=ac;a.style.top=ac;a.style.width=Wb;a.style.height=Wb;a.width=Vb;a.height=Vb;var i,m=_getElementByTagName(d,Ub);if(m==Rb)m=_getElementByTagName(d,j);if(m!=Rb)i=m.cloneNode(Ob);else i=document.createElement(j);a.appendChild(i);d.parentNode.appendChild(a);for(var x=Kb(),v=p>0?p:1,t=d.rows,q=0,c=0;c0)u[h]=Math.min(u[h],k);fb=Math.max(u[h],fb)}a.parentNode.removeChild(a)}function + Kb(){for(var g=p>0?p:1,l=d.rows,f=new Array(g),b=0;b=i.length)break;var e=i[h];if(e.rowSpan>=2&&e.colSpan>=2)for(var c=0;c=g)break;for(var j=0;j=2)for(var c=1;c=g)break;f[b+c][a]=Ob}if(e.colSpan>=2)for(var c=1;c=i.length)break;var e=i[h];e.$FXH_COLINDEX=a;if(e.colSpan==1)k[a]=Qb;h++}return k}function + pb(e,v,x){var n=" fixed_header_display_none_at_print",c=d.cloneNode(Ob),r=_getElementByTagName(d,"CAPTION"),s=_getElementByTagName(d,Ub),u=_getElementByTagName(d,j),k=Rb,p=Rb,o=Rb;if(r!=Rb){k=r.cloneNode(Qb);k.style.backgroundColor=Q;k.style.overflow=Xb;if(e!=TID_HEADER){k.innerHTML=" ";k.style.height=_offsetHeight(r)+PX;k.style.backgroundColor=bc}c.appendChild(k)}var t=0;if(s!=Rb){p=s.cloneNode(Ob);c.appendChild(p);t=s.rows.length}if(u!=Rb&&t=t.length)break;var a=t[l];l++;if(c+a.rowSpan>x)return Ob;f[e]=c+a.rowSpan;if(a.colSpan>=2){for(var h=1;hq)return Ob}_radioCtl(a,"backup");var j=a.cloneNode(Qb);_radioCtl(a,"restore");_linkElement(j,a,g,F,G,Qb);b.appendChild(j);try{var o=c+"."+a.cellIndex;if(u[o]!=undefined){var D=u[o]+yb[o];j.style.width=_offsetWidth(a)-D+PX}else if(_isIE&&_IEver<=8&&a.colSpan>=2)j.style.width=a.clientWidth-fb+PX}catch(E){}var i=j.style;if(c+a.rowSpan==p&&g!=TID_NUMBER){if(J!=Rb)i.borderBottomColor=J;if(K!=Rb)i.borderBottomStyle=K;if(L!=Rb)i.borderBottomWidth=L}if(e+a.colSpan==y&&g!=TID_HEADER){if(J!=Rb)i.borderRightColor=J;if(K!=Rb)i.borderRightStyle=K;if(L!=Rb)i.borderRightWidth=L}e+=a.colSpan}if(_isIE&&_IEver<=9&&l==0){b.style.height=ac;var A=_rect(k).bottom,m=b.parentNode;if(m.tagName!="TABLE")m=m.parentNode;var r=m.rows,n=c-1;while(r[n].style.height==ac)n--;var B=A-_rect(s[n]).top;r[n].style.height=B-Y+PX}}_radioCtl(v,"sync");return Qb}function + s(b){if(b.style.position==cc)return;var f=_rect(a),e=_rect(b),d=e.top-f.top,c=e.left-f.left;if(_isIE){if(d==Sb&&b.$TOP_DIFF==1)d=0;else b.$TOP_DIFF=d;if(c==Sb&&b.$LEFT_DIFF==1)c=0;else b.$LEFT_DIFF=c}if(Math.abs(d)>=1)b.style.top=_pixel(b.style.top)-d+PX;if(Math.abs(c)>=1)b.style.left=_pixel(b.style.left)-c+PX}function + w(a){if(f)a=a.parentNode;_unlinkElement(a);a.parentNode&&a.parentNode.removeChild(a)} +q.onBodyScroll=function(){if(M)i=Math.max(_rect(M).bottom,0);if(N)l=Math.max(_rect(N).right,0);if(f){ub();return}if(_positionMode==POS_ABSOLUTE){if(!_isMobile){if(_getBodyScrollTop()!=A){h(b,Ob);h(e,Ob)}if(_getBodyScrollLeft()!=z){h(c,Ob);h(e,Ob)}}V!=Rb&&clearTimeout(V);V=setTimeout(kb,200)}else kb()}; +function kb(k){V=Rb;var j=_getBodyScrollTop()!=A,g=_getBodyScrollLeft()!=z;A=_getBodyScrollTop();z=_getBodyScrollLeft();if(j&&g)k=Qb;var f=_rect(d),o=b!=Rb&&f.top=b.offsetHeight+i,p=c!=Rb&&f.left=c.offsetWidth+l;b!=Rb&&h(b,o);c!=Rb&&h(c,p);e!=Rb&&h(e,o&&p);if(_positionMode==POS_MIX&&b!=Rb){var a=b.style;if(k||j&&a.position==Zb){a.position=cc;a.left=f.left+PX;a.top=i+PX;if(e!=Rb)e.style.top=a.top;O(Ob)}else if(!j&&g&&a.position==cc){a.position=Zb;a.left=z+f.left+PX;a.top=A+i+PX;vb()}}if(_positionMode==POS_MIX&&c!=Rb){var a=c.style;if(k||g&&a.position==Zb){a.position=cc;a.left=l+PX;a.top=f.top+PX;P(Ob)}else if(!g&&j&&a.position==cc){a.position=Zb;a.left=z+l+PX;a.top=A+f.top+PX;tb()}}if(_positionMode==POS_FIXED){if(b!=Rb&&g)b.style.left=f.left+PX;if(c!=Rb&&j)c.style.top=f.top+PX;if(b!=Rb&&b.style.top!=i+PX){b.style.top=i+PX;if(e!=Rb)e.style.top=b.style.top}}if(_positionMode==POS_ABSOLUTE&&k){if(b!=Rb)b.style.left=z+f.left+PX;if(c!=Rb)c.style.top=A+f.top+PX}if(_positionMode==POS_ABSOLUTE&&(j||g)){var n,m,q=_isMobile?1:4;if(b!=Rb&&j){b.style.top=i+A-_offsetHeight(b)+PX;if(e!=Rb)e.style.top=b.style.top;n=_offsetHeight(b)/q}if(c!=Rb&&g){c.style.left=l+z-_offsetWidth(c)+PX;if(e!=Rb)e.style.left=c.style.left;m=_offsetWidth(c)/q}T!=Rb&&clearTimeout(T);sb(j,g,n,m)}b!=Rb&&g&&O(Ob);c!=Rb&&j&&P(Ob)}function + sb(m,j,k,h){T=Rb;var g=_getBodyScrollTop()+i,f=_getBodyScrollLeft()+l,d=g,a=f;if(b!=Rb&&m){d=_pixel(b.style.top)+k;if(k>0)d=Math.min(d,g);else d=Math.max(d,g);b.style.top=d+PX;if(e!=Rb)e.style.top=b.style.top}if(c!=Rb&&j){a=_pixel(c.style.left)+h;if(h>0)a=Math.min(a,f);else a=Math.max(a,f);c.style.left=a+PX;if(e!=Rb)e.style.left=c.style.left}if(g==d&&f==a){if(b!=Rb&&m){vb();if(e!=Rb)e.style.top=b.style.top}if(c!=Rb&&j){tb();if(e!=Rb)e.style.left=c.style.left}return}var n=function(){sb(m,j,k,h)}; +T=setTimeout(n,20)}function + ub(){if(DIV_BODY_SCROLL==0)return;if(_positionMode==POS_ABSOLUTE)return;var f=b==Rb?Rb:b.parentNode,g=c==Rb?Rb:c.parentNode,j=e==Rb?Rb:e.parentNode,k=_rect(a),o=_rect(d),p=0;if(_isIE11)p=1;var m=Ob,n=Ob;if(f&&(DIV_BODY_SCROLL==2||a.scrollHeight-p<=a.clientHeight)){var q=Math.min(k.bottom,o.bottom);if(k.top=_offsetHeight(b)){if(f.style.position!=cc){f.style.position=cc;f.style.top=i-mb-ob+PX}f.style.left=k.left-ib+PX;h(f,Qb);m=Qb}else{if(f.style.position!=Zb){f.style.position=Zb;f.style.top=S+PX;f.style.left=R+PX}h(f,a.scrollTop>0);s(f)}C(f)&&O(Ob)}if(g&&(DIV_BODY_SCROLL==2||a.scrollWidth<=a.clientWidth)){var r=Math.min(k.right,o.right);if(k.left=_offsetWidth(c)){if(g.style.position!=cc){g.style.position=cc;g.style.left=l-ib-lb+PX}g.style.top=k.top-mb+PX;h(g,Qb);n=Qb}else{if(g.style.position!=Zb){g.style.position=Zb;g.style.top=S+PX;g.style.left=R+PX}h(g,a.scrollLeft>0);s(g)}C(g)&&P(Ob)}if(j){if(m||n){if(j.style.position!=cc)j.style.position=cc;if(m)j.style.top=f.style.top;else j.style.top=g.style.top;if(n)j.style.left=g.style.left;else j.style.left=f.style.left}else if(j.style.position!=Zb){j.style.position=Zb;j.style.top=S+PX;j.style.left=R+PX;s(j)}h(j,C(f)&&C(g))}}function + ab(){var d=b==Rb?Rb:b.parentNode,f=c==Rb?Rb:c.parentNode,g=e==Rb?Rb:e.parentNode,i=d&&d.style.position==cc,k=f&&f.style.position==cc;h(d,a.scrollTop>0||i);h(f,a.scrollLeft>0||k);h(g,C(d)&&C(f));d!=Rb&&s(d);f!=Rb&&s(f);g!=Rb&&s(g);var j=a.scrollLeft!=jb,l=a.scrollTop!=nb;jb=a.scrollLeft;nb=a.scrollTop;if(d!=Rb&&j){d.$FXH_SCROLL_LEFT=a.scrollLeft;d.scrollLeft=a.scrollLeft;d.scrollLeft>0&&O(Ob)}if(f!=Rb&&l){f.$FXH_SCROLL_TOP=a.scrollTop;f.scrollTop=a.scrollTop;f.scrollTop>0&&P(Ob)}}function + Eb(b,c){if(c==TID_HEADER){if(Math.abs(b.scrollLeft-b.$FXH_SCROLL_LEFT)<5)return}else if(Math.abs(b.scrollTop-b.$FXH_SCROLL_TOP)<5)return;if(c==TID_HEADER)a.scrollLeft=b.scrollLeft;else a.scrollTop=b.scrollTop}function + vb(){if(b==Rb)return;var a=_rect(b);if(a.top<=i)return;b.style.top=i+_pixel(b.style.top)-a.top+PX}function + tb(){if(c==Rb)return;var a=_rect(c);if(a.left<=l)return;c.style.left=l+_pixel(c.style.left)-a.left+PX}function + O(q){if(b==Rb)return;if(!q){U!=Rb&&clearTimeout(U);U=setTimeout(function(){O(Qb)},200);return}U=Rb;var h,j;if(f){h=_rect(a).left;j=h+a.clientWidth}else{h=0;j=_body.clientWidth}if(C(c))h+=_offsetWidth(c);for(var m=_cells(d.rows[0]),o=_cells(b.rows[0]),e=0,l=0,i=0;ij)break;var p=_rect(o[i]).left;e+=p-k;l++}if(l==0)return;if(e==0)return;e=e/l;e=Math.round(e);if(e==0)return;if(f){var g=b.parentNode;if(g.style.position==cc){var n=_pixel(g.style.left)-e;g.style.left=n+PX}else{g.$FXH_SCROLL_LEFT=g.scrollLeft+e;g.scrollLeft+=e}}else{var n=_pixel(b.style.left)-e;b.style.left=n+PX}}function + P(p){if(c==Rb)return;if(_isOpera)return;if(!p){W!=Rb&&clearTimeout(W);W=setTimeout(function(){P(Qb)},200);return}W=Rb;var h,j;if(f){h=_rect(a).top;j=h+a.clientHeight}else{h=0;j=_body.clientHeight}if(C(b))h+=_offsetHeight(b);for(var n=d.rows,m=c.rows,e=0,l=0,i=0;ij)break;if(_cells(m[i]).length==0)continue;var q=_rect(m[i]).top;e+=q-k;l++}if(l==0)return;if(e==0)return;e=e/l;e=Math.round(e);if(e==0)return;if(f){var g=c.parentNode;if(g.style.position==cc){var o=_pixel(g.style.top)-e;g.style.top=o+PX}else{g.$FXH_SCROLL_TOP=g.scrollTop+e;g.scrollTop+=e}}else{var o=_pixel(c.style.top)-e;c.style.top=o+PX}}function + C(a){return a!=Rb&&a.style.visibility=="visible"}function + h(a,b){if(a==Rb)return;var c=b?"visible":Xb;if(a.style.visibility==c)return;a.style.visibility=c;if(_isIE)a.style.zIndex=b?Cb:Cb-1;if(_isOpera)a.style.opacity=b?bb:0;if(b&&bb<1){a.style.opacity=bb;a.style.filter="alpha(opacity="+bb*100+")"}}function + Fb(b){if(!_isIE&&!_isIE11&&!_isFirefox&&!_isOpera)return;if(b){m=Db();a.appendChild(m);t(m,_offsetWidth(d));D(m,_offsetHeight(d));d.style.display=Yb}else{d.style.display=Ib;a.removeChild(m);m=Rb}}function + Db(){var a=d.cloneNode(Ob),b=document.createElement(j),e=document.createElement("TR"),c=document.createElement("TD");c.appendChild(document.createTextNode("dummy"));e.appendChild(c);b.appendChild(e);a.appendChild(b);return a}function + I(a,b){Bb(a,o,b)}function + t(a,b){Bb(a,"OFFSET",b)}function + H(a,b){Ab(a,o,b)}function + D(a,b){Ab(a,"OFFSET",b)}function + Bb(a,g,f){var b=f;if(a.$FXH_PADDING_WIDTH!=undefined)b-=a.$FXH_PADDING_WIDTH;for(var d,c,e=0;e<2;e++){if(b0)b+=",";b+=c[a].offsetWidth}return b}function + _rowsHeightList(c){for(var b=Vb,a=0;a0)b+=",";b+=c[a].offsetHeight}return b}function + _trHeight(a){if(_isIE&&_IEver==8&&!_isBackCompat)return a.clientHeight;else return _offsetHeight(a)}function + _cells(e){var a=e.childNodes;if(a.length==0)return a;for(var b=[],d=0;dまたはパスワードが違います。', + # 機能を利用できないとかでいいんじゃ + LOGOUT_REASON_BATCH_PROCESSING: '日次バッチ処理中なので、
生物由来データ参照は使用出来ません。', + LOGOUT_REASON_BATCH_PROCESSING_FOR_MAINTE: '日次バッチ処理中のため、
マスタ-メンテは使用出来ません。', + # 使ってなさそう + LOGOUT_REASON_NOT_LOGIN: 'Loginしてからページにアクセスしてください。', + LOGOUT_REASON_SESSION_EXPIRED: 'セッションが切れています。
再度Loginしてください。', + LOGOUT_REASON_UNEXPECTED: '予期しないエラーが発生しました。
再度Loginするか、
管理者に問い合わせてください。' +} diff --git a/ecs/jskult-webapp/src/system_var/environment.py b/ecs/jskult-webapp/src/system_var/environment.py new file mode 100644 index 00000000..aed9916c --- /dev/null +++ b/ecs/jskult-webapp/src/system_var/environment.py @@ -0,0 +1,22 @@ +import os + +COGNITO_AUTH_DOMAIN = os.environ['COGNITO_AUTH_DOMAIN'] +AUTHORIZE_ENDPOINT = os.environ['AUTHORIZE_ENDPOINT'] +TOKEN_ENDPOINT = os.environ['TOKEN_ENDPOINT'] +COGNITO_IDENTITY_PROVIDER = os.environ['COGNITO_IDENTITY_PROVIDER'] +COGNITO_REDIRECT_URI = os.environ['COGNITO_REDIRECT_URI'] +COGNITO_USER_POOL_ID = os.environ['COGNITO_USER_POOL_ID'] +COGNITO_CLIENT_ID = os.environ['COGNITO_CLIENT_ID'] +COGNITO_CLIENT_SECRET = os.environ['COGNITO_CLIENT_SECRET'] +AWS_REGION = os.environ['AWS_REGION'] +SESSION_TABLE_NAME = os.environ['SESSION_TABLE_NAME'] +BIO_ACCESS_LOG_BUCKET = os.environ['BIO_ACCESS_LOG_BUCKET'] + +DB_HOST = os.environ['DB_HOST'] +DB_PORT = int(os.environ['DB_PORT']) +DB_USERNAME = os.environ['DB_USERNAME'] +DB_PASSWORD = os.environ['DB_PASSWORD'] +DB_SCHEMA = os.environ['DB_SCHEMA'] + +BIO_SEARCH_RESULT_MAX_COUNT = int(os.environ['BIO_SEARCH_RESULT_MAX_COUNT']) +SESSION_EXPIRE_MINUTE=int(os.environ['SESSION_EXPIRE_MINUTE']) \ No newline at end of file diff --git a/ecs/jskult-webapp/src/templates/__init__.py b/ecs/jskult-webapp/src/templates/__init__.py new file mode 100644 index 00000000..d3c1713f --- /dev/null +++ b/ecs/jskult-webapp/src/templates/__init__.py @@ -0,0 +1,6 @@ +import os + +from fastapi.templating import Jinja2Templates + +templates_path = os.path.dirname(os.path.relpath(__file__)) +templates = Jinja2Templates(templates_path) diff --git a/ecs/jskult-webapp/src/templates/_header.html b/ecs/jskult-webapp/src/templates/_header.html new file mode 100644 index 00000000..ae41e99c --- /dev/null +++ b/ecs/jskult-webapp/src/templates/_header.html @@ -0,0 +1,14 @@ + + + + +{{subtitle}} + + + + + + + + + diff --git a/ecs/jskult-webapp/src/templates/_modal.html b/ecs/jskult-webapp/src/templates/_modal.html new file mode 100644 index 00000000..6477db10 --- /dev/null +++ b/ecs/jskult-webapp/src/templates/_modal.html @@ -0,0 +1,40 @@ +{% with + icon_data = { + 'info': { + 'alert': 'alert-primary', + 'icon_src': '/static/img/icon_modal_confirm.png' + }, + 'warning': { + 'alert': 'alert-warning', + 'icon_src': '/static/img/icon_modal_error.png' + } + } +%} + +{% endwith %} diff --git a/ecs/jskult-webapp/src/templates/bioSearchList.html b/ecs/jskult-webapp/src/templates/bioSearchList.html new file mode 100644 index 00000000..b5984481 --- /dev/null +++ b/ecs/jskult-webapp/src/templates/bioSearchList.html @@ -0,0 +1,462 @@ + + + + {% with subtitle = bio.subtitle %} + {% include '_header.html' %} + {% endwith %} + + + + + + + + + +

生物由来検索一覧

+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
卸: + + データ種別: + + 処理日: + + ~ + +
ロット番号: + + データ区分: + + 製品: + +
発伝年月日: + + ~ + + + + + + + +
+ + +
+ + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
データ種別伝票管理NO処理日卸コード卸サブコード卸名卸組織コード伝票番号発伝年月日卸取引区分取引区分名製品コード統一商品コード商品名卸報告商品名納入先コード納入先名卸報告納入先名納入先住所卸報告納入先住所ロット番号数量有効期限データ区分エラー詳細種別訂正前伝票管理NO修正者修正日時施設コード施設名施設住所施設電話番号Veeva卸コードVeeva卸組織コード卸組織名Veeva取引区分コード移行
+ {% if bio.is_form_submitted() and bio.is_data_overflow_max_length() %} +
+ 検索結果が最大件数を超えました。検索条件を見直しして下さい。 +
+ {% endif %} + {% if bio.is_form_submitted() and bio.is_data_empty() %} +
+ 対象のデータが存在しません。 +
+ {% endif %} +
+
+ + + + + + {% with + modal_id='modal_xlsx', + modal_title='確認', + message='生物由来卸販売データ一覧をExcel出力しますか?', + icon_key='info', + modal_close_event='', + buttons = [ + { + 'id': 'excel_confirm_ok', + 'class': 'btn btn-primary', + 'text': 'OK', + 'onclick_event': 'download("filename", "xlsx")' + }, + { + 'id': 'excel_confirm_cancel', + 'class': 'btn btn-secondary', + 'dismiss': 'modal', + 'text': 'Cancel', + } + ] + %} + {% include '_modal.html' %} + {% endwith %} + + {% with + modal_id='modal_csv', + modal_title='確認', + message='生物由来卸販売データ一覧をCSV出力しますか?', + icon_key='info', + modal_close_event='', + buttons = [ + { + 'id': 'csv_confirm_ok', + 'class': 'btn btn-primary', + 'text': 'OK', + 'onclick_event': 'download("filename", "csv")' + }, + { + 'id': 'csv_confirm_cancel', + 'class': 'btn btn-secondary', + 'dismiss': 'modal', + 'text': 'Cancel', + } + ] + %} + {% include '_modal.html' %} + {% endwith %} + + {% with + modal_id='ErrorModal_AWS', + modal_title='エラー', + message='AWS環境に異常が発生しました。管理者にお問い合わせください。', + icon_key='warning', + modal_close_event='location.href="/logout?reason="', + buttons = [ + { + 'id': 'error_modal_aws', + 'class': 'btn btn-primary', + 'text': 'OK', + 'onclick_event': 'location.href="/logout?reason=''"' + } + ] + %} + {% include '_modal.html' %} + {% endwith %} + + {% with + modal_id='ErrorModal_DB', + modal_title='エラー', + message='DB接続に失敗しました。管理者にお問い合わせください。', + icon_key='warning', + modal_close_event='location.href="/logout?reason="', + buttons = [ + { + 'id': 'error_modal_db', + 'class': 'btn btn-primary', + 'text': 'OK', + 'onclick_event': 'location.href="/logout?reason=''"' + } + ] + %} + {% include '_modal.html' %} + {% endwith %} + + + {% with + modal_id='ErrorModal_Unexpected', + modal_title='エラー', + message='サーバーエラーが発生しました。管理者にお問い合わせください。', + icon_key='warning', + modal_close_event='location.href="/logout?reason="', + buttons = [ + { + 'id': 'error_modal_unexpected', + 'class': 'btn btn-primary', + 'text': 'OK', + 'onclick_event': 'location.href="/logout?reason=''"' + } + ] + %} + {% include '_modal.html' %} + {% endwith %} + + +
+
+ 出力中... +
+
+ + \ No newline at end of file diff --git a/ecs/jskult-webapp/src/templates/logout.html b/ecs/jskult-webapp/src/templates/logout.html new file mode 100644 index 00000000..ed17c630 --- /dev/null +++ b/ecs/jskult-webapp/src/templates/logout.html @@ -0,0 +1,75 @@ + + + + {% with subtitle = logout.subtitle %} + {% include '_header.html' %} + {% endwith %} + + + +
+

MeDaCA

+

+

+ {% autoescape False %} + {{logout.reason}} + {% endautoescape %} +

+ +


+

{{logout.link_text}}

+ + + + +
+ + \ No newline at end of file diff --git a/ecs/jskult-webapp/src/templates/maintlogin.html b/ecs/jskult-webapp/src/templates/maintlogin.html new file mode 100644 index 00000000..5c2ac157 --- /dev/null +++ b/ecs/jskult-webapp/src/templates/maintlogin.html @@ -0,0 +1,27 @@ + + + + {% with subtitle = mainte_login.subtitle %} + {% include '_header.html' %} + {% endwith %} + + + +
+

+ MeDaCA
+ Mainte Login +

+

+
+
+ + +
+ + + +
+
+ + \ No newline at end of file diff --git a/ecs/jskult-webapp/src/templates/menu.html b/ecs/jskult-webapp/src/templates/menu.html new file mode 100644 index 00000000..beda056b --- /dev/null +++ b/ecs/jskult-webapp/src/templates/menu.html @@ -0,0 +1,36 @@ + + + + {% with subtitle = menu.subtitle %} + {% include '_header.html' %} + {% endwith %} + + + +
+

MeDaCA
機能メニュー

+

+ {% if menu.is_available_ult_doctor_menu() %} + Ultmarc照会(医師)

+ {% endif %} + {% if menu.is_available_ult_inst_menu() %} + Ultmarc照会(施設)

+ {% endif %} + {% if menu.is_available_bio_menu() %} + {% if not menu.is_batch_processing() %} + 生物由来データ参照

+ {% else %} +
生物由来データ参照は
日次バッチ処理中のため利用出来ません
+ {% endif %} + {% endif %} + {% if menu.is_available_master_maintenance_menu() %} + {% if not menu.is_batch_processing() %} + マスターメンテメニュー

+ {% else %} +
マスターメンテメニューは
日次バッチ処理中のため利用出来ません
+ {% endif %} + {% endif %} +

Logout +
+ + diff --git a/ecs/jskult-webapp/src/util/sanitize.py b/ecs/jskult-webapp/src/util/sanitize.py new file mode 100644 index 00000000..781dd593 --- /dev/null +++ b/ecs/jskult-webapp/src/util/sanitize.py @@ -0,0 +1,14 @@ +"""サニタイズ処理用デコレーター""" + +import html + + +def sanitize(cls): + class SanitizedClass(cls): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + for prop_name, prop_value in self.__dict__.items(): + if isinstance(prop_value, str): + sanitized_value = html.escape(prop_value, quote=True) + setattr(self, prop_name, sanitized_value) + return SanitizedClass diff --git a/ecs/jskult-webapp/src/util/string_util.py b/ecs/jskult-webapp/src/util/string_util.py new file mode 100644 index 00000000..f7e0cad7 --- /dev/null +++ b/ecs/jskult-webapp/src/util/string_util.py @@ -0,0 +1,10 @@ +def is_not_empty(check_string: str) -> bool: + """文字列が空ではない場合True + + Args: + check_string (str): 検査する文字列 + + Returns: + bool: 文字列が殻ではない場合、True + """ + return check_string is not None and len(check_string) > 0