Djangoのデータベース内データをHTMLに出力する方法

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

前回Djangoでログイン機能を実装してみました。
以下がその記事となります。
今回はDjangoのModelsを使ってDBテーブルを作成したり、データをadmin画面から入力してみたり、admin管理画面からでなくてもデータが見えるようにHTMLに出力させてみようと思います。

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

# ディレクトリ・ファイル構成
最終的なデータ構造は以下となります。pollsというアプリを用意しています。
![django-ページ2.drawio (1).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3219385/6df4c19b-dc82-c7cd-c59d-5910935bb785.png)


# 参考サイト等
pollsアプリの作成や、それに紐づいたmodelの作成はDjango公式のtutorialから引っ張ってきております。
●Tutorial01
https://docs.djangoproject.com/ja/4.2/intro/tutorial01/
●Tutorial02
https://docs.djangoproject.com/ja/4.2/intro/tutorial02/

# 環境構築
## pollsアプリケーション作成～データベース反映と確認
manage.pyが見える場所で以下のコマンドを実行し、pollsという名前のアプリを作成します。
```
PS C:\Users\ohtsu\Documents\DjangoEnv\qiita\testPJ> python manage.py startapp polls
PS C:\Users\ohtsu\Documents\DjangoEnv\qiita\testPJ> ls
    ディレクトリ: C:\Users\ohtsu\Documents\DjangoEnv\qiita\testPJ
Mode                 LastWriteTime         Length Name
----                 -------------         ------ ----
d-----        2023/11/11     16:22                polls
d-----        2023/11/04     20:27                templates
d-----        2023/11/04     20:04                testPJ
-a----        2023/11/04     20:46         131072 db.sqlite3
-a----        2023/11/04     20:02            684 manage.py
```
作成したpollsアプリのディレクトリにurls.pyを作成します。
```
PS C:\Users\ohtsu\Documents\DjangoEnv\qiita\testPJ> cd .\polls\
PS C:\Users\ohtsu\Documents\DjangoEnv\qiita\testPJ\polls> New-Item urls.py   
```
次にプロジェクトディレクトリにあるsettings.pyに以下の変更を施します。
pollsというアプリをプロジェクト側に認識させているというイメージかと思います。
```testPJ/settings.py
INSTALLED_APPS = [
    "polls",　★追記
    "django.contrib.admin",
    "django.contrib.auth",
    "django.contrib.contenttypes",
    "django.contrib.sessions",
    "django.contrib.messages",
    "django.contrib.staticfiles",
]
```
アプリ側のmodels.pyに以下のように設定を入れます。ほぼ公式チュートリアルと同じです。
データベースにどのようなデータを入れるのか？ということを定義しているイメージですね。
```polls/models.py
# This is polls app models.py
from django.db import models
from django.utils import timezone
import datetime

class Question(models.Model):
    question_text = models.CharField(max_length=200)
    pub_date = models.DateTimeField("date published", default=timezone.now)

    def __str__(self):
        return self.question_text
    def was_published_recently(self):
        return self.pub_date >= timezone.now() - datetime.timedelta(days=1)

class Choice(models.Model):
    question = models.ForeignKey(Question, on_delete=models.CASCADE)
    choice_text = models.CharField(max_length=200)
    votes = models.IntegerField(default=0)

    def __str__(self):
        return self.choice_text
```
この定義をsqlite3データベースに反映させたいと思います。
makemigrationをした後sqlmigrateを実行。最後にmigrateをしてデータベースに反映します。
- makemigration：Djangoがデータベースの変更を検出して、それをマイグレーションファイルに記録するために使用される。
- sqlmigrate：指定されたマイグレーションファイルに含まれるSQLコードを表示するために使用する。
- migrate：実際にデータベースに反映するために使用する。

