ご質問・お見積り等お気軽にご相談ください
お問い合わせ

pythonのWebフレームワーク、Djangoにチャレンジ!–その5:DjangoでURLごとにWebページを見れるようにする方法–

pythonのWebフレームワーク、Djangoにチャレンジ!–その5:DjangoでURLごとにWebページを見れるようにする方法–

この度は、株式会社ウェブネーションをご訪問頂き、誠にありがとうございます。

今回は、巷で人気のプログラミング言語python、その中のWebフレームワーク、djangoを使ってWeb開発にチャレンジしてみましたので、その手順の一部を紹介いたします。

しかしながら、内容の規模が少々大きく見込まれるため、いくつかパートに分けて紹介いたします。今回はDjangoでURLごとにWebページを見れるようにする方法です。

最後までお読みいただけますと幸いです。

前提条件

下記4点の記事まで到達できていること

  1. pythonのWebフレームワーク、Djangoにチャレンジ!–その1:環境構築–
  2. pythonのWebフレームワーク、Djangoにチャレンジ!–その2:dockerを用いた環境構築やDBのマイグレーション–
  3. pythonのWebフレームワーク、Djangoにチャレンジ!–その3:管理者権限の作成とその自動化–
  4. pythonのWebフレームワーク、Djangoにチャレンジ!–その4:Djangoで自作のWebページを作る方法–

(参考)ディレクトリ

boards
├── .env
├── Dockerfile
├── Pipfile
├── Pipfile.lock
├── boards
│   ├── __init__.py
│   ├── admin.py
│   ├── apps.py
│   ├── management
│   │   ├── __init__.py
│   │   └── commands
│   │       ├── __init__.py
│   │       └── createcustomsuperuser.py
│   ├── migrations
│   │   ├── 0001_initial.py
│   │   └── __init__.py
│   ├── models.py
│   ├── templates
│   │   ├── common.html
│   │   ├── posts
│   │   │   ├── detail.html
│   │   │   └── list.html
│   │   └── profile
│   │       └── index.html
│   ├── tests.py
│   ├── urls.py
│   └── views
│       ├── __init__.py
│       ├── posts.py
│       └── profile.py
├── config
│   ├── __init__.py
│   ├── asgi.py
│   ├── settings.py
│   ├── urls.py
│   └── wsgi.py
├── docker-compose.yml
├── makefile
├── manage.py
└── requirements.txt

2回目のマイグレーション

本題に入る前に、その2で操作したマイグレーションを、makefileの追加変更や初期データの投入と共にもう一度行います。

makefileの変更

# ----------- 変更箇所(始) ------------
# マイグレーションが必要な時(テーブルの変更等)
new-migrate-%:
	python manage.py makemigrations [アプリ名、今回はboards]
	python manage.py migrate [アプリ名、今回はboards] ${@:new-migrate-%=%}

# 初期データの投入
load-%:
	python manage.py loaddata ${@:load-%=%}.json

# 該当のマイグレーションを適用しなかったことにする
fake-%:
	python manage.py migrate --fake [アプリ名、今回はboards] ${@:fake-%=%}
# ----------- 変更箇所(終) ------------

# 管理者権限の自動作成
create-admin:
	python manage.py createcustomsuperuser --username ${ADMIN_USER} --password ${ADMIN_PASSWORD} --email ${ADMIN_EMAIL}  --noinput

マイグレーション

今回のマイグレーションは、カラムの追加/変更とテーブルの追加を行います。

boards/models.py

from django.db import models

# Create your models here.
# ----------- 変更箇所(始) ------------
class user(models.Model):
    slug = models.SlugField(null=False, unique=True)
    name = models.CharField(null=False, max_length=50)
    age = models.PositiveSmallIntegerField(default=0)
    email = models.EmailField(null=False, max_length=200)
    address = models.CharField(max_length=200)
    introduction = models.CharField(max_length=200)
    created_at = models.DateTimeField(null=False, auto_now_add=True)

