DjangoのUpdateViewの実装方法を理解する

こんにちは。
株式会社クラスアクト インフラストラクチャ事業部の大塚です。

前回、DjangoのDeleteView/CreateView周りの実装方法をToDo Webアプリを作成しながら確認しました。
今回は同じTodo webアプリに機能を追加していく形でUpdateViewを実装していきたいと思います。前回の記事は以下となりますので、併せて参考にしていただければと思います。

https://qiita.com/ohtsuka-shota/items/0e651df83ce5415628f8

https://qiita.com/ohtsuka-shota/items/9648ef6beb0ec4d0335a

# Github

今回のコードは以下で確認できます。
適宜ご覧頂ければ幸いです。

https://github.com/ohtsuka-shota/publicDjangoStudy/tree/main/updateTestPJ

# 環境構築
## 環境イメージ
updateTestPJという名前のプロジェクトを作成し、todoAppという名前のアプリケーションを作成しております。
ファイル単位で前回から増えたところはUpdateViewを実装するためのHTMLくらいかと思います。CSSに関しては今回は面倒だったのでCreateViewを実装しているHTML（ここではtodoCreate.html）で使用しているCSSを利用しています。コードベースではアプリ側のurls.pyやviews.pyに対して記載が増えています。
![django-ページ13.drawio (1).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3219385/684f7b6a-5f87-b5cf-03e4-37211c0f9176.png)


## 個人的に重要そうだと思う部分のメモ
### todoApp/urls.py
```todoApp/urls.py
from django.urls import path
from .views import todoListView, todoDetailView, todoCreateView, todoDeleteView, todoUpdateView

app_name = "todoApp"

urlpatterns = [
    path("list/", todoListView.as_view(), name="todoListPage"),
    path("detail/<int:pk>", todoDetailView.as_view(), name="todoDetailPage"),
    path("create/", todoCreateView.as_view(), name="todoCreatePage"),
    path("delete/<int:pk>", todoDeleteView.as_view(), name="todoDeletePage"),
    path("update/<int:pk>", todoUpdateView.as_view(), name="todoUpdatePage"),
]
```
- UpdateViewを実装する時もDetailViewやDeleteViewを実装させる時と同じように\<int:pk\>を指定してあげるといいようです。

### todoApp/views.py
```todoApp/views.py
# todoApp views.py

from django.shortcuts import render
from django.views.generic import ListView, DetailView, DeleteView, CreateView, UpdateView
from .models import todoTable
from django.urls import reverse_lazy

class todoListView(ListView):
    template_name = "todoList.html"
    model = todoTable

class todoDetailView(DetailView):
    template_name = "todoDetail.html"
    model = todoTable

class todoDeleteView(DeleteView):
    template_name = "todoDelete.html"
    model = todoTable
    success_url = reverse_lazy("todoApp:todoListPage")

class todoCreateView(CreateView):
    template_name = "todoCreate.html"
    model = todoTable
    fields = ("title", "memo", "priority", "deadline")
    success_url = reverse_lazy("todoApp:todoListPage")

class todoUpdateView(UpdateView):
    template_name = "todoUpdate.html"
    model = todoTable
    fields = ("title", "memo", "priority", "deadline")
    def get_success_url(self):
        return reverse_lazy("todoApp:todoDetailPage", kwargs={'pk': self.object.pk})
```
- urlsでも記載の通り、viewsでもDetailViewやCreateViewと同じような記載で問題ないようです。ただ、success_urlに関しては少し書き方を変えています。
今回、UpdateViewを実装しているページでToDoの内容を更新した後、DetailViewを実装しているページにリダイレクトしてupdateがされているかをすぐに確認できるようにしたいと思っていました。その場合単純に'success_url = reverse_lazy("todoApp:todoDetailPage")'とか'success_url = reverse_lazy("todoApp:todoDetailPage <int:pk>")'みたいなことをしてもはじかれてしまいます。
単純にToDo一覧に戻るような機構で問題ないのであれば、CreateViewへの記載の通り'success_url = reverse_lazy("todoApp:todoListPage")'と書けばいいかと思います。

