はじめに
Djangoで簡単なTODOアプリを作ったので、セットアップから開発までの流れを記事にしました。
セットアップ
仮想環境の作成にuvを使うので、まずuvをインストールします。
pip install uv
uv init django_todo
cd django_todo
Djangoをインストールします。
uv add django
プロジェクトを作成します。
uv run django-admin startproject django_todo .
アプリを追加します。
uv run manage.py startapp todo
main.pyは不要なので削除します。
rm ./main.py
マイグレーション後に開発用サーバーを起動してhttp://localhost:8000/にアクセスします。
ページが表示されたら成功です。
uv run manage.py migrate
uv run manage.py runserver
Conventional Commitsを強制させる
コミット時にConvetional Commits形式のコメントを強制させます。
pre-commitをインストールします。
uv add pre-commit
.pre-commit-config.yamlを追加して以下を追記します。
default_install_hook_types:
  - pre-commit
  - commit-msg
repos:
  - repo: https://github.com/compilerla/conventional-pre-commit
    rev: v4.2.0
    hooks:
      - id: conventional-pre-commit
        stages: [commit-msg]
以下のコマンドを実行します。
uv run pre-commit install
管理者ユーザを追加
以下のコマンドを実行してadminアカウントを作成します。
uv run .\manage.py createsuperuser
ログイン、ログアウト機能を実装
最初にDjangoの標準機能を使ってログイン、ログアウトを実装します。
django_todo/urls.pyに以下を追加します。
from django.contrib import admin
from django.urls import path
+ from django.contrib.auth.views import LoginView, LogoutView
urlpatterns = [
    path("admin/", admin.site.urls),
+   path("login/", LoginView.as_view(), name="login"),
+   path("logout/", LogoutView.as_view(), name="logout"),
]
django_todo/settings.pyに設定を追加します。
INSTALLED_APPS = [
    "django.contrib.admin",
    "django.contrib.auth",
    "django.contrib.contenttypes",
    "django.contrib.sessions",
    "django.contrib.messages",
    "django.contrib.staticfiles",
+   "todo",
]
...
TEMPLATES = [
    {
        "BACKEND": "django.template.backends.django.DjangoTemplates",
+       "DIRS": [BASE_DIR / "templates"],
        "APP_DIRS": True,
        "OPTIONS": {
            "context_processors": [
                "django.template.context_processors.request",
                "django.contrib.auth.context_processors.auth",
                "django.contrib.messages.context_processors.messages",
            ],
        },
    },
]
...
+ LANGUAGE_CODE = "ja"
+ TIME_ZONE = "Asia/Tokyo"
...
+ LOGIN_URL = "login"
+ LOGIN_REDIRECT_URL = "index"
+ LOGOUT_REDIRECT_URL = "logout"
リポジトリ直下にtemplates/registrationフォルダを作成して以下のhtmlを追加します。
<!DOCTYPE html>
<html lang="jp">
    <head>
        <meta charset="UTF-8">
        <title>ログイン</title>
    </head>
    <body>
        <h2>ログイン</h2>
        <form method="post">
            {% csrf_token %}
            {{ form.as_p }}
            <button type="submit">ログイン</button>
        </form>
    </body>
</html>
<!DOCTYPE html>
<html lang="jp">
    <head>
        <meta charset="UTF-8">
        <title>ログアウト</title>
    </head>
    <body>
        <h2>ログアウトしました</h2>
        <a href="{% url 'login' %}">再ログイン</a>
    </body>
</html>
http://localhost:8000/login/を開いてadminアカウントを入力します。
認証が成功すると以下のエラーが表示されます。
これはログイン後に遷移するページが用意されていないためです。
ログイン後の画面を追加
templatesフォルダ内にtodoフォルダを作成して以下のhtmlファイルを追加します。
<!DOCTYPE html>
<html lang="jp">
    <head>
        <meta charset="UTF-8">
        <title>TODOアプリ</title>
    </head>
    <body>
        <h2>Hello, {{ user.username }}</h2>
        <form method="post" action="{% url 'logout' %}">
            {% csrf_token %}
            <button type="submit">ログアウト</button>
        </form>
    </body>
