【Django】ユーザが作成したデータのみをDBから抽出・表示させる方法

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

Djangoの勉強の一環でtodo webアプリを今まで作成してきておりました。
個人的にはCRUD(Create・Read・Update・Delete)の実装の仕方について理解できたので非常に良かったと思っています。
今回はこのtodo webアプリの機能を拡張していきます。具体的にはログインしたユーザが登録したtodoのみしか表示されないようにしたいと思います。ログインしたユーザが別の人のtodoを見れないようにするということですね。

その機能を実装するために、以下の手順で現行の状態から更改していきたいと思います。

1. Modelの修正と反映できているかの確認
1. ログイン画面を作成する
1. CreateViewで作成するtodoにUser属性を追加する
1. 自分が作成したtodoしか表示させないようにする

今までのtodo Webアプリを作成した方法は以下をご覧ください。

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

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

https://qiita.com/ohtsuka-shota/items/168f2c9fd2cc6c0751d4

# Github

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

# ディレクトリ構造

![django-ページ17.drawio (1).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3219385/31202d92-b30c-d7ae-3a93-d94207228bd0.png)

# 環境構築
## Modelの修正と反映できているかの確認
アプリ内のディレクトリにあるmodels.pyを以下のように修正。
userという項目を追加しています。
```todoApp/models.py
from django.db import models
from django.contrib.auth.models import User

PRIORITY = (("High","high"), ("Normal","normal"), ("Low","low"))

class todoTable(models.Model
    user = models.ForeignKey(User, on_delete=models.CASCADE, default=1)
    title = models.CharField(max_length=100)
    memo = models.TextField()
    priority = models.CharField(
        max_length=50, 
        choices= PRIORITY
        )
    deadline = models.DateField()
    
    def __str__(self):
        return self.title
```
modelの変更をDBに反映します。
```
PS C:\Users\ohtsu\Documents\DjangoEnv\django-dev\userDataPJ> python manage.py makemigrations todoApp
Migrations for 'todoApp':
  todoApp\migrations\0004_todotable_user.py
    - Add field user to todotable

PS C:\Users\ohtsu\Documents\DjangoEnv\django-dev\userDataPJ> python manage.py migrate todoApp
Operations to perform:
  Apply all migrations: todoApp
Running migrations:
  Applying todoApp.0004_todotable_user... OK
```
runserverを実行して、管理画面にアクセスするとUserの項目が追加されていることが確認できました。問題なさそうですね。
![2023122401.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3219385/42a4d9d0-6c3f-550b-fcdb-b7b68761c6c8.png)
現在はadminユーザしか存在しないため、adminしか選択できないようになっています。
![2023122402.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3219385/79af0a02-6236-8f0f-0916-9572a001b9c1.png)
test1というユーザを作成してみます。ユーザ名とパスワードを入力して保存を押下します。
![2023122403.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3219385/987e964a-07ff-be77-6df4-49b79b4eda95.png)
完全に知りませんでしたが、ユーザを作成するとこの画面のように、管理画面で一般ユーザ/スタッフユーザ（この画面にログイン出来るけど権限は弱い）/スーパユーザとかの設定も出来るみたいですね。へー知らなかった。。。
![2023122404.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3219385/2101a5d6-9c05-4ff4-6343-1fbe8726d754.png)
改めてtodoの作成の画面に移動してみると、作成したtest1ユーザがtodoのユーザで選択できるようになりました。便利ですね。

![20231224014.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3219385/bac95b43-86f3-3ad2-cdd1-03600db83b4a.png)


## ログイン画面を作成する
過去に自分で実装したものを参考に作っていきます。

https://qiita.com/ohtsuka-shota/items/5f980e12e5d7132c604a