まずはmakemigrationsを実行。
2つの変更を検知していることがわかる。また、0001_initial.pyという名前のマイグレーションファイルが作成されていることもわかる。
```
PS C:\Users\ohtsu\Documents\DjangoEnv\qiita\testPJ> python manage.py makemigrations polls
Migrations for 'polls':
  polls\migrations\0001_initial.py
    - Create model Question
    - Create model Choice
```
次にsqlmigrateを実行していく。これがmigrateされる際に実際に使用されるSQLコマンドということになる。
```
PS C:\Users\ohtsu\Documents\DjangoEnv\qiita\testPJ> python manage.py sqlmigrate polls 0001
BEGIN;
--
-- Create model Question
--
CREATE TABLE "polls_question" ("id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "question_text" varchar(200) NOT NULL, "pub_date" datetime NOT NULL);
--
-- Create model Choice
--
CREATE TABLE "polls_choice" ("id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "choice_text" varchar(200) NOT NULL, "votes" integer NOT NULL, "question_id" bigint NOT NULL REFERENCES "polls_question" ("id") DEFERRABLE INITIALLY DEFERRED);
CREATE INDEX "polls_choice_question_id_c5b4b260" ON "polls_choice" ("question_id");
COMMIT;
```
最後にmigrateしていきます。
```
PS C:\Users\ohtsu\Documents\DjangoEnv\qiita\testPJ> python manage.py migrate polls
Operations to perform:
  Apply all migrations: polls
Running migrations:
  Applying polls.0001_initial... OK
```
この設定がされているかどうかはdb.sqlite3というファイルの中身を実際に見てみればわかります。
db.sqlite3ファイルを指定してsqlite3コマンド実行すると、その中身を確認することが出来ます。
```
PS C:\Users\ohtsu\Documents\DjangoEnv\qiita\testPJ> sqlite3 .\db.sqlite3
SQLite version 3.41.2 2023-03-22 11:56:21
Enter ".help" for usage hints.
sqlite> .tables
auth_group                  django_admin_log
auth_group_permissions      django_content_type       
auth_permission             django_migrations
auth_user                   django_session
auth_user_groups            polls_choice
auth_user_user_permissions  polls_question
```
さらに今データベースに反映したものをadmin管理画面でも確認できるようにしたいと思います。アプリ側のadmin.pyを以下のように修正します。
models.pyからQuestionを呼び出すという意味だと思っていただければいいのかなと思います。
```
from django.contrib import admin
from .models import Question

admin.site.register(Question)
```
admin管理画面に入ります。Questionsが見えればOKです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3219385/961de6eb-13d0-b9d2-348e-3db164bbfefa.png)
データを作成してみます。Questionsを押下すると以下のように画面のレイアウトが変わります。追加を押下します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3219385/22e130ac-3ffb-76cb-256b-61302d6049ff.png)
適当に書いて、右下の保存を押下します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3219385/10b41f38-0575-f675-ea05-888147638596.png)
こんな感じに表示されればOKです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3219385/1d87f063-fb48-d6e1-fd1e-45c0f7bd9cdb.png)

