初心者開発者が4DaysでDjangoアプリを作成した話【Day1】

## はじめに
先日ある機会でWebアプリを短い期間内で作成する経験をしたので、記事にします。
このアウトプットで、今回の開発を復習し、今後の開発の参考にしたいです。
また、この記事で自分と同じようなアプリ開発初心者の参考にもなればと思います。
ここから4回の投稿に渡って、実際にしてきた開発を振り返っていきたいと思います。

## 作ったもの
勤怠管理ができるアプリを作りました。
アプリ名はChatGPTに考えてもらい、「WorkChronicle」となりました。
機能は簡易的で、勤務時間の打刻と日報の登録ができる仕様にしました。

#### WorkChronicleのリンク
```
https://django-render-bxbk.onrender.com/
```
※無料枠でデプロイしているので、サーバーがスリープモードに入っていると起動まで時間がかかる場合があります。

#### Gitリポジトリのリンク
```
https://github.com/coffee-jinn-2002/attendance
```
## 技術選定
<dl>
  <dt>Django</dt>
  <dd>大学の講義で一度触ったことがあるため、Djangoを使用</dd>
  <dt>render</dt>
  <dd>無料でデプロイできる環境があるため、使用</dd>
</dl>

## Day1でやったこと
- 設計
    - 要件定義
    - データベース設計
    - ワイヤーフレーム作成
- Djangoの基礎復習
- 開発スタート
    - 環境を作る
    - アカウント周りの実装

## 設計
### 要件定義
今回は4日間で実装できる機能とそうでない機能の観点で要件定義を考えていく。
- 機能（実装予定）
    - ユーザーサインアップ
        - 一般ユーザーと管理ユーザーの2種類生成
            - RBACのようなものを実装
    - 管理アカウントは企業の役員や人事のような勤怠管理者が使う想定
    - 管理アカウントから一般ユーザーを作成できるようにする
    - HOSTアカウント（superuser）から管理アカウントに権限を付与
    - 勤怠機能
        - 出勤ボタンや退勤ボタンで打刻できるようにして管理
        - 複数回の休憩も考慮する
    - 日報機能
        - 出勤すると日報のフォームが出てくる
        - 日報を書かないと退勤できないようにする
    - ダッシュボード機能
        - 管理者がユーザーの勤怠情報や日報を確認できる
- 機能（将来的実装観点）
    - ユーザーの削除
    - ユーザー名、パスワードの変更機能
    - 勤怠情報や日報の変更機能
    - 労務情報との紐付け
    - 外部APIとの連携

### データベース設計
![DB.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3368954/36ec78c6-8d05-2bc9-f8cd-49d480a10385.png)
以下のよう要素を考え上記のように設計
- Userのis_adminで管理者アカウントが判別
- Attendanceにstatusを管理できるようにしている
- AttendanceとBreakテーブルを分けることで、休憩を複数回できるようにしている

※上記設計段階のDBは不備があるので、今後修正を予定しています

### ワイヤーフレーム
![Wireframe.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3368954/c592b7a1-7d11-046d-f893-8b3940344539.png)

要件定義とデータベース設計から、ワイヤフレームを作成した
出来上がったアプリを見ていただければわかりますが、デザインはここまで仕上げられていません笑