</html>
todo/views.pyにビューを追加します。
from django.contrib.auth.mixins import LoginRequiredMixin
from django.views.generic import TemplateView
class TaskListView(LoginRequiredMixin, TemplateView):
    template_name = "todo/index.html"
todo/urls.pyを作成して以下を追加します。
from django.urls import path
from todo.views import TaskListView
urlpatterns = [
    path("", TaskListView.as_view(), name="index"),
]
django_todo/urls.pyに以下を追加します。
urlpatterns = [
    path("admin/", admin.site.urls),
    path("login/", LoginView.as_view(), name="login"),
    path("logout/", LogoutView.as_view(), name="logout"),
+   path("", include("todo.urls")),
]
ログイン画面でadminアカウントを入力すると、以下のページにログインします。
ログアウトボタンを押すとログアウトされます。
テンプレートの分割
各htmlファイルでページ全体のhtmlタグを記述するのは冗長なので、テンプレート機能を使います。
<!DOCTYPE html>
<html lang="jp">
    <head>
        <meta charset="UTF-8">
        <title>{% block title %}{% endblock %}</title>
    </head>
    <body>
        {% block content %}
        {% endblock %}
    </body>
</html>
各ページはタイトルとボディの部分だけ記述するようにします。
{% extends "base.html" %}
{% block title %}ログイン{% endblock %}
{% block content %}
    <h2>ログイン</h2>
    <form method="post">
        {% csrf_token %}
        {{ form.as_p }}
        <button type="submit">ログイン</button>
    </form>
{% endblock %}
{% extends "base.html" %}
{% block title %}ログアウト{% endblock %}
{% block content %}
    <h2>ログアウトしました</h2>
    <a href="{% url 'login' %}">再ログイン</a>
{% endblock %}
{% extends "base.html" %}
{% block title %}TODOアプリ{% endblock %}
{% block content %}
    <h2>Hello, {{ user.username }}</h2>
    <form method="post" action="{% url 'logout' %}">
        {% csrf_token %}
        <button type="submit">ログアウト</button>
    </form>
{% endblock %}
サインアップ機能の実装
次はブラウザ上からアカウントを追加できるようにします。
todo/views.pyにサインアップ用のビューを追加します。
from django.views.generic import TemplateView, CreateView
from django.contrib.auth.forms import UserCreationForm
from django.urls import reverse_lazy
from django.contrib.auth import login
class SignupView(CreateView):
    template_name = "registration/signup.html"
    form_class = UserCreationForm
    success_url = reverse_lazy("index")
    def form_valid(self, form):
        response = super().form_valid(form)
        user = form.save()
        login(self.request, user)
        return response
todo/urls.pyにURLを追加します。
urlpatterns = [
    path("", TaskListView.as_view(), name="index"),
+   path("signup/", SignupView.as_view(), name="signup"),
]
templates/registrationにHTMLを追加します。
{% extends "base.html" %}
{% block title %}新規登録{% endblock %}
{% block content %}
    <h2>新規登録</h2>
    <form method="post">
        {% csrf_token %}
        {{ form.as_p }}
        <button type="submit">登録</button>
    </form>
    <a href="{% url 'login' %}">ログイン</a>
{% endblock %}
ログインページにリンクを追加します。
{% extends "base.html" %}
{% block title %}ログイン{% endblock %}
{% block content %}
    <h2>ログイン</h2>
    <form method="post">
        {% csrf_token %}
        {{ form.as_p }}
        <button type="submit">ログイン</button>
    </form>