class message(models.Model):
    slug = models.SlugField(null=False, unique=True)
    title = models.CharField(max_length=50)
    body = models.CharField(max_length=200)
    created_at = models.DateTimeField(null=False, auto_now_add=True)
    user = models.ForeignKey(user, on_delete=models.CASCADE, default='', blank=True, null=True)
# ----------- 変更箇所(終) ------------

※注意
# 外部キーによる参照も行うため、必ず参照先(上の場合ではuser)のテーブルからモデルを定義すること
# 参照先テーブルにNOT NULL制約を加えたい場合、3つ目以降の引数(default='', blank=True, null=True)のような設定を加えないと、下記のエラーが発生する
# NotNullViolation: column "user_id" of relation contains "boards.message" null values

※user.ageについて、IntegerFieldではなくPositiveSmallIntegerFieldとした理由は、下記の記事を参考にして説明すると、年齢に負の値が入ることを想定していないため

※message.userのForeignKeyメソッドの第二引数on_deleteについて、こちらも下記の記事を参考にして説明すると、userが削除された場合はそのユーザから投稿したメッセージも削除させる必要があることから、models.CASCADEを選択。

boards/admin.py

from django.contrib import admin
# ----------- 変更箇所(始) ------------
from .models import message, user
# ----------- 変更箇所(終) ------------

# Register your models here.
admin.site.register(message)
# ----------- 変更箇所(始) ------------
admin.site.register(user)
# ----------- 変更箇所(終) ------------

コンテナboards-appでのDockerターミナル上でのコマンド操作

# 下のmakeコマンドを入力
make new-migrate-0002
python manage.py makemigrations boards

# 下記2点は、いずれもカラム名の変更に関する質問を指す。今回のケースはいずれも実際に変更しているため、両方とも「y」を入力してEnter
Was message.post_at renamed to message.created_at (a DateTimeField)? [y/N] y
Was message.message_title renamed to message.title (a CharField)? [y/N] y

# 訳: データベース内の既存のレコード(下ならばmessage.slugのフィールド)に対して、何らかのデータ追加処理を行わせる必要があることから、最初にNULL許容として追加したカラムを、初期値がないまま後からNOT NULLに変更することはできない。そのため、下記3点のいずれかの修正方法を選んで対処してほしい。
# 今回は、一旦スルーしてマイグレーションを行うため、それを示すフレーズである「2」を入力してEnter
It is impossible to change a nullable field 'slug' on message to non-nullable without providing a default. This is because the database needs something to populate existing rows.
Please select a fix:
 1) Provide a one-off default now (will be set on all existing rows with a null value for this column)
 2) Ignore for now. Existing rows that contain NULL values will have to be handled manually, for example with a RunPython or RunSQL operation.
 3) Quit and manually define a default value in models.py.
Select an option: 2

# マイグレーションファイル(0002_user_rename_post_at_message_created_at_and_more.py)の生成
Migrations for 'boards':
  boards/migrations/0002_user_rename_post_at_message_created_at_and_more.py
    - Create model user
    - Rename field post_at on message to created_at
    - Rename field message_title on message to title
    - Remove field user_name from message
    - Alter field slug on message
    - Add field user to message

# 下記のメッセージまで到達できたら、2回目のマイグレーションは成功
python manage.py migrate boards 0002
Operations to perform:
  Target specific migration: 0002_user_rename_post_at_message_created_at_and_more, from boards
Running migrations:
  Applying boards.0002_user_rename_post_at_message_created_at_and_more... OK

上の最後のメッセージ(「下記のメッセージまで到達…」という箇所)に到達した時点で、二つ目のテーブルuserがDBで定義されたことになります。各自こちらの操作でお確かめください。

マイグレーションがうまくいかない場合のうち、下記2点のいずれかに当てはまっていれば、提示された手順を試してみてください。

