hikaru’s diary

Django Engineer

【Django】出勤表ポートフォリオ

出勤表アプリを作っていきます。製作期間は3日ほど。
誤字脱字とかかなり多いと思うので先にgit貼っておきます。開発環境までのgitです。
https://github.com/hikaru1444/django_portfolio_work


インストール

pythonをインストール
Windows版Pythonのインストール: Python環境構築ガイド - python.jp

Djangoとbootstrap5をインストール

pip install Django
pip install django-bootstrap5
pip list  # 確認

サーバー起動まで

django-admin startproject work
cd work  # manage.pyがあるフォルダに移動
python manage.py runserver

ロケットが打ちあがったら成功。

python manage.py startapp app1

アプリ作成。

settings

コーディングしていきます。


settings.py

from pathlib import Path
import os
import sys
from datetime import datetime
import psycopg2.extras


ALLOWED_HOSTS = ['*']  # 修正


# MAIL
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
EMAIL_HOST = 'smtp.googlemail.com'
EMAIL_USE_SSL = True
EMAIL_PORT = 465
EMAIL_HOST_USER = os.environ.get('EMAIL_ADDRESS')
EMAIL_HOST_PASSWORD = os.environ.get('EMAIL_PASSWORD')

# INSTALLED_APPS
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    # 追加
    'app1.apps.App1Config',  # apps.pyのクラス
    'django_bootstrap5',
]

# MIDDLEWARE
MIDDLEWARE = [
    # 追加
    'django.middleware.common.BrokenLinkEmailsMiddleware',
    # 後で使う
    # 'app1.middleware.middleware.CustomAttrMiddleware',
]


# DATABASES 必要に応じて
DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.postgresql_psycopg2',
        'NAME': os.environ.get('DB_NAME'),
        'USER': os.environ.get('DB_USER'),
        'PASSWORD': os.environ.get('DB_PASSWORD'),
        'HOST': os.environ.get('DB_HOST'),
        'PORT': '',
        'TEST': {
            'MIRROR': "default",
        },
    }
}

# バックアップしたデータベース等を使用した時に警告が出る
connection = psycopg2.connect(
    database=os.environ.get('DB_NAME'),
    user=os.environ.get('DB_USER'),
    password=os.environ.get('DB_PASSWORD'),
    host=os.environ.get('DB_HOST')
)

if DATABASES['default']['NAME'] != 'postgres':
    print("データベースが間違っている可能性があります。", DATABASES['default']['NAME'])
if connection.get_dsn_parameters()['dbname'] != 'postgres':
    print("データベースが間違っている可能性があります。", connection.get_dsn_parameters()['dbname'])
if connection.get_dsn_parameters()['user'] != 'postgres':
    print("データベースが間違っている可能性があります。", connection.get_dsn_parameters()['user'])


LANGUAGE_CODE = 'ja'

TIME_ZONE = 'Asia/Tokyo'



本番環境で実行するにはipconfigでIPv4アドレスを調べて適当なポートを付けてサーバー起動してください。
色々設定していますが無くても大丈夫です。詳細についてはこのブログのどこかで説明していると思います。
settings.pyは_devなどを付けて環境ごとにファイルを分けるのが一般的だと思います。


app1

他のチュートリアルではHttpResponseを使って一旦表示をするっていうのをやっていますが、飛ばしてHTML作っていきます。
HTMLはログイン者、プログラム実行結果などを出力するbase.htmlを使用します。
あと、退勤時に時間の入力と備考を書くことを想定しています。

urls

work/urls.py

from django.contrib import admin
from django.urls import path, include
from django.conf import settings
from django.conf.urls.static import static
from app1.views import top


urlpatterns = [
    path('admin/', admin.site.urls),
    path('app1/', include('app1.urls')),
    path('',  top, name='top'),
]

urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)


app1/urls.py

from django.urls import path
from app1.views import top, work


urlpatterns = [
    path('top/', top, name='top'),
    path('work/', work, name='work'),
]
templates

app1にtemplatesを作成
templates/base.html

{% load static %}
{% load django_bootstrap5 %}
<html lang="ja">
<head>
    <meta charset="utf-8" name="viewport" content="width=1024">
    <title>{% block title %}Django Boards{% endblock %}</title>
    {% bootstrap_css %}
    {% bootstrap_javascript %}
    <link rel="stylesheet" type="text/css" href="{% static 'css/style.css' %}">
    <link rel="icon" href="{% static 'img/favicon.ico' %}">
</head>
<body>
<h1>
    H1
</h1>
{% if check != None %}
<h2>{{ check }}</h2>
{% else %}
<h2>ここにプログラムメッセージが表示されます</h2>
{% endif %}
<div class="content">
    {% block content %}
    {% endblock %}
</div>
</body>
</html>


templates/top.html