## データベースに格納されているデータをHTMLに出力する
では、本題のデータベースのデータをHTMLに出力する方法を確認してみます。
ついでにデータを出力するHTMLにアクセスするためには事前にログインをしておかないといけないというようにしてみたいと思います。
まず、プロジェクト側から設定を入れ込んでいきます。urls.pyを以下のように準備します。★が追加した部分となります。
http://"サーバのIPアドレス":8000/polls/
にアクセスがあった場合、pollsアプリ（≒ディレクトリ）上にあるurls.pyに処理を引き渡すということを示しています。
```testPJ/urls.py
# This is Project urls.py

from django.contrib import admin
from django.urls import path, include
from django.contrib.auth.decorators import login_required
from django.views.generic import TemplateView
from .views import homeView

index_view = TemplateView.as_view(template_name="index.html")

urlpatterns = [
    path('admin/', admin.site.urls),
    path("index/", login_required(index_view), name="index"),
    path("polls/", include("polls.urls")),★追加
    path("auth/", include("django.contrib.auth.urls"), name="login"),
    path('', homeView.as_view(), name="home"),
]
```
導線のイメージです。
![django-ページ2のコピー.drawio.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3219385/d82c1c13-eed0-e453-46d4-d00dcf82433f.png)
次にpollsアプリのurls.pyを以下のように準備しました。
まず、http://"サーバのIPアドレス":8000/pollsというURLにアクセスがあった場合、pollsHomeというクラスに処理を引き渡します。このクラスはアプリのディレクトリ上にあるviews.pyに記載しています。
一方で、http://"サーバのIPアドレス":8000/polls/questionlistというURLにアクセスがあった場合、ログイン認証を先に済ませる処理を組んでおります。ログインが完了すると、templates/polls/QuestionList.htmlをユーザに見せます。この辺りの導線イメージは後々。。。
```polls/urls.py
# This is polls app urls.py

from django.urls import path, include
from django.contrib.auth.decorators import login_required
from django.views.generic import TemplateView
from . import views
from .views import QuestionList, pollsHome

app_name = 'polls'
QuestionList_view = TemplateView.as_view()

urlpatterns = [
    path("questionlist/",  login_required(QuestionList.as_view()), name="QuestionList"),
    path("", pollsHome.as_view(), name="polls"),
]
```
アプリ側のviews.pyを以下のように準備します。
pollsHomeクラスが呼び出されると、templates/polls/polls_home.htmlをユーザに返答します。QuestionListクラスが呼び出されるとmodels.pyで定義・作成したQuestionとtemplates/polls/QuestionList.htmlを呼び出して処理をしてユーザに返します。なおListViewを継承している場合、このタイミングでobject_listリストにデータベース内のデータが格納されるようです。このobject_listはデフォルトで設定されているようですので、気にしたら負けです笑。
```polls/views.py
# This is polls app views.py

from django.shortcuts import render
from django.contrib.auth.mixins import LoginRequiredMixin
from django.views.generic import TemplateView, ListView
from .models import Question

# Create your views here.
class pollsHome(TemplateView):
    template_name = "polls/polls_home.html"

class QuestionList(LoginRequiredMixin, ListView):
    model = Question
    template_name = "polls/QuestionList.html"
```
次に、上記で呼び出されたHTMLファイル2つを用意します。
まずtemplates配下にpollsというディレクトリを用意します。
```
PS C:\Users\ohtsu\Documents\DjangoEnv\qiita\testPJ> cd .\templates\
PS C:\Users\ohtsu\Documents\DjangoEnv\qiita\testPJ\templates> mkdir polls
```
この配下にそれぞれ以下内容のファイルを用意します。
QuestionList.html側のforループはHTMLというよりはPythoの文法になります。
object_listに格納されたデータベース内のデータをすべて順番に取り出す処理をしています。
```templates/polls/polls_home.html
<h1>polls_home.html</h1>
<a href="{% url 'polls:QuestionList' %}">質問一覧</a>
```
```templates/polls/QuestionList.html
{% block main %}
    {{ user.username }}さん、こんにちは。
    <a href="{% url 'logout' %}">ログアウト</a>

    <div class="text-center mb-5">
        <h1 class="display-3 mb-5">QuestionList</h1>
        <p class="text-muted fs-3">Question</p>
    </div>
    
    <div>
        {% for i in object_list %}
        <ul>
            <li>{{ i.question_text }}</li>
        </ul>
        {% endfor %}
    </div>
{% endblock %}
```
またhome.htmlを以下のように修正します。
pollsアプリに対して（ユーザから見える）導線を張ったようなイメージですね。
追加部分の"polls:polls"という表記ですが、最初のpollsがアプリケーション側のurls.pyで指定している"app_name = 'polls'"の部分を指定。後ろのpollsが同じくアプリ側のurls.pyで指定している"path("", pollsHome.as_view(), name="polls"),"の部分を指定しています。
```templates/home.html
<h1>Hello devProject</h1>
<a href="{% url 'login' %}">ログイン</a>
<a href="{% url 'admin:index' %}">ADMIN</a>
<a href="{% url 'polls:polls' %}">polls</a>★追加部分
```
pollsアプリの導線を図に落としてみます。
まずhttp://"サーバのIPアドレス":8000/pollsというURLにアクセスしてきた場合の導線です。赤→オレンジ→黄色のフローとなります。
まずプロジェクト・アプリ双方にあるurls.pyでどのファイルをユーザに返せばいいかを確認。polls_home.htmlを返せばいいと判断が出来たため、それを返しているという感じだと思います。
![django-ページ4.drawio.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3219385/2ebe434e-e70c-0870-e688-7d6189f23605.png)
次にhttp://"サーバのIPアドレス":8000/polls/questionlistというURLにアクセスしてきた場合の導線イメージです。赤→オレンジ→黄色→青→ピンクのフローとなります。
※最も、おそらく青からピンクにかけては逆かもしれないとも思いますし、同時くらいなんじゃないかとも思っています。あくまでイメージということで。。。
urls.pyでどのファイルをユーザに返せばいいかを確認するまでは同じですが、ログインが求められているので、途中でregistration配下のlogin.htmlにアクセスし認証を行います。認証が通り、正規ユーザであることが確認出来たら対象のHTMLファイルとそのHTMLファイルの中身となるデータをデータベースから引っ張ってきてユーザに返答しているというイメージです。
![django-ページ5.drawio.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3219385/3272177e-c08f-550c-24fc-fe49ed61de7b.png)

## 試験
想定通りの処理となっているか確認してきます。
runserverを実施し、Webブラウジングできるようにしておきます。
admin管理画面にて、データベースに格納されているデータを増やしておきます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3219385/dc6c40e3-be08-ee2c-1685-c9341b63a6f7.png)
http://localhost:8000
にアクセスした結果が以下となります。ログインのリンク、ADMINのリンクは以前からありましたので無視しますが、pollsのリンクが新しくできていることがわかります。このリンクを踏んでみます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3219385/749cfdd5-5c8a-d60b-0f24-7f5f11540a06.png)
pollsアプリのホームが出力されていることがわかります。これも想定通りです。質問一覧を押下するとユーザとパスワードが求められ、認証が成功すると先ほどadmin管理画面で見たものがWebブラウジングできる想定です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3219385/ebe7375a-1eb4-6c48-14ec-1d881034e739.png)
想定通りユーザとパスワードが聞かれました。正しいものを入力し認証を通します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3219385/b9701878-6fec-b7ca-3069-82fc1c6d6e07.png)
admin管理画面で見えていたものが確認できました。想定通りですね！
表示されているURLも想定通りです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3219385/c9d525cd-7755-a08d-a311-07b18e795101.png)

今度はレイアウトとかをそれっぽくしてみたいな～・・・