# 1. relation "[テーブル名]" already exists.でマイグレーションが反映できない場合
# DBコンテナ(boards-db)
# Postgresへ接続
psql -d [データベース名]
# 既存のテーブル「boards_message」をデータベースから削除
DROP TABLE boards_message;
# 下のメッセージが出力されたら成功
DROP TABLE

# Appコンテナ(boards-app)
make init-migrate
# 下記のメッセージまで到達できたら、上のコマンド(make new-migrate-0002)をもう一度試してみてください
python manage.py makemigrations boards
Migrations for 'boards':
  boards/migrations/0001_initial.py
    - Create model message
python manage.py migrate boards 0001
Operations to perform:
  Target specific migration: 0001_initial, from boards
Running migrations:
  Applying boards.0001_initial... OK
# 2. Running migrations: No migrations to apply.でマイグレーションが反映できない場合
make fake-0001 # このコマンドを入力する
python manage.py migrate --fake boards 0001
# 下記のメッセージまで到達できたら、上の操作(make new-migrate-0002)をもう一度試してみてください
Operations to perform:
  Target specific migration: 0001_initial, from boards
Running migrations:
  Rendering model states... DONE
  Unapplying boards.0002_user_rename_post_at_message_created_at_and_more... FAKED

コンテナboards-appにて、初期データの投入

最上端ディレクトリのboardsにfixturesフォルダを作成し、下二つのJSONファイルを用意します。

boards/fixtures/message-0002.json

 [
    {
        "model": "boards.message",
        "pk": 1,
        "fields": {
            "slug": "aaaa",
            "title": "djangoの記事について",
            "body": "初めてdjangoを触れます。",
            "created_at": "2024-05-03T07:48:06.990Z",
            "user": 1
        }
    },
    {
        "model": "boards.message",
        "pk": 2,
        "fields": {
            "slug": "bbbb",
            "title": "djangoの記事について",
            "body": "djangoを初めて半年経ちますが、まだ慣れませんね。",
            "created_at": "2024-05-03T07:48:35.223Z",
            "user": 1
        }
    },
    {
        "model": "boards.message",
        "pk": 3,
        "fields": {
            "slug": "cccc",
            "title": "djangoの記事について",
            "body": "djangoはやっぱり難しいですか?",
            "created_at": "2024-05-03T07:49:06.356Z",
            "user": 2
        }
    },
    {
        "model": "boards.message",
        "pk": 4,
        "fields": {
            "slug": "dddd",
            "title": "djangoの記事について",
            "body": "少なくとも簡単ではないですね。",
            "created_at": "2024-05-03T07:49:30.156Z",
            "user": 1
        }
    },
    {
        "model": "boards.message",
        "pk": 5,
        "fields": {
            "slug": "eeee",
            "title": "初めての投稿",
            "body": "初めて投稿します、よろしく。",
            "created_at": "2024-05-03T07:50:54.491Z",
            "user": 3
        }
    },
    {
        "model": "boards.message",
        "pk": 6,
        "fields": {
            "slug": "ffff",
            "title": "掲示板",
            "body": "この掲示板楽しそう。",
            "created_at": "2024-05-03T07:51:35.837Z",
            "user": 4
        }
    },
    {
        "model": "boards.message",
        "pk": 7,
        "fields": {
            "slug": "gggg",
            "title": "初めての投稿",
            "body": "愉快で何より",
            "created_at": "2024-05-03T07:50:54.491Z",
            "user": 3
        }
    }
]

boards/fixtures/user-0002.json

