インターン生の質問をスタミナ管理+勤怠管理アプリを作ってみた

# はじめに
こんにちは。
[株式会社m-Lab](https://m-lab.inc)でインターン生をやらしてもらっている@1millionです。
普段は大学院に通ってます。

インターン開始して、3〜4ヶ月ほど経過しました。
この3〜4ヶ月でDjangoチュートリアルを触って、Flaskを使ってアプリを開発をしました。

```
１ヶ月目・・・ django チュートリアル
２ヶ月目・・・ Flask 学習＆開発着手
３ヶ月目・・・ リファクタリング 等
```

今回、人生初のアプリを記事にします!
暖かい目で見守っていただけると幸いです。
もちろん誤字脱字や、変なこと言ってるなと思ったら遠慮なく指摘してください。


## 読んで欲しい人
プログラミングを初心者の同志
気持ちの余裕がある人にお勧めします笑

## 僕のインターン開始時のスペック
◇ 現在、大学院で触覚の表現について研究中
◇ 今年の４月から弊社でインターン開始、週３勤務
◇ プログラミング経歴は他企業が主催の Python の講習を４日間受けたことがあるw
◇ ディレクトリって何？？という質問をしてしまうほどに初心者ですw

## アプリを作成した背景

### 1.インターン生の質問力を高めたい

弊社では、インターン生の課題の１つに**良い質問ができるようになること**があります。

まず、ここでいう良い質問とは、、、

```
いい感じの頻度で質問→多すぎず少なすぎず!
いい感じに調べてから質問→考える力を養おう!
```

良い質問というより、最低限できて欲しいことに近い気がしますね笑
しかし、この**いい感じ**にというのは、曖昧で人や環境によって異なると思います。
どうにか良い質問を可視化できないか。いい感じに笑


そんな時、弊社の[代表](https://.com/murabo)がこんなqiitaの記事を見つけてくれました。
[『新卒からの質問をソシャゲっぽい仕組みにしたら捗った話』](https://qiita.com/ysktsuna/items/fced3a9515c8f585ca50)
なんと、この記事では良い質問の頻度をいい感じに可視化していました。
そして筆者は日頃からAIアクセラレータ等で弊社がお世話になってる**バイトルドットコム♫**でお馴染みの、**ディップ株式会社**の記事！
やるしかないでしょ!

ということで、上記の記事を簡単に説明すると

```
・ 1回質問すると、1枚消費される質問チケットを新入社員に配布
・ 一定時間でチケット枚数が回復する
・ チケットに保有上限を設定する
```
機能を設ける、これにより

```
・ 質問チケットという大義名分があることで安心して質問できる環境
・ 1回の質問の精度向上
・ 新入社員がつまずいているところを解決する先輩のスゲー感
・ 質問チケット使い方を考えるゲームの戦略性
```

等々が得られたそうです。

弊社でも同様の効果を期待して、**質問チケットをslack上で管理するアプリを開発することにしました。**

### 2.ついでにslack上で勤怠を打刻できる機能も欲しい
上記のチケットアプリを作成している最中に質問チケットの回復のために勤怠記録が必要になりました。
勤怠の記録が必要なら、
**『各々で記録をとっている出退勤の記録をslack上でできたら楽だし便利だよね』**
という意見が出始め、**slack上で使える勤怠管理アプリの開発にも着手しました。**


本記事で「Django」ではなく「Flask」を使用しているのは、開発に着手した時は機能要件が少なかったという理由です。





# 質問チケットアプリ
まずは、タイトル前半のインターン生の質問をスタミナ制にするアプリに関して記述します。
### 機能

```
・ ユーザーの登録を行う
・ 出勤すると、質問チケットが２つ付与される
・ 質問チケットは１時間に２つ追加される
・ 質問をすると、質問チケットを１つ消費する
・ 退勤時に質問チケットを0にする
```

各機能はslackの**Slash Commands**で登録したコマンドがPOSTされたときに動作するようにしました。
質問チケットの上限枚数を5枚とし、インターン生の質問チケットが1時間ごとに2枚付与されるように設計しています。

**実際の挙動**
![質問管理demo.mov.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/445967/95bd995d-4ec5-b003-3aeb-5b9672a3356d.gif)


## 使用技術
### 言語
Python 3
### インフラ
GAE
### フレームワーク
Flask
### テンプレートエンジン
Jinja2
### ORM
SQLAlchemy
### DB
MySQL 5.7（cloud SQL）

## Slack から Flask へのデータの受け渡し


```
/create
/attendance
```
Slackに 上記のようにコマンドを入力することにより、あらかじめ設定しておいたエンドポイントにPOSTする様な仕組みが欲しい。



使用するのは slack api の **slach commands**
https://api.slack.com/custom-integrations/slash-commands

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/445967/19092f7a-ca41-6560-8c76-184cf7d35c70.png)

画像の様に
**A というコマンドが来たら B というエンドポイント(URL)にPOSTを行う**
という設定が可能

POSTデータの例はこんな具合

```
token=xxxxxxxxxxxxxxxxxxxxxxxx
&team_id=T0001
&team_domain=example
&enterprise_id=E0001
&enterprise_name=Globular%20Construct%20Inc
&channel_id=C2147483705
&channel_name=test
&user_id=U2147483697
&user_name=Steve
&command=/weather
&text=94070
&response_url=https://hooks.slack.com/commands/1234/5678
&trigger_id=11111111111.111111111.1111111111d88f008e0
```

つまり、以下の流れができる
![Untitled Diagram (9).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/445967/fdbad6cd-b32c-75a2-292a-cfd1deed6706.png)


###開発時に重宝したサービス
[Postman](https://www.getpostman.com/)を使って、slackのPOSTデータを自作してテストしました。
Postmanはテストの他にも認証、ドキュメント作成、バージョン管理ができるそうです。

**slackのテスト用POSTデータ例**
<img width="646" alt="スクリーンショット 2019-06-19 22.38.05.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/417336/ab0b6e8f-de1e-9c5e-65cd-a470d8ebda10.png">





###質問チケットアプリのテーブル設計

| カラム名           |説明 |
|:-----------------|:------------------|
| id               |         レコード番号 |
| username         |ユーザーのslackの名前 |
| count            | 質問チケットの残り枚数 |
| attendance       |         出勤か否か |
| is_intern        |インターン生と社員の識別 |

```python:
class User(Base):
    __tablename__ = 'slack_question'
    id = Column(String(100), primary_key=True)
    username = Column(String(100), index=True, unique=True)
    count = Column(Integer)
    attendance = Column(Integer, nullable=False)
    is_intern = Column(Integer, nullable=True)
```

###ユーザー登録
**Slash Commands**で登録した任意のコマンド(/create)がPOSTされたときに先述の質問チケットアプリテーブルにslackのuser_nameをユーザーとして登録できるようにしました。その際に、社員の場合は登録したコマンドの後ろに『emp』を含ませてコマンド入力することでインターン生と識別できるようにしました。

```
・ インターン生としての登録コマンド例 →/create
・ 社員としての登録コマンド例 →/ceate emp
```
コード：


```python:
@app.route('/create', methods=['POST'])
def create():
    session = Session()
    created = request.form
    created_name = created["user_name"]
    created_id = created["user_id"]
    created_emp = created["text"]
    usernames = [name for name, in session.query(User.username)]
    session.close()
    if created_emp == "emp":
        newname = User(id=created_id, username=created_name, 
                  count=2, attendance=False, is_intern=False)
        session.add(newname)
        session.commit()
        session.close()
        return created_name + "さんを登録しました！"
    elif not created_name in usernames:
        newname = User(id=created_id, username=created_name, 
                  count=2, attendance=False, is_intern=True)
        session.add(newname)
        session.commit()
        session.close()
        return created_name + "さんを登録しました！"

    else:
        return "もうメンバーですよ！"
```

SQL操作はsqlalchemyの[基本的なクエリ](https://qiita.com/bokotomo/items/a762b1bc0f192a55eae8)(参考ページ)を使いました。

###質問チケットの消費、回復
質問チケットの保有上限を5枚として、インターン生の場合は１時間ごとに2枚回復。
質問チケットの消費は、質問を答えてくれた人が質問をした人の質問チケットの消費をコマンド(/q)として登録したコマンドの後ろにslackの名前を打つことで質問チケットが1枚消費されるようにしました。

```
・ 質問チケットの消費コマンドの例 →/q @millon
```

質問チケットの消費コード:

```python:
@app.route('/question', methods=["POST"])
def question():
    session = Session()
    posted = request.form
    posted_name = posted['text']
    posted_name = posted_name.strip("@")
    if posted_name[-1:] in " ":
        posted_name = posted_name.strip(" ")
    usernames = [name for name, in session.query(User.username)]
    session.close()
    if posted_name in usernames:
        filtered = session.query(User).filter(
                   User.username == posted_name).first()
        filtered_count = filtered.count
        if 0 < filtered.count < 5:
            filtered.count -= 1
            session.commit()
            session.close()
            return "残りの質問回数は" + str(filtered_count) + "回です！"
        else:
            return '質問回数が不足してます！'
    else:
        return "出勤を記録してください！"
```


質問チケットの回復コード:
下記の質問チケットの回復コードが時間毎に動作するように元ホストのエンジニアさん@from_hostにGCEのcronを作っていただきました。

```python:
@app.route("/counter")
def add_question():
    session = Session()
    users = session.query(User).filter(User.is_intern == 
            True, User.attendance).all()

    for i in users:
        if i.count < 4:
            i.count += 2
            session.commit()
        else:
            i.count = 5
            session.commit()
    session.close()
```



#勤怠管理アプリ
上記の質問チケットアプリを開発したあと、質問チケットアプリの追加機能勤怠管理アプリの開発しました。


##機能

```
1.出退勤の記録
2.管理画面へのログイン画面
3.全ユーザーの出退勤の表示画面
4.検索機能
5.編集機能
```
出勤・退勤の記録はslackの社内チャンネルで登録したコマンド(例/att,/fin )を打つことで動くようにしました。
 
**実際の挙動**
![名称未設定 6.mov.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/445967/b91e92d4-d510-faa1-3323-d1d188c02755.gif)


機能の2.以降はFlaskのテンプレートエンジンである**jinja2**とCSSフレームワークの
**Bootstrap4**を使って開発をしました。

**管理画面一覧**
![30秒管理画面動画.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/417336/28b56256-1321-364d-fe29-0503fccf878b.gif)







##テーブル設計

```python:
class WorkTime(Base):
    __tablename__ = 'work_time'
    id = Column(Integer, primary_key=True)
    user_id = Column(String(100), index=True)
    username = Column(String(100), index=True)
    attendance_time = Column(DateTime(), 
                      default=datetime.now(pytz.timezone('Asia/Tokyo')))
    finish_time = Column(DateTime(), 
                  onupdate=datetime.now(pytz.timezone('Asia/Tokyo')))
```
| カラム名           |説明 |
|:-----------------|:------------------|
| id               |  レコード番号 |
| user_id          |ユーザ固有のslackのid |
| username         |ユーザーの名前　　　　　|
| attendance_time  | 出勤時間　　　　　 　 |
| finish_time      |退勤時間　　　　 　 |


MySQL内のTimeZoneをUTC(協定世界時)に設定しました。UTCはJST(日本標準時)と基本的には、9時間の時差があるみたいです。しかし、注意しなければいけない点にうるう年や日本にはない[サマータイム](https://ja.wikipedia.org/wiki/%E5%A4%8F%E6%99%82%E9%96%93)を考慮する必要がありました。結果として、時間整形に関して大変勉強になりました。


##勤怠管理アプリ開発で使った技術
勤怠管理アプリ作成時に面白かった or 苦労した箇所に関して記述します。
###1.ログイン画面

```python:Flask
from flask import session as cook
from flask import flash
@app.route("/login", methods=["GET", "POST"])
def login():
    if request.method == "POST":
        if request.form["loginname"] == "mlab" and 
           request.form["password"] == "password":
            cook['logged_in'] = True
            return show_entries()
        else:
            flash("ログイン名、パスワードを正しく入力してください")
    return render_template("login.html")
```

Flaskのsessionを用いて、認証機能をつけました。
また、Flaskのflashを使うことでログインが失敗したときにメッセージを送ることができました。

###2.出退勤記録の処理
Pythonの標準ライブラリdatetimeを使って、日時（日付や時間・時刻）の処理をしました。

```
・ datetime型とstring型を相互に変換するメソッド:`strftime()`と`strptime()`
・ aware(TimeZone情報を持つ)とnaive(TimeZone情報を持たない)の変換:サードパーティのライブラリ`pytz`
・ TimeZoneの変更：`astimezone()`
```
**3.sqlalchemyのSQL操作**
SQL操作はsqlalchemyの[基本的なクエリ](https://qiita.com/bokotomo/items/a762b1bc0f192a55eae8)(参考ページ)を使いました。
sqlalchemyのSQL操作の中で難しかったこと

- ある日付(start_date)からある日付(end_date)の間のデータの抽出:betweenの利用

例:

```python:Flask
period_date = session.query(Work_time).filter(Work_time.attendance_time.
              between(start_date, end_date)).all()
```

###4.Bootstrap4

```
・ 大体の機能を実装できいるようにしたけれど、HTMLだけでは味気ないなー
・ サクッと体裁を整えられたらいいなー
```
こんな気持ちで**Bootstrap4**を導入してみることにしました。
結果、**Bootstrap**は体裁を整える点で、テンプレートなどがネット上に転がっているのでそれを真似することでざっくり体裁を整えられるので、導入してよかったと思っております。

**導入方法**
**Bootstrap**を導入したいHTMLファイルのヘッドに[Bootstrapの公式HP](https://getbootstrap.com/)のBootstrapCDNをコピペしてあげることでできました。
最初、Bootstrapを導入する方法がわからなくて困り果てました笑

```html:html
<head>
    <meta charset="UTF-8">　
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/
css/bootstrap.min.css"integrity="sha384ggOyR0iXCbMQv3Xipma34MD+dH/
1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">
</head>
```

# GAE&CloudSQL
GAE&CloudSQLに関しては、先輩社員さんに甘えさせていただいちゃいました-。
聞くところによると、『弊社のサービスのインフラはGCPのため、わざわざ他のインフラサービスを利用する事もない。さらに、社内用で、しかもリクエスト回数も少ないため、今回は最小限のコストで運用できる様にGAEのスタンダードを選定した。』だそうです！

**設定**
公式ドキュメントを参考に、２ファイルを作成
残念ながら python3.7 用のドキュメントは無いが、動作確認できた
https://cloud.google.com/appengine/docs/standard/python/getting-started/python-standard-env

```app.yaml
runtime: python37 

handlers: 
- url: /static 
  static_dir: static 
- url: /.* 
  redirect_http_response_code: 301 
  script: auto 
```

以下のファイルは、他のGCPのサービスを使用する際に必要になる。

```appengine_config.py

from google.appengine.ext import vendor 

# Add any libraries installed in the "lib" folder. 
vendor.add('lib')
```


##リファクタリング
可読性をあげるために必要なそうです。
今回、リファクタリングに多くの時間を費やしました。
というのも、最初に出来上がったものは、HTMLファイルを除いた全ての機能を1つのファイルに実装していました。加えて、関数化すらしていませんでした。
結果、長い、長い巻物みたいなコードが出来上がってしまいました。可読性はもちろん皆無です。

どうにか、可読性を高めるために、下記のことを行いました。

```
1.質問チケットアプリと勤怠管理アプリにファイルを分割
2.変数名の改善
3.同じ動作をする箇所を抽出して関数化
4.冗長なコードを簡潔なコードにする
```
上記4つをした結果、コードがみるみる簡潔になっていくことに感動しました笑
最終的には、コードの長さが半分くらいになりました！
リファクタリングをする上で、基礎、ファイルの構成、わかりやすい変数名の命名の大切さを思い知りました。


# まとめ
Flaskの選定には、小さなアプリを作る上でスピーディーな開発ができるそうなので、開発環境として採用しました。開発初期、質問チケットをスタミナ制にする機能だけの開発予定だったので、Djangoと比較してFlaskは、ファイルを行き来する回数がものすごく少なくなって、『お手頃だな』と思っていました。しかし、勤怠管理機能を追加することになり割と大きなアプリになってしまいました。アプリの機能が増えて行くに伴い、Djangoのファイル構成のありがたみを感じるようになりました。結果、Django,Flask双方の良し悪しを感じることができてよかったと思ってます。

総じて、『楽しくアプリ開発をできた』の一言に尽きると思います！
この素敵な環境に日々、感謝感激をしている次第です🙇‍♂️

早く、恩返しをできるようになりたいですね！
次は、面白い成果物を作れたらいいなーなんて思ってます！

最近、だんだんと機能を増やして**slack**の**incoming webhook**を使って質問コマンドが打刻された時にチャンネルメンバーに通知を送れるようにしました。



#Git
https://github.com/yoshiyasugimoto/qa_caounter/tree/master