Help us understand the problem. What is going on with this article?

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

はじめに

こんにちは。
株式会社m-Labでインターン生をやらしてもらっている@1millionです。
普段は大学院に通ってます。

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

1ヶ月目・・・ django チュートリアル
2ヶ月目・・・ Flask 学習&開発着手
3ヶ月目・・・ リファクタリング 等

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

読んで欲しい人

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

僕のインターン開始時のスペック

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

アプリを作成した背景

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

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

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

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

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

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

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

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

機能を設ける、これにより

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

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

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

2.ついでにslack上で勤怠を打刻できる機能も欲しい

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

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

質問チケットアプリ

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

機能

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

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

実際の挙動
質問管理demo.mov.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

画像の様に
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

開発時に重宝したサービス

Postmanを使って、slackのPOSTデータを自作してテストしました。
Postmanはテストの他にも認証、ドキュメント作成、バージョン管理ができるそうです。

slackのテスト用POSTデータ例
スクリーンショット 2019-06-19 22.38.05.png

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

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

コード:

@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の基本的なクエリ(参考ページ)を使いました。

質問チケットの消費、回復

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

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

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

@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を作っていただきました。

@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

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

管理画面一覧
30秒管理画面動画.gif

テーブル設計

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時間の時差があるみたいです。しかし、注意しなければいけない点にうるう年や日本にはないサマータイムを考慮する必要がありました。結果として、時間整形に関して大変勉強になりました。

勤怠管理アプリ開発で使った技術

勤怠管理アプリ作成時に面白かった or 苦労した箇所に関して記述します。

1.ログイン画面

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の基本的なクエリ(参考ページ)を使いました。
sqlalchemyのSQL操作の中で難しかったこと

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

例:

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のBootstrapCDNをコピペしてあげることでできました。
最初、Bootstrapを導入する方法がわからなくて困り果てました笑

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のスタンダードを選定した。』だそうです!

設定
公式ドキュメントを参考に、2ファイルを作成
残念ながら 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双方の良し悪しを感じることができてよかったと思ってます。

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

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

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

Git

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

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした