3
5

DjangoでTodoListを作った話

Last updated at Posted at 2024-05-24

作ったきっかけ

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
.containers/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
.containers/postgres/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
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の記述
requirements.txt
Django==4.1
psycopg2 

postgresqlを操作するためにpsycopg2が必要です

  • .envの記述
.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の編集

todolist/todolist/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の編集

todolist/todolist/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

todolist/todo/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

todolist/todo/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

todolist/todo/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

todolist/todo/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

todolist/todo/admin.py
from django.contrib import admin
from .models import Category,Task

admin.site.register(Category)
admin.site.register(Task)

6.templatesの記述

  • layout.html
todolist/todo/templates/todo/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
todolist/todo/templates/todo/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
todolist/todo/templates/todo/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
todolist/todo/templates/todo/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
todolist/todo/templates/todo/task_confirm.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

todolist/todolist/accounts/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

todolist/todolist/accounts/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/ を検索して

  • list画面
    スクリーンショット 2024-05-24 0.28.37.png

  • 詳細画面
    スクリーンショット 2024-05-24 13.57.04.png

  • 編集画面
    スクリーンショット 2024-05-24 13.57.33.png

  • 削除画面
    スクリーンショット 2024-05-24 13.57.39.png

  • ログアウト画面
    スクリーンショット 2024-05-24 13.57.47.png

  • ログイン画面
    スクリーンショット 2024-05-24 13.57.52.png

こんな感じになりました。

感想

今回はbootstrapとdjangoにある汎用ビューを使って簡単に作りました。

大変だった点としては、ログイン用の汎用ビューがあると聞いて、それを調べるのと、締切日の設定をカレンダーでやる方法を見つけるのに苦労しました。
また、herokuにデプロイしようと思ったのですが、うまくやり方がわからず、困ったのでまた勉強しようと思います。

反省点としては、かなりディレクトリ構成が複雑でtodolist/todolist/というパスになってしまったのがとても複雑だなと感じました。ディレクトリ構成がわかりにく問題は今後改善していきたいです。

後すごく冗長な記事になってしまったのでそれも改善したいです...

3
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
5