PJフォルダのurls.pyを以下のように修正。
http\://localhost:8000/auth/にアクセスしてきた場合、Djangoが用意しているlogin機構を使うようにしました。
```userDataPJ/urls.py
from django.contrib import admin
from django.urls import path, include
from .views import homeView

urlpatterns = [
    path("admin/", admin.site.urls),
    path("auth/", include("django.contrib.auth.urls"), name="login"),
    path("todo/", include("todoApp.urls")),
    path("", homeView.as_view(), name="homePage"),
]
```
次にtemplatesディレクトリ内にregistrationフォルダを作成。
そのフォルダに以下の内容のlogin.htmlファイルを作成しました。
```templates/registration/login.html
<form method="post">
    {% csrf_token %}
    {{ form.as_p }}   
    <button type="submit">ログイン</button>
```
次に、todoの一覧や詳細確認等を表示するページにアクセスするためにはloginをしていないといけないようにする機構を組み立てていきたいと思います。
todoAppのurls.pyを以下のように修正しました。
※今回は全部"login_required"でログインを必須としましたが、views側で処理をさせることも出来そうです。
```todoApp/urls.py
from django.urls import path
from .views import todoListView, todoDetailView, todoCreateView, todoDeleteView, todoUpdateView
from django.contrib.auth.decorators import login_required

app_name = "todoApp"

urlpatterns = [
    path("list/", login_required(todoListView.as_view()), name="todoListPage"),
    path("detail/<int:pk>", login_required(todoDetailView.as_view()), name="todoDetailPage"),
    path("create/", login_required(todoCreateView.as_view()), name="todoCreatePage"),
    path("delete/<int:pk>", login_required(todoDeleteView.as_view()), name="todoDeletePage"),
    path("update/<int:pk>", login_required(todoUpdateView.as_view()), name="todoUpdatePage"),
]
```
ログイン後に表示されるtodoリストを表示するHTMLを以下のように修正。
ログアウト用のボタンをやっつけで付けました。
```templates/todoList.html
{% load static %}

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

        <body>
            <div class="header-container">
                <h1 class="title">task一覧</h1>
            </div>
            <div class="btn-div"><button type="button" class="btn" onclick="window.location.href='{% url 'todoApp:todoCreatePage' %}'">todo作成</button></div>
            {% for todo in object_list %}
                <ul>
                <li class="todo-li"><div class="todo-title">{{ todo.title }}</div><br>
                    <div class="todo-flexbox">
                    <button type="button" class="btn" onclick="window.location.href='{% url 'todoApp:todoDetailPage' todo.pk %}'">todo確認</button>
                    <button type="button" class="btn" onclick="window.location.href='{% url 'todoApp:todoUpdatePage' todo.pk %}'">todo更新</button>
                    <button type="button" class="btnDelete" onclick="window.location.href='{% url 'todoApp:todoDeletePage' todo.pk %}'">todo削除</button>
                    </div>
                </li>
                </ul>
            {% endfor %}
            <div class="btn-div"><button type="button" class="btn" onclick="window.location.href='{% url 'homePage' %}'">ホーム画面に戻る</button></div>
            <div class="btn-div"><button type="button" class="btn" onclick="window.location.href='{% url 'logout' %}'">ログアウト</button></div>
        </body>

    </html>
```
さらにPJフォルダのsettings.pyの最後に以下の3行を追記しております。
それぞれ認証が求められるページにアクセスしたときにリダイレクトされるページ、ログインが成功した時にアクセスされるページ、ログアウト実行時にアクセスされるページとなります。それぞれurls.pyのnameで任意の名前とすることが出来ます。
```userDataPJ/settings.py
LOGIN_URL = "login"
LOGIN_REDIRECT_URL = "todoApp:todoListPage"
LOGOUT_REDIRECT_URL = "homePage"
```
runserverを実行します。Check Taskボタンを押下して、認証画面に遷移されるようになったかを確認していきたいと思います。今まではこのボタンを押下したらすぐにtodoリストが表示され、セキュリティガバガバガバナンス状態でした。
![2023122405.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3219385/e9e1a63e-72d2-587f-1dde-49e9bad12e30.png)
質素ではありますが、ログイン画面が表示されました。CSSも何も設定していないので仕方ないです。今回はadminユーザでログインを試みます。
![2023122406.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3219385/ae136735-5adb-e371-89b2-96b9e3e43185.png)
todoの一覧を確認することが出来ました。
![2023122407.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3219385/1bbab3b3-a931-5ae9-5f72-0928bac7bd0a.png)

