Repository: lapras-inc/exam-swe-template Branch: main Commit: 76f2a4392014 Files: 107 Total size: 141.1 KB Directory structure: gitextract_ue362hp4/ ├── .github/ │ └── workflows/ │ ├── check_isucoutea.yml │ └── check_refactor_code.yml ├── .gitignore ├── README.md ├── db_design_complicated/ │ └── README.md ├── isucoutea/ │ ├── Dockerfile │ ├── README.md │ ├── admin/ │ │ ├── config/ │ │ │ ├── bashrc │ │ │ └── nginx.conf │ │ ├── data/ │ │ │ ├── china_location.txt │ │ │ ├── india_location.txt │ │ │ ├── japan_location.txt │ │ │ ├── kana.txt │ │ │ └── sentence.txt │ │ ├── init.sql │ │ └── insert.py │ ├── benchmark/ │ │ ├── benchmark.py │ │ ├── requirements.txt │ │ └── run_benchmark.sh │ ├── docker/ │ │ └── start_app.sh │ ├── docker-compose.go.yml │ ├── docker-compose.yml │ ├── note.md │ └── webapp/ │ ├── go/ │ │ ├── Dockerfile │ │ ├── css/ │ │ │ └── style.css │ │ ├── go.mod │ │ ├── go.sum │ │ ├── main.go │ │ ├── models.go │ │ └── templates/ │ │ ├── index.html │ │ ├── layout.html │ │ └── new.html │ ├── python/ │ │ ├── app.ini │ │ ├── app.py │ │ ├── css/ │ │ │ └── style.css │ │ ├── pyproject.toml │ │ └── templates/ │ │ ├── index.html │ │ ├── layout.html │ │ └── new.html │ └── ruby/ │ ├── Gemfile │ ├── app.rb │ ├── config.ru │ ├── config_puma.rb │ ├── css/ │ │ └── style.css │ └── views/ │ ├── index.erb │ ├── layout.erb │ └── new.erb ├── refactor_code/ │ ├── .gitignore │ ├── README.md │ ├── docker/ │ │ └── paymentmock/ │ │ ├── Dockerfile │ │ └── mockserver/ │ │ └── app.py │ ├── python/ │ │ ├── .gitignore │ │ ├── README.md │ │ ├── application/ │ │ │ ├── __init__.py │ │ │ ├── admin/ │ │ │ │ ├── __init__.py │ │ │ │ └── create_db.py │ │ │ ├── app.ini │ │ │ ├── app.py │ │ │ ├── forms.py │ │ │ ├── models.py │ │ │ ├── payment_service.py │ │ │ ├── scripts/ │ │ │ │ └── resetdb.sh │ │ │ ├── settings.py │ │ │ ├── templates/ │ │ │ │ ├── cart.html │ │ │ │ ├── checkout.html │ │ │ │ ├── index.html │ │ │ │ ├── layout.html │ │ │ │ ├── login.html │ │ │ │ ├── signup.html │ │ │ │ └── tea.html │ │ │ └── test.py │ │ ├── docker/ │ │ │ └── service/ │ │ │ ├── Dockerfile │ │ │ ├── entrypoint.sh │ │ │ ├── nginx.conf │ │ │ └── run_test.sh │ │ ├── docker-compose.yml │ │ └── pyproject.toml │ └── ruby/ │ ├── .gitignore │ ├── .ruby-version │ ├── Gemfile │ ├── README.md │ ├── Rakefile │ ├── app.rb │ ├── config/ │ │ └── database.yml │ ├── config.ru │ ├── db/ │ │ ├── migrate/ │ │ │ ├── 20181105072432_create_users.rb │ │ │ ├── 20181106023943_create_teas.rb │ │ │ ├── 20181106024402_create_carts.rb │ │ │ └── 20181106042324_create_orders.rb │ │ ├── schema.rb │ │ └── seeds.rb │ ├── docker/ │ │ └── service/ │ │ ├── Dockerfile │ │ ├── entrypoint.sh │ │ └── run_test.sh │ ├── docker-compose.yml │ ├── models.rb │ ├── payment_service.rb │ ├── test/ │ │ ├── app_test.rb │ │ └── test_helper.rb │ └── views/ │ ├── cart.erb │ ├── checkout.erb │ ├── index.erb │ ├── layout.erb │ ├── login.erb │ ├── tea.erb │ └── user_new.erb └── scripts/ └── start_docker_compose.bash ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/workflows/check_isucoutea.yml ================================================ on: workflow_dispatch: pull_request: paths: - "isucoutea/**" - ".github/workflows/check_isucoutea.yml" name: Check isucoutea concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true jobs: isucoutea: name: Check isucoutea runs-on: ubuntu-22.04 timeout-minutes: 60 strategy: fail-fast: false matrix: language: - "python" - "ruby" - "go" steps: - uses: actions/checkout@v2 - run: docker compose build working-directory: isucoutea - run: ../scripts/start_docker_compose.bash "isucoutea-app-1" "start application" working-directory: isucoutea timeout-minutes: 5 env: ISUCOUTEA_RUN_TYPE: run_${{ matrix.language }} - run: docker exec -t isucoutea-benchmark-1 ./benchmark/run_benchmark.sh working-directory: isucoutea timeout-minutes: 5 ================================================ FILE: .github/workflows/check_refactor_code.yml ================================================ on: workflow_dispatch: pull_request: paths: - "refactor_code/**" - ".github/workflows/check_refactor_code.yml" name: Check refactor_code concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true jobs: refactor_code: name: Check refactor_code runs-on: ubuntu-22.04 timeout-minutes: 15 strategy: fail-fast: false matrix: language: - "python" - "ruby" steps: - uses: actions/checkout@v2 - run: docker compose build working-directory: refactor_code/${{ matrix.language }} - run: ../../scripts/start_docker_compose.bash "${{ matrix.language }}-app-1" "start application" working-directory: refactor_code/${{ matrix.language }} - run: docker exec -t ${{ matrix.language }}-app-1 /app/docker/service/run_test.sh working-directory: refactor_code/${{ matrix.language }} ================================================ FILE: .gitignore ================================================ # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class # C extensions *.so # Distribution / packaging .Python env/ build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ wheels/ *.egg-info/ .installed.cfg *.egg # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *.cover .hypothesis/ # Translations *.mo *.pot # Django stuff: *.log local_settings.py # Flask stuff: instance/ .webassets-cache # Scrapy stuff: .scrapy # Sphinx documentation docs/_build/ # PyBuilder target/ # Jupyter Notebook .ipynb_checkpoints # pyenv .python-version # celery beat schedule file celerybeat-schedule # SageMath parsed files *.sage.py # dotenv .env # virtualenv .venv venv/ ENV/ # Spyder project settings .spyderproject .spyproject # Rope project settings .ropeproject # mkdocs documentation /site # mypy .mypy_cache/ .envrc ================================================ FILE: README.md ================================================ # LAPRAS株式会社 - スキルチェック課題(SWE) このリポジトリには、LAPRAS株式会社にソフトウェアエンジニア職として入社を希望する方向けの、スキルチェック課題が格納されています。 公開した意図や出題意図については **[こちらの記事](https://tech-blog.lapras.com/techBlogs/publish_skill_check)** を、弊社の制度や募集中のポジション、チームの様子について気になる方は **[エンジニア採用サイト](https://corp.lapras.com/recruit-engineer/)** をご覧ください! # 用途 - 1次選考としてこれらの課題に取り組んで頂くことを想定しています。 - 採点基準、合格点は事前に決まっており、それに従って合否を判定します。回答に対するフィードバックもお伝えします。 - その後の選考ステップにおいて、解いて頂いた課題を題材にディスカッションをさせて頂く場合もあります。 選考として課題に取り組んで頂ける方には、弊社側でこのリポジトリを複製したPrivateリポジトリを作成し、コラボレータとして招待をお送りします。 解答がこのリポジトリに紐づいて閲覧可能な状態になってしまうため、**解答のためにForkする等はご遠慮ください** ※弊社選考にご興味がある場合は [カジュアル面談予約リンク](https://calendar.app.google/DDLdNajM9B4ig285A) より予約をお願いします。 # 構成 このリポジトリには複数の問題が格納されています。 rootディレクトリ直下の各ディレクトリを1問として扱います(CI等で利用しているものを除く)。 各ポジション毎に取り組んで頂く課題をご案内しています。各課題の概要や採点基準等については各ディレクトリ内のREADME.mdをご確認ください。 |課題| 概要 | |--- | --- | |[isucoutea](https://github.com/lapras-inc/exam-swe-template/tree/main/isucoutea)|わざと遅く作られたWebアプリケーションの高速化| |[refactor_code](https://github.com/lapras-inc/exam-swe-template/tree/main/refactor_code)|わざと汚く作られたECサイトの保守性向上のためのリファクタリング| |[db_design_complicated](https://github.com/lapras-inc/exam-swe-template/tree/main/db_design_complicated)|テキストで書かれたWebサービスの仕様を満たすデータベース設計| ## 採用ポジション毎に取り組んでいただく課題 もしも直接ご案内した内容が以下と異なる場合は、そちらの内容を正として回答してください。 ### SWE(バックエンド, フロントエンド) 「refactor_codeまたはisucoutea」及び「db_design_complicated」に取り組んで頂きます。 バックエンド寄りの課題ですが、フロントエンド/バックエンドのポジションに関わらずご回答をお願いしています。ポジション毎に通過となる採点基準が異なるのと、以降の選考にてポジション固有の課題についても用意しています。 ※実務における通常の機能開発の範囲については、全てのエンジニアがフロントエンド/バックエンドの両方を担当できる状態を目指しています。その他の高度な技術的な課題解決において、どちらを主に担当頂くか、という観点でフロントエンドとバックエンドでポジションを分けて募集しています。 ### SRE 「isucoutea」に取り組んでいただきます。 二次選考ではSREチームの業務に関係の深い模擬プロジェクトを題材にしたディスカッションを行います。 # 注意事項 解答等を広く公開する場合は一度弊社にご相談ください。 解答が複製/公開されることを想定した選考フローとはなっておりますが、他の方の回答をコピーする等の行為を「推奨」するものではございません。 ================================================ FILE: db_design_complicated/README.md ================================================ 技術試験(DB編) ================================= 背景や内部情報 ------------------ * DB設計の実践的な知識を有するか * アプリケーション変更時のデータベース側の影響について大まかに把握できるか 問題 ------------ ## 概要 後述の「サービス概要」の仕様を満たすWebアプリケーションのデータベースを設計してください。 ## 成果物 #### ER図(あるいはそれに相当するもの)の画像 * 概略でかまいません。 * 各エンティティごとの属性よりもエンティティ間のリレーションを重視します。 * 特に1:N, N:N, 1:1or0 等の関係については詳細に記述をお願いします。 * 属性は主要な項目のみ記述頂ければ構いません。 * RDBMS以外のコンポーネントを利用することでパフォーマンスが向上し得る場合は利用箇所を示してください。 * ER図の記法などの正確さは評価基準に含まれません。 * UMLツールを利用する、手書きで作成したものの写真をとる、など作成方法は自由です。 #### 作業メモ * 設計の概要や補足説明、迷った箇所などを記述ください。 ## 提出方法/形式 課題の提出はプルリクエスト形式でお願いします。 * 作業メモは `/db_design_complicated` 直下に `NOTE.md` というファイルを作成してください。 * ER図はプルリクエストの本文に貼り付けてください。 サービス概要 ---------------- すかうtea(仮称)は様々な茶葉を仕入れて、ユーザが独自のブレンドで購入できるB2Cサービスを運営している。 ### 固有情報 以下にすかうtea特有の詳細を示す。以下の3モデル以外にも、後述の**ユースケース**を満たすために複数のモデルを定義する必要がある点に注意せよ。 #### 茶葉 * 茶葉は全体で **1万種類** 存在する * 茶葉は100程度の仕入先がある * 一つの仕入先が複数の茶葉を提供している * 一つの茶葉が複数の仕入先を持つことはない * ある茶葉について **仕入先が変更される** ことはある * 茶葉は紅茶・ウーロン茶・緑茶等、カテゴリがある * 茶葉は渋み、甘み、苦みについて10段階の評価を事前に付けている * 茶葉の産地を気にする(国産派等)ユーザのため、産地情報も保持する * 茶葉ごとに在庫量を管理している * 茶葉には価格が設定されている。これを**問屋価格**とする * 問屋価格は仕入れ状況等で不定期に変更される #### ブレンド茶葉 * 現在のすかうteaでは**2種類以上**の茶葉(素材茶葉)を指定された配合でブレンドしている * ブレンドには独自のタイトル、説明を付与できる * ブレンド茶葉には独自の価格(**販売価格**)を設定することができる * 販売価格の下限は、構成する茶葉の問屋価格とし、原価割れの販売価格は設定できない * 問屋価格との差は販売者の利益(**販売利益**)とする * **問屋価格が変更される場合は、もともとの販売利益額を保持したまま販売価格が変更される** * 作成されているブレンドの中には茶葉の組み合わせや比率が同じものも多く含まれている * ブレンド茶葉には公開・非公開の概念があり、非公開ブレンドは作成者のみが購入できる * すかうteaではブレンドのみが購入・販売可能であり、ベースの茶葉をそのまま販売することはない * 渋み等の属性は、素材茶葉の属性と比率を考慮して合算したものがブレンド茶葉の属性になる * 甘み10の素材茶葉Aを30%, 甘み5の素材茶葉Bを70%でブレンドした場合、ブレンド茶葉の甘みは6.5になる #### ユーザ * **1000万人の利用者がいる** * ブレンド茶葉を作成しているユーザは全体の50% * 残りの50%はブレンドを購入するだけのユーザであり、ブレンド茶葉作成は行っていない * ブレンドを作成しているユーザは1ユーザあたり **平均で100ブレンドほど** 作成している ### ユースケース 以下にすかうteaサービスでのユースケースを示す。 #### ブレンド茶葉作成 * 検索画面より素材となる茶葉を探す * 茶葉名のあいまい検索 * 産地検索 * 属性検索(渋み・甘み・苦味) * 複数検索条件についてはOR, ANDが可能 * ECサイトにおけるカートのように、選んだ茶葉を候補として保持する * 作成画面より、候補の茶葉から実際に使用する茶葉と比率を選択し、その他の情報を入力して作成する * 新しいブレンド茶葉が追加された場合、**1分以内に購入対象になる必要がある** #### ブレンド茶葉管理 * 自身が作成したブレンド茶葉の一覧を確認できる * ブレンド茶葉の名前や構成比率等の更新ができる * 不要になったブレンド茶葉は削除できる #### ブレンド茶葉購入 * ブレンド茶葉を検索し、購入カートに追加する * 検索対象は公開されているものか、自身が作成したもの * 検索結果には以下の情報が表示される * ブレンド名 * 作成者 * 価格 * ブレンド茶葉詳細へのリンク * 検索条件は以下 * 作成者 * 構成茶葉の比率下限、上限 * 価格 * 在庫の有無 * 検索順序はサービスへの貢献度が高いユーザを優先するため、直近1ヶ月の販売量が多いユーザが作成したブレンド茶葉を上位とする #### ブレンド茶葉詳細 * 作成者や比率等の情報が確認できる * ブレンド茶葉には作成者が任意のコメントを追加することができる * 公開されているブレンド茶葉にはすべてのユーザが任意のコメントを追加することができる #### 購入処理 * カートに入っているブレンド茶葉を購入する * ブレンドごとの購入量を変更・削除できる * ブレンド茶葉の量を増加した際に在庫量を問い合わせし、在庫を超える場合は購入不可のメッセージを出す * ブレンドは購入確定時点で、既存の在庫から引き当てて指定比率で配合後に出荷される * 在庫状況次第では、購入ボタン押下後に品切れで買えなかったというケースがあることは許容する * 購入後の配送状況の追跡は外部システムで行うため本システムでは保持しない #### 販売利益確認 * ユーザは現時点での累計販売利益と未受け取り販売利益を確認できる * 販売利益が1000円を超えている場合はリクエストすることで振り込まれる * より売れるブレンド作成の基礎データにするため、販売時のブレンド茶葉について茶葉の構成比率を確認できる #### キャンペーン * 不定期に特定の産地や仕入れ先を対象にキャンペーンが行われる * 条件を満たしたブレンド茶葉は問屋価格が割引かれる * 割引分の半分だけ販売価格が下がる * つまり割引価格の半分だけ販売利益が増える * 複数のキャンペーンが同時に開催されるケースもある * キャンペーン中は、条件を満たすブレンド茶葉の作成が大量にリクエストされる ================================================ FILE: isucoutea/Dockerfile ================================================ FROM ubuntu:22.04 ENV LANG "en_US.UTF-8" # パッケージインストール RUN apt-get update RUN apt-get install -y \ # Common packages wget sudo less vim git curl locales-all figlet \ # https://github.com/rbenv/ruby-build/wiki#ubuntudebianmint autoconf bison build-essential libssl-dev libyaml-dev libreadline6-dev \ zlib1g-dev libncurses5-dev libffi-dev libgdbm6 libgdbm-dev libdb-dev \ # MySQL mysql-client-8.0 mysql-server-8.0 libmysqlclient-dev \ # nginx nginx && \ apt-get clean # ユーザ作成 RUN groupadd -g 1001 scouty && \ useradd -g scouty -G sudo -m -s /bin/bash scouty && \ echo "scouty:scouty" | chpasswd && \ echo "scouty ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers USER scouty WORKDIR /home/scouty # Ruby のインストール ENV PATH="/home/scouty/.rbenv/bin:$PATH" RUN RUBY_VERSION="3.1.4" && \ BUNDLER_VERSION="2.3.19" && \ wget "https://raw.githubusercontent.com/rbenv/rbenv-installer/main/bin/rbenv-installer" && \ cat rbenv-installer | bash && \ rm -f rbenv-installer && \ eval "$(rbenv init -)" && \ rbenv install "$RUBY_VERSION" && \ rbenv global "$RUBY_VERSION" && \ gem update --system && \ gem install bundler -v "$BUNDLER_VERSION" && \ echo "installed version: $(ruby -v)" # Python のインストール ENV PYENV_ROOT="/home/scouty/.pyenv" ENV PATH="$PYENV_ROOT/bin:$PATH" RUN PYTHON_VERSION="3.9.17" && \ POETRY_VERSION="1.1.6" && \ git clone "https://github.com/pyenv/pyenv.git" ~/.pyenv && \ # https://uwsgi-docs.readthedocs.io/en/latest/Install.html sudo apt-get install -y python3-dev && \ eval "$(pyenv init -)" && \ eval "$(pyenv init --path)" && \ pyenv install "$PYTHON_VERSION" && \ pyenv global "$PYTHON_VERSION" && \ pip install --upgrade pip && \ pip install poetry=="$POETRY_VERSION" && \ echo "installed version: $(python -V)" # Go のインストール ENV GOROOT="/usr/local/go" ENV PATH="$GOROOT/bin:$PATH" ENV GOPATH="$HOME/.local/go" RUN GO_VERSION="1.21.0" && \ wget -O go.tgz "https://go.dev/dl/go$GO_VERSION.linux-amd64.tar.gz" && \ sudo tar -C /usr/local -xzf go.tgz && \ rm -f go.tgz && \ echo "installed version: $(go version)" # 初期データのダウンロード RUN mkdir "data/" && \ cd "data/" && \ curl -O "https://s3-ap-northeast-1.amazonaws.com/scouty-sw/exam/assets/dbdump.tar.gz" COPY --chown=scouty:scouty admin/config/bashrc /home/scouty/.bashrc RUN sudo chmod 777 -R /var/run/ ================================================ FILE: isucoutea/README.md ================================================ # 技術試験(アプリケーション高速化) ## 問題の背景 Webアプリケーションを開発する上で、高速化の基本的知識は必要不可欠です。 何気なく書いたコードがとても重い処理を含んでいて、レスポンスに10秒もかかる事態になっていたらユーザ体験は最悪なものになってしまいます。 ここには高速化について全く考えられていない、茶葉の閲覧/投稿サービスがあります。 あなたの手でできる限りの高速化を施してください。 ### 課題の参考元である「ISUCON」について この課題は「[ISUCON](https://isucon.net/)」を参考にしています。「[ISUCON](https://isucon.net/)」は、LINE株式会社の商標または登録商標です。 ## 問題 この課題では、Webアプリケーションのレスポンス速度を可能な限り速くして頂きます。 Webアプリケーションの参考実装として、Python, Ruby, Go での実装を用意していますが、その他の言語を使用して同じ挙動をするアプリケーションを作成していただいても構いません。使用するフレームワークも変更可能です。 ### アプリケーションの起動方法 `isucoutea` ディレクトリ直下で、以下のコマンドを実行してください。 ``` $ docker compose build $ docker compose up ``` ただし Go実装 を Apple Silicon(M1 Macなど) で実行する場合は以下のコマンドを実行してください。 ``` $ docker compose -f docker-compose.yml -f docker-compose.go.yml build $ docker compose -f docker-compose.yml -f docker-compose.go.yml up ``` アプリケーション起動後、 `http://localhost` にアクセスすることでアプリケーションの動作確認をすることができます。 ### 起動するアプリケーションを変更 デフォルトではPython実装が起動されますが、起動する言語をRubyやGoに変更したい場合は、 `docker-compose.yml` ファイルで指定している `ISUCOUTEA_RUN_TYPE` 環境変数を変更してください。 その他の言語を使用してアプリケーションを実装する場合は、 `docker/start_app.sh` ファイルを修正し、任意のWebサーバを立ち上げる設定を追加してください。 - run_python - Python実装を起動 - run_ruby - Ruby実装を起動 - run_go - Go実装を起動 - run_custom - カスタム実装を起動 - 上記以外の言語を選択する場合 ### ベンチマーカーの実行 ベンチマーカーの実行によってどれだけ高速化できたかを計測します。 アプリケーションを起動後、以下のコマンドをコンテナ内で実行します。 ``` $ docker exec -it isucoutea-benchmark-1 ./benchmark/run_benchmark.sh 初期化処理を実施します... ベンチマークを実行します... Request: GET / 5.133428198000047 sec Request: GET /?page=80000 4.191627684999958 sec Request: POST / 4.293627644000026 sec Request: GET /?page=25&query=平塚市 20.01473274 sec (timeout) Request: GET /?page=858&query=日本 20.021639434999997 sec (timeout) Request: GET / 20.00904635400002 sec (timeout) Request: POST / 20.02240306600015 sec (timeout) Request: GET /?query=ミャメビエコフ茶 20.010157561000142 sec (timeout) Request: GET /?query=存在しないお茶 20.021486522000032 sec (timeout) Request: GET / 20.01382161100014 sec (timeout) Result: 153.73479139000005 sec ``` benchmark.py はいくつかのエンドポイントにリクエストを投げて、応答時間を総計して出力しています。 この時間をできるだけ短縮することがこの課題の目標です。 ただし、初期実装では文字列検索(`/?query=hoge`)のレスポンス速度が大変遅く、Timeout(>20sec) します。 ### 課題の提出方法 コードとあわせて、`exam_SWE/isucoutea/note.md` にあなたの高速化ログを残してください。 作業ログは厳密に記録する必要はなく、ある時点で計測を忘れた場合には、結果部分は空白で結構です。 ### 注意事項 #### やってはいけないこと * 茶葉を新規登録したのに一覧画面に表示されない等、本来期待しているアプリケーションの動き通りに動かないこと * ベンチマーカーにはバリデーションの機能が付いており、期待するステータスコードやレスポンスが返却されないとエラーとなります * `/initialize` の処理内容において、削除せず残してある初期データに対して変更を加えること。 * 既存テーブルにカラムを追加してそこに独自のデータを入れることは問題ありません。 * ベンチマーカーがリクエストしているクエリに特化した高速化処理をいれること。 * 別の文字列を検索クエリにしたときにも同等のパフォーマンスが出ることを保証してください。 * teas テーブルの情報を **すべて** メモリ上に置くこと。 * お茶の情報は今後50倍、100倍に増える可能性があるため、(説明文を除いたとしても)tea テーブルのすべてはメモリ上には載らないものとします。 #### やって良いこと * 原則やっていはいけないことに書いたこと以外は何をしても構いません。 * インメモリデータベースや全文検索エンジンなどを追加でインストールすることもOKです。 * 初期データが入っているテーブルを2つに分割することもOKです。ただし入っているデータの文字列に変更は加えないでください。 * やってよいか判断がつかない場合には Issue にて起票ください。 #### 採点基準 * 高速化した手法毎に加点していく方式です、node.md には実施した内容を丁寧に記述してください。 * (実装言語にも依りますが)目安としてベンチマーカーの実行時間を1秒以内にすることを目標として取り組んでください。 ================================================ FILE: isucoutea/admin/config/bashrc ================================================ export LC_ALL=C.UTF-8 export LANG=C.UTF-8 export PATH="$HOME/.rbenv/bin:$PATH" eval "$(rbenv init -)" export PYENV_ROOT="$HOME/.pyenv" export PATH="$PYENV_ROOT/bin:$PATH" if command -v pyenv 1>/dev/null 2>&1; then eval "$(pyenv init --path)" fi export PATH=$PATH:/usr/local/go/bin export GOROOT=/usr/local/go export GOPATH=/home/scouty/.local/go alias ls='ls --color=auto' alias ll='ls -alF' alias la='ls -A' alias l='ls -CF' alias grep='grep --color=auto' ================================================ FILE: isucoutea/admin/config/nginx.conf ================================================ user www-data; worker_processes 1; pid /run/nginx.pid; events { worker_connections 4; } http { include /etc/nginx/mime.types; default_type application/octet-stream; access_log /var/log/nginx/access.log; error_log /var/log/nginx/error.log; upstream app { server 127.0.0.1:8080; } server { listen 80; location / { proxy_set_header Host $host; proxy_pass http://app; } } } ================================================ FILE: isucoutea/admin/data/china_location.txt ================================================ 北京市 周口店 盧溝橋 八達嶺 天津市 大港 塘沽 河北省 石家荘 邯鄲 承徳 唐山 開灤炭田 秦皇島 張家口 保定 山西省 大同 五台 太原 陽泉 大寨 長治 内蒙古自治区 包頭 呼和浩特 集寧 赤峰 烏蘭浩特 海拉爾 黒竜江省 哈爾浜 大慶 斉斉哈爾 北安 綏化 佳木斯 鶴崗 牡丹江 鶏西 黒河 吉林省 白城 吉林 遼源 四平 延吉 梅河口 渾江 長春 遼寧省 阜新 北票 山海関 営口 大連 瓦房店 丹東 鞍山 本渓 遼陽 瀋陽 撫順 旅順 山東省 済南 曲阜 勝利 棗庄 濰坊 竜口 蓬萊 招遠 青島 煙台 威海 上海 宝山 江蘇省 連雲港 徐州 淮陰 淮安 塩城 興化 泰州 揚州 鎮江 如皋 南京 常州 無錫 常熟 蘇州 昆山 安徽省 淮北 蚌埠 淮南 合肥 銅陵 安慶 浙江省 嘉興 杭州 蕭山 寧波 臨海 台州 紹興 温州 江西省 南昌 景徳鎮 萍郷 新余 贛州 宜春 豊城 上饒 瑞金 福建省 寧徳 福州 泉州 廈門 竜岩 漳州 台湾省 台北 淡水 基隆 新竹 台中 彰化 花蓮 嘉義 台南 高雄 台東 河南省 安陽 新郷 焦作 洛陽 鄭州 函谷関 開封 周口 平頂山 南陽 信陽 湖北省 襄樊 襄陽 大神農架 三峡ダム 宜昌 赤壁 安陸 武漢 漢口 武昌 漢陽 黄石 湖南省 懐化 常徳 邵陽 郴州 耒陽 衝陽 株州 湘潭 長沙 岳陽 広東省 広州 中山 湛江 汕頭 潮州 韶関 陽江 深圳 茂名 仏山 肇慶 海南省 海口 三亜 広西壮族自治区 南寧 北海 柳州 桂林 玉林 欽州 百色 河池 陝西省 延安 延長 銅川 咸陽 宝鶏 五丈原 西安 漢中 安康 寧夏回族自治区 石嘴山 銀川 呉忠 甘粛省 玉門関 敦煌 陽関 瓜州 安西 嘉峪関 酒泉 張掖 武威 蘭州 天水 平凉 青海省 西寧 新疆維吾爾自治区 阿勒泰 鳥魯木斉 吐魯番 哈密 楼蘭 和田 莎車 喀什 重慶市 四川省 広元 閬中 南充 成都 綿陽 楽山 自貢 合川 宣賓 瀘州 涪陵 万州 万県 渡口 攀枝花 貴州省 銅仁 貴陽 威寧 安順 雲南省 大理 保山 昆明 曲靖 箇旧 西蔵自治区 拉薩 東北 華北 華中 華南 黒竜江 松花江 鴨緑江 永定河 淮河 子牙河 衛河 黄河 長江 漢水 珠江 銭塘江 湘江 渭水 太湖 洞庭湖 鄱陽湖 青海湖 東北平原 黄土高原 雲貴高原 黄山 泰山 太白山 峨眉山 陰山山脈 伏牛山脈 太行山脈 南嶺山脈 大興安嶺山脈 小興安嶺山脈 天山山脈 ================================================ FILE: isucoutea/admin/data/india_location.txt ================================================ アボハール アチャルプル アガルタラ アーグラ アフマダーバード アフマドナガル アイザウル アジュメール アコーラー アリーガル イラーハーバード アルワル アンバーラー アメーティー アムマカンダカラ アムラーワティー アムレーリー アムリトサル アーナンド アナントプル アーングル アンクレーシュワル アヌーププル アラリヤー アルコット アーラー アルップッコーッタイ アサンソル アショークナガル アシュタミチラ アウランガーバード アスルナンド アーザムガル ベヘラームプル バハードゥルガル ベヘラーイチ ボーラーンギール バーラーガート バラソール バリヤー バリ バルラームプル バーンダー バンガロール バンガナパッレ バーンスワーラー バーラーマティー バーラン バルダマーン バレーリー バルガル バルナーラー バリーパーラー バールメール バッラクプル バルワーニー バーンスワーラー ベルガウム ベッラーリ ベートゥル バーガルプル パヴァーニー バンダーラー バラトプル バルーチ バーヴナガル ビラーイー ビワンディー ビワーニー ボーパール ブバネーシュワル ブジ ビーダル ビハール・シャリーフ ビジュノール ビーカーネール ビラースプル ビラースプル ブッダガヤ ボーカーロー・スチール・シティ ボーンガーイーガーオン バル バルプル バールルガート ブラフマプリー ベーランプル カリカット カンベイ チャーンパーワト チャーマラージャナガル チャンダンナガル チャンディーガル チャプラー チャンドラプル チェンガルパットゥ チェンナイ チャタルプル チンドワーラー チクマガルール チプルーン チトラドゥルガ チトラクート チットゥール コーヤンブットゥール クーヌール カッダロール カタク チューラーチャーンドプル ダードラー ダーホード ダルトンガンジ ダマン ダモー ダルバンガー ダージリン ダティヤー ダーヴァナゲレ デヘガーム デヘラードゥーン デーオガル デーワース デリー ダムタリー ダンバード ダール ダラムプル ダラムシャーラー ダールワード デーンカナール ドールカー ドゥーレー ドゥリアン ディブルーガル ディスプル ディンディーグル ディーウ ドーンビヴリー ダムダム ドゥルグ ドゥルガープル ドワールカー エールール エルナクラム イーロードゥ エーター イターワー ファイザーバード ファリーダーバード ファリードコート ファッルハーバード ファテーガル ファテープル・シークリー フィールーズプル フィールーザーバード ガダグ ガドチローリー ガンディーナガル ガントク ガンジャーム ガヤー ガーズィヤーバード ガーズィープル オールド・ゴア ゴードラー ゴーンディヤー ゴーラクプル グレーターノイダ ゴーヴィンドプリー クーダルール グムラー グナー グンドルペート グントゥール グルグラム ゴーパールガンジ グワーハーティー グワーリヤル ホーシヤールプル ハルディヤー ハルドワーニー ハミールプル ハミールプル ハヌマーンガル ハウラー ハルダー ハルドーイー ハルサーワー ハリドワール フッバッリ ハーサン ハートラス ヒマットナガル ヒンドゥール ヒサール ハーンシー ハイデラバード インドール インパール イーターナガル ジャランダル ジャバルプル ジャグダルプル ジャイプル ジャイサルメール ジャラーラーバード ジャルガーオン ジャーロール ジャンムー ジャームナガル ジャムシェードプル ジャウンプル ジャーブアー ジャーラーワール ジャーンシー ジュンジュヌー ジョードプル ジョールハート ジュナーガド カダパ カーキナーダ グルバルガ カプールタラー カーンチープラム カーンケール カーンプル カンヌール カルール カールワール カリームナガル カーシーラームナガル カトニー カガリヤー カラグプル コーチ コールハープル コルカタ クリシュナナガル コッラム マダナパッレ マドガーオン マディケーリ マドゥライ マハーバレーシュワル マフブーブナガル マーヒ マホーバー マフワ マーレーガーオン マーンドラー マンドサウル マネサル マンガルギリ マンガロール マープサー マールムガーオー マトゥラー マチリーパトナム マハード マイナグリ メーラト メヘサーナー ミーラー・バヤンダル ミラジ ミールザープル モーガー モーハーリー モカメ ムラーダーバード モーティーハーリー マウントアブ ムクトサル ムランプル ムンバイ マスーリー ムルシダーバード ムザッファルナガル ムザッファルプル マイソール ナディヤード ナーガパッティナム ナーガウル ナーガルコーイル ナギーナ ナーグプル ナイニータール ナルゴンダ ナーマッカル ナーンデード ナンドゥルバール ナンガル ナルシンハプル ナーシク ナビムンバイ ナヴサーリー ナワルガル ナーラーガル ニーマチ ネルール ノイダ ニザーマーバード ニザーマーバード ニルマル パーリー パナジー パンチクラー パンダルプル パーニーパト パンナー パンベル パータン パターンコート パティヤーラー パトナ ピトーラーガル ピンプリ・チンチワッド ポーンダー ポッラーッチ ポカラン ポンディシェリ プッタパルティ ポールバンダル ポートブレア プラタープガル プネー プリー プールニヤー プシュカル ライチュール ラーイガル ラーイセーン ラージャムンドリー ラージガル ラージコート ラージナーンドガーオン ラーマナータプラム ラーメーシュワラム ラームガル ラームプル ラーナーガート ラーニー ラーンチー ラーニーケート ラタンガル ラトラーム ラトナーギリー ラーエバレーリー リシケーシュ ルールキー ラーウルケーラー サーガル セーラム サマスティープル サーングリー サンガムネール サティヤマンガラム サーターラー サトナー サハーランプル スィーホール スィヴニー シャージャープル シェーガーオン シヴプル シロン シヴァーニー ソーラープル スィーカル スィルチャル スィルヴァーサー シムラー シホール シリグリ スィングラウリー スィローヒー スィーターマリー スィーワーン ソーニーパト ガンガーナガル シュリーカークラム シュリーカーラハスティ シュリーナガル スーラト スーラトガル スレーンドラナガル スィータープル サンバルプル ================================================ FILE: isucoutea/admin/data/japan_location.txt ================================================ 札幌市 函館市 小樽市 旭川市 室蘭市 釧路市 帯広市 北見市 夕張市 岩見沢市 網走市 留萌市 苫小牧市 稚内市 美唄市 芦別市 江別市 赤平市 紋別市 士別市 名寄市 三笠市 根室市 千歳市 滝川市 砂川市 歌志内市 深川市 富良野市 登別市 恵庭市 伊達市 北広島市 石狩市 北斗市 当別町 新篠津村 松前町 福島町 知内町 木古内町 七飯町 鹿部町 森町 八雲町 長万部町 江差町 上ノ国町 厚沢部町 乙部町 奥尻町 今金町 せたな町 島牧村 寿都町 黒松内町 蘭越町 ニセコ町 真狩村 留寿都村 喜茂別町 京極町 倶知安町 共和町 岩内町 泊村 神恵内村 積丹町 古平町 仁木町 余市町 赤井川村 南幌町 奈井江町 上砂川町 由仁町 長沼町 栗山町 月形町 浦臼町 新十津川町 妹背牛町 秩父別町 雨竜町 北竜町 沼田町 鷹栖町 東神楽町 当麻町 比布町 愛別町 上川町 東川町 美瑛町 上富良野町 中富良野町 南富良野町 占冠村 和寒町 剣淵町 下川町 美深町 音威子府村 中川町 幌加内町 増毛町 小平町 苫前町 羽幌町 初山別村 遠別町 天塩町 猿払村 浜頓別町 中頓別町 枝幸町 豊富町 礼文町 利尻町 利尻富士町 幌延町 美幌町 津別町 斜里町 清里町 小清水町 訓子府町 置戸町 佐呂間町 遠軽町 湧別町 滝上町 興部町 西興部村 雄武町 大空町 豊浦町 壮瞥町 白老町 厚真町 洞爺湖町 安平町 むかわ町 日高町 平取町 新冠町 浦河町 様似町 えりも町 新ひだか町 音更町 士幌町 上士幌町 鹿追町 新得町 清水町 芽室町 中札内村 更別村 大樹町 広尾町 幕別町 池田町 豊頃町 本別町 足寄町 陸別町 浦幌町 釧路町 厚岸町 浜中町 標茶町 弟子屈町 鶴居村 白糠町 別海町 中標津町 標津町 羅臼町 青森市 弘前市 八戸市 黒石市 五所川原市 十和田市 三沢市 むつ市 つがる市 平川市 平内町 今別町 蓬田村 外ヶ浜町 鰺ヶ沢町 深浦町 西目屋村 藤崎町 大鰐町 田舎館村 板柳町 鶴田町 中泊町 野辺地町 七戸町 六戸町 横浜町 東北町 六ヶ所村 おいらせ町 大間町 東通村 風間浦村 佐井村 三戸町 五戸町 田子町 南部町 階上町 新郷村 盛岡市 宮古市 大船渡市 花巻市 北上市 久慈市 遠野市 一関市 陸前高田市 釜石市 二戸市 八幡平市 奥州市 滝沢市 雫石町 葛巻町 岩手町 紫波町 矢巾町 西和賀町 金ケ崎町 平泉町 住田町 大槌町 山田町 岩泉町 田野畑村 普代村 軽米町 野田村 九戸村 洋野町 一戸町 仙台市 石巻市 塩竈市 気仙沼市 白石市 名取市 角田市 多賀城市 岩沼市 登米市 栗原市 東松島市 大崎市 富谷市 蔵王町 七ヶ宿町 大河原町 村田町 柴田町 川崎町 丸森町 亘理町 山元町 松島町 七ヶ浜町 利府町 大和町 大郷町 大衡村 色麻町 加美町 涌谷町 美里町 女川町 南三陸町 秋田市 能代市 横手市 大館市 男鹿市 湯沢市 鹿角市 由利本荘市 潟上市 大仙市 北秋田市 にかほ市 仙北市 小坂町 上小阿仁村 藤里町 三種町 八峰町 五城目町 八郎潟町 井川町 大潟村 羽後町 東成瀬村 山形市 米沢市 鶴岡市 酒田市 新庄市 寒河江市 上山市 村山市 長井市 天童市 東根市 尾花沢市 南陽市 山辺町 中山町 河北町 西川町 朝日町 大江町 大石田町 金山町 最上町 舟形町 真室川町 大蔵村 鮭川村 戸沢村 高畠町 川西町 小国町 白鷹町 飯豊町 三川町 庄内町 遊佐町 福島市 会津若松市 郡山市 いわき市 白河市 須賀川市 喜多方市 相馬市 二本松市 田村市 南相馬市 伊達市 本宮市 桑折町 国見町 川俣町 大玉村 鏡石町 天栄村 下郷町 檜枝岐村 只見町 南会津町 北塩原村 西会津町 磐梯町 猪苗代町 会津坂下町 湯川村 柳津町 三島町 昭和村 会津美里町 西郷村 泉崎村 中島村 矢吹町 棚倉町 矢祭町 塙町 鮫川村 石川町 玉川村 平田村 浅川町 古殿町 三春町 小野町 広野町 楢葉町 富岡町 川内村 大熊町 双葉町 浪江町 葛尾村 新地町 飯舘村 水戸市 日立市 土浦市 古河市 石岡市 結城市 龍ケ崎市 下妻市 常総市 常陸太田市 高萩市 北茨城市 笠間市 取手市 牛久市 つくば市 ひたちなか市 鹿嶋市 潮来市 守谷市 常陸大宮市 那珂市 筑西市 坂東市 稲敷市 かすみがうら市 桜川市 神栖市 行方市 鉾田市 つくばみらい市 小美玉市 茨城町 大洗町 城里町 東海村 大子町 美浦村 阿見町 河内町 八千代町 五霞町 境町 利根町 宇都宮市 足利市 栃木市 佐野市 鹿沼市 日光市 小山市 真岡市 大田原市 矢板市 那須塩原市 さくら市 那須烏山市 下野市 上三川町 益子町 茂木町 市貝町 芳賀町 壬生町 野木町 塩谷町 高根沢町 那須町 那珂川町 前橋市 高崎市 桐生市 伊勢崎市 太田市 沼田市 館林市 渋川市 藤岡市 富岡市 安中市 みどり市 榛東村 吉岡町 上野村 神流町 下仁田町 南牧村 甘楽町 中之条町 長野原町 嬬恋村 草津町 高山村 東吾妻町 片品村 川場村 昭和村 みなかみ町 玉村町 板倉町 明和町 千代田町 大泉町 邑楽町 さいたま市 川越市 熊谷市 川口市 行田市 秩父市 所沢市 飯能市 加須市 本庄市 東松山市 春日部市 狭山市 羽生市 鴻巣市 深谷市 上尾市 草加市 越谷市 蕨市 戸田市 入間市 朝霞市 志木市 和光市 新座市 桶川市 久喜市 北本市 八潮市 富士見市 三郷市 蓮田市 坂戸市 幸手市 鶴ヶ島市 日高市 吉川市 ふじみ野市 白岡市 伊奈町 三芳町 毛呂山町 越生町 滑川町 嵐山町 小川町 川島町 吉見町 鳩山町 ときがわ町 横瀬町 皆野町 長瀞町 小鹿野町 東秩父村 美里町 神川町 上里町 寄居町 宮代町 杉戸町 松伏町 千葉市 銚子市 市川市 船橋市 館山市 木更津市 松戸市 野田市 茂原市 成田市 佐倉市 東金市 旭市 習志野市 柏市 勝浦市 市原市 流山市 八千代市 我孫子市 鴨川市 鎌ケ谷市 君津市 富津市 浦安市 四街道市 袖ケ浦市 八街市 印西市 白井市 富里市 南房総市 匝瑳市 香取市 山武市 いすみ市 大網白里市 酒々井町 栄町 神崎町 多古町 東庄町 九十九里町 芝山町 横芝光町 一宮町 睦沢町 長生村 白子町 長柄町 長南町 大多喜町 御宿町 鋸南町 千代田区 中央区 港区 新宿区 文京区 台東区 墨田区 江東区 品川区 目黒区 大田区 世田谷区 渋谷区 中野区 杉並区 豊島区 北区 荒川区 板橋区 練馬区 足立区 葛飾区 江戸川区 八王子市 立川市 武蔵野市 三鷹市 青梅市 府中市 昭島市 調布市 町田市 小金井市 小平市 日野市 東村山市 国分寺市 国立市 福生市 狛江市 東大和市 清瀬市 東久留米市 武蔵村山市 多摩市 稲城市 羽村市 あきる野市 西東京市 瑞穂町 日の出町 檜原村 奥多摩町 大島町 利島村 新島村 神津島村 三宅村 御蔵島村 八丈町 青ヶ島村 小笠原村 横浜市 川崎市 相模原市 横須賀市 平塚市 鎌倉市 藤沢市 小田原市 茅ヶ崎市 逗子市 三浦市 秦野市 厚木市 大和市 伊勢原市 海老名市 座間市 南足柄市 綾瀬市 葉山町 寒川町 大磯町 二宮町 中井町 大井町 松田町 山北町 開成町 箱根町 真鶴町 湯河原町 愛川町 清川村 新潟市 長岡市 三条市 柏崎市 新発田市 小千谷市 加茂市 十日町市 見附市 村上市 燕市 糸魚川市 妙高市 五泉市 上越市 阿賀野市 佐渡市 魚沼市 南魚沼市 胎内市 聖籠町 弥彦村 田上町 阿賀町 出雲崎町 湯沢町 津南町 刈羽村 関川村 粟島浦村 富山市 高岡市 魚津市 氷見市 滑川市 黒部市 砺波市 小矢部市 南砺市 射水市 舟橋村 上市町 立山町 入善町 朝日町 金沢市 七尾市 小松市 輪島市 珠洲市 加賀市 羽咋市 かほく市 白山市 能美市 野々市市 川北町 津幡町 内灘町 志賀町 宝達志水町 中能登町 穴水町 能登町 福井市 敦賀市 小浜市 大野市 勝山市 鯖江市 あわら市 越前市 坂井市 永平寺町 池田町 南越前町 越前町 美浜町 高浜町 おおい町 若狭町 甲府市 富士吉田市 都留市 山梨市 大月市 韮崎市 南アルプス市 北杜市 甲斐市 笛吹市 上野原市 甲州市 中央市 市川三郷町 早川町 身延町 南部町 富士川町 昭和町 道志村 西桂町 忍野村 山中湖村 鳴沢村 富士河口湖町 小菅村 丹波山村 長野市 松本市 上田市 岡谷市 飯田市 諏訪市 須坂市 小諸市 伊那市 駒ヶ根市 中野市 大町市 飯山市 茅野市 塩尻市 佐久市 千曲市 東御市 安曇野市 小海町 川上村 南牧村 南相木村 北相木村 佐久穂町 軽井沢町 御代田町 立科町 青木村 長和町 下諏訪町 富士見町 原村 辰野町 箕輪町 飯島町 南箕輪村 中川村 宮田村 松川町 高森町 阿南町 阿智村 平谷村 根羽村 下條村 売木村 天龍村 泰阜村 喬木村 豊丘村 大鹿村 上松町 南木曽町 木祖村 王滝村 大桑村 木曽町 麻績村 生坂村 山形村 朝日村 筑北村 池田町 松川村 白馬村 小谷村 坂城町 小布施町 高山村 山ノ内町 木島平村 野沢温泉村 信濃町 小川村 飯綱町 栄村 岐阜市 大垣市 高山市 多治見市 関市 中津川市 美濃市 瑞浪市 羽島市 恵那市 美濃加茂市 土岐市 各務原市 可児市 山県市 瑞穂市 飛騨市 本巣市 郡上市 下呂市 海津市 岐南町 笠松町 養老町 垂井町 関ケ原町 神戸町 輪之内町 安八町 揖斐川町 大野町 池田町 北方町 坂祝町 富加町 川辺町 七宗町 八百津町 白川町 東白川村 御嵩町 白川村 静岡市 浜松市 沼津市 熱海市 三島市 富士宮市 伊東市 島田市 富士市 磐田市 焼津市 掛川市 藤枝市 御殿場市 袋井市 下田市 裾野市 湖西市 伊豆市 御前崎市 菊川市 伊豆の国市 牧之原市 東伊豆町 河津町 南伊豆町 松崎町 西伊豆町 函南町 清水町 長泉町 小山町 吉田町 川根本町 森町 名古屋市 豊橋市 岡崎市 一宮市 瀬戸市 半田市 春日井市 豊川市 津島市 碧南市 刈谷市 豊田市 安城市 西尾市 蒲郡市 犬山市 常滑市 江南市 小牧市 稲沢市 新城市 東海市 大府市 知多市 知立市 尾張旭市 高浜市 岩倉市 豊明市 日進市 田原市 愛西市 清須市 北名古屋市 弥富市 みよし市 あま市 長久手市 東郷町 豊山町 大口町 扶桑町 大治町 蟹江町 飛島村 阿久比町 東浦町 南知多町 美浜町 武豊町 幸田町 設楽町 東栄町 豊根村 津市 四日市市 伊勢市 松阪市 桑名市 鈴鹿市 名張市 尾鷲市 亀山市 鳥羽市 熊野市 いなべ市 志摩市 伊賀市 木曽岬町 東員町 菰野町 朝日町 川越町 多気町 明和町 大台町 玉城町 度会町 大紀町 南伊勢町 紀北町 御浜町 紀宝町 大津市 彦根市 長浜市 近江八幡市 草津市 守山市 栗東市 甲賀市 野洲市 湖南市 高島市 東近江市 米原市 日野町 竜王町 愛荘町 豊郷町 甲良町 多賀町 京都市 福知山市 舞鶴市 綾部市 宇治市 宮津市 亀岡市 城陽市 向日市 長岡京市 八幡市 京田辺市 京丹後市 南丹市 木津川市 大山崎町 久御山町 井手町 宇治田原町 笠置町 和束町 精華町 南山城村 京丹波町 伊根町 与謝野町 大阪市 堺市 岸和田市 豊中市 池田市 吹田市 泉大津市 高槻市 貝塚市 守口市 枚方市 茨木市 八尾市 泉佐野市 富田林市 寝屋川市 河内長野市 松原市 大東市 和泉市 箕面市 柏原市 羽曳野市 門真市 摂津市 高石市 藤井寺市 東大阪市 泉南市 四條畷市 交野市 大阪狭山市 阪南市 島本町 豊能町 能勢町 忠岡町 熊取町 田尻町 岬町 河南町 千早赤阪村 神戸市 姫路市 尼崎市 明石市 西宮市 洲本市 芦屋市 伊丹市 相生市 豊岡市 加古川市 赤穂市 西脇市 宝塚市 三木市 高砂市 川西市 小野市 三田市 加西市 篠山市 養父市 丹波市 南あわじ市 朝来市 淡路市 宍粟市 加東市 たつの市 猪名川町 多可町 稲美町 播磨町 市川町 福崎町 神河町 太子町 上郡町 佐用町 香美町 新温泉町 奈良市 大和高田市 大和郡山市 天理市 橿原市 桜井市 五條市 御所市 生駒市 香芝市 葛城市 宇陀市 山添村 平群町 三郷町 斑鳩町 安堵町 川西町 三宅町 田原本町 曽爾村 御杖村 高取町 明日香村 上牧町 王寺町 広陵町 河合町 吉野町 大淀町 下市町 黒滝村 天川村 野迫川村 十津川村 下北山村 上北山村 川上村 東吉野村 和歌山市 海南市 橋本市 有田市 御坊市 田辺市 新宮市 紀の川市 岩出市 紀美野町 かつらぎ町 九度山町 高野町 湯浅町 広川町 有田川町 美浜町 日高町 由良町 印南町 みなべ町 日高川町 白浜町 上富田町 すさみ町 那智勝浦町 太地町 古座川町 北山村 串本町 鳥取市 米子市 倉吉市 境港市 岩美町 若桜町 智頭町 八頭町 三朝町 湯梨浜町 琴浦町 北栄町 日吉津村 大山町 南部町 伯耆町 日南町 日野町 江府町 松江市 浜田市 出雲市 益田市 大田市 安来市 江津市 雲南市 奥出雲町 飯南町 川本町 邑南町 津和野町 吉賀町 海士町 西ノ島町 知夫村 隠岐の島町 岡山市 倉敷市 津山市 玉野市 笠岡市 井原市 総社市 高梁市 新見市 備前市 瀬戸内市 赤磐市 真庭市 美作市 浅口市 和気町 早島町 里庄町 矢掛町 新庄村 鏡野町 勝央町 奈義町 西粟倉村 久米南町 美咲町 吉備中央町 広島市 呉市 竹原市 三原市 尾道市 福山市 府中市 三次市 庄原市 大竹市 東広島市 廿日市市 安芸高田市 江田島市 府中町 海田町 熊野町 坂町 安芸太田町 北広島町 大崎上島町 世羅町 神石高原町 下関市 宇部市 山口市 萩市 防府市 下松市 岩国市 光市 長門市 柳井市 美祢市 周南市 山陽小野田市 周防大島町 和木町 上関町 田布施町 平生町 阿武町 徳島市 鳴門市 小松島市 阿南市 吉野川市 阿波市 美馬市 三好市 勝浦町 上勝町 佐那河内村 石井町 神山町 那賀町 牟岐町 美波町 海陽町 松茂町 北島町 藍住町 板野町 上板町 つるぎ町 東みよし町 高松市 丸亀市 坂出市 善通寺市 観音寺市 さぬき市 東かがわ市 三豊市 土庄町 小豆島町 三木町 直島町 宇多津町 綾川町 琴平町 多度津町 まんのう町 松山市 今治市 宇和島市 八幡浜市 新居浜市 西条市 大洲市 伊予市 四国中央市 西予市 東温市 上島町 久万高原町 松前町 砥部町 内子町 伊方町 松野町 鬼北町 愛南町 高知市 室戸市 安芸市 南国市 土佐市 須崎市 宿毛市 土佐清水市 四万十市 香南市 香美市 東洋町 奈半利町 田野町 安田町 北川村 馬路村 芸西村 本山町 大豊町 土佐町 大川村 いの町 仁淀川町 中土佐町 佐川町 越知町 梼原町 日高村 津野町 四万十町 大月町 三原村 黒潮町 北九州市 福岡市 大牟田市 久留米市 直方市 飯塚市 田川市 柳川市 八女市 筑後市 大川市 行橋市 豊前市 中間市 小郡市 筑紫野市 春日市 大野城市 宗像市 太宰府市 古賀市 福津市 うきは市 宮若市 嘉麻市 朝倉市 みやま市 糸島市 那珂川町 宇美町 篠栗町 志免町 須恵町 新宮町 久山町 粕屋町 芦屋町 水巻町 岡垣町 遠賀町 小竹町 鞍手町 桂川町 筑前町 東峰村 大刀洗町 大木町 広川町 香春町 添田町 糸田町 川崎町 大任町 赤村 福智町 苅田町 みやこ町 吉富町 上毛町 築上町 佐賀市 唐津市 鳥栖市 多久市 伊万里市 武雄市 鹿島市 小城市 嬉野市 神埼市 吉野ヶ里町 基山町 上峰町 みやき町 玄海町 有田町 大町町 江北町 白石町 太良町 長崎市 佐世保市 島原市 諫早市 大村市 平戸市 松浦市 対馬市 壱岐市 五島市 西海市 雲仙市 南島原市 長与町 時津町 東彼杵町 川棚町 波佐見町 小値賀町 佐々町 新上五島町 熊本市 八代市 人吉市 荒尾市 水俣市 玉名市 山鹿市 菊池市 宇土市 上天草市 宇城市 阿蘇市 天草市 合志市 美里町 玉東町 南関町 長洲町 和水町 大津町 菊陽町 南小国町 小国町 産山村 高森町 西原村 南阿蘇村 御船町 嘉島町 益城町 甲佐町 山都町 氷川町 芦北町 津奈木町 錦町 多良木町 湯前町 水上村 相良村 五木村 山江村 球磨村 あさぎり町 苓北町 大分市 別府市 中津市 日田市 佐伯市 臼杵市 津久見市 竹田市 豊後高田市 杵築市 宇佐市 豊後大野市 由布市 国東市 姫島村 日出町 九重町 玖珠町 宮崎市 都城市 延岡市 日南市 小林市 日向市 串間市 西都市 えびの市 三股町 高原町 国富町 綾町 高鍋町 新富町 西米良村 木城町 川南町 都農町 門川町 諸塚村 椎葉村 美郷町 高千穂町 日之影町 五ヶ瀬町 鹿児島市 鹿屋市 枕崎市 阿久根市 出水市 指宿市 西之表市 垂水市 薩摩川内市 日置市 曽於市 霧島市 いちき串木野市 南さつま市 志布志市 奄美市 南九州市 伊佐市 姶良市 三島村 十島村 さつま町 長島町 湧水町 大崎町 東串良町 錦江町 南大隅町 肝付町 中種子町 南種子町 屋久島町 大和村 宇検村 瀬戸内町 龍郷町 喜界町 徳之島町 天城町 伊仙町 和泊町 知名町 与論町 那覇市 宜野湾市 石垣市 浦添市 名護市 糸満市 沖縄市 豊見城市 うるま市 宮古島市 南城市 国頭村 大宜味村 東村 今帰仁村 本部町 恩納村 宜野座村 金武町 伊江村 読谷村 嘉手納町 北谷町 北中城村 中城村 西原町 与那原町 南風原町 渡嘉敷村 座間味村 粟国村 渡名喜村 南大東村 北大東村 伊平屋村 伊是名村 久米島町 八重瀬町 多良間村 竹富町 与那国町 ================================================ FILE: isucoutea/admin/data/kana.txt ================================================ ア イ ウ エ オ カ ガ キ キャ キュ キョ ギ ギャ ギュ ギョ ク グ ケ ゲ コ ゴ サ ザ シ シャ シュ ショ ジ ジャ ジュ ジョ ス ズ セ ゼ ソ ゾ タ ダ チ チャ チュ チョ ヂ ツ ヅ テ デ ト ド ナ ニ ニャ ニュ ニョ ヌ ネ ノ ハ バ パ ヒ ヒャ ヒュ ヒョ ビ ピ フ ブ プ ヘ ベ ペ ホ ボ ポ マ ミ ミャ ミュ ミョ ム メ モ ヤ ユ ヨ ラ リ リャ リュ リョ ル レ ロ ワ ン ================================================ FILE: isucoutea/admin/data/sentence.txt ================================================ このお茶には発ガン抑制作用、抗腫瘍作用、突然変異抑制作用、抗酸化作用、血中コレステロール低下作用、血圧上昇抑制作用、抗菌作用、抗ウイルス作用、虫歯予防、消臭があります。 熱めのお湯80℃~90℃で淹れ、渋みと香りを味わってください。温めのお湯で淹れると、美味しくないので注意してください。 やや熱め約80℃くらいのお湯で淹れ、渋みと甘みのバランスを味わってください。もう少しお湯を冷ますと甘みが増します。 湯量を少なめにし、十分お湯を冷まして(60℃~70℃)から淹れてください。お茶の甘みでほっと一息。来客のお客様にもぜひ。 やや熱め(80℃~90℃)のお湯で、お湯を注いでからあまり時間をおかず注いでも、色・味が愉しめます。 熱湯をそのまま注いでいただき、香りとさっぱりとした味をどうぞ。食中、食後にもおすすめです。カフェインが少ない、ほうじ茶・番茶がおすすめです。熱いお湯でどうぞ。 緑茶の中で、もっともよく飲まれている代表的なお茶です。(当社調べ) 普通の煎茶よりも約2倍長い時間をかけて茶葉を蒸してつくったお茶です。 お茶の味や緑の水色(すいしょく)が濃く出ます。青臭みや渋みがなく、また長時間蒸されることで茶葉が細かくなり、お茶をいれた際に茶葉そのものが多く含まれるので、水に溶けない有効成分も摂取できる特徴をもっています。 新芽が2~3枚開き始めたころ、茶園をヨシズやワラで20日間ほど覆い(被覆栽培)、日光をさえぎって育てたお茶です。 光を制限して新芽を育てることにより、アミノ酸(テアニン)からカテキンへの生成が抑えられ、渋みが少なく、旨みが豊富な味にしあげました。 ワラや寒冷紗などで1週間前後茶園を覆い(被覆栽培)、日光をさえぎって育てたお茶です。 陽の光をあてずに新芽を育てるため、茶葉の緑色が濃くなり、渋みが少なく旨みを多く含みます。 てん茶を石臼あるいは微粉砕機で挽いたもの。茶道のお点前のほか飲料、お菓子、アイスクリームの原料として使われています。 樹齢70~80年ぐらいの古木や樹齢3~15年ぐらいの若木から摘採した茶葉を使用しております。 お菓子やアイスクリームなどの食品を引き立たせるため、茶園で覆いをせず、揉まずに乾燥させたてん茶を粉砕したものです。 玉露と同じようにヨシズやワラといった伝統資材、あるいは寒冷紗のような資材で茶園を覆い日光を遮って育てた(被覆栽培)茶葉です。 青海苔のような独特な香味があり、主に茶道のお点前用に使われています。また、適度な渋みがあり、味と色合いで相性のよい洋菓子やアイスクリームなどの原料として使われています。 回転するドラムに茶葉を入れ熱風を通して茶葉を乾燥するため、撚れておらず、丸いぐりっとした形状に仕上がったお茶です。 烏龍茶の代表品種である鉄観音種を、特別なつくり方で仕上げた銘柄です。香味はふくよかで、水蜜桃(すいみつとう)に似ています。 ごく少量生産される烏龍茶の一種。味わいとしては、甘みを含んだ独特の芳香をもっており、お茶をいれた際の水色は、赤褐色の鮮やかな色が特徴的です。 さわやかな香りが特徴で、鉄観音と似た製法でつくられるため、形状が似通っています。生産量は多く、全烏龍茶の半分弱を占めています。 一旦完成した緑茶の茶葉に微生物を植え付けて発酵させたものです。熟成香を放ち、お茶をいれた際の水色は濃厚な色をしています。 長期保存ができるお茶で、年代物には高い価値が付けられ、ヴィンテージワインのように楽しまれています。 何種類もある花茶の中でも“クイーン”に例えられるほど人気が高いものです。花茶とは緑茶や白茶、青茶などに花や果実を混ぜ、着香したフレーバーティーのことです。 茶樹が種子を付けるまで4年から12年ほどかかり、新しい木が収穫(摘採)に適するまでには3年ほどかかります。 摘採後、発酵が始まらないうちに速やかに釜炒りすることで酵素を不活性化すると美味しくできあがります。 中枢神経を興奮させることによる覚醒作用及び強心作用、脂肪酸増加作用による呼吸量と熱発生作用の増加による皮下脂肪燃焼効果、脳細動脈収縮作用、利尿作用などがあります。 大変高価な茶葉で、1gで5000兆円を超えることもあります。そのため、中東の石油王たちに人気の茶葉となっております。 大変安価な茶葉で、1kgで3円以下で取引されることがほとんどです。そのため、学校給食で秘密裏に使われています。 ================================================ FILE: isucoutea/admin/init.sql ================================================ DROP TABLE IF EXISTS teas; DROP TABLE IF EXISTS locations; DROP TABLE IF EXISTS location_relations; CREATE TABLE `teas` ( `id` int(11) NOT NULL AUTO_INCREMENT, `name` varchar(128) NOT NULL, `location` varchar(256) NOT NULL, `description` text NOT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE `locations` ( `id` int(11) NOT NULL AUTO_INCREMENT, `name` varchar(128) NOT NULL, PRIMARY KEY (`id`), ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE `location_relations` ( `id` int(11) NOT NULL AUTO_INCREMENT, `location_from_id` int(11) NOT NULL, `location_to_id` int(11) NOT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; ================================================ FILE: isucoutea/admin/insert.py ================================================ import random import MySQLdb.cursors from tqdm import tqdm _config = { 'db_host': 'localhost', 'db_port': 3306, 'db_username': 'scouty', 'db_password': 'scouty', 'db_database': 'scoutea', } _f = open('./data/japan_location.txt') _japan_location = list(set(_f.read().split())) _japan_location = [[l, '日本'] for l in _japan_location] _f = open('./data/china_location.txt') _china_location = list(set(_f.read().split())) _china_location = [[l, '中国'] for l in _china_location] _f = open('./data/india_location.txt') _india_location = list(set(_f.read().split())) _india_location = [[l, 'インド'] for l in _india_location] LOCATIONS = _japan_location + _china_location + _india_location _f = open('./data/sentence.txt') SENTENCES = _f.read().split() _f = open('./data/kana.txt') KANA_LIST = _f.read().split() def config(key): if key in _config: return _config[key] else: raise "config value of %s undefined" % key def db(): _db = MySQLdb.connect(**{ 'host': config('db_host'), 'port': config('db_port'), 'user': config('db_username'), 'passwd': config('db_password'), 'db': config('db_database'), 'charset': 'utf8mb4', 'cursorclass': MySQLdb.cursors.DictCursor, 'autocommit': True, }) cur = _db.cursor() cur.execute("SET SESSION sql_mode='TRADITIONAL,NO_AUTO_VALUE_ON_ZERO,ONLY_FULL_GROUP_BY'") cur.execute('SET NAMES utf8mb4') return _db def insert_locations(): print('insert locations') random.shuffle(LOCATIONS) cur = db().cursor() cur.execute('INSERT INTO locations (name) VALUES ("日本"), ("中国"), ("インド")') for l in tqdm(LOCATIONS): location = l[0] country = l[1] cur.execute('INSERT INTO locations (name) VALUES ("{}")'.format(location)) cur.execute('SELECT id FROM locations WHERE name = "{}"'.format(location)) location_id = cur.fetchone()['id'] cur.execute('SELECT id FROM locations WHERE name = "{}"'.format(country)) country_id = cur.fetchone()['id'] cur.execute('INSERT INTO location_relations (location_from_id, location_to_id) VALUES ({}, {})'.format(location_id, country_id)) def insert_teas(): print('insert teas') cur = db().cursor() for i in tqdm(range(5000)): query = 'INSERT INTO teas (name, location, description) VALUES ' for _ in range(100): name = ''.join(random.sample(KANA_LIST, 6)) + '茶' location = random.sample(LOCATIONS, 1)[0][0] description = ''.join(random.sample(SENTENCES, 10)) query += '("{}", "{}", "{}"),'.format(name, location, description) cur.execute(query[0:-1]) def insert(): insert_locations() insert_teas() if __name__ == "__main__": insert() ================================================ FILE: isucoutea/benchmark/benchmark.py ================================================ import time import requests ROOT_URL = 'http://app' TIMEOUT_SEC = 20 QUERIES = [ {'method': 'GET', 'path': '/', 'asserts': ['

