はじめに
今回、Djangoについてudemyで軽く学習をしたので
より理解を深めるためにDjangoの公式チュートリアルを
実施していきます。
環境
- Mac
- 仮想環境
- Python
- Django
- DB
自分のレベル
- HTML/CSSで簡単なWebサイト画面を作成できる
- Pythonで基本的なコードが書ける
- オブジェクト指向についてある程度理解してる
公式チュートリアルスタート
クイックインストールガイド
Pythonインストール確認
python3 --version
Python 3.13.6
Django_tutorial作成
mkdir Django_tutorial
python3 -m venv tutorial
source tutorial/bin/activate
cd Django_tutorial
◆はじめてのDjangoアプリ作成、その1
Pollアプリケーション構成
- ユーザが投票したり結果を表示したりできる公開用サイト
- 投票項目の追加、変更、削除を行うための管理 (admin) サイト
Djangoインストール
pip3 install "Django==4.2.*"
Collecting Django==4.2.*
Using cached django-4.2.24-py3-none-any.whl.metadata (4.2 kB)
Collecting asgiref<4,>=3.6.0 (from Django==4.2.*)
Using cached asgiref-3.9.2-py3-none-any.whl.metadata (9.3 kB)
Collecting sqlparse>=0.3.1 (from Django==4.2.*)
Using cached sqlparse-0.5.3-py3-none-any.whl.metadata (3.9 kB)
Using cached django-4.2.24-py3-none-any.whl (8.0 MB)
Using cached asgiref-3.9.2-py3-none-any.whl (23 kB)
Using cached sqlparse-0.5.3-py3-none-any.whl (44 kB)
Installing collected packages: sqlparse, asgiref, Django
Successfully installed Django-4.2.24 asgiref-3.9.2 sqlparse-0.5.3
インストール確認
python3 -m django --version
4.2.24
いったんDjangoフレームワークの仕組みを理解をする
プロジェクトを作成する
django-admin startproject mysite
実行すると次のような構造が作られる
mysite/
├── manage.py
└── mysite/
├── __init__.py
├── settings.py
├── urls.py
├── asgi.py
└── wsgi.py
-
mysite
: プロジェクトのフォルダ。名前はなんでもいいがこの後のチュートリアル通りに進めるなら変えないのが無難 -
manage.py
: Djangoプロジェクトを作成すると自動生成される管理用スクリプト。このファイルを通して、サーバーの起動やマイグレーション、管理コマンドの実行などを行う。Djangoが用意している機能をまとめて呼び出すための「入口」のようなもの。
よく使う管理コマンド
# 開発サーバーを起動
python3 manage.py runserver
# モデルの変更を検知してマイグレーションファイルを作成
python3 manage.py makemigrations
# マイグレーションをDBに反映
python3 manage.py migrate
# Djangoシェルを起動(DB操作や動作確認に使う)
python3 manage.py shell
# 管理ユーザーを作成
python3 manage.py createsuperuser
-
mysite/__init__.py
: このディレクトリがPythonパッケージであることをpythonに伝えるための空ファイル。 -
mysite/settings.py
: Djangoプロジェクトの設定ファイル。 -
mysite/urls.py
: Django プロジェクトの URL 宣言、いうなれば Django サイトにおける「目次」に相当します。 -
mysite/asgi.py
: プロジェクトを提供する ASGI 互換 Web サーバーのエントリポイント -
mysite/wsgi.py
: プロジェクトをサーブするための WSGI 互換 Web サーバーとのエントリーポイント
開発用サーバー
mysiteディレクトリに移動
cd mysite
サーバーを立ち上げる
python3 manage.py runserver
Watching for file changes with StatReloader
Performing system checks...
System check identified no issues (0 silenced).
You have 18 unapplied migration(s). Your project may not work properly until you apply the migrations for app(s): admin, auth, contenttypes, sessions.
Run 'python manage.py migrate' to apply them.
September 27, 2025 - 03:46:12
Django version 4.2.24, using settings 'mysite.settings'
Starting development server at http://127.0.0.1:8000/
Quit the server with CONTROL-C.
[27/Sep/2025 03:46:29] "GET / HTTP/1.1" 200 10664
デフォルトでは runserver コマンドは内部 IP のポート 8000 で起動します。
http://127.0.0.1:8000/
で確認
サーバーのポートを変えたい場合
python3 manage.py runserver 8080
サーバのIPを指定するとき
python3 manage.py runserver 0.0.0.0:8000
Pollsアプリケーションを作る
アプリの構造を作成
python3 manage.py startapp polls
ポイント:manage.py
と同じディレクトリで実施する
作成されたアプリのディレクトリ構成
Django_tutorial/
├── mysite/ ← プロジェクト本体(設定)
│ ├── settings.py
│ ├── urls.py
│ └── ...
├── plls/ ← pollsアプリ
│ ├── __init__.py
│ ├── admin.py
│ ├── apps.py
│ ├── migration.py
│ │ └──__init__.py
│ ├── models.py
│ ├── tests.py
│ └── views.py
はじめてのビュー作成
code mysite
from django.shortcuts import render
# Create your views here.
from django.http import HttpResponse
def index(request):
return HttpResponse("Hello, world. You're at the polls index.")
HttpResponseをインポートする
requestを受け取ったらHello, world. You're at the polls index.という文字列を返す
-
request:Django が リクエストごとに生成する
HttpRequest
インスタンス(情報の入れ物)。
型:django.http.HttpRequest
生成:ブラウザからのアクセスごとに 1リクエスト=1インスタンス を Django が作成
役割:そのアクセスに関するあらゆる情報をまとめてビューへ渡すコンテナ
- よく使う
request
情報
request.method # "GET", "POST" など
request.GET # クエリ文字列 (/polls/?page=2 → {"page": "2"})
request.POST # フォームデータ
request.headers # ヘッダ
request.COOKIES # クッキー
request.user # 認証ユーザー(auth 使用時)
request.session # セッション
request.path # "/polls/"
request.build_absolute_uri() # "http://127.0.0.1:8000/polls/"
-
response:ビューが返す
HttpResponse
(ブラウザへ戻る内容)。
実行フロー
- ブラウザが
GET /polls/?page=2
へアクセス - ASGI/WSGI サーバ経由で Django へ
- Middleware を通過
-
urls.py
で URL パターンにマッチ → 対応ビューを決定 - その時点の情報で
HttpRequest
インスタンスを生成(=request
) - ビューに
request
(+URLパラメータなど)を渡す - ビューが
HttpResponse
を返却 → ミドルウェアを逆順に通過 → ブラウザへ
urls.pyを作成する
Django_tutorial/
├── mysite/
│ ├── settings.py
│ ├── urls.py
│ └── ...
├── plls/
│ ├── __init__.py
│ ├── admin.py
│ ├── apps.py
│ ├── migration.py
│ │ └──__init__.py
│ ├── models.py
│ ├── tests.py
│ ├── urls.py ← 新規作成!!
│ └── views.py
polls ディレクトリに URLconf を作るには urls.py に下記を記述する
from django.urls import path
from . import views
urlpatterns = [
path("", views.index, name="index"),
]
-
views.index
:そのURLに来たとき実行する ビュー関数。
つまり「/polls/
に来たらviews.index
を呼ぶ」。 -
name="index"
:このURLに 名前 を付ける。逆引きで使える(reverse("polls:index") や {% url 'polls:index' %} で/polls/
を生成)。/polls/
にアクセスが来たらviews.index
を実行する。その経路名は index。
※polls/urls.py
にapp_name = "polls"
を書いていると経路名がpolls:index
になる
from django.contrib import admin
from django.urls import include, path
urlpatterns = [
path("polls/", include("polls.urls")),
path("admin/", admin.site.urls),
]
polls/ にマッチしたら polls アプリの urls.py に処理を委譲。
includeの役割:「この先は polls アプリ内で細かい道案内をしてね」という案内所。
python3 manage.py runserver
◆はじめてのDjangoアプリ作成、その2
Database の設定
DATABASES = {
"default": {
"ENGINE": "django.db.backends.sqlite3",
"NAME": BASE_DIR / "db.sqlite3",
}
}
ENGINE
:どのDBを使うかを指定。外部ライブラリを使えば他のDBも利用可能。
'django.db.backends.sqlite3' # SQLite
'django.db.backends.postgresql' # PostgreSQL
'django.db.backends.mysql' # MySQL / MariaDB
'django.db.backends.oracle' # Oracle
NAME
- DBの名前や場所を指定
- SQLite の場合:
NAME = BASE_DIR / 'db.sqlite3'
→ プロジェクト直下にファイルが作られる - PostgreSQL / MySQL / Oracle の場合:
NAME = 'mydatabase'
→ DBサーバーに存在するDB名を指定
→ USER / PASSWORD / HOST / PORT も必要
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql',
'NAME': 'mydb',
'USER': 'myuser',
'PASSWORD': 'mypassword',
'HOST': 'localhost',
'PORT': '5432',
}
}
BASE_DIR
- BASE_DIR = プロジェクトの基準ディレクトリを指す絶対パス
- これ自体が場所を登録するのではなく、各設定(TEMPLATES/STATIC/MEDIA/DB など)でパスを組み立てるための出発点として使う
- 通常は
manage.py
がある階層(プロジェクトのルート)を指す
Django_tutorial/ ← ここが BASE_DIR
├─ manage.py
├─ db.sqlite3
├─ templates/
├─ static/
└─ mysite/
├─ settings.py
├─ urls.py
└─ wsgi.py / asgi.py
TIME_ZONE
:TIME_ZONE = "Asia/Tokyo"
INSTALLED_APPS = [
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
]
INSTALLED_APPS
- このプロジェクトで“有効化するアプリの登録簿”
- ここに列挙されたアプリだけが Django に読み込まれ、各機能が反映される
Django 標準アプリ
admin
- /admin/ で使える管理サイト。モデルを登録するとCRUD UIが自動生成。
- 使うには
mysite/urls.py
にpath("admin/", admin.site.urls)
が必要。 - 依存:
auth, contenttypes
auth
- ユーザー認証・権限。
User/Group/Permission
モデル、パスワードハッシュ化、ログイン/ログアウトview
など。 -
request.user
を提供(Middlewareが必要)。 - “認可”は
user.has_perm("app.codename") / user.is_staff / user.is_superuser
などで判定。
contenttypes
- どのアプリのどのモデルかを汎用的に識別する仕組み(ContentType)。
- 権限や汎用外部キー(GenericForeignKey)で使われる“基盤”。
- ほぼ外せない土台。単体で直接触る機会は少なめ。
sessions
- セッション管理。
request.session["key"] = "value"
のようにユーザーごとデータ保持。 - デフォルトはDBセッション(テーブル django_session)。Redis 等への切り替えも可。
- ログイン状態維持にも使われることが多い。
messages
- フラッシュメッセージ(1回きりの通知)を扱う。
- 例:
from django.contrib import messages
messages.success(request, "保存しました") # 次のレスポンスで1回だけ表示
- 表示にはテンプレートタグ {% for message in messages %} と Middleware/コンテキストプロセッサが必要(デフォルト有効)。
staticfiles
- 静的ファイル(CSS/JS/画像)の探索と配信(開発サーバ用)、本番配信用の
collectstatic
を提供。 - 典型コマンド:python manage.py collectstatic
- テンプレートで {% load static %} →
<link href="{% static 'app/style.css' %}">
アプリ / モデル / テーブル の関係
-
モデル = テーブルの設計図
-
models.Model
を1つ定義 → 通常はテーブル1つ作成 - ManyToManyは中間テーブルも自動追加
-
-
アプリ = 機能の入れ物
- 1アプリにモデルは 0個でも複数でもOK
- モデル“があるアプリ”だけDBテーブルが増える
- モデル不要のアプリ(APIだけ、外部連携だけ等)も あり得る
-
INSTALLED_APPS に登録すると
- そのアプリのモデルが migrate対象 になる
- admin/テンプレ/static/ready() など アプリの提供物が有効化
python3 manage.py migrate
Operations to perform:
Apply all migrations: admin, auth, contenttypes, sessions
Running migrations:
Applying contenttypes.0001_initial... OK
Applying auth.0001_initial... OK
Applying admin.0001_initial... OK
Applying admin.0002_logentry_remove_auto_add... OK
Applying admin.0003_logentry_add_action_flag_choices... OK
Applying contenttypes.0002_remove_content_type_name... OK
Applying auth.0002_alter_permission_name_max_length... OK
Applying auth.0003_alter_user_email_max_length... OK
Applying auth.0004_alter_user_username_opts... OK
Applying auth.0005_alter_user_last_login_null... OK
Applying auth.0006_require_contenttypes_0002... OK
Applying auth.0007_alter_validators_add_error_messages... OK
Applying auth.0008_alter_user_username_max_length... OK
Applying auth.0009_alter_user_last_name_max_length... OK
Applying auth.0010_alter_group_name_max_length... OK
Applying auth.0011_update_proxy_permissions... OK
Applying auth.0012_alter_user_first_name_max_length... OK
Applying sessions.0001_initial... OK
INSTALLED_APPS
に入っていて、公式の *migrations*
を同梱しているアプリのテーブルが一気に作成される
モデルの作成
- アプリ内でデータ構造と振る舞いを定義するテーブルの設計図
-
django.db.models.Model
を継承して作成。通常は1モデル=DBテーブル1つ(多対多は中間テーブル追加)。
from django.db import models
class Question(models.Model):
question_text = models.CharField(max_length=200)
pub_date = models.DateTimeField("date published")
class Choice(models.Model):
question = models.ForeignKey(Question, on_delete=models.CASCADE)
choice_text = models.CharField(max_length=200)
votes = models.IntegerField(default=0)
コード解説
class Question(models.Model):
question_text = models.CharField(max_length=200)
pub_date = models.DateTimeField("date published")
-
Question
は「質問」テーブルの設計図(= モデル)。 -
question_text = models.CharField(max_length=200)
↪︎models.pyモジュールのCharFieldクラスを呼び出して設定。文字列カラム。最大200文字の制約(DBレベル/バリデーション)。 -
pub_date = models.DateTimeField("date published")
↪︎日時カラム。括弧内はverbose_name
(管理画面などでの表示名)。
class Choice(models.Model):
question = models.ForeignKey(Question, on_delete=models.CASCADE)
choice_text = models.CharField(max_length=200)
votes = models.IntegerField(default=0)
- Choice は「選択肢」テーブルの設計図。
- question = models.ForeignKey(Question, on_delete=models.CASCADE)
↪︎models.pyモジュールのForeignKeyクラスを呼び出して設定。外部キーで Question と 多対一(Choice 多 : 1 Question)を表現。
on_delete=models.CASCADE は 親(Question)が削除されたら子(Choice)も一緒に削除の意味。 - choice_text = models.CharField(max_length=200)
選択肢の本文。 - votes = models.IntegerField(default=0)
票数。デフォルト0でレコード作成時に未指定でも0が入る。
Djangoモデル定義の要点
1 : モデル定義
-
models.Model
を継承した Pythonクラスを作る = テーブル名になる(設計図) - クラス内の 属性に Field を置く(
CharField
,IntegerField
,ForeignKey
など)
→ それぞれ DBの列になる
2 : テーブル間の連携(リレーション)
-
1対多:
ForeignKey(Parent, on_delete=...)
-
多対多:
ManyToManyField(Other)
(中間テーブル自動生成) -
1対1:
OneToOneField(Other)
- 逆参照は
related_name
を付けると読みやすい(parent.children.all()
など)
モデルを有効にする
- アプリ (例: polls) を作成しただけでは、まだプロジェクトに認識されていない
- DBスキーマ作成や API 利用をするにはプロジェクトにアプリを登録する必要がある
INSTALLED_APPS = [
"polls.apps.PollsConfig",
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
]
python3 manage.py makemigrations polls
Migrations for 'polls':
polls/migrations/0001_initial.py
- Create model Question
- Create model Choice
makemigrations
-
makemigrations:モデル変更を検知し、マイグレーションファイル(DBスキーマ変更の設計図)を作成
- 例:
polls/migrations/0001_initial.py
- 人間可読で、必要なら手動で微調整も可能
- 例:
- migrate:作成済みマイグレーションを実行し、DBスキーマに反映
- sqlmigrate:あるマイグレーションがどんなSQLを実行するかを表示
- showmigrations:適用/未適用の一覧を表示
python3 manage.py migrate
Operations to perform:
Apply all migrations: admin, auth, contenttypes, polls, sessions
Running migrations:
Applying polls.0001_initial... OK
マイグレーションとは
- Django の「データベース変更管理ツール」
- models.py を変更しても、DBを作り直さずに反映できる
- データを失わずにテーブル構造をアップデート可能
3ステップの流れ
-
モデルを変更する
→ models.py にフィールド追加・修正など -
マイグレーションファイルを作成する
→ コマンド: python manage.py makemigrations
→ 変更内容が migrations/ に記録される -
データベースへ適用する
→ コマンド: python manage.py migrate
→ DBのテーブル構造が更新される
ポイント
- DBやテーブルを削除・再作成する必要なし
- データを保持したまま安全に変更できる
- 実際の開発では何度も繰り返し使う基本ワークフロー
APIで遊んでみる
from django.db import models
# Create your models here.
class Question(models.Model):
question_text = models.CharField(max_length=200)
pub_date = models.DateTimeField("date published")
class Choice(models.Model):
question = models.ForeignKey(Question, on_delete=models.CASCADE)
choice_text = models.CharField(max_length=200)
votes = models.IntegerField(default=0)
↓ 変更
import datetime
from django.db import models
from django.utils import timezone
# Create your models here.
class Question(models.Model):
question_text = models.CharField(max_length=200)
pub_date = models.DateTimeField("date published")
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
DB反映手順
モデル変更 → マイグレーションファイル生成
python manage.py makemigrations polls
生成されたマイグレーションが発行するSQLを確認
python manage.py sqlmigrate polls 0001
マイグレーション適用(DB反映)
python manage.py migrate
管理ユーザーを作成する
python3 manage.py createsuperuser
Username (leave blank to use 'rai'): admin
Email address: admin@example.com
Password:
Password (again):
Superuser created successfully.
開発サーバー起動
python3 manage.py runserver
Watching for file changes with StatReloader
Performing system checks...
System check identified no issues (0 silenced).
September 27, 2025 - 20:44:25
Django version 4.2.24, using settings 'mysite.settings'
Starting development server at http://127.0.0.1:8000/
Quit the server with CONTROL-C.
ブラウザを起動して、 http://127.0.0.1:8000/admin/
にアクセス。以下のような admin
のログイン画面が表示される:
settings.py 最小例(管理画面を日本語にしたい)
LANGUAGE_CODE = "en-us"
↓日本語に変更
LANGUAGE_CODE = "ja" # ← これで admin ログイン画面も日本語に
USE_I18N = True # 既定で True(翻訳ON)
adminサイトに入る
ちなみに管理画面はデフォルトで設定されている下記の2つのアプリによって作成されています。
django.contrib.admin
役割:Djangoの管理サイト(/admin/)を提供。登録したモデルのCRUD UIを自動生成。
django.contrib.auth
役割:認証・権限フレームワーク(User/Group/Permission、ログイン状態、request.user
など)を提供。
Pollアプリをadmin上で編集できるようにする
from django.contrib import admin
from .models import Question
admin.site.register(Question)
はじめてのDjangoアプリ作成、その3
ビューとは
もっとビューを書いてみる
from django.shortcuts import render
# Create your views here.
from django.http import HttpResponse
def index(request):
return HttpResponse("Hello, world. You're at the polls index.")
# 追加
def detail(request, question_id):
return HttpResponse("You're looking at question %s." % question_id)
def results(request, question_id):
response = "You're looking at the results of question %s."
return HttpResponse(response % question_id)
def vote(request, question_id):
return HttpResponse("You're voting on question %s." % question_id)
from django.urls import path
from . import views
urlpatterns = [
# ex: /polls/
path("", views.index, name="index"),
# ex: /polls/5/ # 追加
path("<int:question_id>/", views.detail, name="detail"),
# ex: /polls/5/results/ # 追加
path("<int:question_id>/results/", views.results, name="results"),
# ex: /polls/5/vote/ # 追加
path("<int:question_id>/vote/", views.vote, name="vote"),
]
-
「〇〇が来たら → views.py のこの関数を呼ぶ」を、path() で手で紐付けている。
-
int:question_id のような部分は引数として渡す約束(例:detail(request, question_id))。
-
name= はテンプレートやコードからURLを生成するための名前(対応関係とは別物)。
実際に動作するビューを書く
ビュー関数の2つの基本的役割
-
HttpResponse を返す
- リクエストに応じたレスポンスを生成して返す
- 例: HTML, テキスト, JSON, リダイレクト など
-
例外を送出する
- 主に
Http404
(対象が存在しない場合)など - Django がキャッチして適切なエラーページを返す
- 主に
デザインを綺麗にするために HTML/CSS を使う。
Django_tutorial/
├── mysite/
│ ├── settings.py
│ ├── urls.py
│ └── ...
├── polls/
│ ├── templates/polls ← 新規作成
│ │ └── index.html ← 新規作成
│ ├── __init__.py
│ ├── admin.py
│ ├── apps.py
│ ├── migration.py
│ │ └──__init__.py
│ ├── models.py
│ ├── tests.py
│ ├── urls.py
│ └── views.py
{% if latest_question_list %}
<ul>
{% for question in latest_question_list %}
<li><a href="/polls/{{ question.id }}/">{{ question.question_text }}</a></li>
{% endfor %}
</ul>
{% else %}
<p>No polls are available.</p>
{% endif %}
このテンプレートを使用するためにviews.pyの設定をする
from django.shortcuts import render
# Create your views here.
from django.http import HttpResponse
from django.template import loader
from .models import Question
# 追加
def index(request):
latest_question_list = Question.objects.order_by("-pub_date")[:5]
template = loader.get_template("polls/index.html")
context = {
"latest_question_list": latest_question_list,
}
return HttpResponse(template.render(context, request))
def detail(request, question_id):
return HttpResponse("You're looking at question %s." % question_id)
def results(request, question_id):
response = "You're looking at the results of question %s."
return HttpResponse(response % question_id)
def vote(request, question_id):
return HttpResponse("You're voting on question %s." % question_id)
コードの流れ(views.index)
-
データベースから最新の質問を取得
latest_question_list = Question.objects.order_by("-pub_date")[:5]
- Questionモデルを参照
- 公開日を新しい順に並べ替え
- 先頭から5件だけ取得
-
テンプレートを読み込む
template = loader.get_template("polls/index.html")
-
polls/index.html
を探してテンプレートオブジェクトを取得
-
-
コンテキスト(テンプレートに渡すデータ)を準備
context = {"latest_question_list": latest_question_list}
- テンプレート内で使う変数を辞書にまとめる
-
テンプレートをレンダリングしてレスポンスを返す
return HttpResponse(template.render(context, request))
- コンテキストを埋め込んでHTMLを生成
- 生成したHTMLを
HttpResponse
でブラウザへ返す
ショートカット:render()
def index(request):
latest_question_list = Question.objects.order_by("-pub_date")[:5]
template = loader.get_template("polls/index.html")
context = {
"latest_question_list": latest_question_list}
return HttpResponse(template.render(context, request))
↓ショートカット
def index(request):
latest_question_list = Question.objects.order_by("-pub_date")[:5]
context = {"latest_question_list": latest_question_list}
return render(request, "polls/index.html", context)
何が省略されたか
-
テンプレート読み込みの省略
- 旧:
template = loader.get_template("polls/index.html")
- 新: (不要)→
render()
が内部でテンプレートを探索・読込
- 旧:
-
HttpResponse 生成の省略
- 旧:
HttpResponse(template.render(context, request))
- 新:
render(request, "polls/index.html", context)
が HttpResponse を直接返す
- 旧:
-
変数
template
の削除- 旧:
template
オブジェクトを保持 - 新: 不要(1行で完結)
- 旧:
-
RequestContext(コンテキストプロセッサ)の自動適用
-
render()
はrequest
を受け取り、user
やcsrf_token
などの共通変数を自動で注入
-
# A: フル記法
return HttpResponse(
loader.get_template("polls/index.html").render(context, request)
)
# B: ショートカット
return render(request, "polls/index.html", context)
404エラーの送出
from django.http import Http404
from django.shortcuts import render
from .models import Question
def detail(request, question_id):
try:
question = Question.objects.get(pk=question_id)
except Question.DoesNotExist:
raise Http404("Question does not exist")
return render(request, "polls/detail.html", {"question": question})
{{ question }}
ショートカット:get_object_or_404()
def detail(request, question_id):
try:
question = Question.objects.get(pk=question_id)
except Question.DoesNotExist:
raise Http404("Question does not exist")
return render(request, "polls/detail.html", {"question": question})
↓ get_object_or_404()
でショートカット
def detail(request, question_id):
question = get_object_or_404(Question, pk=question_id)
return render(request, "polls/detail.html", {"question": question})
ショートカット:get_object_or_404()
何者?
-
目的:モデルから1件取得したい時に、見つからなければ自動で
Http404
を返すお手軽関数。 -
場所:
from django.shortcuts import get_object_or_404
いつ使う?
- URLのパラメータ(例:
/polls/5/
の5
)で 対象レコードを1件取得したい時 - 見つからないなら 404 ページでOK、というパターン
テンプレートシステムを使う
<h1>{{ question.question_text }}</h1>
<ul>
{% for choice in question.choice_set.all %}
<li>{{ choice.choice_text }}</li>
{% endfor %}
</ul>
{{ question.question_text }} のドット検索とは
テンプレートで A.B と書いたとき、Django は B をこういう順で探すよ、というルールの説明。
-
辞書キーとして探す
A["B"] を試す
例:A = {"question_text": "こんにちは"} ならここでヒット -
属性として探す
A.B(Pythonの属性アクセス)を試す
例:A が Question モデルのインスタンスなら、A.question_text で見つかる -
インデックスとして探す
A[int("B")] を試す(B が数字っぽい文字列だった場合)
例:A = ["a","b"] で {{ A.1 }} → "b"
つまり {{ question.question_text }} は、
まず question["question_text"] を試し、ダメなら question.question_text を試す。
{% for choice in question.choice_set.all %} のメソッド呼び出しとは
テンプレート内では 引数なしの呼び出せるものを見つけると自動で呼んでくれる という挙動がある。
だから 括弧が無くても 実際には all()
が呼ばれた とみなされる
- テンプレートに書いているのは question.choice_set.all(括弧なし)
- Django はこれを question.choice_set.all()(括弧あり)として解釈
- all() は 反復可能なオブジェクト(QuerySet) を返すので、{% for %} で回せる
choice_set とは
Choice モデルが Question を ForeignKey で参照していると、逆参照用のマネージャ が自動で生える。
デフォルト名が choice_set。これが「この質問に紐づく選択肢」をまとめて扱う入り口。
- question.choice_set … RelatedManager(関連をたどる“入り口”)
- question.choice_set.all() … その質問に属する全 Choice の QuerySet
テンプレート内のハードコードされたURLを削除
<li><a href="/polls/{{ question.id }}/">{{ question.question_text }}</a></li>
もし URL 構造を変えたくなったら(例: /polls/12/ → /polls/specifics/12/)、
この形のリンクを書いたテンプレートを全部探して書き換えないといけないので名前をつけて解決する
URL名の名前空間
-
複数アプリで同名のURL名(例:detail)があると衝突する
-
解決策:各アプリの
urls.py
にapp_name
を設定して名前空間を付けるとテンプレートやreverse()
では アプリ名:URL名(例:polls:detail)で一意に指定できる
from django.urls import path
from . import views
app_name = "polls" #追加
urlpatterns = [
path("", views.index, name="index"),
path("<int:question_id>/", views.detail, name="detail"),
path("<int:question_id>/results/", views.results, name="results"),
path("<int:question_id>/vote/", views.vote, name="vote"),
]
<li><a href="{% url 'detail' question.id %}">{{ question.question_text }}</a></li>
↓ 変更
<li><a href="{% url 'polls:detail' question.id %}">{{ question.question_text }}</a></li>
簡単なフォームを書く
<form action="{% url 'polls:vote' question.id %}" method="post">
{% csrf_token %}
<fieldset>
<legend><h1>{{ question.question_text }}</h1></legend>
{% if error_message %}<p><strong>{{ error_message }}</strong></p>{% endif %}
{% for choice in question.choice_set.all %}
<input type="radio" name="choice" id="choice{{ forloop.counter }}" value="{{ choice.id }}">
<label for="choice{{ forloop.counter }}">{{ choice.choice_text }}</label><br>
{% endfor %}
</fieldset>
<input type="submit" value="Vote">
</form>
投票フォーム
from django.shortcuts import get_object_or_404, render
# Create your views here.
from django.http import HttpResponse, HttpResponseRedirect
from django.urls import reverse
from .models import Choice, Question
def index(request):
latest_question_list = Question.objects.order_by("-pub_date")[:5]
context = {"latest_question_list": latest_question_list}
return render(request, "polls/index.html", context)
def detail(request, question_id):
question = get_object_or_404(Question, pk=question_id)
return render(request, "polls/detail.html", {"question": question})
def results(request, question_id):
response = "You're looking at the results of question %s."
return HttpResponse(response % question_id)
def vote(request, question_id):
question = get_object_or_404(Question, pk=question_id)
try:
selected_choice = question.choice_set.get(pk=request.POST["choice"])
except (KeyError, Choice.DoesNotExist):
# Redisplay the question voting form.
return render(
request,
"polls/detail.html",
{
"question": question,
"error_message": "You didn't select a choice.",
},
)
else:
selected_choice.votes += 1
selected_choice.save()
# Always return an HttpResponseRedirect after successfully dealing
# with POST data. This prevents data from being posted twice if a
# user hits the Back button.
return HttpResponseRedirect(reverse("polls:results", args=(question.id,)))
目的
特定の Question に紐づく Choice をラジオボタンで表示し、ユーザーの選択を投票先ビューへ送信する。
送信先と送信方法
-
action:
{% url 'polls:vote' question.id %}
→polls
名前空間のvote
に、対象question.id
を付けて送信。 -
method:
post
(更新系は POST が原則。GETは取得用途)
CSRF対策
-
{% csrf_token %}
を 自サイト内に送るPOSTフォームすべてに入れる。
ラジオボタンの構成
- ループ:
{% for choice in question.choice_set.all %}
→Question
→Choice
の逆参照で選択肢一覧を取得。 - 各入力の属性
-
name="choice"
:同名で1つだけ選択できるグループ -
value="{{ choice.id }}"
:送信される実値(選んだChoiceのID) -
id="choice{{ forloop.counter }}"
と<label for="...">
:ラベル紐づけ
-
-
送信されるPOST例:
choice=3
ループカウンタ
-
forloop.counter
:テンプレートfor
の1始まりの周回数。連番ID付けに使用。
エラーメッセージ
-
{% if error_message %}...{% endif %}
:未選択などのエラー表示に使用。
見出しと構造
-
<fieldset>
と<legend>
:フォーム内容の意味的グルーピングと見出し表示({{ question.question_text }}
)。
def results(request, question_id):
question = get_object_or_404(Question, pk=question_id)
return render(request, "polls/results.html", {"question": question})
<h1>{{ question.question_text }}</h1>
<ul>
{% for choice in question.choice_set.all %}
<li>{{ choice.choice_text }} -- {{ choice.votes }} vote{{ choice.votes|pluralize }}</li>
{% endfor %}
</ul>
<a href="{% url 'polls:detail' question.id %}">Vote again?</a>
汎用ビューを使う:コードは少ない方がいい
- URLconf を変換する。
- 古い不要なビューを削除する。
- 新しいビューに Djangoの汎用ビューを設定する。
URLconfの修正
from django.urls import path
from . import views
app_name = "polls"
urlpatterns = [
path("", views.IndexView.as_view(), name="index"),
path("<int:pk>/", views.DetailView.as_view(), name="detail"),
path("<int:pk>/results/", views.ResultsView.as_view(), name="results"),
path("<int:question_id>/vote/", views.vote, name="vote"),
]
from django.http import HttpResponseRedirect
from django.shortcuts import get_object_or_404, render
from django.urls import reverse
from django.views import generic
from .models import Choice, Question
class IndexView(generic.ListView):
template_name = "polls/index.html"
context_object_name = "latest_question_list"
def get_queryset(self):
"""Return the last five published questions."""
return Question.objects.order_by("-pub_date")[:5]
class DetailView(generic.DetailView):
model = Question
template_name = "polls/detail.html"
class ResultsView(generic.DetailView):
model = Question
template_name = "polls/results.html"
def vote(request, question_id):
question = get_object_or_404(Question, pk=question_id)
try:
selected_choice = question.choice_set.get(pk=request.POST["choice"])
except (KeyError, Choice.DoesNotExist):
# Redisplay the question voting form.
return render(
request,
"polls/detail.html",
{
"question": question,
"error_message": "You didn't select a choice.",
},
)
else:
selected_choice.votes += 1
selected_choice.save()
# Always return an HttpResponseRedirect after successfully dealing
# with POST data. This prevents data from being posted twice if a
# user hits the Back button.
return HttpResponseRedirect(reverse("polls:results", args=(question.id,)))
はじめてのDjangoアプリ作成、その5
自動テストの導入
自動テストとは
自動テストとは、一度テストを作成するとアプリを変更するたびに自動で意図した通りにコードが動作するかを確認することができる。
自動テストを導入する意味
- 手動でやらないので時間の節約になる
- 問題点の検出だけでなく、問題の発生を防ぐことができる
- テストがないコードは信用されない
- チーム開発の際にメンバーからコードを壊されるのを防ぐ
基本的なテストの方針
テストは実施にコードを書く前に書くというのが原則。もし先にコードを書いてしまっていても新しい機能の追加やバグ修正を行う際にテスト書く
テスト作成
バグを見つけた時
>>> import datetime
>>> from django.utils import timezone
>>> from polls.models import Question
>>> # create a Question instance with pub_date 30 days in the future
>>> future_question = Question(pub_date=timezone.now() + datetime.timedelta(days=30))
>>> # was it published recently?
>>> future_question.was_published_recently()
>>> True
shellでしたことを自動テストでするために変換する。
テストを書く場所は、アプリケーション内の tests.py ファイル内。テストシステムが test で始まる名前のファイルの中から、自動的にテストを見つけてくれる
import datetime
from django.test import TestCase
from django.utils import timezone
from .models import Question
class QuestionModelTests(TestCase):
def test_was_published_recently_with_future_question(self):
"""
was_published_recently() returns False for questions whose pub_date
is in the future.
"""
time = timezone.now() + datetime.timedelta(days=30)
future_question = Question(pub_date=time)
self.assertIs(future_question.was_published_recently(), False)
テスト実行
python3 manage.py test polls
# 実行結果
Found 1 test(s).
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
F
======================================================================
FAIL: test_was_published_recently_with_future_question (polls.tests.QuestionModelTests.test_was_published_recently_with_future_question)
was_published_recently() returns False for questions whose pub_date
----------------------------------------------------------------------
Traceback (most recent call last):
File "/Users/takuma/Desktop/python/Django_tutorial/mysite/polls/tests.py", line 17, in test_was_published_recently_with_future_question
self.assertIs(future_question.was_published_recently(), False)
~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
AssertionError: True is not False
----------------------------------------------------------------------
Ran 1 test in 0.000s
FAILED (failures=1)
Destroying test database for alias 'default'...
-
manage.py test polls
は、polls
アプリケーション内にあるテストを探す -
django.test.TestCase
クラスのサブクラスを見つける - テストのためのデータベースを作成する
- テスト用のメソッドとして、test で始まるメソッドを探す
-
test_was_published_recently_with_future_question
の中で、pub_date
フィールドに今日から30日後の日付を持つ Question インスタンスが作成される - assertIs() メソッドを使うことで
was_published_recently()
がTrue
を返していることを見つける
バグを修正する
def was_published_recently(self):
now = timezone.now()
return now - datetime.timedelta(days=1) <= self.pub_date <= now
def was_published_recently(self):
return self.pub_date >= timezone.now() - datetime.timedelta(days=1)
def was_published_recently(self):
now = timezone.now()
return now - datetime.timedelta(days=1) <= self.pub_date <= now
def test_was_published_recently_with_old_question(self):
"""
was_published_recently() returns False for questions whose pub_date
is older than 1 day.
"""
time = timezone.now() - datetime.timedelta(days=1, seconds=1)
old_question = Question(pub_date=time)
self.assertIs(old_question.was_published_recently(), False)
def test_was_published_recently_with_recent_question(self):
"""
was_published_recently() returns True for questions whose pub_date
is within the last day.
"""
time = timezone.now() - datetime.timedelta(hours=23, minutes=59, seconds=59)
recent_question = Question(pub_date=time)
self.assertIs(recent_question.was_published_recently(), True)
より包括的なテスト
def test_was_published_recently_with_old_question(self):
"""
was_published_recently() returns False for questions whose pub_date
is older than 1 day.
"""
time = timezone.now() - datetime.timedelta(days=1, seconds=1)
old_question = Question(pub_date=time)
self.assertIs(old_question.was_published_recently(), False)
def test_was_published_recently_with_recent_question(self):
"""
was_published_recently() returns True for questions whose pub_date
is within the last day.
"""
time = timezone.now() - datetime.timedelta(hours=23, minutes=59, seconds=59)
recent_question = Question(pub_date=time)
self.assertIs(recent_question.was_published_recently(), True)
ビューに対するテスト
使用できるツール
Djangoテストクライアント
- Django は、ビューレベルでのユーザとのインタラクションをシミュレートすることができる
- Client を用意しています。これを tests.py の中や shell でも使うことができる
>>> from django.test.utils import setup_test_environment
>>> setup_test_environment()
>>> from django.test import Client
>>> # create an instance of the client for our use
>>> client = Client()
>>> # get a response from '/'
>>> response = client.get("/")
Not Found: /
>>> # we should expect a 404 from that address; if you instead see an
>>> # "Invalid HTTP_HOST header" error and a 400 response, you probably
>>> # omitted the setup_test_environment() call described earlier.
>>> response.status_code
404
>>> # on the other hand we should expect to find something at '/polls/'
>>> # we'll use 'reverse()' rather than a hardcoded URL
>>> from django.urls import reverse
>>> response = client.get(reverse("polls:index"))
>>> response.status_code
200
>>> response.content
b'\n<ul>\n \n <li>\n <a href="/polls/1/"\n >What's up?</a\n >\n </li>\n \n</ul>\n\n'
>>> response.context["latest_question_list"]
<QuerySet [<Question: What's up?>]>
ビューを改良する
from django.utils import timezone
def get_queryset(self):
"""
Return the last five published questions (not including those set to be
published in the future).
"""
return Question.objects.filter(pub_date__lte=timezone.now()).order_by("-pub_date")[
:5
]
新しいビューをテストする
from django.urls import reverse
def create_question(question_text, days):
"""
Create a question with the given `question_text` and published the
given number of `days` offset to now (negative for questions published
in the past, positive for questions that have yet to be published).
"""
time = timezone.now() + datetime.timedelta(days=days)
return Question.objects.create(question_text=question_text, pub_date=time)
class QuestionIndexViewTests(TestCase):
def test_no_questions(self):
"""
If no questions exist, an appropriate message is displayed.
"""
response = self.client.get(reverse("polls:index"))
self.assertEqual(response.status_code, 200)
self.assertContains(response, "No polls are available.")
self.assertQuerySetEqual(response.context["latest_question_list"], [])
def test_past_question(self):
"""
Questions with a pub_date in the past are displayed on the
index page.
"""
question = create_question(question_text="Past question.", days=-30)
response = self.client.get(reverse("polls:index"))
self.assertQuerySetEqual(
response.context["latest_question_list"],
[question],
)
def test_future_question(self):
"""
Questions with a pub_date in the future aren't displayed on
the index page.
"""
create_question(question_text="Future question.", days=30)
response = self.client.get(reverse("polls:index"))
self.assertContains(response, "No polls are available.")
self.assertQuerySetEqual(response.context["latest_question_list"], [])
def test_future_question_and_past_question(self):
"""
Even if both past and future questions exist, only past questions
are displayed.
"""
question = create_question(question_text="Past question.", days=-30)
create_question(question_text="Future question.", days=30)
response = self.client.get(reverse("polls:index"))
self.assertQuerySetEqual(
response.context["latest_question_list"],
[question],
)
def test_two_past_questions(self):
"""
The questions index page may display multiple questions.
"""
question1 = create_question(question_text="Past question 1.", days=-30)
question2 = create_question(question_text="Past question 2.", days=-5)
response = self.client.get(reverse("polls:index"))
self.assertQuerySetEqual(
response.context["latest_question_list"],
[question2, question1],
)
DetailViewのテスト
class DetailView(generic.DetailView):
...
def get_queryset(self):
"""
Excludes any questions that aren't published yet.
"""
return Question.objects.filter(pub_date__lte=timezone.now())
はじめてのDjangoアプリ作成、その6
スタイルシートや画像を追加する
静的 (static) ファイル
- サーバで生成するHTML以外に、Webページをレンダリングするために、画像、JavaScript、CSSなどのファイル
アプリの構造をカスタマイズする
li a {
color: green;
}
{% load static %}
<link rel="stylesheet" href="{% static 'polls/style.css' %}">
背景画像を追加する
polls/static/polls/
ディレクトリの中に images
サブディレクトリを作る。ディレクトリの中に背景と使用したい画像ファイルを追加。チュートリアルでは `background.png` という名前ファイルを使用。このファイルのフルパスは polls/static/polls/images/background.png
となるようにする。
さらに、スタイルシート (polls/static/polls/style.css) に画像への参照を追加。
body {
background: white url("images/background.png") no-repeat;
}
はじめてのDjangoアプリ作成、その7
adminフォームのカスタマイズ
from django.contrib import admin
from .models import Question
class QuestionAdmin(admin.ModelAdmin):
fields = ["pub_date", "question_text"]
admin.site.register(Question, QuestionAdmin)
from django.contrib import admin
from .models import Question
class QuestionAdmin(admin.ModelAdmin):
fieldsets = [
(None, {"fields": ["question_text"]}),
("Date information", {"fields": ["pub_date"]}),
]
admin.site.register(Question, QuestionAdmin)
リレーションを張ったオブジェクトの追加
from django.contrib import admin
from .models import Choice, Question
# ...
admin.site.register(Choice)
from django.contrib import admin
from .models import Choice, Question
class ChoiceInline(admin.StackedInline):
model = Choice
extra = 3
class QuestionAdmin(admin.ModelAdmin):
fieldsets = [
(None, {"fields": ["question_text"]}),
("Date information", {"fields": ["pub_date"], "classes": ["collapse"]}),
]
inlines = [ChoiceInline]
admin.site.register(Question, QuestionAdmin)
class ChoiceInline(admin.TabularInline):
...
管理サイトのチェンジリストページをカスタマイズする
class QuestionAdmin(admin.ModelAdmin):
# ...
list_display = ["question_text", "pub_date"]
class QuestionAdmin(admin.ModelAdmin):
# ...
list_display = ["question_text", "pub_date", "was_published_recently"]
from django.contrib import admin
class Question(models.Model):
# ...
@admin.display(
boolean=True,
ordering="pub_date",
description="Published recently?",
)
def was_published_recently(self):
now = timezone.now()
return now - datetime.timedelta(days=1) <= self.pub_date <= now
polls/admin.py ファイルをもう一度編集して、Question のチェンジリストのページに list_filter を追加して、さらに改良しましょう。それには、QuestionAdmin に次に行を追加します。
list_filter = ["pub_date"]
search_fields = ["question_text"]
管理サイトのルック&フィールをカスタマイズする
TEMPLATES = [
{
"BACKEND": "django.template.backends.django.DjangoTemplates",
"DIRS": [BASE_DIR / "templates"],
"APP_DIRS": True,
"OPTIONS": {
"context_processors": [
"django.template.context_processors.debug",
"django.template.context_processors.request",
"django.contrib.auth.context_processors.auth",
"django.contrib.messages.context_processors.messages",
],
},
},
]
templates
の中に admin
という名前のディレクトリを作成。Django
自体のソースコード内にある、デフォルトの Django admin
テンプレートディレクトリ (django/contrib/admin/templates)
を探して、 admin/base_site.html
というテンプレートを、新しく作ったディレクトリにコピーする。
良かったところ
- Djangoでwebアプリケーションを作成する流れの理解
- 各ファイルの役割仕組みを一通り学ぶことができた
悪かったところ
- 難しい言葉で書かれていたことと、所々日本語がおかしいところがある
- 図解がないため分かりづらい
難しかったこと
- どのファイルでどういった設定をするのかがごちゃごちゃになる時がある
- 設定されているクラスがブラックボックス化されているためイメージが湧きづらい
- 呼び出すべきクラスやメソッドの量が多く把握しきれない
- 自動テストの箇所はほとんど理解できなかった
さいごに
公式チュートリアルを通してDjangoの全体像と各ファイルの役割や仕組みは理解できたので、これからは実践を通してDjango特有のクラスやメソッドの理解を深めて使えるようになっていきたいです!