作ったきっかけ
djangoでのwebアプリ開発をしたかったので、簡単に作れそうなtodolistwebアプリケーションを作りました。
この記事は開発日記のようなものなのでかなり乱雑です、参考になる部分は少ないと思います
ファイル構成
.
├── README.md
├── containers
│ ├── django
│ │ └── Dockerfile
│ └── postgres
│ └── Dockerfile
├── docker-compose.yml
├── requirements.txt
└── todolist
├── accounts
│ ├── __init__.py
│ ├── admin.py
│ ├── apps.py
│ ├── migrations
│ │ └── __init__.py
│ ├── models.py
│ ├── templates
│ │ └── registration
│ │ ├── layout.html
│ │ ├── login.html
│ │ ├── logout.html
│ │ └── signup.html
│ ├── tests.py
│ ├── urls.py
│ └── views.py
├── manage.py
├── todo
│ ├── __init__.py
│ ├── admin.py
│ ├── apps.py
│ ├── forms.py
│ ├── migrations
│ │ ├── 0001_initial.py
│ │ └── __init__.py
│ ├── models.py
│ ├── static
│ │ └── todo
│ │ └── style.css
│ ├── templates
│ │ └── todo
│ │ ├── layout.html
│ │ ├── task_confirm_delete.html
│ │ ├── task_detail.html
│ │ ├── task_form.html
│ │ └── task_list.html
│ ├── tests.py
│ ├── urls.py
│ └── views.py
└── todolist
├── __init__.py
├── asgi.py
├── settings.py
├── urls.py
└── wsgi.py
開発環境構築
dockercomposeでの環境構築
今回は、djangoとpostgresqlの環境をdockercomposeで構築しました。
- djangoの
Dockerfile
FROM python:3.9
ENV PYTHONUNBUFFERED=1
ENV PYTHONDONTWRITEBYTECODE=1
WORKDIR /code
COPY requirements.txt /code/
RUN pip install --upgrade pip
RUN pip install --no-cache-dir -r requirements.txt
COPY . /code/
PYTHONUNBUFFERED=1では、標準出力、標準エラーのバッファリングを行わないので、リアルタイムでのタイムログが見れます。なくてもいいですが、リアルタイムでエラーがわかるので念のため書きました。
PYTHONDONTWRITEBYTECODE=1では、pychachファイル(キャッシュファイル)を生成されなくなります。.gitignoreし忘れると面倒なので書きました
- postgresqlの
Dockerfile
FROM postgres:15.2
ARG DB_LANG=ja_JP
RUN localedef -i $DB_LANG -c -f UTF-8 -A /usr/share/locale/locale.alias $DB_LANG.UTF-8
ENV LANG $DB_LANG.utf8
postgresqlのver指定の後ろは、一応日本語対応させるために書きました。しなくても日本語対応しているらしいですが、不安なので書きました。
docker-compose.yml
version: '3'
services:
web:
container_name: app
build:
context: .
dockerfile: containers/django/Dockerfile
working_dir: /code/todolist/
volumes:
- .:/code
ports:
- "8000:8000"
command: python manage.py runserver 0.0.0.0:8000
depends_on:
- db
env_file:
- .env
db:
container_name: postgres
build:
context: .
dockerfile: containers/postgres/Dockerfile
environment:
- POSTGRES_DB=${DATABASE_NAME}
- POSTGRES_USER=${DATABASE_USER}
- POSTGRES_PASSWORD=${DATABASE_PASSWORD}
volumes:
- db_data:/var/lib/postgresql/data
ports:
- "5432:5432"
volumes:
db_data:
-
requirements.txt
の記述
Django==4.1
psycopg2
postgresqlを操作するためにpsycopg2が必要です
-
.env
の記述
SECRET_KEY= 'プロジェクトファイルに書いてあるsecretkeyを書きました'
DATABASE_NAME= tododb
DATABASE_USER= admin
DATABASE_PASSWORD= todops
DATABASE_HOST= db
DATABASE_PORT = 5432 #PostgreSQLなら
LANG = ja_JP.utf8
LC_CTYPE = ja_JP.utf8
勉強で作っただけなのでsecretkey書くか迷いました。
以下のコマンドでシークレットキーは生成できます
docker compose run web python manage.py shell
>>>from django.core.management.utils import get_random_secret_key
>>> print(get_random_secret_key())
プロジェクトファイルの設定
todolistというproject名で作成しました。
以下のコマンドで作成しました。
docker-compose run web django-admin startproject todolist .
1.setting.py
の編集
ALLOWED_HOSTS = [
'0.0.0.0',
'127.0.0.1',
'localhost'
]
INSTALLED_APPS = [
#configをつけるとカスタム設定でできるらしい
'accounts.apps.AccountsConfig',
'todo',
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
]
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [os.path.join(BASE_DIR,'templates'),
os.path.join(BASE_DIR, 'todolist', 'templates'), # 追加
os.path.join(BASE_DIR, 'todolist', 'todo', 'templates'),
],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]
DATABASES = {
'default': {
# 'ENGINE': 'django.db.backends.sqlite3',
# 'NAME': BASE_DIR / 'db.sqlite3',
'ENGINE': 'django.db.backends.postgresql',
'NAME': os.environ.get('DATABASE_NAME'),
'USER': os.environ.get('DATABASE_USER'),
'PASSWORD': os.environ.get('DATABASE_PASSWORD'),
'HOST': os.environ.get('DATABASE_HOST'),
'PORT': os.environ.get('DATABASE_PORT'),
}
}
#時間を日本に設定
LANGUAGE_CODE = 'ja'
TIME_ZONE = 'Asia/Tokyo'
2.urls.py
の編集
from django.contrib import admin
from django.urls import path,include
from django.views.generic import RedirectView
urlpatterns = [
path('admin/', admin.site.urls),
path('todo/',include('todo.urls')),
path('accounts/',include('accounts.urls')),
path('',RedirectView.as_view(url='todo/')),
]
アプリの作成
以下のコマンドでアプリを作成しました。
docker compose run web python manage.py startapp todo
1.urls.py
from django.urls import path
from . import views
app_name = 'todo'
urlpatterns = [
path('', views.ListView.as_view(), name='list'),
path('<int:pk>/', views.DetailView.as_view(), name='detail'),
path('create/',views.CreateView.as_view(),name='create'),
path('<int:pk>/update/',views.UpdateView.as_view(),name='update'),
path('<int:pk>/delete/',views.DeleteView.as_view(),name='delete'),
path('<int:pk>/complete/',views.complete_task,name='complete'),
path('<int:pk>/reverse/',views.reverse_task,name='reverse'),
]
2.views.py
from django.forms import DateInput
from django.http import HttpResponse
from django.shortcuts import render, redirect, get_object_or_404
from django.urls import reverse_lazy
from django.views import generic
from .models import Task
from .forms import TaskForm
#ログインしていないとviewを表示できないように
from django.contrib.auth.mixins import LoginRequiredMixin
def index(request):
return HttpResponse("Hello, world. You're at the polls index.")
class ListView(LoginRequiredMixin,generic.ListView):
model = Task
template_name = "todo/task_list.html"
#コンテキストを加工するメソッド
def get_context_data(self):
context = super().get_context_data()
user = self.request.user
#prioritykeyに対して降順に表示させるためにorder_by('pk')
context["completed_tasks"] = Task.objects.filter(author = user,completed=True).order_by('pk')
context["incompleted_tasks"] = Task.objects.filter(author = user,completed=False).order_by('pk')
return context
class DetailView(LoginRequiredMixin,generic.DetailView):
model = Task
fields = '__all__'
class CreateView(LoginRequiredMixin,generic.edit.CreateView):
template_name = 'todo/task_form.html'
form_class = TaskForm
class UpdateView(LoginRequiredMixin,generic.edit.UpdateView):
template_name = 'todo/task_form.html'
form_class = TaskForm
#UpdateView is missing a QuerySet. Define UpdateView.model, UpdateView.queryset, or override UpdateView.get_queryset().
#解決するためにget_querysetメソッドをオーバーライド
def get_queryset(self):
return Task.objects.all()
class DeleteView(LoginRequiredMixin,generic.edit.DeleteView):
template_name = 'todo/task_confirm_delete.html'
model = Task
#reverse_lazyはクラスベースビューで主に使う
#URLパターンが読み込まれていなくても使える
#必要になったら指定した先にリダイレクトする
success_url = reverse_lazy('todo:list')
def complete_task(request, pk):
task = get_object_or_404(Task,pk=pk)
task.completed = True
task.save()
return redirect('todo:list')
def reverse_task(request,pk):
task = get_object_or_404(Task,pk=pk)
task.completed = False
task.save()
return redirect('todo:list')
CreateViewが機能せず困ったことがありましたが、解決しました。
気になった方はこちらをご覧ください。
3.forms.py
from django import forms
from django.contrib.admin.widgets import AdminDateWidget
from .models import Task
class TaskForm(forms.ModelForm):
class Meta:
model = Task
fields = ['title', 'author', 'category', 'deadline', ]
#ウィジェットはフォームとかの部品の種類を指定する要素
#djangoのフォームウィジェットにHTML属性を付与する(attrs)
widgets = {
'title': forms.TextInput(attrs={'class': 'form-control'}),
'author': forms.Select(attrs={'class': 'form-control'}),
'category': forms.Select(attrs={'class': 'form-control'}), # カテゴリが選択肢の場合
'deadline': forms.DateTimeInput(attrs={'type': 'datetime-local', 'class': 'form-control'}),
}
4.models.py
from django.db import models
from django.urls import reverse
#カテゴリモデル
class Category(models.Model):
#カテゴリ名
name = models.CharField(max_length=255)
#カテゴリを設定した人
author = models.ForeignKey(
'auth.User',on_delete=models.CASCADE,
)
#作成日
created_at = models.DateTimeField(auto_now_add=True)
#更新日
updated_at = models.DateTimeField(auto_now=True)
#これ入れないと管理画面でcategoryobjectって名前になる
def __str__(self):
return self.name
class Task(models.Model):
#タスク名
title = models.CharField(max_length=255)
# タスクを設定した人
author = models.ForeignKey(
'auth.User',
on_delete=models.CASCADE,
)
#カテゴリ(外部キー参照)
category = models.ForeignKey(
Category, on_delete=models.PROTECT
)
#完了確認
completed = models.BooleanField(default=False)
#締切日
deadline = models.DateTimeField()
#タスク作成日
created_at = models.DateTimeField(auto_now_add=True)
#タスク完了日
updated_at = models.DateTimeField(auto_now=True)
def __str__(self):
return self.title
def get_absolute_url(self):
return reverse('todo:detail', kwargs={'pk': self.pk})
マイグレートは以下のコマンドでやりました。
docker compose run web puthon manage.py makemigrations
docker compose run web python manage.py migrate
5.admin.py
from django.contrib import admin
from .models import Category,Task
admin.site.register(Category)
admin.site.register(Task)
6.templates
の記述
layout.html
{% load static %}
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="utf-8" />
<title>TodoList</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
{# <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">#}
{# バージョン5.0.2だとユーザー名の位置がずれたので4.0.0で固定#}
<link rel='stylesheet' href='https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css' integrity='sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm' crossorigin='anonymous'>
<link rel="stylesheet" href="{% static 'todo/style.css' %}">
<style>
body {padding-top: 80px;}
</style>
</head>
<body>
<!-- ブレイクポイントはsmall -->
<nav class='navbar navbar-expand-sm navbar-dark bg-dark fixed-top'>
<a class='navbar-brand ' href='{% url "todo:list" %}'>TodoList</a>
<ul class='navbar-nav'>
{# ユーザーが認証されているかチェック#}
{% if user.is_authenticated %}
<li class='nav-time'>
<span class='navbar-text'>{{ user }} - </span>
</li>
<li class='nav-item'>
<a href='{% url "accounts:logout" %}' class='logout nav-link'>Logout</a>
</li>
{% else %}
<a href='{% url "accounts:login" %}' class='login nav-link'>Login</a>
{% endif %}
</ul>
</nav>
<div class="container">
{% block content%}
{% endblock %}
</div>
</body>
</html>
task_list.html
{% extends './layout.html' %}
{% block content %}
<div class="header ">
<h1>Task一覧</h1>
<a class="btn btn-primary" href="{% url 'todo:create' %}">作成</a>
</div>
<table class="table incompleted_table">
<thead class="thead-dark">
<tr>
<th class="th-custom">タスクNo</th>
<th class="th-custom">タスク名</th>
<th class="th-custom">操作</th>
</tr>
</thead>
<tbody>
{% for task in incompleted_tasks %}
{% if not task.completed %}
<tr>
<td>{{ task.pk }}</td>
<td>{{ task.title }}</td>
<td>
<a class="btn btn-info" href="{% url 'todo:detail' task.pk %}">詳細</a>
<a class='btn btn-secondary' href="{% url 'todo:update' task.pk %}">編集</a>
<a class='btn btn-danger' href="{% url 'todo:delete' task.pk %}">削除</a>
<a class='btn btn-success' href="{% url 'todo:complete' task.pk %}">完了</a>
</td>
</tr>
{% endif %}
{% endfor %}
</tbody>
</table>
<h1>完了済みタスク一覧</h1>
<table class="table completed_table">
<thead class="thead-dark">
<tr>
<th class="th-custom">タスクNo</th>
<th class="th-custom">タスク名</th>
<th class="th-custom">操作</th>
</tr>
</thead>
<tbody>
{% for task in completed_tasks %}
{% if task.completed %}
<tr>
<td>{{ task.pk }}</td>
<td>{{ task.title }}</td>
<td>
<a class="btn btn-info" href="{% url 'todo:detail' task.pk %}">詳細</a>
<a class='btn btn-secondary' href="{% url 'todo:update' task.pk %}">編集</a>
<a class='btn btn-danger' href="{% url 'todo:delete' task.pk %}">削除</a>
<a class='btn btn-secondary' href="{% url 'todo:reverse' task.pk %}">元に戻す</a>
</td>
</tr>
{% endif %}
{% endfor %}
</tbody>
</table>
{% endblock %}
task_form.html
{% extends './layout.html' %}
{% block content %}
<div>
<form method='post'>{% csrf_token %}
<div class="mb-3">
<label class="form-label">タスク名</label>
{{ form.title }}
</div>
<div class="mb-3">
<label class="form-label">作成者</label>
{{ form.author }}
</div>
<div class="mb-3">
<label class="form-label">カテゴリ</label>
{{ form.category }}
</div>
<div class="mb-3">
<label class="form-label">締切</label>
{{ form.deadline }}
</div>
{#既存のデータがあったら更新、なかったら作成表示#}
<div class="btn-container d-flex justify-content-between">
<a class="btn btn-secondary" href="JavaScript:history.back()">戻る</a>
<button type="submit" class="btn btn-primary">{{ object|yesno:'更新,作成' }}</button>
</div>
</form>
</div>
{% endblock %}
task_detail.html
{% extends './layout.html' %}
{% block content %}
<div class='card text-center'>
<div class="card-header bg-info text-white">
<h1>{{ task.title }}</h1>
</div>
<div class="card-body">
<p class="card-text">タスクを設定した人: {{ task.author }}</p>
<p class="card-text">カテゴリ: {{ task.category.name }}</p>
<p class="card-text">締切: {{ task.deadline }}</p>
</div>
</div>
<div class="d-flex justify-content-between">
<a class = ' btn btn-secondary' href='{% url "todo:list" %}'>一覧</a>
<a class = 'btn btn-primary' href="{% url 'todo:update' task.pk%}">編集</a>
</div>
{% endblock %}
task_comfirm_delete.html
{% extends './layout.html' %}
{% block content %}
<div class="card text-center">
<div class="card-header bg-danger text-white" >
<h1>タスクの削除</h1>
</div>
<div class="card-body">
<div class="card-text">
<p>{{ task.title }}を削除しますか?</p>
</div>
</div>
</div>
<div class="d-flex justify-content-between">
<div>
<a class="btn btn-secondary" href="JavaScript:history.back()">戻る</a>
</div>
<form method='post'>{% csrf_token %}
<button type='submit' class='submit delete btn btn-danger'>削除</button>
</form>
</div>
{% endblock %}
ログイン管理のアプリ作成
以下のコマンドで作成しました
docker compose run web python manage.py startapp.py accounts
1.urls.py
from django.urls import path
from .views import SignupView,CustomLoginView,CustomLogoutView
app_name = 'accounts'
urlpatterns = [
path('signup/', SignupView.as_view(),name='signup'),
path('login/',CustomLoginView.as_view(),name='login'),
path('logout/',CustomLogoutView.as_view(),name='logout'),
]
2.views.py
from django.shortcuts import render
from django.urls import reverse_lazy
from django.views import generic
from django.contrib.auth.forms import UserCreationForm,AuthenticationForm
from django.contrib.auth.views import LoginView as AuthLoginView,LogoutView as AuthLogoutView
class SignupView(generic.CreateView):
form_class = UserCreationForm
success_url = reverse_lazy('accounts:login')
template_name = 'registration/signup.html'
class CustomLoginView(AuthLoginView):
form_class = AuthenticationForm
template_name = 'registration/login.html'
#ログインできたら移るurlを指定
def get_success_url(self):
return reverse_lazy('todo:list')
class CustomLogoutView(AuthLogoutView):
template_name = 'registration/logout.html'
コンテナの起動
コンテナのイメージを作成、コンテナの起動を以下のコマンドで行います。
docker compose up --build
http://localhost:8000/ を検索して
こんな感じになりました。
感想
今回はbootstrapとdjangoにある汎用ビューを使って簡単に作りました。
大変だった点としては、ログイン用の汎用ビューがあると聞いて、それを調べるのと、締切日の設定をカレンダーでやる方法を見つけるのに苦労しました。
また、herokuにデプロイしようと思ったのですが、うまくやり方がわからず、困ったのでまた勉強しようと思います。
反省点としては、かなりディレクトリ構成が複雑でtodolist/todolist/
というパスになってしまったのがとても複雑だなと感じました。ディレクトリ構成がわかりにく問題は今後改善していきたいです。
後すごく冗長な記事になってしまったのでそれも改善したいです...