2
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

pythonを使ってリアクティブなWebアプリを作りたい【開発編3】

Last updated at Posted at 2021-07-19

こちらの続きです
pythonを使ってリアクティブなWebアプリを作りたい【開発編2】

今回のゴール

https://github.com/geeorgey/vue_flask_template
こちらのvue.js/Flaskのテンプレートに1ページ追加する。

ログイン後のメニューに項目を追加する

Vue_Material_Admin_Template.png

ファイルはどこにあるか。
frontend/src/components/AppDrawer.vue
これの、この部分が左カラムのメニューを読み込んでいる部分です。

frontend/src/components/AppDrawer.vue
    <vue-perfect-scrollbar class="app-drawer__scrollbar">
      <div class="app-drawer__inner">
        <nav-list :items="computeMenu" :mini="mini" />
      </div>
    </vue-perfect-scrollbar>

:items="computeMenu"
がここで表示しているものの実態ですね。
それの定義はどこにあるかというと、もう少し下の方に行ってここです

frontend/src/components/AppDrawer.vue
  computed: {
    computeMenu() {
      return this.filterRouteItem(routes[0].children)
    },

this.filterRouteItem(routes[0].children)

が何を示すかというとこちらです。

frontend/src/components/AppDrawer.vue
import { protectedRoute as routes } from '@/router/config'

router/config が何かというとこちらを読み込んでいます
frontend/src/router/config.js

その中のこれですね。
protectedRoute

frontend/src/router/config.js
export const protectedRoute = [
  {
    path: '/',
    component: LayoutDefault,
    meta: {
      title: 'home',
      icon: '',
    },
    redirect: '/dashboard',
    children: [
      {
        path: '/dashboard',
        name: 'dashboard',
        meta: {
          title: 'dashboard',
          icon: 'mdi-view-dashboard',
        },
        component: () => import('@/views/Dashboard.vue'),
      },
      //calendar
      {
        path: '/calendar',
        meta: {
          title: 'calendar',
          icon: 'mdi-calendar-check',
        },
        name: 'calendar',
        props: (route) => ({
          type: route.query.type,
        }),
        component: () => import('@/views/Calendar.vue'),
      },

routes[0].children

となっているので、上述のソースの
children: [
の中身の配列を読み込む形になっています。

見てみると
dashboard
calendar
kanboard
...
と、画像の通りの順番でJSONが設定されていることがわかります。
ということで、dashboardの上に一つメニューを追加するのであれば以下の部分にコードを追加します。

frontend/src/router/config.js

    children: [
//ここに一つ項目を追加すればOK
      {
        path: '/dashboard',
        name: 'dashboard',
        meta: {
          title: 'dashboard',
          icon: 'mdi-view-dashboard',
        },
        component: () => import('@/views/Dashboard.vue'),
      },

JSONの中身の説明

  • path :これはそのままURLのpathになるものです。
  • name: 呼び出すときに使うページの固有名
  • meta
    • title:ページタイトル
    • icon:メニューに使われるアイコン
  • component:ページの表示に使うvueファイル

アイコンはここから検索できます。使いたいものを拾ってきましょう。
https://materialdesignicons.com/

ということで、メニューに以下の項目(ToDo)を追加します。

frontend/src/router/config.js

    children: [
      {
        path: '/todo',
        name: 'todo',
        meta: {
          title: 'todo',
          icon: 'mdi-file-tree',
        },
        component: () => import('@/views/todo/ToDo.vue'),
      },

これでメニュー部分は完成。このままビルドしてもToDo.vueがないのでエラーになるので注意。

ToDo.vueファイルを作成する

スクリーンショット_2021-07-13_6_02_23.png
左側のファイルメニューからfrontend/viewsを探して
まずは格納用のフォルダを作ります。
frontend/views/todo
controlを押しながらクリックするとメニューが出てくるので、New Folderを選択してフォルダを作成します。
次に、todoフォルダをcontrolを押しながらクリックするとメニューが出てくるので、New Fileを選択してファイル作成します。
ファイル名は先程決めた「ToDo.vue」。これで空ファイルができました。
初期値は、こんな風にスケルトンにしておきましょう。

frontend/views/todo/ToDo.vue
<template>
  <div class="page-todo">
    <v-container>
        This is ToDo page!
    </v-container>
  </div>
</template>

<script>
export default {
  name: 'ToDo',
  components: {
  },
  data: () => ({

  }),
  computed: {
  }
}
</script>

ここまで来たら一度ビルドしてみましょう。
ターミナルでfrontendディレクトリに移動してビルドします。
cd ~/QA/vue_flask_template/frontend
yarn build

注意:これまでに一度もyarn installをしていない場合はビルド時にエラーになると思うので、エラーになったら
yarn install
を行ってからやり直してください。

フロントエンドの更新を確認する

ビルドが終わったらこれをやりたいですよね。
更新を確認するためには、一度ブラウザのキャッシュを消す必要があります。
今回はGoogle Chromeで行っています。
スクリーンショット_2021-07-13_6_14_22.png

右上の設定ボタン>その他のツール>デベロッパーツール
をクリックしてください。その後、ページのリロードボタンをcontrolボタンを押しながらクリックすると、リロード方法が表示されます。

スクリーンショット_2021-07-13_6_13_46.png

キャッシュの消去とハード再読み込みを選択することによってブラウザキャッシュが消えて最新状態が表示されます。
こんな風になりましたか?

Vue_Material_Admin_Template.png

これでページを作るための準備ができました。

ToDoリストの部品を探す

そもそもTaskっていうページがあるので、これをアレンジしてみましょう。
http://vma.isocked.com/#/task/list
Vue_Material_Admin_Template.png
ちなみに、ローカルでビルドした場合は、APIが使えないので空になります。
ここにハマるようなAPIをFlaskで作っていくというのが今回のメインテーマです。
Vue_Material_Admin_Template.png

ソースはこちらです
frontend/src/views/task/TaskList.vue

これの中身を
frontend/src/views/todo/ToDo.vue
にそのままコピーしてしまいましょう。

ToDoリストのテーブル構成を考える

先程のタスクリストは色んな項目がありすぎなので、シンプルにいきます。
ToDoテーブルは

  • id:タスクのID。自動採番。
  • Name:タスク名
  • user_id:タスクの所有者のuser.idが入る

これだけです。シンプル!

migrateの準備をします

必要なファイルは2つ。
backend/models.py
backend/flask-migrateDB.py
前者はFlaskからテーブルのモデルを呼び出すための定義です。
後者はデータベースをupgradeするために必要になるスキーマ定義をするファイルです。

まずはこちらを編集していきます。
backend/flask-migrateDB.py
ファイルの最後に、以下を加えます。

backend/flask-migrateDB.py
class ToDo(db.Model):
    id = db.Column(db.Integer, primary_key=True) # primary keys are required by SQLAlchemy
    name = db.Column(db.String(1000))
    user_id = db.Column(db.Integer, db.ForeignKey("user.id"))
    def __repr__(self):
        return '<ToDo {}>'.format(self.name)

続いて
backend/models.py
も同様に。
def init_db(app):
の前に入れてください。

backend/models.py
class ToDo(db.Model):
    id = db.Column(db.Integer, primary_key=True) # primary keys are required by SQLAlchemy
    name = db.Column(db.String(1000))
    user_id = db.Column(db.Integer, db.ForeignKey("user.id"))

ファイルを保存したらDBにupgradeをしてみましょう。
ターミナルから

cd ~/QA/vue_flask_template/backend
flask db migrate
flask db upgrade

これでDBのカラムが更新されていることがわかると思います。
postgreSQLの中身を簡単に見たい場合は posticoを使ってみましょう。
PostgreSQLのデータをGUIでいじる(macOS/Postico)

q4_–_qa4u.png

こんな風に to_do テーブルが作成されていればOK。試しにto_doテーブルをダブルクリックして中身を見ていきましょう。

to_do_–q4–_qa4u.png

先程定義したとおり、id / name / user_id カラムの存在が確認できます。

frontend/src/views/todo/ToDo.vue を編集する

現状、frontend/src/views/task/TaskList.vue をコピーしただけなので、余計な物がいっぱいあります。
シンプルに使うものだけ残していきましょう。
templateはdata-tableが必要です。
それに加えて、ToDoを追加するための入力フォームも必要です。

frontend/src/views/todo/ToDo.vue
<template>
  <v-container>
    <v-row>
      <v-col cols="12">
        <v-card>
          <v-toolbar text dense flat>
            <v-toolbar-title>
              ToDo登録
            </v-toolbar-title>
            <v-spacer></v-spacer>
          </v-toolbar>
          <v-divider />
          <v-card-text>
            <v-text-field
              outlined
              label="ToDo"
              placeholder="ToDoを入力"
              v-model="formModel.todo"
              :append-icon="'mdi-location'"
              required
            />
          </v-card-text>
          <v-divider class="mt-5"></v-divider>
          <v-card-actions>
            <v-spacer />
            <v-btn tile color="accent" @click="handleSubmitForm">登録</v-btn>
          </v-card-actions>
        </v-card>
      </v-col>
    </v-row>
    <v-row>
      <v-col cols="12">
        <v-card tile>
          <v-card-text class="pa-0">
            <v-data-table
              :loading="loadingItems"
              :headers="headers"
              :items="items"
              :items-per-page-options="[15, 30, 50]"
              :server-items-length="serverItemsLength"
              :items-per-page="itemsPerPage"
              :page.sync="filter['page']"
              item-key="id"
              show-select
              @update:page="handlePageChanged"
            >
            </v-data-table>
          </v-card-text>
        </v-card>
      </v-col>
    </v-row>
  </v-container>
</template>

最初にToDo追加フォームから作る

登録フォームで重要なのは登録ボタンです。

<v-btn tile color="accent" @click="handleSubmitForm">登録</v-btn>

@clickでhandleSubmitFormを呼び出しています。
この関数は、ToDo.vueの methods部分に定義します。

frontend/src/views/todo/ToDo.vue
  methods: {
    handleSubmitForm() {
      console.log('handleSubmitForm')
      console.log(this.formModel)
      console.log('todo: ' + this.formModel.todo)
      console.log('userid: ' + this.getUserId)
      this.loading = true
      if(this.formModel.todo!=null){
        this.$store
          .dispatch('addtodo', {todo: this.formModel.todo, user_id: this.getUserId})
          .then(() => {
            this.loading = false
            this.$router.push('/todo')
          })
          .catch(() => {
            this.loading = false
          })
      }
    },

console.log部分は、不要になったら削除してください。
これは、デバッグ用の記載で、このように書いておくと、ブラウザのデベロッパーツールのconsole部分にログを出すことができます。

Notification_Center.png

今回で言えば登録ボタンを押すたびに
handleSubmitForm
が呼び出されるのでこれらがブラウザに表示されるはずです。(※本番運用時は消してください)

      console.log('handleSubmitForm')
      console.log(this.formModel)
      console.log('todo: ' + this.formModel.todo)
      console.log('userid: ' + this.getUserId)

先程のhandleSubmitFormで重要なのはなにかというと
.dispatch('addtodo', {todo: this.formModel.todo, user_id: this.getUserId})
です。
addtodoという関数を呼び出しています。
付帯情報として、todoとuser_idをくっつけて送信しています。
つまり、こんな情報が流れてくるんですよね。

key value
todo todoの名前
user_id ユーザ作成時につけられたID

jsにactionsを追加する

先程作った
frontend/src/views/todo/ToDo.vue
に対応するjsは
frontend/src/store/modules/todo.js
で定義されます。
このjsのactionsの中にこれを追加しましょう

frontend/src/store/modules/todo.js
// actions
const actions = {
//中略
  addtodo({ commit }, { todo, user_id }) {
    return request({
      url: '/addtodo',
      method: 'post',
      data: {
        todo,
        user_id,
      },
    }).then((resp) => {
      console.log('After addtodo')
      console.log(resp)
      commit('SET_TODO', { todos: resp.todo_list })
      //dispatch('fetchGoogleMembers',email)
    })
  },

先程のToDo.vueのdispatchで渡した引数2つを
.dispatch('addtodo', {todo: this.formModel.todo, user_id: this.getUserId})
ここで受け取っています
addtodo({ commit }, { todo, user_id }) {

frontend/src/store/modules/todo.js
    return request({
      url: '/addtodo',
      method: 'post',
      data: {
        todo,
        user_id,
      },

ここはrequestというmethodを使っているのですが、それは一行目に定義が書かれています
import request from '@/util/request'
何やってるかは、
frontend/src/util/request.js
を読みましょう。
axios使ってrequestを投げてることがわかります。

シンプルに意味を説明すると、urlに指定したエンドポイントにdataをjsonにつめてmethod:postでリクエストを投げています。このactionsの処理によってVue.js→Flask側へ処理が渡ります。

ということで、次はこれを受け取って処理するAPIを作る必要があります。

Flask(python)側でAPIを作ろう

先程のtodo.jsで指定したエンドポイントに対応するAPIを作りましょう。
作るのは
backend/main.py
で良いと思います。

backend/main.py
@main.route('/addtodo', methods=['POST'])
def addtodo():
    print('Start addtodo -----')
    todo = request.json.get("todo", None)
    user_id = request.json.get("user_id", None)
    new_todo = ToDo(name=todo, user_id=user_id)
    db.session.add(new_todo)
    db.session.commit()

    todos = []
    todos = ToDo.query.filter_by(user_id=user_id).all()

    res_todos = []
    for todo in todos:
        todo_dict = {}
        todo_dict['id'] = todo.id
        todo_dict['user_id'] = todo.user_id
        todo_dict['name'] = todo.name
        res_todos.append(todo_dict)
    return jsonify({'todo_list': res_todos}), 200

こんな感じになります。
一行目でroute及びmethodを規定します。
渡ってきた引数をどうやって引き取るかというと

backend/main.py
    todo = request.json.get("todo", None)
    user_id = request.json.get("user_id", None)

この形式になります。

backend/main.py
    new_todo = ToDo(name=todo, user_id=user_id)
    db.session.add(new_todo)
    db.session.commit()

ここでは、ToDoというモデルをこのファイルの最初の方でimportしていますので、それを使います。

backend/main.py
from .models import ToDo

todoテーブルにはnameとuser_idカラムがあるので、その中に対応するデータを入れてToDoインスタンスを作ります。
次の二行は、それをセッションに詰めてコミットする(ここではaddを確定する)という司令になってます。

backend/main.py
    db.session.add(new_todo)
    db.session.commit()

それ以降の行は、当該ユーザが持っているtodoをデータベースからすべて取得してjsonにつめて返す処理です。

backend/main.py
    todos = []
    todos = ToDo.query.filter_by(user_id=user_id).all()

    res_todos = []
    for todo in todos:
        todo_dict = {}
        todo_dict['id'] = todo.id
        todo_dict['user_id'] = todo.user_id
        todo_dict['name'] = todo.name
        res_todos.append(todo_dict)
    return jsonify({'todo_list': res_todos}), 200

最初の二行でToDoテーブルの指定したuser_idのデータをすべて取得します。
その後、res_todosという配列に、todo_dictという辞書形式のデータを詰め込み、jsonifyでjsonにして送信しています。返答コードは200だと正常処理です。
400だとエラー時のレスポンスとして返すことができます。
これでFlask→Vue.jsにデータを返すことができました。

Vue.jsでFlaskから返ってくるデータを受け取る

frontend/src/store/modules/todo.js
に戻りましょう

frontend/src/store/modules/todo.js
    }).then((resp) => {
      console.log('After addtodo')
      console.log(resp)
      commit('SET_TODO', { todos: resp.todo_list })
      //dispatch('fetchGoogleMembers',email)
    })

Flaskからのデータは
then((resp) => {
のrespの中に入っています。

ここで大事なのは
commit('SET_TODO', { todos: resp.todo_list })
です。

さらにSET_TODOというmutationsを呼び出しています。
なんでそんな面倒なことをするのかというと、そういうルールだからですね。
Vue.jsのデータを書き換える(つまりUI上に現れるデータを書き換える)場合はmutationsを使えということになっています。

SET_TODO
に引数名
todos
というものに
resp.todo_list
を投げています。
それは何かというと
Flaskからreturnするときに
return jsonify({'todo_list': res_todos}), 200
と書いたからです。
todo_listという引数名でデータを投げているものを、resp.todo_listで受け取るという形です。

ではそのSET_TODOというmutationsはどんな風に書くかというと

frontend/src/store/modules/todo.js
  SET_TODO(state, payload) {
    console.log('SET_TODO')
    console.log(payload)
    state.todos = payload
  },

stateは同ファイルの上の方に書いてあります

frontend/src/store/modules/todo.js
const state = {
  todos: []
}

ここにデータを詰め込みましょうということです。
先程のdict形式のリストデータが
state.todos = payload
によって上書きされます。

ToDo.vueでToDoリストを表示する

続編はこちら
pythonを使ってリアクティブなWebアプリを作りたい【開発編3-2】

関連リンク

2
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?