1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Last updated at Posted at 2025-01-14

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

この記事は何か

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

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

やっていく!

conda で仮想環境を作成

  • conda でまず env を作成します
conda create django-todo-app-env python=3.13.0
  • 作成した env を activate します。ターミナルの先頭に(django-todo-app-env)と表示されればOK
conda activate django-todo-app-env
(django-todo-app-env) (base) xxxx:django_todo_app xxxx$ 

プロジェクトの作成

  • Djangoをインストール
pip install django
  • プロジェクトを作成します。名前はdjango_todo_appとします
django-admin startproject django_todo_app
cd django_todo_app
  • 一旦ローカルサーバを起動します。ブラウザでローカルホストを開いて下記のようにDjangoの初期画面が表示されればOK
python manage.py runserver

django_start.png

DBのセットアップ

  • conda環境にPostgreSQLを使用するためのパッケージをインストール
conda install -c conda-forge psycopg2
  • pg_ctlがインストールされていることを確認します
which pg_ctl
/Users/xxxx/anaconda3/envs/django-todo-app-env/bin/pg_ctl
  • 次にDBクラスタを作成します
initdb -D /Users/xxxx/anaconda3/envs/django-todo-app-env/postgresql/data
  • DBクラスタができたらDBを起動します
pg_ctl -D /Users/xxxx/anaconda3/envs/django-todo-app-env/postgresql/data -l logfile start
  • statusコマンドでrunnningならOK
pg_ctl status
pg_ctl: server is running (PID: 50877)
  • DBを作成します。DB名はdjango_todo_app_db、ユーザー名はtestuser、パスワードはtestpasswordとします
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

DB接続のセットアップ

  • 次にPostgreSQL接続するためにクライアントをインストールします
pip install psycopg
  • Djangoが先ほど作成したDBに接続するための設定を行います。そのためにdjango-environをインストールしてDB情報を含めた秘匿情報を.envファイルで管理するようにします
pip install django-environ
  • .envファイルを作成します
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.pydjango-environdocsを参考に更新します

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に適用します
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アプリケーションを作成します
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の空ファイルだけを作成しておきます
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__: モデルのインスタンスを出力した際に表示する文字列を規定しています
  • モデル実装ができたので、マイグレーションファイルを作成します

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
python manage.py migrate

URL設計とビューの作成

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

    URL 画面名 ログイン認証
    signup/ サインアップ 不要
    login/ ログイン 不要
    logout/ ログアウト 不要
    tasks/ タスク一覧 必要
    tasks/create タスク登録 必要
    tasks//update タスク編集 必要
    tasks//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つを用意します
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>
  • 次にログイン画面、ユーザー登録画面を作成します。最初に空のファイルを作成します
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

signup.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

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

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

1
0
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
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?