### templates/todoDetail.html
```templates/todoDetail.html
{% load static %}

<!DOCTYPE html>
    <html lang="ja">
    
        <head>
            <meta charset="utf-8">
            <meta name="viewport", content="width=device-width">
            <title>{{ object.title }}</title>
            <link href="{% static 'todoStyleSheet/todoDetailStyle.css' %}" rel="stylesheet">
        </head>

        <body>
            <h1 class="title">{{ object.title }}</h1>
            <h3 class="sub-title">タスク詳細<h5 class="content">{{ object.memo }}</h5></h3>
            <h3 class="sub-title">タスク優先度<h5 class="content">{{ object.priority }}</h5></h3>
            <h3 class="sub-title">タスク完了期日<h5 class="content">{{ object.deadline }}</h5></h3>
            <div id="submit-form">
                <button type="button" class="btn" onclick="window.location.href='{% url 'todoApp:todoUpdatePage' pk=object.pk %}'">todo更新</button>
                <button type="button" class="btn" onclick="window.location.href='{% url 'todoApp:todoListPage' %}'">一覧に戻る</button>
            </div>
        </body>
    
    </html>
```
- ToDoの詳細を確認するDeitalViewを表示しているHTMLになりますが、このページからそのToDoの更新をかけるページに移動するためのコードとして'\<button type="button" class="btn" onclick="window.location.href='{% url 'todoApp:todoUpdatePage' pk=object.pk %}'"\>todo更新\</button\>'を追記しています。object.pkとするようで、今開いているToDoのpkを取得し、そのToDoの更新ページに遷移できるようです。


### templates/todoUpdate.html
```templates/todoUpdate.html
{% load static %}

<!DOCTYPE html>
<html lang="ja">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width">
    <title>todo更新</title>
    <link href="{% static 'todoStyleSheet/todoCreateStyle.css' %}" rel="stylesheet">
</head>

<body>
    <div>
        <div class="header-container">
            <h1 class="title">task更新</h1>
        </div>
        <form method="post" action="{% url 'todoApp:todoUpdatePage' pk=object.pk %}" class="form-group">
            {% csrf_token %}
            <div class="task-create-form">
                <h3>タスク名</h3>
                <input class="task-create-content" type="text" placeholder="Title"
                    name="{{ form.title.html_name }}" value="{{ object.title }}" autofocus="" autocapitalize="none" maxlength="100" required="">
            </div>
            <div class="task-create-form">
                <h3>タスク詳細</h3>
                <textarea class="task-create-content" name="{{ form.memo.html_name }}" autofocus="" autocapitalize="none" maxlength="250" required="">{{ object.memo }}</textarea>
            </div>
            <div class="task-create-form">
                <h3>タスク優先度</h3>
                <select class="task-create-content" name="{{ form.priority.html_name }}" required="">
                    {% for value, label in form.fields.priority.choices %}
                        <option value="{{ value }}" {% if value == object.priority %}selected{% endif %}>{{ label }}</option>
                    {% endfor %}
                </select>
            </div>
            <div class="task-create-form">
                <h3>タスク完了期日</h3>
                <input class="task-create-content" type="date" placeholder="Deadline"
                       name="{{ form.deadline.html_name }}" value="{{ object.deadline|date:'Y-m-d' }}" autofocus="" autocapitalize="none" maxlength="250" required="">
            </div>

            <div id="submit-form">
                <button id="task-create-button" type="submit">task更新</button>
                <button type="button" class="btn" onclick="window.location.href='{% url 'todoApp:todoListPage' %}'">リストに戻る</button>
            </div>
        </form>
    </div>
</body>

</html>

```
- もともと記入していたものをフォームに表示させるために、各フォームのvalue属性に対して{{ object.field_name }}を記載しています。

# 実行結果
まずListViewを実装しているページの出力結果です。各ToDoに対して更新ページに遷移するためのボタンを追加しています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3219385/c86835b6-a184-69a4-90c2-3c6ab1a24e87.png)
今回は以下のタスクを更新してみたいと思います。このページはDetailViewを実装しているページとなります。todo更新を押下します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3219385/b55fda89-32bb-a90a-1bca-687ca263f636.png)
DetailViewを実装していたページで見えていた内容が更新画面でも最初から表示されていますね。これはUpdateViewを実装しているhtmlのvalueにもろもろの設定を入れているからですね。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3219385/e2c6100d-33cc-8e95-d010-2601b5d2f173.png)
内容を以下の様に変えてみます。task更新を押下します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3219385/61cc44f3-fd68-f418-0f17-dcb3f8e6e3e1.png)
更新が上手くいくと、DetailViewを実装したページにリダイレクトされます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3219385/4cd81b2c-5bb3-886f-5488-6a75e5b86c2f.png)
ListViewを実装しているページでも更新されていることが確認できますね。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3219385/c76dd589-0ef3-297a-c885-519d2d3f9e2e.png)