## CreateViewで作成するtodoにUser属性を追加する
Modelの修正は完了していますが、ユーザがWebブラウザ経由でtodoを作成しても今のままではadminユーザが作成したものとして登録されてしまいます。
※models.pyのところで"default=1"としてしまっているため。
アプリのディレクトリにあるviews.pyを以下に修正します。この修正を加えることで、Webブラウザに表示されているtodoの作成ページ上ではユーザの入力を求められませんが、ログインユーザのIDをDjangoが自動で取得してDBに入力してくれるようになります。
```todoApp/views.py
# todoApp/views.py

from django.shortcuts import render, redirect
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")

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

    def get_success_url(self):
        return 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})
```
実際に試してみます。
test1ユーザでログインします。
![2023122408.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3219385/cf66d294-b625-30c3-b637-51e37e4be736.png)
以下のような内容のタスクを登録します。
![2023122409.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3219385/3ece4c57-2f78-da87-63ae-37b3a588e82a.png)
リストに表示されています。表面上は問題なく作成されていそうです。
![20231224010.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3219385/fb9ab8f8-8dde-fa98-a7af-a599ebd4a087.png)
管理画面でUserを見てみるとログインしていたtest1ユーザが表示されています。自動でIDを取れていそうですね。
![20231224011.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3219385/d1f292ff-a684-8a6b-c641-15e7ec8028c0.png)

## 自分が作成したtodoしか表示させないようにする
改めてアプリのディレクトリにあるviews.pyを修正します。
return todoTable.objects.filter(user=self.request.user)を追加することで、todoTableテーブルからログインしているユーザが作成したtodoだけを表示するようにしています。
```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
from django.contrib.auth.decorators import login_required
from django.utils.decorators import method_decorator

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

    def get_queryset(self):  
        return todoTable.objects.filter(user=self.request.user)

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")

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

    def get_success_url(self):
        return 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})
```
さらにtodoリストを表示するためのHTMLファイルを修正しました。
Pythonのforループを回す際の元々のリストを"user_todos"に変更してます。
```templates/todoList.html
{% load static %}

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

        <body>
            <div class="header-container">
                <h1 class="title">task一覧</h1>
            </div>
            <div class="btn-div"><button type="button" class="btn" onclick="window.location.href='{% url 'todoApp:todoCreatePage' %}'">todo作成</button></div>
            {% for todo in user_todos %}
                <ul>
                <li class="todo-li"><div class="todo-title">{{ todo.title }}</div><br>
                    <div class="todo-flexbox">
                    <button type="button" class="btn" onclick="window.location.href='{% url 'todoApp:todoDetailPage' todo.pk %}'">todo確認</button>
                    <button type="button" class="btn" onclick="window.location.href='{% url 'todoApp:todoUpdatePage' todo.pk %}'">todo更新</button>
                    <button type="button" class="btnDelete" onclick="window.location.href='{% url 'todoApp:todoDeletePage' todo.pk %}'">todo削除</button>
                    </div>
                </li>
                </ul>
            {% endfor %}
            <div class="btn-div"><button type="button" class="btn" onclick="window.location.href='{% url 'homePage' %}'">ホーム画面に戻る</button></div>
            <div class="btn-div"><button type="button" class="btn" onclick="window.location.href='{% url 'logout' %}'">ログアウト</button></div>
        </body>

    </html>
```
動作を確認します。
test1ユーザでログインします。
![2023122408.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3219385/cf66d294-b625-30c3-b637-51e37e4be736.png)
test1ユーザで何件かtodoを追加して以下のようにしました。
![20231224012.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3219385/cacd78a2-24f4-6619-8666-29963c7c8d92.png)
test1ユーザからログアウトして、adminユーザでログインをしてみます。
![2023122406.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3219385/ae136735-5adb-e371-89b2-96b9e3e43185.png)
todoの一覧を確認してみると、test1で追加したtodoが表示されていないことがわかります。それっぽいアプリになってきましたね（UIは除く）
![20231224013.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3219385/dfb0bafc-a36a-27ea-4a98-7c9bcdcc1429.png)