{% extends 'base.html' %}
{% load django_bootstrap5 %}
{% block title %}メインページ{% endblock %}
{% block content %}
<p>I am top. Please write.</p>
{% endblock %}


work.html

{% extends 'base.html' %}
{% load django_bootstrap5 %}
{% block title %}出勤表{% endblock %}
{% block content %}
<p>I am work. Please write.</p>
{% endblock %}

static,cssを設定
飛ばしても大丈夫です。

現段階で3つページにアクセスできるはずです。
http://127.0.0.1:8000/

http://127.0.0.1:8000/app1/top/

http://127.0.0.1:8000/app1/work/


ページの移動が面倒なのでヘッダーにリンクを張る
base.html

<body>
<h1>
    <a>ログイン:{{ user }}</a>
    <a href="{% url 'top' %}">top</a>
    <a href="{% url 'work' %}">work</a>
</h1>
{% if check != None %}
<h2>{{ check }}</h2>
{% else %}
<h2>ここにプログラムメッセージが表示されます</h2>
{% endif %}
<div class="content">
    {% block content %}
    {% endblock %}
</div>
</body>
models

命名とか型宣言とかすごく苦手です。参考にしないでください。

models.py

from django.db import models

# Create your models here.


class Works(models.Model):
    name = models.CharField('名前', max_length=100)
    date = models.DateField('日付')
    in_time = models.TimeField('出勤')
    out_time = models.TimeField('退勤')
    rest_time = models.TimeField('休憩')
    other = models.CharField('備考', max_length=1000, default='', blank=True)

    def __str__(self):
        return self.name

adminで確認したいのでユーザー作成します。
コマンドプロンプトでmanage.pyのあるディレクトリですること
パスワード8桁以下だと警告がでます。

python manage.py createsuperuser

admin.py

from django.contrib import admin
from app1.models import Works
# Register your models here.

admin.site.register(Works)

adminサイトにアクセス後、出勤表で使用するユーザーを作成。

ログイン機能

forms.py

from django import forms
from app1.models import Works
import datetime


class TopForm(forms.Form):
    name = forms.CharField(label='名前')
    password = forms.CharField(label='パスワード', widget=forms.PasswordInput)

views.py

def top(request):
    params = {'form': None}
    if request.method == 'POST':
        form = TopForm(request.POST)
        if 'login' in request.POST:
            name = request.POST['name']
            password = request.POST['password']
            user = authenticate(request, username=name, password=password)
            # 認証に成功したらuserに名前が入り、失敗したらuserはnoneになる
            if user is not None:
                login(request, user)
                params['check'] = "ログインしました!"
            else:
                params['check'] = "ユーザー名またはパスワードが間違っています。"
        elif 'logout' in request.POST:
            logout(request)
            params['check'] = "ログアウトしました!"
        print("POST")
        params['form'] = form
    else:
        params['form'] = TopForm()
    return render(request, 'top.html', params)

top.html

{% extends 'base.html' %}
{% load django_bootstrap5 %}
{% block title %}メインページ{% endblock %}
{% block content %}
<p>I am top. Please write.</p>
<form method="POST">
    {% csrf_token %}
    {{ form.as_p }}
    {% bootstrap_button "ログイン" name="login" button_type="submit" %}
    {% bootstrap_button "ログアウト" name="logout" button_type="submit" %}
</form>
{% endblock %}


出勤表

forms
forms.py

from django import forms
from app1.models import Works


class WorksForm(forms.Form):
    dt_now = datetime.datetime.now()
    name = forms.CharField(label='名前')
    date = forms.DateField(label='日付', widget=forms.DateInput(attrs={"type": "date"}))
    in_time = forms.TimeField(label='出勤時間',
                              widget=forms.TimeInput(attrs={"type": "time"}, format='%H:%M:%S'))
    out_time = forms.TimeField(label='退勤時間',
                               widget=forms.TimeInput(attrs={"type": "time"}, format='%H:%M:%S'))
    rest_time = forms.TimeField(label='休憩時間',
                                widget=forms.TimeInput(attrs={"type": "time"}, format='%H:%M:%S'))
    other = forms.CharField(label='備考', required=False)


work.html

{% extends 'base.html' %}
{% load django_bootstrap5 %}
{% block title %}出勤表{% endblock %}
{% block content %}
<p>I am work. Please write.</p>
<form method="POST">
    {% csrf_token %}
    {{ form.as_p }}
    {% bootstrap_button "表示" name="btn2" button_type="submit" %}
    {% bootstrap_button "入力" name="btn" button_type="submit" %}
</form>
{% endblock %}

カスタムテンプレート

出勤表を見やすくするために使用します。
work/project/templatetags/tags.py

from django import template

register = template.Library()


@register.filter
def modulo(num, val):
    return num % val

work.html

