Djangoで簡単なToDoタスク管理ツールを開発する

# Djangoで簡単なToDoタスク管理ツールを開発する

## この記事は何か

- 表題の通り、Djangoで簡単なToDoタスク管理ツールを開発します。技術構成は下記のとおりです

  - 言語、FW: Python, Django
  - 仮想環境: conda
  - DB: PostgreSQL
  - CSS: Bootstrap

## やっていく！

### conda で仮想環境を作成

- conda でまず env を作成します

```bash
conda create django-todo-app-env python=3.13.0
```

- 作成した env を activate します。ターミナルの先頭に`(django-todo-app-env)`と表示されればOK

```bash
conda activate django-todo-app-env
(django-todo-app-env) (base) xxxx:django_todo_app xxxx$ 
```

### プロジェクトの作成

- Djangoをインストール

```bash
pip install django
```

- プロジェクトを作成します。名前は`django_todo_app`とします

```bash
django-admin startproject django_todo_app
cd django_todo_app
```

- 一旦ローカルサーバを起動します。ブラウザで[ローカルホスト](http://127.0.0.1:8000/)を開いて下記のようにDjangoの初期画面が表示されればOK

```bash
python manage.py runserver
```

![django_start.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3811583/f2c6c7e7-305b-61db-ebd8-61dd7dad2af2.png)

### DBのセットアップ

- conda環境にPostgreSQLを使用するためのパッケージをインストール

```bash
conda install -c conda-forge psycopg2
```

- pg_ctlがインストールされていることを確認します

```bash
which pg_ctl
/Users/xxxx/anaconda3/envs/django-todo-app-env/bin/pg_ctl
```

- 次にDBクラスタを作成します

```bash
initdb -D /Users/xxxx/anaconda3/envs/django-todo-app-env/postgresql/data
```

- DBクラスタができたらDBを起動します

```bash
pg_ctl -D /Users/xxxx/anaconda3/envs/django-todo-app-env/postgresql/data -l logfile start
```
- statusコマンドでrunnningならOK

```bash
pg_ctl status
pg_ctl: server is running (PID: 50877)
```

- DBを作成します。DB名は`django_todo_app_db`、ユーザー名は`testuser`、パスワードは`testpassword`とします

```bash
psql postgres
CREATE DATABASE django_todo_app_db;
CREATE USER testuser WITH PASSWORD 'testpassword';
GRANT ALL PRIVILEGES ON DATABASE django_todo_app_db TO testuser;
```

- `\l`で作成したデータベースが表示されていればOK

```
postgres=# \l
                                                        List of databases
        Name        | Owner  | Encoding | Locale Provider |   Collate   |    Ctype    | Locale | ICU Rules |  Access privileges
--------------------+--------+----------+-----------------+-------------+-------------+--------+-----------+---------------------
 django_todo_app_db | xxxx | UTF8     | libc            | ja_JP.UTF-8 | ja_JP.UTF-8 |        |           | =Tc/xxxx         +
                    |        |          |                 |             |             |        |           | xxxx=CTc/xxxx  +
                    |        |          |                 |             |             |        |           | testuser=CTc/xxxx
```

- PostgreSQLコマンドは下記を参照してください

  - [PostgreSQL 9.4.5文書](https://www.postgresql.jp/docs/9.4/app-psql.html)

  - [PostgreSQLの基本的なコマンド](https://qiita.com/H-A-L/items/fe8cb0e0ee0041ff3ceb)

### DB接続のセットアップ

- 次にPostgreSQL接続するためにクライアントをインストールします

```bash
pip install psycopg
```

- Djangoが先ほど作成したDBに接続するための設定を行います。そのために`django-environ`をインストールしてDB情報を含めた秘匿情報を`.env`ファイルで管理するようにします

```bash
pip install django-environ
```

- `.env`ファイルを作成します

```bash
touch .env
```

- `.env`ファイルに環境変数を設定します

```
DEBUG=on
SECRET_KEY="xxxxx"
DATABASE_URL=psql://testuser:testpassword@127.0.0.1/django_todo_app_db
```
- `.env`の設定内容について
  - DEBUG: 開発環境ではonを設定します
  - SECRET_KEY: `django_todo_app_db/settings.py`にベタ打ちされている文字列をこちらに移動します
  - DATABASE_URL: `psql://${user_name}:${db_password}@${db_host}/${db_name}`のフォーマットになっています

- `settings.py`を`django-environ`の[docs](https://django-environ.readthedocs.io/en/latest/quickstart.html)を参考に更新します

```django_todo_app/settings.py
import os

import environ

env = environ.Env(DEBUG=(bool, False))

BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))

environ.Env.read_env(os.path.join(BASE_DIR, ".env"))

DEBUG = env("DEBUG")

SECRET_KEY = env("SECRET_KEY")

(中略)

DATABASES = {
    'default': env.db(),
}

(中略)

LANGUAGE_CODE = "ja"

TIME_ZONE = "Asia/Tokyo"

(中略)

STATIC_URL = "static/"

STATICFILES_DIRS = [
    os.path.join(BASE_DIR, "static"),
]

DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"

LOGIN_URL = "/login/"

LOGOUT_REDIRECT_URL = "/login/"

LOGIN_REDIRECT_URL = "/tasks/"
```

- `settings.py`の設定内容について
  - DEBUG: 開発環境かどうかを表します
  - SECRET_KEY: 
  - DATABASES: 
  - LANGUAGE_CODE: 
  - TIME_ZONE: 
  - STATIC_URL: 静的ファイルのurlを指します
  - STATICFILES_DIRS: 静的ファイルの配置パスを表します。今回はプロジェクトルートディレクトリの下に`static`ディレクトリを作ってそちらを配置パスにしています
  - LOGIN_URL: ログインを行うためのURL（詳しくは後述します）
  - LOGOUT_REDIRECT_URL: ログアウトした際にリダイレクトするURL（詳しくは後述します）
  - LOGIN_REDIRECT_URL: ログイン後に遷移するURL（詳しくは後述します）


### DBマイグレーション(1回目)

- ペンディングになっているマイグレーションがあるので作成したDBに適用します

```bash
python manage.py migrate

Operations to perform:
  Apply all migrations: admin, auth, contenttypes, sessions
Running migrations:
  Applying contenttypes.0001_initial... OK
  Applying auth.0001_initial... OK
  Applying admin.0001_initial... OK
  Applying admin.0002_logentry_remove_auto_add... OK
  Applying admin.0003_logentry_add_action_flag_choices... OK
  Applying contenttypes.0002_remove_content_type_name... OK
  Applying auth.0002_alter_permission_name_max_length... OK
  Applying auth.0003_alter_user_email_max_length... OK
  Applying auth.0004_alter_user_username_opts... OK
  Applying auth.0005_alter_user_last_login_null... OK
  Applying auth.0006_require_contenttypes_0002... OK
  Applying auth.0007_alter_validators_add_error_messages... OK
  Applying auth.0008_alter_user_username_max_length... OK
  Applying auth.0009_alter_user_last_name_max_length... OK
  Applying auth.0010_alter_group_name_max_length... OK
  Applying auth.0011_update_proxy_permissions... OK
  Applying auth.0012_alter_user_first_name_max_length... OK
  Applying sessions.0001_initial... OK
```

### ToDoアプリケーションの用意

- まずToDoアプリケーションを作成します

```bash
python manage.py startapp todo
```

- プロジェクトのurlpatternにToDoアプリケーションのurlを追記します

```django_todo_app/urls.py
urlpatterns = [
    path('admin/', admin.site.urls),
    path('', include('todo/urls')),
]
```

- ToDoアプリケーション内にとりあえず`urls.py`の空ファイルだけを作成しておきます

```bash
touch todo/urls.py
```

### モデルの作成

- `models.py`の実装を進めていきます。テーブル構造としてはTask情報を定義していきます

```todo/models.py
class Task(models.Model):
    STATUS = (
        (0, "未着手"),
        (1, "進行中"),
        (2, "完了"),
        (3, "保留"),
    )
    CATEGORY = (
        (0, "管理系"),
        (1, "バックエンド開発系"),
        (2, "フロントエンド開発系"),
        (3, "インフラ系"),
        (4, "モバイル系"),
    )
    id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
    title = models.CharField(max_length=255)
    description = models.TextField()
    category = models.IntegerField(choices=CATEGORY)
    user = models.ForeignKey(User, on_delete=models.CASCADE)
    status = models.IntegerField(choices=STATUS)
    due_date = models.DateTimeField()
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

    def __str__(self):
        return self.title

```

- モデル実装について
  - STATUS: こちらはTask情報に対してステータスの情報を持たせているのですが、ステータスの取りうる値を定義しています
  - Task: ToDoアプリのタスク情報になります。idはUUIDとして、作成日時のcreated_at, 更新日時のupdated_atを用意しています
  - CATEGORY: Task情報に関するカテゴリー情報の取りうる値を定義しています
  - `__str__`: モデルのインスタンスを出力した際に表示する文字列を規定しています


- モデル実装ができたので、マイグレーションファイルを作成します

```bash
python manage.py makemigrations
```

- マイグレーションファイルの内容がモデルで実装した通りになっているか(型情報やNULL制約、インデックス、外部キーの設定など)確認を行います

```todo/migrations/0001_initial.py
class Migration(migrations.Migration):

    initial = True

    dependencies = [
        migrations.swappable_dependency(settings.AUTH_USER_MODEL),
    ]

    operations = [
        migrations.CreateModel(
            name="Task",
            fields=[
                (
                    "id",
                    models.UUIDField(
                        default=uuid.uuid4,
                        editable=False,
                        primary_key=True,
                        serialize=False,
                    ),
                ),
                ("title", models.CharField(max_length=255)),
                ("description", models.TextField()),
                (
                    "category",
                    models.IntegerField(
                        choices=[
                            (0, "管理系"),
                            (1, "バックエンド開発系"),
                            (2, "フロントエンド開発系"),
                            (3, "インフラ系"),
                            (4, "モバイル系"),
                        ]
                    ),
                ),
                (
                    "status",
                    models.IntegerField(
                        choices=[(0, "未着手"), (1, "進行中"), (2, "完了"), (3, "保留")]
                    ),
                ),
                ("due_date", models.DateTimeField()),
                ("created_at", models.DateTimeField(auto_now_add=True)),
                ("updated_at", models.DateTimeField(auto_now=True)),
                (
                    "user",
                    models.ForeignKey(
                        on_delete=django.db.models.deletion.CASCADE,
                        to=settings.AUTH_USER_MODEL,
                    ),
                ),
            ],
        ),
    ]
```

- 問題なさそうならマイグレーションの適用を行います。`Applying todo.0001_initial... OK`と表示されればOK

```bash
python manage.py migrate
```

### URL設計とビューの作成

- ざっとモデルの実装が終わったので今度はURL設計を行います。下記のようにURLを設計しました

    | URL | 画面名 | ログイン認証 |
    | ---- | ---- | ---- |
    | signup/ | サインアップ | 不要 |
    | login/ | ログイン | 不要 |
    | logout/ | ログアウト | 不要 |
    | tasks/ | タスク一覧 | 必要 |
    | tasks/create | タスク登録 | 必要 |
    | tasks/<id>/update | タスク編集 | 必要 |
    | tasks/<id>/delete | タスク削除 | 必要 |

- 上記からToDoアプリケーションのurlpatternを定義します。次に各Viewを作成していきます

```todo/urls.py
urlpatterns = [
    path("login/", UserLoginView.as_view(), name="login"),
    path("logout/", LogoutView.as_view(), name="logout"),
    path("signup/", UserRegistrationView.as_view(), name="signup"),
    path("tasks/", TaskListView.as_view(), name="task-list"),
    path("tasks/create/", TaskCreateView.as_view(), name="task-create"),
    path("tasks/<uuid:pk>/update/", TaskUpdateView.as_view(), name="task-update"),
    path("tasks/<uuid:pk>/delete/", TaskDeleteView.as_view(), name="task-delete"),
]
```

### ビューの作成

- ビューを作成します。長いですがざっと解説していきます

```todo/views.py
class UserLoginView(LoginView):
    template_name = "todo/login.html"
    form_class = UserLoginForm
    success_url = reverse_lazy("task-list")


class UserRegistrationView(CreateView):
    template_name = "todo/signup.html"
    form_class = UserRegistrationForm
    success_url = reverse_lazy("login")

    def form_valid(self, form):
        user = form.save()
        login(self.request, user)
        return super().form_valid(form)


class TaskListView(LoginRequiredMixin, ListView):
    template_name = "todo/tasks/list.html"
    model = Task

    def get_queryset(self):
        return Task.objects.select_related("user")


class TaskCreateView(LoginRequiredMixin, CreateView):
    template_name = "todo/tasks/create.html"
    model = Task
    form_class = TaskCreateForm
    success_url = reverse_lazy("task-list")

    def form_valid(self, form):
        form.instance.user_id = self.request.user.id
        return super().form_valid(form)


class TaskUpdateView(LoginRequiredMixin, UpdateView):
    template_name = "todo/tasks/update.html"
    model = Task
    form_class = TaskUpdateForm
    success_url = reverse_lazy("task-list")


class TaskDeleteView(LoginRequiredMixin, DeleteView):
    template_name = "todo/tasks/delete.html"
    model = Task
    success_url = reverse_lazy("task-list")
```

- ビューについてざっと説明します
  - UserLoginView: ログイン処理のためのビューになります。後ほどクライアントから送信される情報を使って認証を行う`UserLoginForm`というフォームクラスを作成するのですが、そのフォームクラスやテンプレート、ログイン成功後のURLを設定しています
  - UserRegistrationView: ユーザー登録するためのビューです。こちらも`UserRegistrationForm`というユーザー登録を担当するフォームクラスを後ほど実装します。また、`form_valid`メソッドを上書きしています。こちらのメソッドはユーザーから送信されたユーザー登録情報が正しい情報であった際に行う処理を記載します。今回はDBに対象ユーザー情報を登録しています
  - TaskListView: タスク一覧画面のためのビューになります
  - TaskCreateView: タスクを登録する画面を提供するためのビューになります。`form_valid`メソッドを上書きしていて、フォーム情報が正しい場合`user_id`にログイン中のユーザーIDを設定するようにしています
  - TaskUpdateView: タスク更新のためのビューになります
  - TaskDeleteView: タスク削除のためのビューになります

### フォームの作成

- 次にforms.pyを作成します。フォームはユーザーが情報を入力したりその情報を送信した際に内部的にその情報を処理するためのクラスになります

```todo/forms.py
class UserLoginForm(AuthenticationForm):
    remember_me = forms.BooleanField(required=False, widget=forms.CheckboxInput())

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.fields["username"].widget.attrs.update(
            {"class": "form-control", "placeholder": "Username"}
        )
        self.fields["password"].widget.attrs.update(
            {"class": "form-control", "placeholder": "Password"}
        )


class UserRegistrationForm(UserCreationForm):
    email = forms.EmailField(required=True)

    class Meta:
        model = User
        fields = ("username", "password1", "password2", "email")

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.fields["email"].widget.attrs.update(
            {"class": "form-control", "placeholder": "Email"}
        )
        self.fields["username"].widget.attrs.update(
            {"class": "form-control", "placeholder": "Username"}
        )
        self.fields["password1"].widget.attrs.update(
            {"class": "form-control", "placeholder": "Password"}
        )
        self.fields["password2"].widget.attrs.update(
            {"class": "form-control", "placeholder": "Password"}
        )

    def save(self, commit=False):
        user = super().save(commit=False)
        validate_password(self.cleaned_data.get("password1"), user)
        user.set_password(self.cleaned_data.get("password1"))
        user.save()
        return user


class TaskUpdateForm(forms.ModelForm):
    class Meta:
        model = Task
        fields = ["title", "description", "category", "status", "due_date"]
        widgets = {
            "title": forms.TextInput(
                attrs={
                    "class": "form-control",
                    "placeholder": "タイトルを入力してください",
                }
            ),
            "description": forms.Textarea(
                attrs={"class": "form-control", "placeholder": "説明を入力してください"}
            ),
            "category": forms.Select(
                attrs={
                    "class": "form-control",
                    "placeholder": "カテゴリーを入力してください",
                }
            ),
            "status": forms.Select(
                attrs={
                    "class": "form-control",
                    "placeholder": "ステータスを入力してください",
                }
            ),
            "due_date": forms.DateTimeInput(
                attrs={"class": "form-control", "type": "datetime-local"}
            ),
        }


class TaskCreateForm(forms.ModelForm):
    class Meta:
        model = Task
        fields = ["title", "description", "category", "status", "due_date"]
        widgets = {
            "title": forms.TextInput(
                attrs={
                    "class": "form-control",
                    "placeholder": "タイトルを入力してください",
                }
            ),
            "description": forms.Textarea(
                attrs={"class": "form-control", "placeholder": "説明を入力してください"}
            ),
            "category": forms.Select(
                attrs={
                    "class": "form-control",
                    "placeholder": "カテゴリーを入力してください",
                }
            ),
            "status": forms.Select(
                attrs={
                    "class": "form-control",
                    "placeholder": "ステータスを入力してください",
                }
            ),
            "due_date": forms.DateTimeInput(
                attrs={"class": "form-control", "type": "datetime-local"}
            ),
        }

```

- 各フォームクラスの解説

  - UserLoginForm: ログイン処理を担当するフォームです。`__init__`を上書きしてcssのデザインを調整したりplaceholderを設定しています。`AuthenticationForm`を継承しているのでusername, passwordを使用できますが、それに加えて`remember_me`を設定しています
  - UserRegistrationForm: ユーザー登録に使用するフォームです。username, password1, password2に加えて`email`を設定しています。UserLoginFormと同様にcssのデザインを調整したりplaceholderを設定しています。また、`save`メソッドを上書きしてパスワードの検証を行なっています
  - TaskUpdateForm: タスク更新を担当するフォームです
  - TaskCreateForm: タスク登録を担当するフォームです

### テンプレートの作成

- 次にテンプレートを作成します。まず色んなテンプレートから参照する共通テンプレートを作成します。プロジェクトルートに`templates`ディレクトリを作成してそちらを配置パスとします。今回はユーザー登録、ログイン画面用の共通テンプレート(`auth_base.html`)とログイン後のタスク管理のための共通テンプレート(`base.html`)の2つを用意します

```bash
mkdir templates
touch templates/auth_base.html
touch templates/base.html
```

- ログイン画面用の共通テンプレートから作成します。ポイントは下記の通りです

  - `load static`で静的ファイルをロード
  - BootstrapのCSSとJSファイルをCDNからインポート
  - `block content`で各ページ部分を呼び出す

```templates/auth_base.html
{% load static %}
<!DOCTYPE html>
<html lang="ja">
	<head>
    <title>DjangoTodoApp</title>
		<meta charset="UTF-8">
		<meta name="viewport" content="width=device-width, initial-scale=1.0">
		<meta http-equiv="X-UA-Compatible" content="ie=edge">
		<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-9ndCyUaIbzAi2FUVXJi0CjmCapSmO7SnpJef0486qhLnuZ2cdeRhO02iuK6FUUVM" crossorigin="anonymous">
		<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js" integrity="sha384-geWF76RCwLtnZ8qwWowPQNguL3RmwHVBC9FhGdlKrxdiJJigb/j/68SIy3Te4Bkz" crossorigin="anonymous"></script>
	</head>

	<body>
		<div class="container text-center m-4">
			<img class="mb-4" src="{% static 'images/animal_chara_computer_usagi.png' %}" alt="logo" width="100" height="100">
			{% block content %}{% endblock %}
			<p class="mt-5 mb-3 text-muted">© 2023-2024</p>
		</div>
	</body>
</html>
```

 - 次にログイン画面、ユーザー登録画面を作成します。最初に空のファイルを作成します

```bash
mkdir -p todo/templates/todo
touch todo/templates/todo/login.html
touch todo/templates/todo/signup.html
```

- ログイン画面、ユーザー登録画面のポイントは下記の通りです

  - `extends "auth_base.html"`で共通テンプレートを指定。`block content`内でページの中身の部分を実装
  - form要素で`method=post`を指定し、ログイン画面では`url 'login'`、ユーザー登録画面では`url 'signup'`を指定
  - `csrf_token`でCSRF対策
  - `form.errors`でフォームのエラー内容を表示
  - `form.username`などでフォームの入力エリアを表示

```todo/templates/todo/login.html
{% extends "auth_base.html" %}

{% block content %}
  <div>
    <h1 class="h3 mb-3 font-weight-normal">Please sign in</h1>
    <form class="w-50 p-3 m-auto" method="post" action="{% url 'login' %}">
      {% csrf_token %}
      {% if form.errors %}
        {{form.errors}}
      {% endif %}
      <div class="form-group mb-3">
        {{ form.username }}
        {{ form.username.errors }}
      </div>
      <div class="form-group mb-3">
          {{ form.password }}
          {{ form.password.errors }}
      </div>
      <button class="btn btn-lg btn-primary btn-block" type="submit">Sign in</button>
    </form>
  </div>
{% endblock content %}
```

```todo/templates/todo/signup.html
{% extends "auth_base.html" %}

{% block content %}
  <div>
    <h1 class="h3 mb-3 font-weight-normal">Please sign up</h1>
    <form class="w-50 p-3 m-auto" method="post" action="{% url 'signup' %}">
      {% csrf_token %}
      {% if form.errors %}
        {{form.errors}}
      {% endif %}
      <div class="form-group mb-3">
        {{ form.email }}
        {{ form.email.errors }}
      </div>
      <div class="form-group mb-3">
        {{ form.username }}
        {{ form.username.errors }}
      </div>
      <div class="form-group mb-3">
        {{ form.password1 }}
        {{ form.password1.errors }}
      </div>
      <div class="form-group mb-3">
        {{ form.password2 }}
        {{ form.password2.errors }}
      </div>
      <button class="btn btn-lg btn-primary btn-block" type="submit">Sign up</button>
    </form>
  </div>
{% endblock content %}

```

- ログイン画面、ユーザー登録画面は下記のようなイメージになります

![signin.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3811583/17b509bd-54cf-49ad-08f0-526b8c980d23.png)

![signup.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3811583/3e348a7e-ebd5-f88b-2d07-bcf3a462578f.png)


- 次にタスク管理ページ用の共通テンプレートを作成します

```templates/base.html
{% load static %}
<!DOCTYPE html>
<html lang="ja">
	<head>
    <title>DjangoTodoApp</title>
		<meta charset="UTF-8">
		<meta name="viewport" content="width=device-width, initial-scale=1.0">
		<meta http-equiv="X-UA-Compatible" content="ie=edge">
		<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-9ndCyUaIbzAi2FUVXJi0CjmCapSmO7SnpJef0486qhLnuZ2cdeRhO02iuK6FUUVM" crossorigin="anonymous">
		<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js" integrity="sha384-geWF76RCwLtnZ8qwWowPQNguL3RmwHVBC9FhGdlKrxdiJJigb/j/68SIy3Te4Bkz" crossorigin="anonymous"></script>
	</head>

	<body>
		<header class="p-3 mb-3 border-bottom">
			<div class="container">
				<div class="d-flex justify-content-between align-items-center justify-content-center">
					<a class="navbar-brand col-md-3 col-lg-2 me-0 px-3" href="{% url 'task-list' %}">DjangoTodoApp</a>
					<div class="dropdown text-end">
						<a href="#" class="d-block link-body-emphasis text-decoration-none dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
							<img src="{% static 'images/animal_usagi.png' %}" alt="mdo" width="32" height="32" class="rounded-circle">
						</a>
						<ul class="dropdown-menu text-small" style="position: absolute; inset: 0px 0px auto auto; margin: 0px; transform: translate(0px, 34px);" data-popper-placement="bottom-end">
							<li><a class="dropdown-item" href="#">Profile</a></li>
							<li><hr class="dropdown-divider"></li>
							<li>
								<form method="post" action="{% url 'logout' %}">
									{% csrf_token %}
									<button type="submit" class="nav-link px-3">Log out</button>
								</form>
							</li>
						</ul>
					</div>
				</div>
			</div>
		</header>
		<div class="container-fluid">
			{% block content %}{% endblock %}
		</div>
	</body>
</html>
```

- 次にタスク一覧、編集、登録画面を作成していきます

```todo/templates/todo/tasks/list.html
{% extends "base.html" %}

{% block content %}
<a class="btn btn-primary my-3" href="{% url 'task-create' %}" role="button">Create</a>
<div class="table-responsive">
  <table class="table table-striped table-sm">
    <thead>
      <tr>
        <th scope="col">title</th>
        <th scope="col">user</th>
        <th scope="col">category</th>
        <th scope="col">status</th>
        <th scope="col">due_date</th>
        <th scope="col">edit</th>
        <th scope="col">delete</th>
      </tr>
    </thead>
    <tbody>
      {% for task in object_list %}
        <tr>
          <td>{{ task.title }}</td>
          <td>{{ task.user.username }}</td>
          <td>{{ task.get_category_display }}</td>
          <td>{{ task.get_status_display }}</td>
          <td>{{ task.due_date|date:"Y/m/d" }}</td>
          <td><a class="btn btn-primary" href="{% url 'task-update' pk=task.id %}" role="button">Edit</a></td>
          <td>
            <form method="post" action="{% url 'task-delete' pk=task.id %}">
              {% csrf_token %}
              <button type="submit" class="btn btn-danger">Delete</button>
            </form>
          </td>
        </tr>
      {% endfor %}
    </tbody>
  </table>
</div>
{% endblock content %}

```

![list.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3811583/4a914429-43af-696f-02a9-e2c4c4ec3b7c.png)


```todo/templates/todo/tasks/update.html
{% extends "base.html" %}

{% block content %}
<form class="form-group" method="post">
  {% csrf_token %}
  {{ form.as_p }}
  <input type="submit" class="btn btn-primary" value="Update">
</form>
{% endblock content %}

```

![update.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3811583/ace23eff-52e8-1b4a-938c-fac2ea8fa3f3.png)


```todo/templates/todo/tasks/create.html
{% extends "base.html" %}

{% block content %}
<form class="form-group" method="post">
  {% csrf_token %}
  {{ form.as_p }}
  <input type="submit" class="btn btn-primary" value="Create">
</form>
{% endblock content %}

```
![create.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3811583/07492bab-88f8-cc01-8581-4b6d0dc4043a.png)