Djangoで勤怠管理Webアプリケーションを作ってみた

#はじめに
現在アルバイトしているバーではタイムカードを表計算ソフトで管理しているのですが、これをもうちょっと楽にできないかなと思い、勤怠管理Webアプリケーションを作ってみました。

まだまだ改善の余地があり、追加したい機能などもありますが、行き詰まりまして...
大枠は完成したので、一度ここでアウトプットして整理してみます。

ソースコードはこちら↓↓
https://github.com/TaroNoguchi/attendance-app

[追記]
:point_up: いつの間にかリポジトリをPrivateにしてしまっていたのでPublicに戻しました。

#環境など

|    |    |
|:---|:---|
|OS  |macOS Mojave|
|使用言語|Python 3.6.7|
|フレームワーク|Django 2.2|
|バージョン管理|Git|
|確認用ブラウザ|Google Chrome|
|制作期間|約2週間|

#大枠を作成

##最初の準備

###conda仮想環境構築
Anacondaで仮想環境を作成して、そこで動かしていくので作成します。
詳しくはこちらの方が記事で書いておられますので読んでみてください。→[【初心者向け】Anacondaで仮想環境を作ってみる](https://qiita.com/ozaki_physics/items/985188feb92570e5b82d)
`conda create -n(または--name) 環境名 python(=バージョン指定)`
でpythonのバージョンを指定して仮想環境を作成できます。
また、`conda activate 指定した環境名`で作成した環境をアクティベートできます。

```sh
conda create -n py36 python=3.6.7
conda activate py36
```

###Djangoのインストール
この仮想環境を立ち上げた状態でインストールします。

```console
(py36) TaronoMacBook-Pro:clock taro$ conda install django
```
これで環境構築できました。
今後はここで作業していくので、以降シェルコマンドはこの環境上で動かしているとみなしてください。
実際に環境構築で一番手間取ったかもしれません。最終的に動いているのはこの状態なのですが、ここまで来るのに右往左往してしまいました。

##Djangoでの最初の作業

###startproject

プロジェクトのディレクトリを保存したい場所でターミナルを開き、以下のコマンドを入力。
clockは勝手につけた名前。

```
django-admin startproject clock
```
次のような構造のディレクトリができます。

```
clock
├── clock
│   ├── __init__.py
│   ├── __pycache__
│   │   ├── __init__.cpython-36.pyc
│   │   ├── settings.cpython-36.pyc
│   │   ├── urls.cpython-36.pyc
│   │   └── wsgi.cpython-36.pyc
│   ├── settings.py
│   ├── urls.py
│   └── wsgi.py
└── manage.py
```
外側のclockディレクトリはただの入れ物なのでリネーム可能。

###startapp 
以下のコマンドを入力し、外側のディレクトリ内(このプロジェクトのルートディレクトリ)でattendanceという名前のアプリの入れ物を作ります。

```
python manage.py startapp attendance
```
次のようにattendanceディレクトリができます。

```
clock
├── attendance
│   ├── __init__.py
│   ├── __pycache__
│   ├── admin.py
│   ├── apps.py
│   ├── migrations
│   ├── models.py
│   ├── tests.py
│   └── views.py
├── clock
└── manage.py
```
##よく使うDjangoコマンド
- Djangoのデータベースをマイグレーションコマンドで作成。

```
python manage.py makemigrations
python manage.py migrate
```
2段階のコマンドで、一度マイグレーションする物のリストを作ってから、実際にマイグレーションしています。(git add から git commit の流れに似ています)

- サーバーを動かす。

```
python manage.py runserver
```

##Djangoの全体像
![django_archi.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214141/d371349c-434c-14b4-f91c-e472c482c506.png)

超ざっくりした絵ですみません笑

とっても簡略化すると、

1.ブラウザからリクエストされたURLをDjangoのURLディスパッチャなるコアモジュールが受ける
2.そのディスパッチャが、urls.pyに記述したURLconf(URLのパターンとそれぞれのパターンに対するリクエストの送り先を示した地図のようなもの)に従って、ビューにリクエストを送る
3.ビューがモデルオブジェクトを取得してビジネスロジックを実行する(状況に応じてmodel.pyの内容とデータベースが同期される)
4.最終的にビューが情報をテンプレートにレンダリングしてレスポンスを作成する
5.作成されたレスポンスをディスパッチャがブラウザに送る

という流れです。
主にコードを書いていくのはmodels.pyとviews.pyになります。あとテンプレートファイルとしてのHTMLファイルですね。

##setting.pyの編集
Djangoの設定ファイルを編集します。

```setting.py
INSTALLED_APPS = [
    'attendance.apps.AttendanceConfig', # djangoにattendanceアプリの存在を知らせる
    #...
]

#...

LANGUAGE_CODE = 'ja' # 言語を日本語に  

TIME_ZONE = 'Asia/Tokyo' # タイムゾーンを日本時間に
```

##URLconfの編集

urls.pyを編集します。
ルートのURLconfにコードがたくさん書かれるのを防ぐために、アプリケーションごとにURLconfを用意し、ルートには各アプリのURLconfへの道を教えてあげます。

↓ルート

```clock/urls.py
from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path('', include('attendance.urls')),
    path('admin/', admin.site.urls),
]
```

↓attendanceアプリ(新しくurls.pyファイルを作成してください)

```attendance/urls.py
from django.urls import path
from . import views # ←これ忘れないようにする！！

app_name = 'attendance'
urlpatterns = [
    path('', views.index, name='index'),
    path('result/', views.result, name='result'),
]
```

他の場所からの呼び出しが簡単になるように、app_name変数を定義してください。
まだviews.pyには何もありません。これから記述していきます。

##モデルの作成

勤怠時間の打刻モデルということで、SubmitAttendanceクラスを作成し、モデルとします。

```attendance/models.py
from django.db import models
from django.contrib.auth import get_user_model

# Create your models here.
class SubmitAttendance(models.Model):
    
    class Meta:
        db_table = 'attendance'
    
    PLACES = (
        (1, 'Bar Foo'),
        (2, 'Bar Baz'),
        (3, 'Bar Qux'),
        (4, 'Bar Quux'),
        (5, 'Bar Corge'),
        (6, 'Bar Grault'),
    )
    IN_OUT = (
        (1, 'IN'),
        (0, 'OUT'),
    )

    staff = models.ForeignKey(get_user_model(), verbose_name="スタッフ", on_delete=models.CASCADE, default=None)
    place = models.IntegerField(verbose_name='出勤場所名', choices=PLACES, default=None)
    in_out = models.IntegerField(verbose_name='IN/OUT', choices=IN_OUT, default=None)
    time = models.TimeField(verbose_name="打刻時間")
    date = models.DateField(verbose_name='打刻日')
```
- models.Modelを継承したクラスを定義します。
- データベースに格納された時のメタデータとして、テーブル名を与えています。
- バーの店舗と、出勤or退勤の選択肢を作るために、ここでタプルに格納しています。(もちろん店舗名は架空の名前に置き換えています。笑)
- 変数は以下の通り↓
    - staff:ログイン中のユーザー名
    - place:PLACESタプルから選ぶ
    - in_out:IN_OUTタプルから選ぶ
    - time:打刻した時間
    - date:打刻した日付
- verbose_nameはデータベースのcolumnの名前

##フォームの作成
最初のページで、バーの店舗名と出勤か退勤かを選択させるフォームを作ります。
コードはforms.pyを作り、そこに記述します。

```attendance/forms.py
from django import forms
from .models import SubmitAttendance

class SubmitAttendanceForm(forms.ModelForm):

    class Meta:
        model = SubmitAttendance
        fields = ('place', 'in_out')
```

##indexビューの作成

最初の画面を出すためのIndexViewと、結果画面を出すためのResultViewに分けています。

```attendance/views.py
from django.shortcuts import render, redirect
from django.views import View
from django.contrib.auth.mixins import LoginRequiredMixin
from .models import SubmitAttendance
from .forms import SubmitAttendanceForm
from datetime import datetime
from django.utils import timezone
# Create your views here.
class IndexView(LoginRequiredMixin, View):
    def get(self, request):
        form = SubmitAttendanceForm
        context = {
            'form': form,
            "user": request.user,
        }
        return render(request, 'attendance/index.html', context)
index = IndexView.as_view()
```
※ログインに関する記述がありますが、これは後ほど記述します。

クラスベースのビューを記述しています。
先ほど作成したフォームを、オブジェクトとして変数formに代入。
ログイン中のユーザーを変数userに代入。
context辞書に、テンプレートへ送る変数をまとめて、render関数をつかってリクエストをテンプレートへ送ります。
最後にこのクラスをas_view()を用いて、ビューとしてインスタンス化します。

##indexテンプレートの作成

まずはテンプレートファイルを保存するディレクトリを作ります。
attendance/template/attendanceとなるようにディレクトリを新たに２つ追加し、内側のattendanceの中に、base.htmlとindex.htmlを作ります。
templatesディレクトリの中にattendanceディレクトリをもう一度作っていますが、これは「お約束」みたいなものです。

###テンプレートの継承
全てのhtmlファイルに共通な部分を、テンプレートファイルを作るごとに何回も何回も書いていたら大変なので、そういった部分はbase.htmlに書いて、index.htmlで継承するという手続きを踏みます。

```django:base.html
{% load static %}
<html>
<head>
    <meta charset='utf-8'>
    <link rel="stylesheet" type="text/css" href="{% static 'attendance/css/style.css' %}">
    <title>勤怠打刻ページ</title>
</head>    
<body>
    {% block content  %}
    {% endblock  %}
</body>
</html>

```
冒頭で`{% extends 継承元 %}`と記述することで継承元のファイルを継承し、`{% block content %}`と`{% endblock %}`の中に内容を記述します。
static(静的)ファイルに関する記述があります。Bootstrapのstyle.cssをstaticディレクトリの中に保存して使っています。

```django:index.html
{% extends "attendance/base.html" %}
{% block content %}
    
    {% if user.is_authenticated %}
        <div class="container">
            <form method="post" action="{% url 'attendance:result' %}">
            {% csrf_token %}
                <h1>{{  user  }} さん</h1>
                <h2 id="time"></h2>
                    <script>
                    time();
                    function time(){
                        var now = new Date();
                        document.getElementById("time").innerHTML = now.toLocaleString();
                    }
                    setInterval('time()',1000);
                    </script>
                <p>出勤場所：<input type="hidden" name="place">{{ form.place }}</p>
                <p>IN/OUT：<input type="hidden" name="in_out">{{ form.in_out }}</p>
                <p><input class="btn btn-primary" type="submit" value="Submit"></p>
            </form>
            <a href="../accounts/logout">ログアウト</a>
        </div>
    {% endif %}
{% endblock  %}
```

- {%%}はテンプレートタグ。HTML上にPythonのようなコードを書くことができるタグです。
- scriptタグで囲まれた部分は、現在時刻を表示しています。
- {{ }}で囲まれた部分には、先ほどビューから送った変数です。
- formタグ内で入力された値はview.resultに送られるようaction属性を指定します。
- `{% csrf_token %}`はこのページのように入力データを送るときに記述しなければならない、セキュリティのお約束のようなものです。

##resultビューの作成

```attendance/views.py
#...
class ResultView(View):
    def post(self, request):
        form = SubmitAttendanceForm(request.POST)
        now = datetime.now()
        month = now.month
        day = now.day
        hour = now.hour
        minute = now.minute
        
        obj = form.save(commit=False)
        obj.place = request.POST["place"]
        obj.in_out = request.POST["in_out"]
        obj.staff = request.user
        obj.date = datetime.now().date()
        obj.time = datetime.now().time()
        obj.save()
        if request.POST["in_out"] == '1':
            comment = str(month) + "月" + str(day) +"日" + str(hour) + "時" + str(minute) + "分\n" + "出勤確認しました。今日も頑張りましょう！"
        else:
            comment = str(month) + "月" + str(day) +"日" + str(hour) + "時" + str(minute) + "分\n" + "退勤確認しました。お疲れ様でした(^-^)！"
        context = {
            'place': SubmitAttendance.PLACES[int(obj.place)-1][1],
            'comment': comment,
        }
        return render(request, 'attendance/result.html', context)
result = ResultView.as_view()
```
- 入力値とともに送られてきたPOSTメソッドのリクエストをformに代入。
- objが並んでいるところは入力値をモデルの変数に代入し、データベースに保存しています。
- 出勤か退勤かで変数commentに代入する文字列を変えて、result.htmlに送ります。

##resultテンプレートの作成
```django:result.html
{% extends 'attendance/base.html' %}
{% block content %}
    <p>出勤場所：{{ place }}</p>
    <p>{{ comment }}</p>
    <a href="../accounts/logout">ログアウト</a>
{% endblock  %}
```

#実際の作業2(ログインページの作成)

こちらのサイトを参考にしました↓↓
[Django2 でユーザー認証（ログイン認証）を実装するチュートリアル -2- サインアップとログイン・ログアウト](https://it-engineer-lab.com/archives/544)

##設定とURLconf

accountsアプリを作り、setting.pyのINSTALLED_APPに、

```python
accounts.app.AccountsConfig
```
を記述してaccountsアプリの存在を示してあげます。
また、accountsアプリとルートのURLconfも編集します。

```clock/urls.py
#...
#以下を追加
    path('accounts/', include('django.contrib.auth.urls')),
#...
```

```accounts/urls.py
from django.urls import path
from . import views

app_name = 'accounts'
urlpatterns = [
    path('login/', views.login, name='login'),
    path('logout/', views.logout, name='logout')
]
```
Djangoが用意してくれているログインフォームがあるので、ビューの編集はしません。

##テンプレート
Djangoではログインページのテンプレートファイルは、`accounts/templates/registration/login.html`というところに書くと決まっていますので、素直にこの構造を作ります。

```django:accounts/templates/registration/login.html
{% extends 'admin/base.html' %}

{% block content %}
 
{% if form.errors %}
<p>ユーザー名とパスワードが一致しません。</p>
{% endif %}
 
{% if next %}
    {% if user.is_authenticated %}
    <p>アクセス権のあるアカウントでログインしてください。</p>
    {% else %}
    <p>ログインしてください。</p>
    {% endif %}
{% endif %}
 
<form method="post" action="{% url 'login' %}">
{% csrf_token %}
<table class="container">
<tr>
    <td>{{ form.username.label_tag }}</td>
    <td>{{ form.username }}</td>
</tr>
<tr>
    <td>{{ form.password.label_tag }}</td>
    <td>{{ form.password }}</td>
</tr>
</table>
 
<input type="submit" value="login" />
<input type="hidden" name="next" value="{{ next }}" />
</form>
{% endblock %}
```

これで、以下のようなテンプレートが作成されます。

<img width="1280" alt="スクリーンショット 2019-05-24 14.15.33.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214141/8b3a73ba-af0d-1257-958a-b63ef8a5c175.png">

ここまでで、先ほどつくったindex.htmlやresult.htmlは動くと思います。

ただ、ユーザーが登録されていないので、登録します。

#実際の作業3(管理者アカウントの作成と管理者ページ)

##admin.pyの編集
モデルをDjango管理画面で管理できるようにするには、アプリのディレクトリ内のadmin.pyを次のように編集します。

```attendance/admin.py
from django.contrib import admin
from .models import SubmitAttendance
# Register your models here.
admin.site.register(SubmitAttendance)
```

##管理者アカウントの作成
ターミナルで`python manage.py createsuperuser`と入力すると、管理者ユーザーの登録内容を求められるので好きな値を入力してください。

##管理者ページ
http://127.0.0.1:8000/admin
にアクセスして、先ほど入力したIDとパスワードを入力します。

管理者ページは次のような感じになっています。
![スクリーンショット 2019-05-24 14.25.01.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214141/17920996-ec72-daf8-af89-e45424181686.jpeg)

認証と認可のところのユーザーをクリックすると、ユーザーの登録・削除ができます。

#実際の作業4(デモ)
ここまでで作ったものを表示してみます。
http://127.0.0.1:8000
へアクセスし、先ほど登録したユーザーでログインしてみましょう。
以下のような画面が表示されるはずです。

<img width="451" alt="スクリーンショット 2019-05-24 14.42.24.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214141/c98d31c7-18f8-96ad-be5b-e8a95a254f29.png">

出勤場所と、IN/OUTを選択してSubmitすると...

<img width="474" alt="スクリーンショット 2019-05-24 14.42.43.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214141/b4c12a59-8f9c-5371-036d-0a709c930d67.png">

こんな感じになります！（hogehogeのところには設定した店舗名が表示されます。）

##管理者ページで見てみる(5/28編集)

先ほどの管理者ページ内にあったSubmitAttendanceをクリックすると、登録されたオブジェクトが見れます。

![スクリーンショット 2019-05-24 14.25.06.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214141/449bc934-f6d4-16c7-fd4a-419b69dacd53.jpeg)

index.htmlで値が入力されて生成されたオブジェクトもしっかりあります。
１つクリックしてみると...

![スクリーンショット 2019-05-24 14.25.10.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214141/5b5b40f6-61ca-c6cd-25b3-63cd162ed650.jpeg)

はい、こんな感じです。

これではみにくいので、同時生成されるデータベースファイルdb.sqlite3をのぞいてみます。

<img width="506" alt="スクリーンショット 2019-05-24 14.37.30.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214141/cea9854a-bd43-5b7f-a3b1-83aef649d715.png">

見やすいとは言えないですが、何がどこにあるかはわかります。

###models.pyの追加編集(5/28)
以下の項目を追加することで、管理者画面のオブジェクト一覧でそれぞれのオブジェクトに<SubmitAttendance>と表示されていてわかりにくかったものが、<ユーザー名 : 出勤場所名 IN/OUT>と表示され、わかりやすくなります。

```attendance/models.py
#辞書化
    place_dict = dict(PLACES)
    in_out_dict = dict(IN_OUT)

    def __str__(self):
        return  str(User.objects.get(id=self.staff_id)) + ' : ' + str(self.place_dict[self.place]) + ' ' + str(self.in_out_dict[self.in_out])
    
```
モデル編集後はターミナルで、makemigrationsとmigrateを実行します。

#最後に
長々とまとまりのない文章だったかもしれませんが、最後まで読んでいただいた方、ありがとうございます。

#これから
- 上にあるデータベースファイルから「ユーザー」ごとの「勤務時間」を計算し、お給料を計算するモデルを作成中です。←ここで詰まりました...
- 出勤場所からしかアクセスできないように、アクセス可能なIPアドレスを制限する。(不正打刻防止のために、これは必須)


#参考にしたWebサイトや本
未経験からやってみたのでおそらく累計30~50個(もっとかな？)ほどのwebサイトを参考にさせていただいたと思うのですが、流石に覚えていないので、よく拝見したものを掲載しておきます。

- [Django2 でユーザー認証（ログイン認証）を実装するチュートリアル -2- サインアップとログイン・ログアウト](https://it-engineer-lab.com/archives/544)
- https://docs.djangoproject.com/ja/2.2/intro/tutorial01/
- [Django Girls のチュートリアル
](https://tutorial.djangogirls.org/ja/)
- [Djangoを最速でマスターする part1
](https://qiita.com/gragragrao/items/373057783ba8856124f3)
- [Djangoにおけるクラスベース汎用ビューの入門と使い方サンプル](https://qiita.com/felyce/items/7d0187485cad4418c073)
- 『現場で使えるDjangoの教科書 基礎編』(横瀬 明仁 著)