{% load tags %}
{% if total != None %}
<h2>{{ total|safe }}</h2>
{% else %}
<h2>ここにプログラムメッセージが表示されます</h2>
{% endif %}
        <table class="table">
            <tbody>
            <tr>
                <th>名前</th>
                <th>日付</th>
                <th>曜日</th>
                <th>出勤</th>
                <th>退勤</th>
                <th>休憩</th>
                <th>備考</th>
            </tr>
            </tbody>
            {% for i in table %}
    {% if forloop.counter|modulo:2 == 1 %}
        <tr class="table-primary">
    {% else %}
            <tr class="table-light">
        {% endif %}
            <td>{{ i.name }}</td>
            <td>{{ i.date|date:"n-d" }}</td>
            <td>{{ i.date|date:"l" }}</td>
            <td>{{ i.in_time }}</td>
            <td>{{ i.out_time }}</td>
            <td>{{ i.rest_time }}</td>
            <td>{{ i.other }}</td>
    </tr>
    {% endfor %}
        </table>

settings.py

INSTALLED_APPS = [
    # 追加
    'project',
]


views.py

def work(request):
    params = {'form': WorksForm()}
    initial_dict = dict(name=request.user, date=datetime.date.today(), in_time='09:00:00',
                        out_time=datetime.datetime.now().strftime('%H:%M:%S'), rest_time='01:00:00')
    if request.method == 'POST':
        form = WorksForm(request.POST)
        """
        送信を押して名前日付の条件で
        0件は追加1件は変更それ以外は削除
        最後にログイン情報と現在月でグラフを表示
        
        """
        if 'btn' in request.POST:
            w = Works.objects.filter(name=request.POST['name'], date=request.POST['date'])
            if w.count() == 0:  # 追加
                w1 = Works(name=request.POST['name'], date=request.POST['date'],
                           in_time=request.POST['in_time'], out_time=request.POST['out_time'],
                           rest_time=request.POST['out_time'], other=request.POST['other'])
                w1.save()
                params['check'] = "追加しました!"

            elif w.count() == 1:  # 変更
                w = Works.objects.get(name=request.POST['name'], date=request.POST['date'])
                w.in_time = request.POST['in_time']
                w.out_time = request.POST['out_time']
                w.rest_time = request.POST['rest_time']
                w.other = request.POST['other']
                w.save()
                params['check'] = "変更しました!"
            else:
                params['check'] = "エラーがあります。件数=" + str(w.count())
        # 表示する
        params['table'] = Works.objects.filter(
            name=request.POST['name'],
            date__year=request.POST['date'][:4],
            date__month=request.POST['date'][5:7]
        )
        params['form'] = form

        else:
        params['date'] = str(datetime.date.today())
        params['form'] = WorksForm(initial=initial_dict)

    # 表示する
    params['table'] = Works.objects.filter(
        name=request.user,
        date__year=params['date'][:4],
        date__month=params['date'][5:7]
    )
    params['test1'] = Works.objects.filter(
        name=request.user,
        date__year=params['date'][:4],
        date__month=params['date'][5:7]
    ).values_list('in_time', 'out_time', flat=False)
    time_sum = datetime.timedelta()
    datetime.datetime.strptime(datetime.time(9, 0, 0).strftime('%H:%M:%S'), '%H:%M:%S')
    for i in params['test1']:
        a = datetime.datetime.combine(datetime.date(2022, 1, 1), i[0])
        b = datetime.datetime.combine(datetime.date(2022, 1, 1), i[1])
        time_sum += b - a
    time_sum = time_sum / datetime.timedelta(hours=1)
    params['total'] = "名前:" + str(request.user) + "<br>出勤:" + str(round(time_sum, 1)) + "時間"
    return render(request, 'work.html', params)


以前はとりあえずrequest.POSTをparamsという辞書に入れていたのですが、どっちが良いのだろう...


git

あんまり詳しくないので下記のサイトを参考にしてください。
【Windows】Gitの環境構築をしよう! | プログラミングの入門なら基礎から学べるProgate[プロゲート]

gitignoreは.ideaに入っているんですけど私は分かりづらいので一つ上の階層に新しく作っています。


.gitignore

#コメント
*.pyc
*.log
.idea/

requirements.txtの出力

pip list  # 確認
pip freeze > requirements.txt
公開

AWSでやります。

EC2インスタンスを作成

windowsであればPuTTYを使用して接続
PuTTY を使用した Windows から Linux インスタンスへの接続 - Amazon Elastic Compute Cloud

設定
タイムゾーンとか言語などの初期設定をした後、pythonをインストール

本番環境用のsettings.pyを作成。
sqlite3のバージョンが古くて起動できなかったのでバージョンアップしました。
Django2.2で開発サーバー起動時にSQLite3のエラーが出た場合の対応 - Qiita
出来なかったのでPostgreSQLを使うことにしました。(pip install pysqlite3で出来たかも)

NginxとGunicornをインストールしてアクセス確認。
あとはドメイン変更してHTTPS化して完了。


以上、お疲れさまでした。