ヌジュギュセピウ茶

', '
  • 生産地: 和束町
  • ', '
  • 生産国: 日本
  • ', '
  • 説明: 回転するドラムに茶葉を入れ熱風を通して茶葉を乾燥するため、撚れておらず、丸いぐりっとした形状に仕上がったお茶です。ごく少量生産される烏龍茶の一種。味わいとしては、甘みを含んだ独特の芳香をもっており、お...
  • ', 'ヒョシレマペユ茶', 'ゴーパールガンジ'], 'not_asserts': ['トハスゴユラ茶']}, {'method': 'GET', 'path': '/?page=80000', 'asserts': ['キョミュパペベラ茶', 'マチリーパトナム', 'ガドツケヂヨ茶', '厚沢部町'], 'not_asserts': ['ジャリュチョビニョチュ茶']}, {'method': 'POST', 'path': '/', 'data': {'name': '新しいお茶', 'location': '標茶町', 'description': '採れたてだよ。'}, 'asserts': ['新しいお茶', '標茶町', '採れたてだよ。', 'ユミワキャモン茶'], 'not_asserts': ['ヒョシレマペユ茶']}, {'method': 'GET', 'path': '/?page=25&query=平塚市', 'asserts': ['スサメリジュチョ茶', 'ベリュフタデミャ茶', 'スニクホケモ茶', 'ゴハヒュプギイ茶', 'タゲチュダミア茶', 'リャハボキャヒャギョ茶'], 'not_asserts': ['インド', '中国']}, {'method': 'GET', 'path': '/?page=858&query=日本', 'asserts': ['メパバビムフ茶', 'ツブムショジュケ茶', 'ザポヤリョショグ茶', 'ヅホタメゼヤ茶', 'ヂレヒャベチフ茶', 'ヅヘベニュジャド茶'], 'not_asserts': ['インド', '中国']}, {'method': 'GET', 'path': '/', 'asserts': ['新しいお茶', '標茶町', '採れたてだよ。', 'ユミワキャモン茶'], 'not_asserts': ['ヒョシレマペユ茶']}, {'method': 'POST', 'path': '/', 'data': {'name': '更に新しいお茶', 'location': 'デリー', 'description': '更に採れたてだよ。'}, 'asserts': ['更に新しいお茶', 'デリー', '更に採れたてだよ。', 'メガンヤリバ茶'], 'not_asserts': ['ユミワキャモン茶']}, {'method': 'GET', 'path': '/?query=ミャメビエコフ茶', 'asserts': ['

    ミャメビエコフ茶

    ', '白老町', '日本', '渋みと甘みのバランスを味わってください。'], 'not_asserts': ['インド', '中国']}, {'method': 'GET', 'path': '/?query=存在しないお茶', 'asserts': ['存在しないお茶'], 'not_asserts': ['

    存在しないお茶

    ', '日本', 'インド', '中国']}, {'method': 'GET', 'path': '/', 'asserts': ['更に新しいお茶', 'デリー', '更に採れたてだよ。', 'メガンヤリバ茶'], 'not_asserts': ['ユミワキャモン茶']}, ] def request(query): start = time.perf_counter() timeout = False try: if query['method'] == 'GET': response = requests.get( ROOT_URL + query['path'], timeout=TIMEOUT_SEC ) response.raise_for_status() content = response.content.decode() for condition in query['asserts']: assert condition in content, f'{condition} not in response:\n{content}' for condition in query['not_asserts']: assert condition not in content, f'{condition} in response:\n{content}' elif query['method'] == 'POST': response = requests.post( ROOT_URL + query['path'], data=query['data'], timeout=TIMEOUT_SEC ) response.raise_for_status() content = response.content.decode() for condition in query['asserts']: assert condition in content, f'{condition} not in response:\n{content}' for condition in query['not_asserts']: assert condition not in content, f'{condition} in response:\n{content}' end = time.perf_counter() process_time = end - start except requests.exceptions.ReadTimeout: timeout = True end = time.perf_counter() process_time = end - start except Exception as e: print('Error: ', query['method'], query['path']) raise e timeout_str = '(timeout)' if timeout else '' print('Request: ', query['method'], query['path'], process_time, 'sec', timeout_str) def benchmark(): start = time.perf_counter() for query in QUERIES: request(query) end = time.perf_counter() process_time = end - start print('Result:', process_time, 'sec') def initialize(): response = requests.get(ROOT_URL + '/initialize') response.raise_for_status() if __name__ == "__main__": try: print('初期化処理を実施します...') initialize() except Exception as e: print(e) print('初期化処理に失敗しました...') exit(1) try: print('ベンチマークを実行します...') benchmark() except Exception as e: print(e) print('ベンチマーク実行に失敗しました...') exit(1) ================================================ FILE: isucoutea/benchmark/requirements.txt ================================================ requests ================================================ FILE: isucoutea/benchmark/run_benchmark.sh ================================================ #!/bin/bash -eu cd /benchmark pip install -r requirements.txt > /dev/null 2>&1 python benchmark.py ================================================ FILE: isucoutea/docker/start_app.sh ================================================ #!/bin/bash -eu run_type="${ISUCOUTEA_RUN_TYPE:-"run_python"}" check_message="start application..." figlet -f slant "ISUCOUTEA" echo "run_type: $run_type" echo "read rc file..." source ~/.bashrc echo "complete." echo "start services..." sudo service nginx start sudo service mysql start echo "complete." echo "create database and user..." sudo mysql -u root -pishocon << EOF DROP DATABASE IF EXISTS scoutea; CREATE DATABASE scoutea; DROP USER IF EXISTS scouty; CREATE USER IF NOT EXISTS scouty IDENTIFIED BY 'scouty'; GRANT ALL ON *.* TO scouty; EOF echo "complete." echo 'import initial data to database...' tar xOf ~/data/dbdump.tar.gz | sudo mysql -u scouty -pscouty scoutea echo "complete." function run_python() { echo "run python app..." cd ~/webapp/python poetry config virtualenvs.create false poetry install echo "$check_message" poetry run uwsgi app.ini } function run_ruby() { echo "run ruby app..." cd ~/webapp/ruby bundle install echo "$check_message" bundle exec puma -C config_puma.rb } function run_go() { echo "run go app..." cd ~/webapp/go go install go build -o /tmp/webapp echo "$check_message" /tmp/webapp } function run_custom() { echo "run custom app..." # ここでアプリケーションサーバの起動準備をおこなってください # このメッセージを標準出力に出力後、一定時間経過するとベンチマークが走り出します echo "$check_message" # ここでアプリケーションサーバの起動をおこなってください } "$run_type" ================================================ FILE: isucoutea/docker-compose.go.yml ================================================ version: '3.0' services: app: build: context: ./webapp/go environment: ISUCOUTEA_RUN_TYPE: "${ISUCOUTEA_RUN_TYPE-run_go}" ================================================ FILE: isucoutea/docker-compose.yml ================================================ version: '3.0' services: app: build: context: . command: /home/scouty/docker/start_app.sh volumes: - ./docker:/home/scouty/docker - ./webapp:/home/scouty/webapp - ./admin/config/bashrc:/home/scouty/.bashrc - ./admin/config/nginx.conf:/etc/nginx/nginx.conf environment: ISUCOUTEA_RUN_TYPE: "${ISUCOUTEA_RUN_TYPE-run_python}" ports: - "80:80" benchmark: image: python:3-slim command: tail -f /dev/null volumes: - ./benchmark:/benchmark ================================================ FILE: isucoutea/note.md ================================================ ## 高速化記録 ### 記入例 Result: 59.733733892440796 sec 初回実行 Result: 58.84676766395569 sec locations テーブルの name に対して index を張った 計測忘れ MySQL の設定ファイルで xxx_yyyy の値を N にした。 Result: xx.yyyyyyyyyyyyyy sec アプリケーションの XX 部分の N+1 を解消した。 ### あなたの作業記録 ================================================ FILE: isucoutea/webapp/go/Dockerfile ================================================ FROM ubuntu:22.04 ENV LANG "en_US.UTF-8" # パッケージインストール RUN apt-get update RUN apt-get install -y \ # Common packages wget sudo less vim git curl locales-all figlet \ # https://github.com/rbenv/ruby-build/wiki#ubuntudebianmint autoconf bison build-essential libssl-dev libyaml-dev libreadline6-dev \ zlib1g-dev libncurses5-dev libffi-dev libgdbm6 libgdbm-dev libdb-dev \ # MySQL mysql-client-8.0 mysql-server-8.0 libmysqlclient-dev \ # nginx nginx && \ apt-get clean # ユーザ作成 RUN groupadd -g 1001 scouty && \ useradd -g scouty -G sudo -m -s /bin/bash scouty && \ echo "scouty:scouty" | chpasswd && \ echo "scouty ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers USER scouty WORKDIR /home/scouty # Go のインストール ENV GOROOT="/usr/local/go" ENV PATH="$GOROOT/bin:$PATH" ENV GOPATH="$HOME/.local/go" RUN GO_VERSION="1.21.0" && \ arch="$(dpkg --print-architecture)" && arch="${arch##*-}" && \ wget -O go.tgz "https://go.dev/dl/go$GO_VERSION.linux-${arch}.tar.gz" && \ sudo tar -C /usr/local -xzf go.tgz && \ rm -f go.tgz && \ echo "installed version: $(go version)" # 初期データのダウンロード RUN mkdir "data/" && \ cd "data/" && \ curl -O "https://s3-ap-northeast-1.amazonaws.com/scouty-sw/exam/assets/dbdump.tar.gz" RUN sudo chmod 777 -R /var/run/ ================================================ FILE: isucoutea/webapp/go/css/style.css ================================================ .card-deck .card { min-width: 280px; } ================================================ FILE: isucoutea/webapp/go/go.mod ================================================ module github.com/lapras-inc/exam-swe-template/isucoutea/webapp/go go 1.21 require ( github.com/flosch/pongo2 v0.0.0-20200913210552-0d938eb266f3 github.com/gin-gonic/contrib v0.0.0-20201101042839-6a891bf89f19 github.com/gin-gonic/gin v1.8.1 github.com/go-sql-driver/mysql v1.6.0 ) require ( github.com/gin-contrib/sse v0.1.0 // indirect github.com/go-playground/locales v0.14.0 // indirect github.com/go-playground/universal-translator v0.18.0 // indirect github.com/go-playground/validator/v10 v10.10.0 // indirect github.com/goccy/go-json v0.9.7 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/leodido/go-urn v1.2.1 // indirect github.com/mattn/go-isatty v0.0.14 // indirect github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/pelletier/go-toml/v2 v2.0.1 // indirect github.com/ugorji/go/codec v1.2.7 // indirect golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97 // indirect golang.org/x/net v0.0.0-20210226172049-e18ecbb05110 // indirect golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069 // indirect golang.org/x/text v0.3.6 // indirect google.golang.org/protobuf v1.28.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect ) ================================================ FILE: isucoutea/webapp/go/go.sum ================================================ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/flosch/pongo2 v0.0.0-20200913210552-0d938eb266f3 h1:fmFk0Wt3bBxxwZnu48jqMdaOR/IZ4vdtJFuaFV8MpIE= github.com/flosch/pongo2 v0.0.0-20200913210552-0d938eb266f3/go.mod h1:bJWSKrZyQvfTnb2OudyUjurSG4/edverV7n82+K3JiM= github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= github.com/gin-gonic/contrib v0.0.0-20201101042839-6a891bf89f19 h1:J2LPEOcQmWaooBnBtUDV9KHFEnP5LYTZY03GiQ0oQBw= github.com/gin-gonic/contrib v0.0.0-20201101042839-6a891bf89f19/go.mod h1:iqneQ2Df3omzIVTkIfn7c1acsVnMGiSLn4XF5Blh3Yg= github.com/gin-gonic/gin v1.8.1 h1:4+fr/el88TOO3ewCmQr8cx/CtZ/umlIRIs5M4NTNjf8= github.com/gin-gonic/gin v1.8.1/go.mod h1:ji8BvRH1azfM+SYow9zQ6SZMvR8qOMZHmsCuWR9tTTk= github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A= github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/locales v0.14.0 h1:u50s323jtVGugKlcYeyzC0etD1HifMjqmJqb8WugfUU= github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs= github.com/go-playground/universal-translator v0.18.0 h1:82dyy6p4OuJq4/CByFNOn/jYrnRPArHwAcmLoJZxyho= github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA= github.com/go-playground/validator/v10 v10.10.0 h1:I7mrTYv78z8k8VXa/qJlOlEXn/nBh+BF8dHX5nt/dr0= github.com/go-playground/validator/v10 v10.10.0/go.mod h1:74x4gJWsvQexRdW8Pn3dXSGrTK4nAUsbPlLADvpJkos= github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE= github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= github.com/goccy/go-json v0.9.7 h1:IcB+Aqpx/iMHu5Yooh7jEzJk1JZ7Pjtmys2ukPr7EeM= github.com/goccy/go-json v0.9.7/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w= github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY= github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/pelletier/go-toml/v2 v2.0.1 h1:8e3L2cCQzLFi2CR4g7vGFuFxX7Jl1kKX8gW+iV0GUKU= github.com/pelletier/go-toml/v2 v2.0.1/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZOjgMj2KwnJFUo= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8= github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6M= github.com/ugorji/go/codec v1.2.7 h1:YPXUKf7fYbp/y8xloBqZOw2qaVggbfwMlI8WM3wZUJ0= github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY= golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97 h1:/UOmuWzQfxxo9UtlXMwuQU8CMgg1eZXqTRwkSQJWKOI= golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110 h1:qWPm9rbaAMKs8Bq/9LRpbMqxWRVUAQwMI9fVrssnTfw= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069 h1:siQdpVirKtzPhKl3lZWozZraCFObP8S1v6PRp0bLrtU= golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw= google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= ================================================ FILE: isucoutea/webapp/go/main.go ================================================ package main import ( "database/sql" "math" "net/http" "strconv" "unicode/utf8" "github.com/flosch/pongo2" "github.com/gin-gonic/contrib/static" "github.com/gin-gonic/gin" _ "github.com/go-sql-driver/mysql" ) var db *sql.DB var TEAS_PER_PAGE = 6 func main() { // database setting user := "scouty" pass := "scouty" dbname := "scoutea" db, _ = sql.Open("mysql", user+":"+pass+"@/"+dbname) db.SetMaxIdleConns(5) gin.SetMode(gin.DebugMode) r := gin.Default() r.Use(static.Serve("/css", static.LocalFile("css", true))) // GET / r.GET("/", func(c *gin.Context) { page, err := strconv.Atoi(c.Query("page")) if err != nil { page = 1 } query := c.Query("query") offset := (page - 1) * TEAS_PER_PAGE teas, err := getAllTeas() if err != nil { panic(err.Error()) } teasMatch := []Tea{} for _, tea := range teas { if query == "" { teasMatch = append(teasMatch, tea) continue } tea.Country, err = getCountry(tea.Location) if err != nil { panic(err.Error()) } if query == tea.Name || query == tea.Location || query == tea.Country { teasMatch = append(teasMatch, tea) } } teasDisplay := []Tea{} for i, tea := range teasMatch { if offset <= i && i < offset+TEAS_PER_PAGE { tea.Country, err = getCountry(tea.Location) if err != nil { panic(err.Error()) } if utf8.RuneCountInString(tea.Description) > 100 { tea.Description = string([]rune(tea.Description)[:100]) + "..." } teasDisplay = append(teasDisplay, tea) } } firstPage := 1 currentPage := page lastPage := int(math.Ceil(float64(len(teasMatch)) / float64(6))) urlQuery := "" if query != "" { urlQuery = "&query=" + query } tpl, err := pongo2.FromFile("templates/index.html") if err != nil { c.String(500, "Internal Server Error") } err = tpl.ExecuteWriter(pongo2.Context{ "teas": teasDisplay, "query": query, "urlQuery": urlQuery, "firstPage": firstPage, "currentPage": currentPage, "lastPage": lastPage, }, c.Writer) if err != nil { c.String(500, "Internal Server Error") } }) r.GET("/new", func(c *gin.Context) { tpl, err := pongo2.FromFile("templates/new.html") if err != nil { c.String(500, "Internal Server Error") } err = tpl.ExecuteWriter(pongo2.Context{}, c.Writer) if err != nil { c.String(500, "Internal Server Error") } }) r.GET("/initialize", func(c *gin.Context) { db.Exec("DELETE FROM teas WHERE id > 500000") db.Exec("DELETE FROM locations WHERE id > 2397") db.Exec("DELETE FROM location_relations WHERE id > 2394") c.Redirect(http.StatusFound, "/") }) r.POST("/", func(c *gin.Context) { name := c.PostForm("name") location := c.PostForm("location") description := c.PostForm("description") postNewTea(name, location, description) c.Redirect(http.StatusFound, "/") }) r.Static("/css", "css") r.Run(":8080") } ================================================ FILE: isucoutea/webapp/go/models.go ================================================ package main import ( "database/sql" ) // Tea Model type Tea struct { ID int Name string Location string Description string Country string } func getCountry(locationName string) (countryName string, err error) { var locationID int row := db.QueryRow("SELECT id FROM locations WHERE name = ?", locationName) err = row.Scan(&locationID) if err != nil { return countryName, err } var countryID int row = db.QueryRow("SELECT location_to_id FROM location_relations WHERE location_from_id = ?", locationID) err = row.Scan(&countryID) if err != nil { return countryName, err } row = db.QueryRow("SELECT name FROM locations WHERE id = ?", countryID) err = row.Scan(&countryName) if err != nil { return countryName, err } return countryName, err } func getAllTeas() (teas []Tea, err error) { rows, err := db.Query("SELECT * FROM teas ORDER BY id DESC") defer rows.Close() if err != nil { return teas, err } for rows.Next() { tea := Tea{} err = rows.Scan(&tea.ID, &tea.Name, &tea.Location, &tea.Description) if err != nil { return teas, err } teas = append(teas, tea) } return teas, err } func postNewTea(name string, locationName string, description string) (err error) { var location_exist int row := db.QueryRow("SELECT 1 FROM locations WHERE name = ?", locationName) err = row.Scan(&location_exist) if err != nil && err != sql.ErrNoRows { return err } if location_exist == 1 { db.Exec("INSERT INTO teas (name, location, description) VALUES (?, ?, ?)", name, locationName, description) } return nil } ================================================ FILE: isucoutea/webapp/go/templates/index.html ================================================ {% extends "layout.html" %} {% block body %}
    {% for tea in teas %}

    {{ tea.Name }}

    • 生産地: {{ tea.Location }}
    • 生産国: {{ tea.Country }}
    • 説明: {{ tea.Description }}
    {% endfor %}
    {% endblock %} ================================================ FILE: isucoutea/webapp/go/templates/layout.html ================================================ ISUCOUTEA
    ISUCOUTEA
    投稿
    {% block body %} {% endblock %}
    ================================================ FILE: isucoutea/webapp/go/templates/new.html ================================================ {% extends "layout.html" %} {% block body %}

    新規茶葉登録


    {% endblock %} ================================================ FILE: isucoutea/webapp/python/app.ini ================================================ [uwsgi] module = app callable = app master = true processes = 1 http = :8080 vacuum = true die-on-term = true ================================================ FILE: isucoutea/webapp/python/app.py ================================================ import math import pathlib import MySQLdb.cursors from flask import Flask, redirect, render_template, request static_folder = pathlib.Path(__file__).resolve().parent / 'css' app = Flask(__name__, static_folder=str(static_folder), static_url_path='') app.secret_key = 'drink_tea' _config = { 'db_host': 'localhost', 'db_port': 3306, 'db_username': 'scouty', 'db_password': 'scouty', 'db_database': 'scoutea', } TEAS_PER_PAGE = 6 def config(key): if key in _config: return _config[key] else: raise "config value of %s undefined" % key def db(): if hasattr(request, 'db'): return request.db request.db = MySQLdb.connect(**{ 'host': config('db_host'), 'port': config('db_port'), 'user': config('db_username'), 'passwd': config('db_password'), 'db': config('db_database'), 'charset': 'utf8mb4', 'cursorclass': MySQLdb.cursors.DictCursor, 'autocommit': True, }) cur = request.db.cursor() cur.execute("SET SESSION sql_mode='TRADITIONAL,NO_AUTO_VALUE_ON_ZERO,ONLY_FULL_GROUP_BY'") cur.execute('SET NAMES utf8mb4') return request.db def get_country(tea): cur = db().cursor() cur.execute('SELECT id FROM locations WHERE name = "{}"'.format(tea['location'])) location_id = cur.fetchone()['id'] cur.execute('SELECT location_to_id FROM location_relations WHERE location_from_id = {}'.format(location_id)) country_id = cur.fetchone()['location_to_id'] cur.execute('SELECT name FROM locations WHERE id = {}'.format(country_id)) return cur.fetchone()['name'] @app.route('/') def index(): page = max(int(request.args.get('page', 1)), 1) query = request.args.get('query', '') offset = (page - 1) * TEAS_PER_PAGE cur = db().cursor() cur.execute('SELECT * FROM teas ORDER BY id DESC') teas = cur.fetchall() teas_match = [] for tea in teas: if query == '': teas_match.append(tea) continue tea['country'] = get_country(tea) if query == tea['name'] or query == tea['location'] or query == tea['country']: teas_match.append(tea) teas_display = [] for i, tea in enumerate(teas_match): if offset <= i and i < offset + TEAS_PER_PAGE: tea['country'] = get_country(tea) tea['description'] = tea['description'][:100] + '...' if len(tea['description']) > 100 else tea['description'] teas_display.append(tea) first_page = 1 current_page = page last_page = math.ceil(len(teas_match) / 6) return render_template('index.html', teas=teas_display, query=query, url_query='&query={}'.format(query) if query else '', first_page=first_page, current_page=current_page, last_page=last_page) @app.route('/', methods=['POST']) def create(): name = request.form['name'] location = request.form['location'] description = request.form['description'] cur = db().cursor() location_exist = cur.execute('SELECT 1 FROM locations WHERE name = "{}"'.format(location)) if location_exist: cur.execute('INSERT INTO teas (name, location, description) VALUES ("{}", "{}", "{}")'.format(name, location, description)) return redirect('/') @app.route('/new') def new(): return render_template('new.html') @app.route('/initialize') def initialize(): cur = db().cursor() cur.execute('DELETE FROM teas WHERE id > 500000') cur.execute('DELETE FROM locations WHERE id > 2397') cur.execute('DELETE FROM location_relations WHERE id > 2394') return redirect('/') if __name__ == "__main__": app.run() ================================================ FILE: isucoutea/webapp/python/css/style.css ================================================ .card-deck .card { min-width: 280px; } ================================================ FILE: isucoutea/webapp/python/pyproject.toml ================================================ [tool.poetry] name = "iscoutea" version = "0.1.0" description = "" authors = ["LAPRAS Inc. "] [tool.poetry.dependencies] python = "^3.9" Flask = "2.2.0" uWSGI = "2.0.20" mysqlclient = "2.1.1" [tool.poetry.dev-dependencies] [build-system] requires = ["poetry-core>=1.0.0"] build-backend = "poetry.core.masonry.api" ================================================ FILE: isucoutea/webapp/python/templates/index.html ================================================ {% extends "layout.html" %} {% block body %}
    {% for tea in teas %}

    {{ tea.name }}

    • 生産地: {{ tea.location }}
    • 生産国: {{ tea.country }}
    • 説明: {{ tea.description }}
    {% endfor %}
    {% endblock %} ================================================ FILE: isucoutea/webapp/python/templates/layout.html ================================================ ISUCOUTEA
    ISUCOUTEA
    投稿
    {% block body %} {% endblock %}
    ================================================ FILE: isucoutea/webapp/python/templates/new.html ================================================ {% extends "layout.html" %} {% block body %}

    新規茶葉登録


    {% endblock %} ================================================ FILE: isucoutea/webapp/ruby/Gemfile ================================================ source 'https://rubygems.org' gem 'sinatra' gem 'mysql2' gem 'mysql2-cs-bind' gem 'erubis' gem 'puma' ================================================ FILE: isucoutea/webapp/ruby/app.rb ================================================ require 'sinatra/base' require 'mysql2' require 'mysql2-cs-bind' require 'erubis' module Isucoutea class AuthenticationError < StandardError; end class PermissionDenied < StandardError; end end class Isucoutea::WebApp < Sinatra::Base TEAS_PER_PAGE = 6 set :erb, escape_html: true set :public_folder, File.expand_path('../css', __FILE__) set :protection, true helpers do def config @config ||= { db: { host: 'localhost', port: 3306, username: 'scouty', password: 'scouty', database: 'scoutea' } } end def db return Thread.current[:isucoutea_db] if Thread.current[:isucoutea_db] client = Mysql2::Client.new( host: config[:db][:host], port: config[:db][:port], username: config[:db][:username], password: config[:db][:password], database: config[:db][:database], reconnect: true ) client.query_options.merge!(symbolize_keys: true) Thread.current[:isucoutea_db] = client client end def get_country(tea) location_id = db.xquery('SELECT id FROM locations WHERE name = ?', tea[:location]).first[:id] country_id = db.xquery('SELECT location_to_id FROM location_relations WHERE location_from_id = ?', location_id).first[:location_to_id] return db.xquery('SELECT name FROM locations WHERE id = ?', country_id).first[:name] end end get '/' do page = [(params[:page].to_i || 1), 1].max query = params[:query] || '' offset = (page - 1) * TEAS_PER_PAGE teas = db.xquery('SELECT * FROM teas ORDER BY id DESC') teas_match = [] teas.each do |tea| if query == '' teas_match.push(tea) next end tea[:country] = get_country(tea) if query == tea[:name] || query == tea[:location] || query == tea[:country] teas_match.push(tea) end end teas_display = [] teas_match.each_with_index do |tea, i| if offset <= i && i < offset + TEAS_PER_PAGE tea[:country] = get_country(tea) tea[:description] = tea[:description].length > 100 ? tea[:description][0, 100] + '...' : tea[:description] teas_display.push(tea) end end first_page = 1 current_page = page last_page = (teas_match.length / (TEAS_PER_PAGE * 1.0)).ceil erb :index, locals: { teas: teas_display, query: query, url_query: query.empty? ? '' : '&query=' + query, first_page: first_page, current_page: current_page, last_page: last_page } end post '/' do name = params[:name] location = params[:location] description = params[:description] location_exist = db.xquery('SELECT 1 FROM locations WHERE name = ?', location) if location_exist db.xquery('INSERT INTO teas (name, location, description) VALUES (?, ?, ?)', name, location, description) end redirect '/' end get '/new' do erb :new, locals: {query: ''} end get '/initialize' do db.query('DELETE FROM teas WHERE id > 500000') db.query('DELETE FROM locations WHERE id > 2397') db.query('DELETE FROM location_relations WHERE id > 2394') redirect '/' end end ================================================ FILE: isucoutea/webapp/ruby/config.ru ================================================ require_relative './app.rb' run Isucoutea::WebApp ================================================ FILE: isucoutea/webapp/ruby/config_puma.rb ================================================ #!/usr/bin/env puma workers 1 bind "tcp://0.0.0.0:8080" preload_app! worker_timeout 60 ================================================ FILE: isucoutea/webapp/ruby/css/style.css ================================================ .card-deck .card { min-width: 280px; } ================================================ FILE: isucoutea/webapp/ruby/views/index.erb ================================================
    <% teas.each do |tea| %>

    <%= tea[:name] %>

    • 生産地: <%= tea[:location] %>
    • 生産国: <%= tea[:country] %>
    • 説明: <%= tea[:description] %>
    <% end %>
    ================================================ FILE: isucoutea/webapp/ruby/views/layout.erb ================================================ ISUCOUTEA
    ISUCOUTEA
    投稿
    <%== yield %>
    ================================================ FILE: isucoutea/webapp/ruby/views/new.erb ================================================

    新規茶葉登録


    ================================================ FILE: refactor_code/.gitignore ================================================ history.csv ================================================ FILE: refactor_code/README.md ================================================ # 技術試験(アプリケーションのリファクタリング) ## 問題の背景 アプリケーションの開発は、スケジュールや当時の状況等により必ずしも常に最適な構成にはできません。ある程度サービスが落ち着いた段階でコードを整理し今後の開発をやりやすいように改善できるスキルが必要です。 ここに、とにかくサービスを公開しなければいけないという強いプレッシャーで開発された雑なサービスのコードがあります。 あなたの手でできる限りきれいにリファクタリングしてください。 ## 問題 本アプリケーション(Refactea)は茶葉を販売するECサイトです。サービスを至急公開する必要があり、間に合わせで実装されました。本アプリケーションについて、持続開発可能な形にリファクタリングしてください。なお、サイト上のデザインについてはリファクタリングの対象外とします。 ### 環境説明 本アプリケーションにはPythonとRuby実装があります。いずれかの実装を選択してリファクタリングしてください。選択した環境のディレクトリ内で`docker compose`にて、必要なコンテナが起動します。 ``` # Python実装を起動する場合 $ cd python $ docker compose up ``` コンテナは3つです。 ``` $ docker compose ps Name Command State Ports -------------------------------------------------------------------------------------------- xxxxxxxxxxxxx_app_1 docker/service/entrypoint. ... Up 0.0.0.0:80->80/tcp xxxxxxxxxxxxx_paymentmock_1 python /app/app.py Up 0.0.0.0:8810->80/tcp xxxxxxxxxxxxx_storage_app_1 sh Exit 0 ``` |container| role| info| |---------|-----|-----| |app| メインアプリケーション| リファクタリング対象のアプリケーションが稼働します。| |paymentmock|決済サービス| appからアクセスされる決済サービスのモックサーバです。| |storage_app|ストレージ| app用のストレージコンテナです。停止したままで問題ありません。| `paymentmock`はリファクタリングの対象外です。`paymentmock`はいくつかの決済方法に対応するAPIサーバとなっており、リクエストを受けると`refactor_code/docker/paymentmock/mockserver/history.csv`に処理履歴を書出します。今回は、ここに履歴が書き出されれば決済完了として扱ってください。 ### アプリケーション説明 アプリケーションには `http://localhost/` でアクセスできます。 本アプリケーションには以下のページがあります。 |url|method|機能| |----|---|---| |`/signup`|GET,POST| signupページです。| |`/login`|GET,POST| loginページです。| |`/logout`|GET| logoutページです。| |`/` or `/index`|GET| 茶葉の一覧です。ログインしている場合はここから購入できます。| |`/tea/:id`|GET|茶葉の詳細です。説明を見ることができます。ここからも購入できます。| |`/cart`|GET,POST|現在カートに入っている茶葉の情報を見ることができます。また茶葉の購入量を下げることができます。| |`/change_cart`|POST | カートに入っている茶葉の量を変更します。| |`/checkout`|GET | 購入ページです。カートに入っている茶葉について決済方法を選択し、購入します。| |`/buy`|POST | 購入を確定し、注文状態にします。| |`/settle`|GET|購入状態になっている注文の決済処理を行うバッチ処理用のエンドポイントです。 | `/settle`はcron等で定期的にアクセスされる想定ですが、今回はスケジューラまでは組んでいないので適宜手動でアクセスして動作確認してください。 #### 決済について Refacteaでは、カード決済、PoyJp(架空決済サービス)、銀行引き落としが用意されています。いずれも購入確定時にアカウント情報を入力します。`/settle`へアクセスすると、その時点で未処理のOrderについて、決済サービスを通じて処理をします。 処理完了後には処理済みのステータスに変更し、決済に使用したカード番号等の情報は破棄されます。 ## 採点基準 本アプリケーションへ今後行う修正がどの程度容易になるかという点を評価します。 例えば、決済方法が追加された場合や送料が一定以上で無料になるような機能拡張をする場合の変更等をイメージすると わかりやすいかもしれません。これらのための適切なレイヤー分けや関心事を考慮した分割等を検討してください。 今回のアプリケーションは小規模なものですが、大規模なアプリケーションを扱っているという前提でリファクタリングを行ってください。(例えば現在モデルは4つですが、これが10倍になった時に何をどこに書くべきかということです) また、明らかにアンチパターンと思われるコード・データ構造もあればその修正も試みてください。 ================================================ FILE: refactor_code/docker/paymentmock/Dockerfile ================================================ FROM python:3.9.17 EXPOSE 80 ENV LC_ALL=C.UTF-8 LANG=C.UTF-8 WORKDIR /app RUN apt-get update RUN pip install --upgrade pip && \ pip install bottle==0.12.23 ADD . /app ================================================ FILE: refactor_code/docker/paymentmock/mockserver/app.py ================================================ import csv import datetime from pprint import pprint from bottle import get, post, request, response, run, template, redirect HISTORY_FILE = './history.csv' def _write_history(*args): with open(HISTORY_FILE, 'a') as f: writer = csv.writer(f) data_list = [datetime.datetime.now()] + list(args) writer.writerow(data_list) @get('/api/card') @post('/api/card') def card_payment(): req = request.json pprint(req) _write_history('card', req['card_number'], req['price']) return {'success': True} @get('/api/poyjp') @post('/api/poyjp') def card_payment(): req = request.json pprint(req) _write_history('poyjp', req['account_number'], req['price']) return {'success': True} @get('/api/bank') @post('/api/bank') def card_payment(): req = request.json pprint(req) _write_history('bank', req['branch_number'], req['account_number'], req['price']) return {'success': True} if __name__ == '__main__': run(host='0.0.0.0', port=80, reloader=True, debug=True) ================================================ FILE: refactor_code/python/.gitignore ================================================ *.csv ================================================ FILE: refactor_code/python/README.md ================================================ Python実装固有の説明 ============= #### appコンテナについて `app`コンテナではFlaskとnginxが起動します。ホストOS上の`http://localhost:80/`から本サービスにアクセスできます。Flaskはuwsgiで動作しており、コードを変更するとオートリロードします。リロード時には、`admin/create_db.py`が実行され各モデルは初期化されます。初期データとして`testuser/testuser`というアカウントと20種類の茶葉データがあるのでこちらを利用して開発してください。必要に応じて初期データは変更して構いません。 #### アプリケーションフレームワークについて [Flask](http://flask.pocoo.org/) + [Flask-login](https://flask-login.readthedocs.io/en/latest/)を使用しています。ORMには[peewee](http://docs.peewee-orm.com/en/latest/index.html)を使用しています。 #### バックエンドDBについて `/var/lib/data/refactea.db`というsqlite3を使用しています。 #### unittestについて `app`コンテナ内で以下の手順にてunittestを実行することができます。 ``` $ cd /app/application $ python test.py ``` ================================================ FILE: refactor_code/python/application/__init__.py ================================================ ================================================ FILE: refactor_code/python/application/admin/__init__.py ================================================ ================================================ FILE: refactor_code/python/application/admin/create_db.py ================================================ from peewee import Model import models from settings import DATABASE def _find_models(): model_list = [] for name in dir(models): try: attr = getattr(models, name) if issubclass(attr, Model) and attr != Model: model_list.append(getattr(models, name)) except: continue return model_list def _insert_data(): user_cls = models.User user_cls.create(name='testuser', password='testuser') tea_cls = models.Tea for i in range(20): tea_cls.create( name='tea_{}'.format(i), price=(10 + i % 3 * 10), stock_amount=(100 + i % 5 * 100), description='description of {}'.format(i), ) def init_db(): DATABASE.connect() model_list = _find_models() print('target_models', model_list) print('#### drop tables') DATABASE.drop_tables(_find_models()) print('#### crate tables') DATABASE.create_tables(_find_models(), safe=True) print('#### insert data') _insert_data() ================================================ FILE: refactor_code/python/application/app.ini ================================================ [uwsgi] module = app callable = app master = true processes = 1 http = :8080 vacuum = true die-on-term = true py-autoreload = 1 ================================================ FILE: refactor_code/python/application/app.py ================================================ import datetime import json from flask import Flask, render_template, request, redirect, url_for, make_response, jsonify from flask_login import LoginManager, login_required, login_user, logout_user, current_user import models import payment_service from admin.create_db import init_db from forms import UserLoginForm from settings import DATABASE app = Flask(__name__) app.config['TEMPLATES_AUTO_RELOAD'] = True app.secret_key = 'refactea' login_manager = LoginManager() login_manager.init_app(app) # init data with app.app_context(): init_db() @login_manager.user_loader def load_user(user_id): user_cls = models.User users = models.User.select().where( user_cls.id == user_id ) if users: return users[0] return None @login_manager.unauthorized_handler def unauthorized_callback(): return redirect('/login') @app.route('/login', methods=['GET', 'POST']) def login_view(): form = UserLoginForm(request.form) error = None if request.method == 'POST' and form.validate(): user_cls = models.User hashed_password = user_cls.get_hashed_password(form.password.data) users = list(user_cls.select().where( user_cls.name == form.username.data, user_cls.password == hashed_password, )) print(users) if users: login_user(users[0]) return redirect(url_for('index_view')) else: error = 'Invalid username or password.' return render_template('login.html', form=form, error=error) @app.route('/logout') def logout_view(): logout_user() return redirect(url_for('index_view')) @app.route('/signup', methods=['GET', 'POST']) def signup_view(): form = UserLoginForm(request.form) error = None if request.method == 'POST': if form.validate(): user_cls = models.User user = user_cls.create( name=form.username.data, password=form.password.data, ) if user: print(user.name) login_user(user) return redirect(url_for('index_view')) else: error = 'Invalid username or password.' return render_template('signup.html', form=form, error=error) @app.route('/') @app.route('/index') def index_view(): cart_cls = models.Cart tea_cls = models.Tea if current_user.is_authenticated: cart, created = cart_cls.get_or_create(user=current_user.id) teas = cart.teas_dict else: teas = {} query = request.args.get('q', '') tea_list = [] if query != '': query_str = '%{}%'.format(query) query_set = tea_cls.select().where( (tea_cls.name ** query_str) | (tea_cls.description ** query_str) ).order_by('id') else: query_set = tea_cls.select().order_by('id') for tea in query_set: tea_list.append({ 'id': tea.id, 'name': tea.name, 'price': tea.price, 'stock_amount': tea.stock_amount - teas.get(str(tea.id), 0), # 自分でカートに入れている分は除く }) return render_template('index.html', tea_list=tea_list, query=query) @app.route('/tea/') def tea_view(tea_id): tea_cls = models.Tea tea = tea_cls.get(id=tea_id) return render_template('tea.html', tea=tea) @app.route('/cart', methods=['POST', 'GET']) @login_required def cart_view(): cart_cls = models.Cart cart, created = cart_cls.get_or_create(user=current_user.id) if request.method == 'POST': teas = cart.teas_dict tea_id = request.form['teaId'] tea_amount = int(request.form['teaAmount']) if tea_id not in teas: teas[tea_id] = 0 teas[tea_id] += tea_amount cart.update_teas_data(teas) return redirect(url_for('index_view')) else: tea_cls = models.Tea teas = cart.teas_dict cart_data = [] for tea_id, amount in teas.items(): cart_data.append({ 'tea': tea_cls.get(id=int(tea_id)), 'amount': amount, }) return render_template('cart.html', cart_data=cart_data) @app.route('/change_cart', methods=['POST']) @login_required def change_cart_view(): cart_cls = models.Cart cart = cart_cls.get(user=current_user.id) teas = cart.teas_dict tea_id = request.form['teaId'] tea_amount = int(request.form['teaAmount']) teas[tea_id] = tea_amount if tea_amount == 0: del teas[tea_id] cart.update_teas_data(teas) return redirect(url_for('cart_view')) @app.route('/checkout', methods=['GET']) @login_required def checkout_view(): cart_cls = models.Cart cart = cart_cls.get(user=current_user.id) tea_cls = models.Tea teas = cart.teas_dict cart_data = [] total_price = 0 total_amount = 0 for tea_id, amount in teas.items(): tea = tea_cls.get(id=int(tea_id)) cart_data.append({ 'tea': tea, 'price': amount * tea.price, }) total_price += amount * tea.price total_amount += amount return render_template('checkout.html', cart_data=cart_data, total_price=total_price, total_amount=total_amount) @app.route('/buy', methods=['POST']) @login_required def buy_view(): payment_type = request.form['paymentType'] payment_info_1 = request.form['paymentInfo1'] # optional payment_info_2 = request.form.get('paymentInfo2') cart_cls = models.Cart tea_cls = models.Tea order_cls = models.Order cart = cart_cls.get(user=current_user.id) teas = cart.teas_dict total_price = 0 total_amount = 0 with DATABASE.transaction(): for tea_id, amount in teas.items(): tea = tea_cls.get(id=int(tea_id)) if tea.stock_amount < amount: raise Exception('not enough amount for {}'.format(tea)) tea.stock_amount -= amount tea.save() total_price += amount * tea.price total_amount += amount total_price += total_price * 1.08 + (total_amount / 100 * 30) if payment_type != 'bank': order_cls.create( user=current_user.id, teas_dict=teas, payment_type=payment_type, payment_info=payment_info_1, total_price=total_price, ) else: order_cls.create( user=current_user.id, teas_dict=teas, payment_type=payment_type, payment_info=json.dumps({ 'branch_number': payment_info_1, 'account_number': payment_info_2, }), total_price=total_price, ) cart.delete_instance() return redirect(url_for('index_view')) @app.route('/settle', methods=['GET']) def settle_view(): """ 定期的にバッチから叩かれ、決済処理をまとめて行う :return: """ order_cls = models.Order target_orders = order_cls.select().where( order_cls.status == 0 ) is_success = True for order in target_orders: print('target order', order) if order.payment_type == 'card': result = payment_service.card_payment(order.payment_info, order.total_price) if result: order.status = 1 order.payment_info = None order.updated_at = datetime.datetime.now() order.save() else: is_success = False if order.payment_type == 'poyjp': result = payment_service.poyjp_payment(order.payment_info, order.total_price) if result: order.status = 1 order.payment_info = None order.updated_at = datetime.datetime.now() order.save() else: is_success = False if order.payment_type == 'bank': payment_info = json.loads(order.payment_info) result = payment_service.bank_payment(payment_info['branch_number'], payment_info['account_number'], order.total_price) if result: order.status = 1 order.payment_info = None order.updated_at = datetime.datetime.now() order.save() else: is_success = False return make_response(jsonify({'success': is_success})) if __name__ == "__main__": app.run(port=8080) ================================================ FILE: refactor_code/python/application/forms.py ================================================ from wtforms import Form, StringField, PasswordField, validators, IntegerField class UserLoginForm(Form): username = StringField('Username', [validators.DataRequired(), validators.Length(min=1, max=25)]) password = PasswordField('Password', [validators.DataRequired(), validators.Length(min=1, max=200)]) ================================================ FILE: refactor_code/python/application/models.py ================================================ import datetime import json from hashlib import sha256 from peewee import * from flask_login import UserMixin from settings import DATABASE class User(UserMixin, Model): name = CharField() password = CharField() class Meta: database = DATABASE def __str__(self): return '{}:{}'.format(self.id, self.name) @classmethod def create(cls, **query): query['password'] = cls.get_hashed_password(query['password']) return super().create(**query) def set_password(self, raw_password): self.password = self.get_hashed_password(raw_password) self.save() @classmethod def get_hashed_password(cls, raw_password): return sha256(raw_password.encode('utf-8')).hexdigest() class Tea(Model): name = CharField() price = IntegerField() description = TextField() stock_amount = IntegerField() updated_at = DateTimeField(default=datetime.datetime.now()) class Meta: database = DATABASE @property def display_updated_at(self): return datetime.datetime.strftime(self.updated_at, '%Y-%m-%d') class Cart(Model): user = ForeignKeyField(User, backref='mycart') teas_data = TextField(default=json.dumps({})) class Meta: database = DATABASE @classmethod def create(cls, **query): if 'teas_dict' in query: query['teas_data'] = cls._teas_dict_to_json(query['teas_dict']) del query['teas_dict'] return super().create(**query) @classmethod def _teas_dict_to_json(cls, teas_dict): return json.dumps(teas_dict) @classmethod def _teas_json_to_dict(cls, teas_json): return json.loads(teas_json) @property def teas_dict(self): return self._teas_json_to_dict(self.teas_data) def update_teas_data(self, teas_dict): self.teas_data = self._teas_dict_to_json(teas_dict) self.save() class Order(Model): user = ForeignKeyField(User, backref='mycart') teas_data = TextField(default=json.dumps({})) # cart.teas_dataのスナップショット payment_type = TextField(default='card') payment_info = TextField(null=True) # カード番号とか status = IntegerField(default=0) # 0 未払い, 1 引き落とし済み total_price = IntegerField() bought_at = DateTimeField(default=datetime.datetime.now()) updated_at = DateTimeField(default=datetime.datetime.now()) class Meta: database = DATABASE def __str__(self): return '{}: user:{}, pay:{}, price:{}'.format( self.id, self.user, self.payment_type, self.total_price ) @classmethod def create(cls, **query): if 'teas_dict' in query: query['teas_data'] = cls._teas_dict_to_json(query['teas_dict']) del query['teas_dict'] return super().create(**query) @classmethod def _teas_dict_to_json(cls, teas_dict): return json.dumps(teas_dict) @classmethod def _teas_json_to_dict(cls, teas_json): return json.loads(teas_json) @property def teas_dict(self): return self._teas_json_to_dict(self.teas_data) ================================================ FILE: refactor_code/python/application/payment_service.py ================================================ """ モックの外部APIを叩く """ import requests CARD_PAYMENT_API = 'http://paymentmock/api/card' POYJP_PAYMENT_API = 'http://paymentmock/api/poyjp' BANK_PAYMENT_API = 'http://paymentmock/api/bank' def card_payment(card_number, price): res = requests.post(CARD_PAYMENT_API, json={ 'card_number': card_number, 'price': price }) return res.json()['success'] def poyjp_payment(account_number, price): res = requests.post(POYJP_PAYMENT_API, json={ 'account_number': account_number, 'price': price }) return res.json()['success'] def bank_payment(branch_number, account_number, price): res = requests.post(BANK_PAYMENT_API, json={ 'branch_number': branch_number, 'account_number': account_number, 'price': price }) return res.json()['success'] ================================================ FILE: refactor_code/python/application/scripts/resetdb.sh ================================================ #!/usr/bin/env bash rm -rf /var/lib/data/ ================================================ FILE: refactor_code/python/application/settings.py ================================================ from peewee import SqliteDatabase DATABASE = SqliteDatabase('/var/lib/data/refactea.db') ================================================ FILE: refactor_code/python/application/templates/cart.html ================================================ {% extends "layout.html" %} {% block body %}

    Cart.

    {% if cart_data %} Prceed to checkout {% endif %}
    {% if cart_data %} {% for cart in cart_data %} {% endfor %}
    Name Price(/100g)AmountOperation
    {{ cart.tea.name }} {{ cart.tea.price }} {{ cart.amount }}
    {% else %}

    empty cart.

    {% endif %} {% endblock %} {% block extrascript %} {% endblock %} ================================================ FILE: refactor_code/python/application/templates/checkout.html ================================================ {% extends "layout.html" %} {% block body %}

    CheckOut.

    {% if cart_data %} {% for cart in cart_data %} {% endfor %}
    Name Price
    {{ cart.tea.name }} {{ cart.price }} yen
    summary
    total price(without tax) {{ total_price }} yen
    tax {{ (total_price * 0.08)| int }} yen
    postage {{ (total_amount / 100 * 30) | int }} yen({{ total_amount }}g)
    total price(with tax, postage) {{ (total_price * 1.08 + (total_amount / 100 * 30)) | int }} yen
    {% else %}

    empty cart.

    {% endif %} {% endblock %} {% block extrascript %} {% endblock %} ================================================ FILE: refactor_code/python/application/templates/index.html ================================================ {% extends "layout.html" %} {% block body %}

    Teas.

    {% for tea in tea_list %} {% endfor %}
    Name Price(/100g)StockOperation
    {{ tea.name }} {{ tea.price }} {% if tea.stock_amount == 0 %} Sold Out {% else %} {{ tea.stock_amount }} {% endif %} {% if tea.stock_amount > 0 and current_user.is_authenticated %} {% else %} {% endif %}
    {% endblock %} {% block extrascript %} {% endblock %} ================================================ FILE: refactor_code/python/application/templates/layout.html ================================================ REFACTEA
    REFACTEA
    {% if current_user.is_authenticated %} {% else %} {% endif %}
    {% block body %} {% endblock %}
    {% block extrascript %} {% endblock %} ================================================ FILE: refactor_code/python/application/templates/login.html ================================================ {% extends "layout.html" %} {% block body %}

    LogIn

    {% if error %}
    {% endif %}
    username: {{ form.username() }}
    password: {{ form.password() }}
    {% endblock %} ================================================ FILE: refactor_code/python/application/templates/signup.html ================================================ {% extends "layout.html" %} {% block body %}

    SignUp

    {% if error %}
    {% endif %}
    username: {{ form.username() }}
    password: {{ form.password() }}
    {% endblock %} ================================================ FILE: refactor_code/python/application/templates/tea.html ================================================ {% extends "layout.html" %} {% block body %}

    {{ tea.name }}

    {{ tea.description }}


    Price:{{ tea.price }}, Stock: {{ tea.stock_amount }}

    {% if current_user.is_authenticated %} {% else %} {% endif %}
    {% endblock %} {% block extrascript %} {% endblock %} ================================================ FILE: refactor_code/python/application/test.py ================================================ import unittest import requests from admin import create_db BASE_URL = 'http://localhost' class TestRefactea(unittest.TestCase): def test_get_index_view(self): """ お茶のリストがIndexに表示されているか :return: """ resp = requests.get(BASE_URL + '/') self.assertEqual(resp.status_code, 200) self.assertTrue('tea_0' in resp.text) self.assertTrue('tea_1' in resp.text) self.assertTrue('tea_2' in resp.text) def test_get_tea_search_view(self): """ 検索した際にそのお茶のみが出ているか :return: """ resp = requests.get(BASE_URL + '/?q=tea_0') self.assertEqual(resp.status_code, 200) self.assertTrue('tea_0' in resp.text) self.assertTrue('tea_1' not in resp.text) def test_get_signup_view(self): """ SignUp画面が表示されるか :return: """ resp = requests.get(BASE_URL + '/signup') self.assertEqual(resp.status_code, 200) self.assertTrue('username' in resp.text) def test_get_login_view(self): """ login画面が表示されるか :return: """ resp = requests.get(BASE_URL + '/login') self.assertEqual(resp.status_code, 200) self.assertTrue('username' in resp.text) def test_get_tea_view(self): """ お茶の詳細画面が正しく開けるか :return: """ resp = requests.get(BASE_URL + '/tea/1') self.assertEqual(resp.status_code, 200) self.assertTrue('description of 0' in resp.text) def test_get_settle_view(self): """ settleが正常なレスポンスを戻すか :return: """ resp = requests.get(BASE_URL + '/settle') self.assertEqual(resp.status_code, 200) def test_signup_and_login_scenario(self): """ SingUpからlogin/logoutまでできるか :return: """ s = requests.Session() resp = s.post(BASE_URL + '/signup', data={'username': 'denzow', 'password': 'denzow_pass'}) self.assertEqual(resp.status_code, 200) resp = s.get(BASE_URL + '/logout') self.assertEqual(resp.status_code, 200) resp = s.post(BASE_URL + '/login', data={'username': 'denzow', 'password': 'denzow_pass'}) self.assertEqual(resp.status_code, 200) self.assertTrue('tea_0' in resp.text) resp = s.get(BASE_URL + '/logout') self.assertEqual(resp.status_code, 200) # ログイン失敗 resp = s.post(BASE_URL + '/login', data={'username': 'invalid', 'password': 'invalid'}) self.assertEqual(resp.status_code, 200) self.assertTrue('Invalid username or password.' in resp.text) s.close() def test_buy_tea_scenario(self): """ 購入が正しくできるか :return: """ s = requests.Session() resp = s.post(BASE_URL + '/signup', data={'username': 'denzow2', 'password': 'denzow_pass2'}) self.assertEqual(resp.status_code, 200) resp = s.post(BASE_URL + '/cart', data={'teaId': '2', 'teaAmount': '200'}) self.assertEqual(resp.status_code, 200) self.assertTrue('Sold' in resp.text) resp = s.get(BASE_URL + '/cart') self.assertEqual(resp.status_code, 200) self.assertTrue('tea_1' in resp.text) self.assertTrue('200' in resp.text) resp = s.get(BASE_URL + '/checkout') self.assertEqual(resp.status_code, 200) self.assertTrue('4380 yen' in resp.text) resp = s.post(BASE_URL + '/buy', data={'paymentType': 'poyjp', 'paymentInfo1': '123456789'}) self.assertEqual(resp.status_code, 200) self.assertTrue('Teas.' in resp.text) s.close() if __name__ == "__main__": create_db.init_db() unittest.main() ================================================ FILE: refactor_code/python/docker/service/Dockerfile ================================================ FROM python:3.9.17 ENV LC_ALL=C.UTF-8 LANG=C.UTF-8 WORKDIR /app # Nginx のインストール RUN apt-get update RUN apt-get install -y nginx ADD ./nginx.conf /etc/nginx/nginx.conf # Poetry のインストール RUN pip install --upgrade pip && \ pip install poetry=="1.1.6" RUN mkdir -p /var/lib/data ENTRYPOINT ["docker/service/entrypoint.sh"] ================================================ FILE: refactor_code/python/docker/service/entrypoint.sh ================================================ #!/usr/bin/env bash poetry config virtualenvs.create false poetry install service nginx start echo "start application." exec "$@" ================================================ FILE: refactor_code/python/docker/service/nginx.conf ================================================ user www-data; worker_processes 1; pid /run/nginx.pid; events { worker_connections 4; } http { include /etc/nginx/mime.types; default_type application/octet-stream; access_log /var/log/nginx/access.log; error_log /var/log/nginx/error.log; upstream app { server 127.0.0.1:8080; } server { listen 80; location / { proxy_set_header Host $host; proxy_pass http://app; } } } ================================================ FILE: refactor_code/python/docker/service/run_test.sh ================================================ #!/bin/bash -eu python /app/application/test.py ================================================ FILE: refactor_code/python/docker-compose.yml ================================================ services: app: build: context: ./docker/service command: poetry run uwsgi /app/application/app.ini working_dir: /app environment: - PYTHONPATH=/app/application volumes: - .:/app ports: - "80:80" paymentmock: build: ../docker/paymentmock command: 'python /app/app.py' environment: - PYTHONUNBUFFERED=1 volumes: - ../docker/paymentmock/mockserver:/app ports: - "8810:80" ================================================ FILE: refactor_code/python/pyproject.toml ================================================ [tool.poetry] name = "refactor_code" version = "0.1.0" description = "" authors = ["LAPRAS Inc. "] [tool.poetry.dependencies] python = "^3.9" Flask = "^2.2.1" Flask-Login = "^0.6.2" peewee = "^3.15.1" WTForms = "^3.0.1" requests = "^2.28.1" uWSGI = "^2.0.20" [tool.poetry.dev-dependencies] [build-system] requires = ["poetry-core>=1.0.0"] build-backend = "poetry.core.masonry.api" ================================================ FILE: refactor_code/ruby/.gitignore ================================================ vendor *.sqlite3 ================================================ FILE: refactor_code/ruby/.ruby-version ================================================ 3.1.4 ================================================ FILE: refactor_code/ruby/Gemfile ================================================ source "https://rubygems.org" gem 'rake' gem 'sinatra' gem 'activerecord' gem 'sinatra-activerecord' gem 'sqlite3' gem 'authlogic' gem 'activesupport' gem 'faraday' gem 'puma' gem 'scrypt' group :development do gem 'sinatra-contrib' gem 'pry-byebug' end group :test do gem 'rspec' gem 'minitest' gem 'rack-test' gem 'rack-minitest' gem 'test-unit' gem 'database_cleaner' end ================================================ FILE: refactor_code/ruby/README.md ================================================ Ruby実装固有の説明 =========== #### appコンテナについて `app`コンテナではSinatraが起動します。ホストOS上の`http://localhost:80/`から本サービスにアクセスできます。Sinatraはdevelopmentモードで起動してあれば、コードを変更するとオートリロードします。初期データとして`testuser/testuser`というアカウントと20種類の茶葉データを`db/seeds.rb` により追加しています。こちらを利用して開発してください。必要に応じて初期データは変更して構いません。 #### アプリケーションフレームワークについて [Sinatra](http://sinatrarb.com/)を使用しています。ORMにはActiveRecordを、認証には[Authlogic](https://github.com/binarylogic/authlogic)使用しています。 #### バックエンドDBについて `/app/db/development.sqlite3` というsqlite3を使用しています。 #### unittestについて `app`コンテナ内で以下の手順にてunittestを実行することができます。 ``` $ cd /app $ bundle exec rake test ``` ================================================ FILE: refactor_code/ruby/Rakefile ================================================ require 'sinatra/activerecord' require 'sinatra/activerecord/rake' require 'rake/testtask' Rake::TestTask.new do |t| t.pattern = "test/*_test.rb" end namespace :db do task :load_config do require './models' end end ================================================ FILE: refactor_code/ruby/app.rb ================================================ require 'active_support' require 'sinatra' require 'sinatra/reloader' if development? require 'pry' if development? require './models' require './payment_service' set :bind, '0.0.0.0' enable :sessions before '/cart' do redirect '/login' unless !!UserSession.find end before '/change_cart' do redirect '/login' unless !!UserSession.find end before '/checkout' do redirect '/login' unless !!UserSession.find end before '/buy' do redirect '/login' unless !!UserSession.find end before do if !!UserSession.find @current_user = UserSession.find.try(:user) end end get '/' do if @current_user cart = Cart.find_or_create_by(user: @current_user) @teas = cart.teas_data else @teas = {} end @query = params[:q] @tea_list = [] if @query @tea_list = Tea.where('name like ? or description like ?', "%#{@query}%", "%#{@query}%").order('id') else @tea_list = Tea.all().order('id') end erb :index end get '/users/new' do @user = User.new erb :user_new end post '/users' do begin User.create!(symbolize_params.slice(:login, :email, :password)) redirect '/' rescue ActiveRecord::RecordInvalid => e @error = e @user = e.record erb :user_new end end get '/login' do @session = UserSession.new erb :login end post '/login' do begin UserSession.create!(symbolize_params.slice(:login, :password)) redirect '/' rescue Authlogic::Session::Existence::SessionInvalidError => e @error = 'Invalid username or password' erb :login end end get '/logout' do UserSession.find.try(:destroy) redirect '/login' end get '/teas/:id' do id = params[:id] @tea = Tea.find(id) erb :tea end get '/cart' do cart = Cart.find_or_create_by(user: @current_user) @cart_data = [] cart.teas_data.each do |tea| @cart_data.append({ tea: Tea.find(tea[0].to_i), amount: tea[1] }) end erb :cart end post '/cart' do cart = Cart.find_or_create_by(user: @current_user) teas = cart.teas_data tea_id = params[:teaId] tea_amount = params[:teaAmount].to_i unless teas[tea_id] teas[tea_id] = 0 end teas[tea_id] += tea_amount cart.teas_data = teas cart.save! redirect '/' end post '/change_cart' do cart = Cart.find_by(user: @current_user) teas = cart.teas_data tea_id = params[:teaId] tea_amount = params[:teaAmount].to_i teas[tea_id] = tea_amount if tea_amount == 0 teas.delete(tea_id) end cart.teas_data = teas cart.save! redirect '/cart' end get '/checkout' do cart = Cart.find_by(user: @current_user) teas = cart.teas_data @cart_data = [] @total_price = 0 @total_amount = 0 teas.each do |data| tea = Tea.find(data[0].to_i) @cart_data.append({ tea: tea, price: data[1] * tea.price }) @total_price += data[1] * tea.price @total_amount += data[1] end erb :checkout end post '/buy' do payment_type = params[:paymentType] payment_info_1 = params[:paymentInfo1] # optional payment_info_2 = params[:paymentInfo2] cart = Cart.find_by(user: @current_user) teas = cart.teas_data total_price = 0 total_amount = 0 ActiveRecord::Base.transaction do teas.each do |data| tea_id = data[0] amount = data[1] tea = Tea.find(tea_id.to_i) if tea.stock_amount < amount raise "Not enough amount for #{tea}" end tea.stock_amount -= amount tea.save! total_price += amount * tea.price total_amount += amount end total_price += total_price + 1.08 + (total_amount / 100 * 30) if payment_type != 'bank' Order.create( user: @current_user, teas_data: teas, payment_type: payment_type, payment_info: payment_info_1, total_price: total_price ) else Order.create( user: @current_user, teas_data: teas, payment_type: payment_type, payment_info: { branch_number: payment_info_1, account_number: payment_info_2 }, total_price: total_price ) end cart.delete end redirect '/' end # 定期的にバッチから叩かれ,決済処理をまとめて行う get '/settle' do target_orders = Order.where(status: 0) is_success = true target_orders.each do |order| puts("target order #{order}") case order.payment_type when 'card' then result = PaymentService.card_payment(order.payment_info, order.total_price) if result order.status = 1 order.payment_info = nil order.save else is_success = false end when 'poyjp' then result = PaymentService.poyjp_payment(order.payment_info, order.total_price) if result order.status = 1 order.payment_info = nil order.save else is_success = false end when 'bank' then payment_info = order.payment_info result = PaymentService.bank_payment(payment_info[:branch_number], payment_info[:account_number], order.total_price) if result order.status = 1 order.payment_info = nil order.save else is_success = false end end end {success: is_success}.to_json end def symbolize_params @normalized ||= params.deep_symbolize_keys! end ================================================ FILE: refactor_code/ruby/config/database.yml ================================================ development: adapter: sqlite3 database: db/development.sqlite3 test: adapter: sqlite3 database: db/test.sqlite3 ================================================ FILE: refactor_code/ruby/config.ru ================================================ require './app' run Sinatra::Application ================================================ FILE: refactor_code/ruby/db/migrate/20181105072432_create_users.rb ================================================ class CreateUsers < ActiveRecord::Migration[5.2] def change create_table :users do |t| t.string :login, null: false t.string :email, null: false t.string :crypted_password, null: false t.string :password_salt, null: false t.string :persistence_token t.string :single_access_token t.string :perishable_token t.integer :login_count, default: 0, null: false t.integer :failed_login_count, default: 0, null: false t.datetime :last_request_at t.datetime :current_login_at t.datetime :last_login_at t.string :current_login_ip t.string :last_login_ip t.timestamps null: false end end end ================================================ FILE: refactor_code/ruby/db/migrate/20181106023943_create_teas.rb ================================================ class CreateTeas < ActiveRecord::Migration[5.2] def change create_table :teas do |t| t.string :name, null: false t.integer :price, null: false t.text :description t.integer :stock_amount, null: false t.timestamps null: false end end end ================================================ FILE: refactor_code/ruby/db/migrate/20181106024402_create_carts.rb ================================================ class CreateCarts < ActiveRecord::Migration[5.2] def change create_table :carts do |t| t.references :user t.text :teas_data, default: {}.to_yaml t.timestamps null: false end end end ================================================ FILE: refactor_code/ruby/db/migrate/20181106042324_create_orders.rb ================================================ class CreateOrders < ActiveRecord::Migration[5.2] def change create_table :orders do |t| t.references :user t.text :teas_data t.text :payment_type, default: 'card' t.text :payment_info, default: nil t.integer :status, default: 0 t.integer :total_price t.datetime :bought_at t.timestamps null: false end end end ================================================ FILE: refactor_code/ruby/db/schema.rb ================================================ # This file is auto-generated from the current state of the database. Instead # of editing this file, please use the migrations feature of Active Record to # incrementally modify your database, and then regenerate this schema definition. # # This file is the source Rails uses to define your schema when running `bin/rails # db:schema:load`. When creating a new database, `bin/rails db:schema:load` tends to # be faster and is potentially less error prone than running all of your # migrations from scratch. Old migrations may fail to apply correctly if those # migrations use external dependencies or application code. # # It's strongly recommended that you check this file into your version control system. ActiveRecord::Schema[7.0].define(version: 2018_11_06_042324) do create_table "carts", force: :cascade do |t| t.integer "user_id" t.text "teas_data", default: "--- {}\n" t.datetime "created_at", precision: nil, null: false t.datetime "updated_at", precision: nil, null: false t.index ["user_id"], name: "index_carts_on_user_id" end create_table "orders", force: :cascade do |t| t.integer "user_id" t.text "teas_data" t.text "payment_type", default: "card" t.text "payment_info" t.integer "status", default: 0 t.integer "total_price" t.datetime "bought_at", precision: nil t.datetime "created_at", precision: nil, null: false t.datetime "updated_at", precision: nil, null: false t.index ["user_id"], name: "index_orders_on_user_id" end create_table "teas", force: :cascade do |t| t.string "name", null: false t.integer "price", null: false t.text "description" t.integer "stock_amount", null: false t.datetime "created_at", precision: nil, null: false t.datetime "updated_at", precision: nil, null: false end create_table "users", force: :cascade do |t| t.string "login", null: false t.string "email", null: false t.string "crypted_password", null: false t.string "password_salt", null: false t.string "persistence_token" t.string "single_access_token" t.string "perishable_token" t.integer "login_count", default: 0, null: false t.integer "failed_login_count", default: 0, null: false t.datetime "last_request_at", precision: nil t.datetime "current_login_at", precision: nil t.datetime "last_login_at", precision: nil t.string "current_login_ip" t.string "last_login_ip" t.datetime "created_at", precision: nil, null: false t.datetime "updated_at", precision: nil, null: false end end ================================================ FILE: refactor_code/ruby/db/seeds.rb ================================================ User.create(login: 'testuser', email: 'testuser@example.com', password: 'testuser') for i in 0..20 do Tea.create( name: "tea_#{i}", price: (10 + i % 3 * 10), stock_amount: (100 + i % 5 * 100), description: "description of #{i}" ) end ================================================ FILE: refactor_code/ruby/docker/service/Dockerfile ================================================ FROM ruby:3.1.4-slim ENV LC_ALL=C.UTF-8 LANG=C.UTF-8 WORKDIR /app RUN set -ex && \ apt-get update && \ apt-get install -y build-essential \ libsqlite3-dev ================================================ FILE: refactor_code/ruby/docker/service/entrypoint.sh ================================================ #!/usr/bin/env bash bundle install --path vendor/bundle if [ -f ./db/development.sqlite3 ]; then RACK_ENV=development bundle exec rake db:reset RACK_ENV=test bundle exec rake db:seed else RACK_ENV=development bundle exec rake db:create RACK_ENV=development bundle exec rake db:migrate RACK_ENV=development bundle exec rake db:seed RACK_ENV=test bundle exec rake db:migrate RACK_ENV=test bundle exec rake db:seed fi echo "start application." exec "$@" ================================================ FILE: refactor_code/ruby/docker/service/run_test.sh ================================================ #!/bin/bash -eu cd /app bundle exec rake test ================================================ FILE: refactor_code/ruby/docker-compose.yml ================================================ services: app: build: context: ./docker/service command: bundle exec ruby app.rb entrypoint: docker/service/entrypoint.sh working_dir: /app links: - paymentmock:paymentmock volumes: - .:/app ports: - "80:4567" paymentmock: build: ../docker/paymentmock command: 'python /app/app.py' environment: - PYTHONUNBUFFERED=1 volumes: - ../docker/paymentmock/mockserver:/app ports: - "8810:80" ================================================ FILE: refactor_code/ruby/models.rb ================================================ require 'sinatra/activerecord' require 'active_record' require 'authlogic' class User < ActiveRecord::Base acts_as_authentic do |config| config.login_field = :login config.require_password_confirmation = false config.crypto_provider = ::Authlogic::CryptoProviders::SCrypt end validates :email, uniqueness: { case_sensitive: false } validates :login, uniqueness: { case_sensitive: false } end class UserSession < Authlogic::Session::Base end class Tea < ActiveRecord::Base end class Cart < ActiveRecord::Base belongs_to :user serialize :teas_data end class Order < ActiveRecord::Base belongs_to :user serialize :teas_data serialize :payment_info end ================================================ FILE: refactor_code/ruby/payment_service.rb ================================================ require 'faraday' class PaymentService class << self CARD_PAYMENT_API = 'http://paymentmock/api/card' POYJP_PAYMENT_API = 'http://paymentmock/api/poyjp' BANK_PAYMENT_API = 'http://paymentmock/api/bank' def card_payment(card_number, price) res = Faraday.post(CARD_PAYMENT_API, JSON.generate({ card_number: card_number, price: price }), content_type: "application/json") return JSON.parse(res.body)["success"] end def poyjp_payment(account_number, price) res = Faraday.post(POYJP_PAYMENT_API, JSON.generate({ account_number: account_number, price: price }), content_type: "application/json") return JSON.parse(res.body)["success"] end def bank_payment(branch_number, account_number, price) res = Faraday.post(BANK_PAYMENT_API, JSON.generate({ branch_number: branch_number, account_number: account_number, price: price }), content_type: "application/json") return JSON.parse(res.body)["success"] end end end ================================================ FILE: refactor_code/ruby/test/app_test.rb ================================================ ENV['RACK_ENV'] = 'test' ENV["SINATRA_ENV"] = "test" require './app' require './test/test_helper' def app Sinatra::Application end describe "/" do it "index" do get '/' _(last_response).must_be_ok _(last_response.body).must_include 'tea_0' _(last_response.body).must_include 'tea_1' _(last_response.body).must_include 'tea_2' end it "search" do get '/?q=tea_0' _(last_response).must_be_ok _(last_response.body).must_include 'tea_0' end end describe "/users/new" do it do get '/users/new' _(last_response).must_be_ok _(last_response.body).must_include 'username' end end describe "/login" do it do get '/login' _(last_response).must_be_ok _(last_response.body).must_include 'username' end end describe "/settle" do it do get '/settle' _(last_response).must_be_ok end end describe "signup and login scenario" do it do post '/users', {login: 'denzow', email: 'denzow@example.com', password: 'denzow_pass'} _(last_response.status).must_equal 302 get 'logout' _(last_response.status).must_equal 302 post '/login', {login: 'denzow', password: 'denzow_pass'} _(last_response.status).must_equal 302 post '/login', {login: 'invalid', password: 'invalid'} _(last_response).must_be_ok _(last_response.body).must_include 'Invalid username or password' end end describe "buy tea scenario" do it do post '/users', {login: 'h3poteto', email: 'h3poteto@example.com', password: 'h3poteto_pass'} _(last_response.status).must_equal 302 post '/cart', {teaId: '2', teaAmount: '200'} _(last_response.status).must_equal 302 get '/' _(last_response.body).must_include 'Sold' get '/cart' _(last_response).must_be_ok _(last_response.body).must_include 'tea_1' _(last_response.body).must_include '200' get '/checkout' _(last_response).must_be_ok _(last_response.body).must_include '4380 yen' post '/buy', {paymentType: 'poyjp', paymentInfo1: '123456789'} _(last_response.status).must_equal 302 get '/' _(last_response.body).must_include 'Teas.' end end ================================================ FILE: refactor_code/ruby/test/test_helper.rb ================================================ require 'minitest/autorun' require "rack-minitest/test" require 'database_cleaner' ActiveRecord::Migration.maintain_test_schema! DatabaseCleaner.strategy = :transaction class Minitest::Spec before :each do DatabaseCleaner.start end after :each do DatabaseCleaner.clean end end ================================================ FILE: refactor_code/ruby/views/cart.erb ================================================

    Cart.

    <% if @cart_data %> Prceed to checkout <% end %>
    <% if @cart_data %> <% for cart in @cart_data %> <% end %>
    Name Price(/100g)AmountOperation
    <%= cart[:tea].name %> <%= cart[:tea].price %> <%= cart[:amount] %>
    <% else %>

    empty cart.

    <% end %> ================================================ FILE: refactor_code/ruby/views/checkout.erb ================================================

    CheckOut.

    <% if @cart_data %> <% @cart_data.each do |cart| %> <% end %>
    Name Price
    <%= cart[:tea].name %> <%= cart[:price] %> yen
    summary
    total price(without tax) <%= @total_price %> yen
    tax <%= (@total_price * 0.08).to_i %> yen
    postage <%= (@total_amount / 100 * 30).to_i %> yen(<%= @total_amount %>g)
    total price(with tax, postage) <%= (@total_price * 1.08 + (@total_amount / 100 * 30)).to_i %> yen
    <% else %>

    empty cart.

    <% end %> ================================================ FILE: refactor_code/ruby/views/index.erb ================================================

    Teas.

    <% @tea_list.each do |tea| %> <% end %>
    Name Price(/100g)StockOperation
    <%= tea.name %> <%= tea.price %> <% if tea.stock_amount - @teas[tea.id.to_s].to_i == 0 %> Sold Out <% else %> <%= tea.stock_amount - @teas[tea.id.to_s].to_i %> <% end %> <% if tea.stock_amount > 0 and @current_user %> <% else %> <% end %>
    ================================================ FILE: refactor_code/ruby/views/layout.erb ================================================ REFACTEA
    REFACTEA
    <% if @current_user %> <% else %> <% end %>
    <%= yield %>
    ================================================ FILE: refactor_code/ruby/views/login.erb ================================================

    Login

    <% if @error %>
    <% end %>
    username:
    password:
    ================================================ FILE: refactor_code/ruby/views/tea.erb ================================================

    <%= @tea.name %>

    <%= @tea.description %>


    Price:<%= @tea.price %>, Stock: <%= @tea.stock_amount %>

    <% if @current_user %> <% else %> <% end %>
    ================================================ FILE: refactor_code/ruby/views/user_new.erb ================================================

    Sign Up

    <% if @error %>
    <% end %>
    username: >
    email: >
    password: >
    ================================================ FILE: scripts/start_docker_compose.bash ================================================ #!/usr/bin/bash -eu service_name="$1" condition="$2" start_time="$(date -u +%FT%T)" docker compose up -d while [ "$(docker compose logs --no-color --timestamps | awk '$1=="'"$service_name"'"&&$3>="'"$start_time"'"' | grep -c "$condition")" -eq 0 ]; do echo "Waiting for: service_name=$service_name, condition=$condition" sleep 10 if [ "$(docker compose ps | grep -Ec "$service_name.+Up")" -eq 0 ] then echo "Failed to start." docker compose logs docker compose ps exit 1 fi done echo "Ready." sleep 10