pythonのWebフレームワーク、Djangoにチャレンジ!–その5:DjangoでURLごとにWebページを見れるようにする方法–
この度は、株式会社ウェブネーションをご訪問頂き、誠にありがとうございます。
今回は、巷で人気のプログラミング言語python、その中のWebフレームワーク、djangoを使ってWeb開発にチャレンジしてみましたので、その手順の一部を紹介いたします。
しかしながら、内容の規模が少々大きく見込まれるため、いくつかパートに分けて紹介いたします。今回はDjangoでURLごとにWebページを見れるようにする方法です。
最後までお読みいただけますと幸いです。
前提条件
下記4点の記事まで到達できていること
- pythonのWebフレームワーク、Djangoにチャレンジ!–その1:環境構築–
- pythonのWebフレームワーク、Djangoにチャレンジ!–その2:dockerを用いた環境構築やDBのマイグレーション–
- pythonのWebフレームワーク、Djangoにチャレンジ!–その3:管理者権限の作成とその自動化–
- 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とした理由は、下記の記事を参考にして説明すると、年齢に負の値が入ることを想定していないため
- [Django] モデルフィールド 設定テンプレート #Python – Qiita(2024/05/18閲覧)
※message.userのForeignKeyメソッドの第二引数on_deleteについて、こちらも下記の記事を参考にして説明すると、userが削除された場合はそのユーザから投稿したメッセージも削除させる必要があることから、models.CASCADEを選択。
- Django ForeignKeyのon_deleteキーについてまとめてみた(2024/05/18閲覧)
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でログイン機能を作る方法について紹介いたします。
今回はここまでとさせていただきます。
最後までお読みいただき、誠にありがとうございます。
参考リンク
- Djangoで開発中、データベースへ初期データを入力する【バックアップしたデータをloaddataコマンドでリストア】 – 自動化無しに生活無し(2024/05/18閲覧)
- python – django.db.utils.ProgrammingError: relation “app_bugs” already exists – Stack Overflow(2024/05/18閲覧)
- python – Django database migrations are gives no “No migrations to apply.” error – Stack Overflow(2024/05/18閲覧)
- クエリを作成する | Django ドキュメント | Django(2024/05/18閲覧)