[
    {
        "model": "boards.user",
        "pk": 1,
        "fields": {
            "slug": "1111",
            "name": "田中太郎",
            "name_kana": "たなかたろう",
            "age": 32,
            "email": "abcdef@ghi.jp",
            "address": "〇〇県△△市□□町××-1234",
            "introduction": "こんにちは。",
            "created_at": "2024-05-03T07:48:06.990Z"
        }
    },
    {
        "model": "boards.user",
        "pk": 2,
        "fields": {
            "slug": "2222",
            "name": "伊藤花子",
            "name_kana": "いとうはなこ",
            "age": 34,
            "email": "jklmn@opqrs.jp",
            "address": "□□□□県△□△市□△□-〇〇-5678",
            "introduction": "花子です。",
            "created_at": "2024-05-03T07:48:35.223Z"
        }
    },
    {
        "model": "boards.user",
        "pk": 3,
        "fields": {
            "slug": "3333",
            "name": "豊橋陽太郎",
            "name_kana": "とよはしようたろう",
            "email": "tuvwx@yzyzyz.jp",
            "address": "△△△県〇×〇市□△□区××-2929",
            "introduction": "初めての掲示板です。",
            "created_at": "2024-05-03T07:49:06.356Z"
        }
    },
    {
        "model": "boards.user",
        "pk": 4,
        "fields": {
            "slug": "4444",
            "name": "太田由伸",
            "name_kana": "おおたよしのぶ",
            "age": 45,
            "introduction": "掲示板でのやり取りが楽しみです。",
            "created_at": "2024-05-03T07:49:30.156Z"
        }
    }
]

用意ができたら、コンテナboards-appのターミナルで下記2つのコマンドを入力してEnterします。

make load-user-0002 # まずはこのコマンドを入力
python manage.py loaddata user-0002.json
Installed 4 object(s) from 1 fixture(s)

make load-message-0002 # 続いてこのコマンドを入力
python manage.py loaddata message-0002.json
Installed 7 object(s) from 1 fixture(s)

# ※最初にmessage-0002を入力すると、下のエラーが出てしまうため、user-0002.jsonから入力すること。
django.db.utils.IntegrityError: Problem installing fixtures: insert or update on table "boards_message" violates foreign key constraint "boards_message_user_id_eae54b2a_fk_boards_user_id"
DETAIL:  Key (user_id)=(1) is not present in table "boards_user".

管理画面をログインし、下2点のURLが書かれたリンク先に行くと、userとmessageのデータが、提示された画像のように、それぞれ4つと7つ保存されたことが確認できます。

http://localhost:8000/admin/boards/user/

http://localhost:8000/admin/boards/message/

URLごとにWebページを表示

ここからは、先ほど生成したデータをWeb上に表示できるようにします。

URLの設定

まずは、URLの設定から始めます。

# boards/urls.py、アプリ内のURL
from django.urls import path
from boards.views.posts import list, detail
from boards.views.profile import index
from django.contrib import admin

urlpatterns = [
    path("posts/list/", list, name="list"),
    # ----------- 変更箇所(始) ------------
    path("posts/detail/<str:slug>/", detail, name="detail"),
    path("profile/<str:slug>/", index, name="profile"),
    # ----------- 変更箇所(終) ------------
    path('admin/', admin.site.urls),
]
  • <str:slug>:「<型:パラメータ値を設定するための変数>」を示す。例えば、URLに「http://localhost:8000/posts/detail/aaaa/」が入力された場合は、slug=”aaaa”としてパラメータが設定された状態で、PostDetailクラス内のメソッドを実行させる。

続いて、ViewとTemplateも変更します。

View

# boards/views/posts.py
from django.views.generic import TemplateView
from typing import Any
# ----------- 変更箇所(始) ------------
from boards.models import message
# ----------- 変更箇所(終) ------------

class PostsList(TemplateView):
    template_name = 'posts/list.html'

    def get_context_data(self, **kwargs: Any) -> dict[str, Any]:
        # ----------- 変更箇所(始) ------------
        result = message.objects.all()
        # ----------- 変更箇所(終) ------------
        context = super().get_context_data(**kwargs)
        context = {
            'title':'投稿一覧',
            'messages':result
        }
        return context