## Djangoの基礎復習
大学の講義でさわりの部分は知ってるのですが、不安があるため改めて復習した
[Djangoチュートリアル](https://docs.djangoproject.com/ja/5.0/intro/tutorial01/)を用いて復習する
- [Djangoチュートリアル1](https://docs.djangoproject.com/ja/5.0/intro/tutorial01/)
- [Djangoチュートリアル2](https://docs.djangoproject.com/ja/5.0/intro/tutorial02/)
- [Djangoチュートリアル3](https://docs.djangoproject.com/ja/5.0/intro/tutorial03/)
- [Djangoチュートリアル4](https://docs.djangoproject.com/ja/5.0/intro/tutorial04/)

チュートリアルその4まで復習する。
その5以降は、テストになってくるので割愛しました。

## 開発スタート
### 環境を作る
まず、[チュートリアル1](https://docs.djangoproject.com/ja/5.0/intro/tutorial01/)を参考に進める

仮想環境を適当に作る（自分の場合はvenvとした）

<b>プロジェクトを作成する</b>
```
django-admin startproject app
```
<b>アプリケーションをつくる</b>
```
python manage.py startapp attendance_app
```

<b>開発用サーバーと立てる</b>
ここでロケットが飛べばひとまずクリア
```
python manage.py runserver
```
### アカウント周りの実装
アプリケーションを使えるように、urls.pyを変更する
``` app/urls.py
from django.contrib import admin
from django.urls import include, path

urlpatterns = [
    # path("attendance_app/", include("attendance_app.urls")),
    path("admin/", admin.site.urls),
    path('', include("attendance_app.urls"))
]
```
アカウント周りに必要なDBもmodels.pyに記述する
``` attendance_app/models.py
from django.db import models
from django.contrib.auth.models import (BaseUserManager, AbstractBaseUser, PermissionsMixin)
from django.utils.translation import gettext_lazy as _
from django.conf import settings
from django.utils import timezone

class UserManager(BaseUserManager):
    def create_user(self, username, email, password=None, organization_name=None, **extra_fields):
        if not email:
            raise ValueError('Users must have an email address')

        email = self.normalize_email(email)
        user = self.model(
            email=email,
            username=username,
            organization_name=organization_name,
            **extra_fields
        )
        user.set_password(password)
        user.save(using=self._db)
        return user

    def create_superuser(self, username, email, password=None, organization_name=None, **extra_fields):
        extra_fields.setdefault('is_admin', True)
        extra_fields.setdefault('is_staff', True)
        extra_fields.setdefault('is_superuser', True)
        if extra_fields.get('is_admin') is not True:
            raise ValueError('Superuser must have is_admin=True.')

        return self.create_user(username, email, password, organization_name, **extra_fields)

    def create_staff(self, username, email, password=None, organization_name=None, **extra_fields):
        extra_fields.setdefault('is_admin', False)
        extra_fields.setdefault('is_staff', False)
        return self.create_user(username, email, password, organization_name, **extra_fields)

class User(AbstractBaseUser, PermissionsMixin):
    user_id = models.AutoField(primary_key=True)
    username = models.CharField(max_length=50, unique=True)
    email = models.CharField(max_length=50, unique=True)
    organization_name = models.CharField(max_length=100, blank=True, null=True)
    is_admin = models.BooleanField(default=False)
    is_staff = models.BooleanField(default=False) 
    is_superuser = models.BooleanField(default=False) 
    date_joined = models.DateField(auto_now_add=True)

    objects = UserManager()

    USERNAME_FIELD = 'username'
    REQUIRED_FIELDS = ['email']

    def __str__(self):
        return self.username
```

modelに合わせたviews.py、form.pyを作成する。
そして、views.pyに合わせたurls.pyも作成する。
``` attendance_app/views.py
from django.shortcuts import render
from django.http import HttpResponse
from django.contrib.auth import login, authenticate
from django.views.generic import TemplateView, CreateView
from django.contrib.auth.views import LoginView as BaseLoginView,  LogoutView as BaseLogoutView
from django.urls import reverse_lazy
from .forms import SignUpForm, LoginFrom

class IndexView(TemplateView):
    """ ホームビュー """
    template_name = "attendance_app/index.html"


class SignupView(CreateView):
    """ ユーザー登録用ビュー """
    form_class = SignUpForm # 作成した登録用フォームを設定
    template_name = "attendance_app/signup.html" 
    success_url = reverse_lazy("attendance_app:index") # ユーザー作成後のリダイレクト先ページ

    def form_valid(self, form):
        # ユーザー作成後にそのままログイン状態にする設定
        response = super().form_valid(form)
        username = form.cleaned_data.get("username")
        password = form.cleaned_data.get("password1")
        user = authenticate(username=username, password=password)
        login(self.request, user)
        return response

# ログインビューを作成
class LoginView(BaseLoginView):
    form_class = LoginFrom
    template_name = "attendance_app/login.html"

# LogoutViewを追加
class LogoutView(BaseLogoutView):
    success_url = reverse_lazy("attendance_app:index")

```

``` attendance_app/form.py
from django.contrib.auth.forms import UserCreationForm, AuthenticationForm
from .models import User

class SignUpForm(UserCreationForm):
    class Meta:
        model = User
        fields = (
            "username",
            "email",
        )

# ログインフォームを追加
class LoginFrom(AuthenticationForm):
    class Meta:
        model = User

```


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

app_name = "attendance_app"

urlpatterns = [
    path("", views.IndexView.as_view(), name="index"),
    path('signup/', views.SignupView.as_view(), name="signup"),
    path('login/', views.LoginView.as_view(), name="login"),
    path('logout/', views.LogoutView.as_view(), name="logout"),
]
```

上記まで実装できれば、ログインのバック側は動くようになっている。
なので、次は適当にテンプレートを作成し、Web上で動かせるようにする。
- base.html
    - テンプレートの継承のベースにする
- index.html
    - トップページ
        - ログインボタンとサインインボタンを設置
- login.html
    - ログインができるページ
- singup.html
    - アカウントを作るページ
    
``` attendance_app/templates/attendance_app/base.html
<!DOCTYPE html>
<html lang="ja">

<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <!-- Bootstrap5のCDNを設定 -->
  <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet"
    integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">
  <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.bundle.min.js"
    integrity="sha384-MrcW6ZMFYlzcLA8Nl+NtUVF0sA7MsXsP1UyJoMp4YLEuNSfAP+JcXn/tWtIaxVXM"
    crossorigin="anonymous"></script>
  <title>attendance_app</title>
</head>

<body>
  <div class="container mx-auto text-center">
    {% block title %}
    {% endblock %}
    {% block content %}
    {% endblock %}
  </div>
</body>

</html>
```

``` attendance_app/templates/attendance_app/index.html
{% extends 'attendance_app/base.html' %}

{% block title %}
{% if user.is_authenticated %}
<div class="h1">MyPage</div>
{% else %}
<div class="h1">Main</div>
{% endif %}
{% endblock %}

{% block content %}
{% if user.is_authenticated %}
<div class="h2">Welcome {{ user.username }}</div>
<!-- ↓追加 -->
<!-- <a href="{% url 'attendance_app:logout' %}" class="btn btn-primary">Logout</a>  -->
<form action="{% url 'attendance_app:logout' %}" method="post">
    {% csrf_token %}
    <button type="submit" class="btn btn-primary">Logout</button>
</form>
{% else %}
<a href="{% url 'attendance_app:signup' %}" class="btn btn-primary">Signup</a>
<a href="{% url 'attendance_app:login' %}" class="btn btn-primary">Login</a>
{% endif %}
{% endblock %}
```

``` attendance_app/templates/attendance_app/login.html
{% extends 'attendance_app/base.html' %}
{% block title %}
<div class="h1">Login</div>
{% endblock %}
{% block content %}
<form method="post">
    {% csrf_token %}
    {{ form.non_field_errors }}
    {% for field in form %}
    {{ field.label }}
    {{ field }}
    {{ field.errors }}
    <br>
    {% endfor %}
    <div class="mt-3">
        <button type="submit" class="btn btn-primary">Login</button>
        <a href="{% url 'attendance_app:index' %}" class="btn btn-secondary">Back</a>
    </div>
</form>
{% endblock %}
```

``` attendance_app/templates/attendance_app/signup.html
{% extends 'attendance_app/base.html' %}
{% block title %}
<div class="h1">Create account</div>
{% endblock %}
{% block content %}
<div>
  <br>
  <form method="POST">
    {% csrf_token %}
    {{ form.non_field_errors }}
    {% for field in form %}
    {{ field.label }}
    {{ field }}
    {{ field.errors }}
    <br>
    {% endfor %}
    <div class="mt-3">
      <button type="submit" class="btn btn-primary">Create</button>
      <a href="{% url 'attendance_app:index' %}" class="btn btn-secondary">Back</a>
    </div>
  </form>
</div>
{% endblock %}
```
### ここまでの実装
上記までで、簡易的なログイン機能は動かせるようになった。
以下の記事を参考にして、少し自分の設計に合うように調整した。

https://qiita.com/ryo-keima/items/e6ce42bb4c11432ea829

## Day1振り返り
Day1ではアプリ開発の設計から簡易的なログイン機能までの実装を行いました。
開発の優先順位として、アカウント周りが揃っていないの、次に進まないためDay1でログイン機能を実装しました。
Day1は設計から始まり開発まで着手しました。
今振り返ってみると、もう少し開発のペースを上げられたのではないかと思いますが、アカウント周りの実装は調べながらやっていたので、時間がかかってしまいました。
参考にした記事を見て、Djangoのドキュメントを照らし合わせると理解はできました。
今回は軽くコードをさらってますが、後々アカウント周りのまとめ記事も自分の備忘録として残したいと思います。

この記事はちょくちょくコードを端折ってる場所があるので、後々追記して行く予定です。
今しばらくお待ちください。