+   <a href="{% url 'signup' %}">新規登録</a>
{% endblock %}
サインアップページでIDとパスワードを入力すると、アカウントが作成されてそのままログインできます。
タスクを追加する
タスクを管理するモデルを追加します。
ユーザー毎にタスクを管理するために、Userモデルを外部キーとして紐づけます。
from django.db import models
from django.contrib.auth.models import User
class Task(models.Model):
    user = models.ForeignKey(User, on_delete=models.CASCADE)
    title = models.CharField(max_length=200)
    completed = models.BooleanField(default=False)
    def __str__(self):
        return self.title
モデルを追加したのでマイグレーションを実行します。
uv run manage.py makemigrations
uv run manage.py migrate
Taskを管理サイトに登録します。
from django.contrib import admin
from .models import Task
admin.site.register(Task)
TaskListViewにタスクを取得する処理と、タスクを追加する処理を追加します。
class TaskListView(LoginRequiredMixin, TemplateView):
    template_name = "todo/index.html"
+   def get_context_data(self, **kwargs):
+       context = super().get_context_data(**kwargs)
+       context["tasks"] = Task.objects.filter(user=self.request.user)
+       return context
+   def post(self, request, *args, **kwargs):
+       title = request.POST.get("title")
+       if title:
+           Task.objects.create(user=request.user, title=title)
+       return redirect("index")
完了フラグを更新するビューとタスクを削除するビューも追加します。
class TaskToggleView(View):
    def post(self, request, *args, **kwargs):
        task = Task.objects.get(id=kwargs["id"])
        task.completed = not task.completed
        task.save()
        return redirect("index")
class TaskDeleteView(View):
    def post(self, request, *args, **kwargs):
        task = Task.objects.get(id=kwargs["id"])
        task.delete()
        return redirect("index")
todo/urls.pyにリンクを追加します。
urlpatterns = [
    path("", TaskListView.as_view(), name="index"),
    path("signup/", SignupView.as_view(), name="signup"),
+   path("toggle/<int:id>", TaskToggleView.as_view(), name="toggle"),
+   path("delete/<int:id>", TaskDeleteView.as_view(), name="delete"),
]
index.htmlを修正します。
{% block content %}
-   <h2>Hello, {{ user.username }}</h2>
+   <h2>{{ user.username }} のTODOリスト</h2>
+   <form method="post">
+       {% csrf_token %}
+       <input type="text" name="title" placeholder="新しいタスク" required>
+       <button type="submit">追加</button>
+   </form>
+   <ul>
+       {% for task in tasks %}
+       <li>
+           <span class="{% if task.completed %}completed{% endif %}">{{ task.title }}</span>
+           <form action="{% url 'toggle' task.id %}" style="display: inline;" method="post">
+               {% csrf_token %}
+               <button type="submit">✔</button>
+           </form>
+           <form action="{% url 'delete' task.id %}" style="display: inline;" method="post">
+               {% csrf_token %}
+               <button type="submit">削除</button>
+           </form>
+       </li>
+       {% endfor %}
+   </ul>
+   <hr />
    <form method="post" action="{% url 'logout' %}">
        {% csrf_token %}
        <button type="submit">ログアウト</button>
    </form>
{% endblock %}
タスクの追加、更新、削除ができるようになりました。
CSSを適用させる
TODOリストのページにCSSを適用させます。
django_todo/settings.pyに設定項目を追加します。
...
STATIC_URL = "static/"
+ STATICFILES_DIRS = [BASE_DIR / "static"]
...
リポジトリ直下にstatic/css作成して、以下のCSSを作成します。
span {
    display: inline-block;
    min-width: 300px;
}
.completed {
    text-decoration: line-through;
    color: gray;
}
templates/base.htmlでCSSを読み込むようにします。
+ {% load static %}
<!DOCTYPE html>
<html lang="jp">
    <head>
        <meta charset="UTF-8">
        <title>{% block title %}{% endblock %}</title>
+       <link rel="stylesheet" type="text/css" href="{% static 'css/style.css' %}">
    </head>
    <body>
        {% block content %}
        {% endblock %}
    </body>
</html>
「✔」ボタンを押すと打消し線が表示されるようになりました。
参考文献