class PostDetail(TemplateView):
    template_name = 'posts/detail.html'

    def get_context_data(self, **kwargs: Any) -> dict[str, Any]:
        # ----------- 変更箇所(始) ------------
        result = message.objects.get(slug=kwargs['slug'])
        # ----------- 変更箇所(終) ------------
        context = super().get_context_data(**kwargs)
        context = {
            'title':'投稿詳細',
            'message':result
        }
        return context
 
list = PostsList.as_view()
detail = PostDetail.as_view()
# boards/views/profile.py
from django.views.generic import TemplateView
from typing import Any
# ----------- 変更箇所(始) ------------
from boards.models import user
# ----------- 変更箇所(終) ------------

class Profile(TemplateView):
    template_name = 'profile/index.html'

    def get_context_data(self, **kwargs: Any) -> dict[str, Any]:
        context = super().get_context_data(**kwargs)
        # ----------- 変更箇所(始) ------------
        result = user.objects.get(slug=kwargs['slug'])
        # ----------- 変更箇所(終) ------------
        context = {
            'title':'プロフィール',
            'profile': result,
        }
        return context

index = Profile.as_view()
  • [model].objects.all():該当のテーブルのデータをすべて取得。
  • [model].objects.get(variable=kwargs[‘variable’]):該当のテーブルのうち、variable=kwargs[‘variable’]を満たすデータを取得(※kwargs[‘variable’]は、URL内のvariableに対応したパラメータ値を示す)。

Template

<!-- boards/templates/posts/list.html -->
{% extends 'common.html' %}

{% block content %}
    <h1>{{title}}</h1>
    <hr>
    {% for message in messages %}
        {% comment %} ----------- 変更箇所(始) ------------ {% endcomment %}
        <div><p>{{message.title}}</p></div>
        <div><p><a href='{% url "detail" message.slug %}'>{{message.body}}</a></p></div>
        <div><p><a href='{% url "profile" message.user.slug %}'>{{message.user.name}}</a></p></div>
        <div><p>{{message.created_at}}</p></div>
        {% comment %}  ----------- 変更箇所(終) ------------ {% endcomment %}
        <hr>
    {% endfor %}
{% endblock %}
<!-- boards/templates/posts/detail.html -->
{% extends 'common.html' %}

{% block content %}
    <h1>{{title}}</h1>
    {% comment %} ----------- 変更箇所(始) ------------ {% endcomment %}
    <div><p>{{message.title}}</p></div>
    <div><p>{{message.body}}</p></div>
    <div><p><a href='{% url "profile" message.user.slug %}'>{{message.user.name}}</a></p></div>
    {% comment %}  ----------- 変更箇所(終) ------------ {% endcomment %}
{% endblock %}
<!-- boards/templates/profile/index.html(変更箇所なし) -->
{% extends 'common.html' %}

{% block content %}
    <h1>{{title}}</h1>
    <div>
        <p>氏名:{% with profile.name|add:'('|add:profile.name_kana|add:')' as profile_name %}
            {{ profile_name }}
        {% endwith %}</p>
    </div>
    <div><p>年齢:{{profile.age}}</p></div>
    <div><p>メールアドレス:{{profile.email}}</p></div>
    <div><p>住所:{{profile.address}}</p></div>
    <div><p>自己紹介:{{profile.introduction}}</p></div>
{% endblock %}

Webページの表示を確認

下3点のURLで一度画面を開いてみましょう(リンクをクリックして確認することもできます )。

それぞれ画像のような結果になれば成功です。

http://localhost:8000/posts/list/

http://localhost:8000/posts/detail/aaaa/

http://localhost:8000/profile/1111/

最後に

いかがでしたでしょうか?

URLごとにWebページが見れるように組み込めば、このURLがどの内容を示す画面なのかを判別できるようになると思います。

次回は、Djangoでログイン機能を作る方法について紹介いたします。

今回はここまでとさせていただきます。

最後までお読みいただき、誠にありがとうございます。

参考リンク

過去の記事

この記事を書いた人
通りすがりのエンジニアB
通りすがりのエンジニアB(匿名)です。