1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Django公式チュートリアルやってみた

Posted at

はじめに

今回、Djangoについてudemyで軽く学習をしたので
より理解を深めるためにDjangoの公式チュートリアルを
実施していきます。

環境

  • Mac
  • 仮想環境
  • Python
  • Django
  • DB

自分のレベル

  • HTML/CSSで簡単なWebサイト画面を作成できる
  • Pythonで基本的なコードが書ける
  • オブジェクト指向についてある程度理解してる

公式チュートリアルスタート

クイックインストールガイド

Pythonインストール確認

Python
python3 --version
Python 3.13.6

Django_tutorial作成

Python
mkdir Django_tutorial

python3 -m venv tutorial

source tutorial/bin/activate

cd Django_tutorial

◆はじめてのDjangoアプリ作成、その1

Pollアプリケーション構成

  • ユーザが投票したり結果を表示したりできる公開用サイト
  • 投票項目の追加、変更、削除を行うための管理 (admin) サイト

Djangoインストール

Django_tutorial
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

インストール確認

Django_tutorial
python3 -m django --version 
4.2.24

いったんDjangoフレームワークの仕組みを理解をする

プロジェクトを作成する

Django_tutorial
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ディレクトリに移動

Django_tutorial
cd mysite

サーバーを立ち上げる

Django_tutorial/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/ で確認

スクリーンショット 2025-09-27 12.48.52.png

サーバーのポートを変えたい場合

Django_tutorial/mysite
python3 manage.py runserver 8080

サーバのIPを指定するとき

Django_tutorial/mysite
python3 manage.py runserver 0.0.0.0:8000

Pollsアプリケーションを作る

アプリの構造を作成

Django_tutorial/mysite
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

はじめてのビュー作成

Django_tutorial
code mysite
polls/views.py
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(ブラウザへ戻る内容)。

実行フロー

  1. ブラウザが GET /polls/?page=2 へアクセス
  2. ASGI/WSGI サーバ経由で Django へ
  3. Middleware を通過
  4. urls.py で URL パターンにマッチ → 対応ビューを決定
  5. その時点の情報で HttpRequest インスタンスを生成(=request
  6. ビューに request(+URLパラメータなど)を渡す
  7. ビューが 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 に下記を記述する

polls/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.pyapp_name = "polls" を書いていると経路名が polls:index になる

mysite/urls.py
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

スクリーンショット 2025-09-27 13.33.11.png

◆はじめてのDjangoアプリ作成、その2

Database の設定

mysite/settings.py(標準仕様)

DATABASES = {
    "default": {
        "ENGINE": "django.db.backends.sqlite3",
        "NAME": BASE_DIR / "db.sqlite3",
    }
}

ENGINE:どのDBを使うかを指定。外部ライブラリを使えば他のDBも利用可能。

mysite/settings
    '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 も必要
PostgreSQL
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"

mysite/settings.py
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.pypath("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() など アプリの提供物が有効化
Django_tutorial/mysite
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つ(多対多は中間テーブル追加)。
polls/models.py
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)

コード解説

polls/models.py
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(管理画面などでの表示名)。
polls/models.py
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対1OneToOneField(Other)
  • 逆参照は related_name を付けると読みやすい(parent.children.all() など)

モデルを有効にする

  • アプリ (例: polls) を作成しただけでは、まだプロジェクトに認識されていない
  • DBスキーマ作成や API 利用をするにはプロジェクトにアプリを登録する必要がある
mysite/settings.py
INSTALLED_APPS = [
    "polls.apps.PollsConfig",
    "django.contrib.admin",
    "django.contrib.auth",
    "django.contrib.contenttypes",
    "django.contrib.sessions",
    "django.contrib.messages",
    "django.contrib.staticfiles",
]
Django_tuutorial/mysite
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:適用/未適用の一覧を表示
Django_tutorial/mysite
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ステップの流れ

  1. モデルを変更する
    → models.py にフィールド追加・修正など

  2. マイグレーションファイルを作成する
    → コマンド: python manage.py makemigrations
    → 変更内容が migrations/ に記録される

  3. データベースへ適用する
    → コマンド: python manage.py migrate
    → DBのテーブル構造が更新される

ポイント

  • DBやテーブルを削除・再作成する必要なし
  • データを保持したまま安全に変更できる
  • 実際の開発では何度も繰り返し使う基本ワークフロー

APIで遊んでみる

polls/models.py

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)
    

↓ 変更

polls/models.py

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

管理ユーザーを作成する

Django_tutorial/mysite
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 のログイン画面が表示される:

スクリーンショット 2025-09-27 20.45.43.png

settings.py 最小例(管理画面を日本語にしたい)

LANGUAGE_CODE = "en-us"
日本語に変更
LANGUAGE_CODE = "ja"        # ← これで admin ログイン画面も日本語に
USE_I18N = True             # 既定で True(翻訳ON)

スクリーンショット 2025-09-27 20.50.44.png

adminサイトに入る

スクリーンショット 2025-09-27 21.12.07.png


ちなみに管理画面はデフォルトで設定されている下記の2つのアプリによって作成されています。

django.contrib.admin
役割:Djangoの管理サイト(/admin/)を提供。登録したモデルのCRUD UIを自動生成。

django.contrib.auth
役割認証・権限フレームワーク(User/Group/Permission、ログイン状態、request.user など)を提供。

Pollアプリをadmin上で編集できるようにする

polls/admin.py
from django.contrib import admin

from .models import Question

admin.site.register(Question)

スクリーンショット 2025-09-27 21.56.19.png

はじめてのDjangoアプリ作成、その3

ビューとは

もっとビューを書いてみる

polls/views.py
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)
polls/urls.py

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つの基本的役割

  1. HttpResponse を返す

    • リクエストに応じたレスポンスを生成して返す
    • 例: HTML, テキスト, JSON, リダイレクト など
  2. 例外を送出する

    • 主に 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
Django_tutorial/polls/templates/polls/index

{% 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の設定をする

polls/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)

  1. データベースから最新の質問を取得
    latest_question_list = Question.objects.order_by("-pub_date")[:5]

    • Questionモデルを参照
    • 公開日を新しい順に並べ替え
    • 先頭から5件だけ取得
  2. テンプレートを読み込む
    template = loader.get_template("polls/index.html")

    • polls/index.html を探してテンプレートオブジェクトを取得
  3. コンテキスト(テンプレートに渡すデータ)を準備
    context = {"latest_question_list": latest_question_list}

    • テンプレート内で使う変数を辞書にまとめる
  4. テンプレートをレンダリングしてレスポンスを返す
    return HttpResponse(template.render(context, request))

    • コンテキストを埋め込んでHTMLを生成
    • 生成したHTMLを HttpResponse でブラウザへ返す

ショートカット:render()

polls/views.py
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))

↓ショートカット

polls/views.py
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 を受け取り、usercsrf_token などの共通変数を自動で注入
# A: フル記法
return HttpResponse(
    loader.get_template("polls/index.html").render(context, request)
)

# B: ショートカット
return render(request, "polls/index.html", context)

404エラーの送出

polls/views.py
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})
polls/templates/polls/detail.html
{{ question }}

ショートカット:get_object_or_404()

mysite/polls/views.py
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() でショートカット

mysite/polls/views.py
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、というパターン

テンプレートシステムを使う

polls/templates/polls/detail.html
<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を削除

polls/index.html
<li><a href="/polls/{{ question.id }}/">{{ question.question_text }}</a></li>

もし URL 構造を変えたくなったら(例: /polls/12/ → /polls/specifics/12/)、
この形のリンクを書いたテンプレートを全部探して書き換えないといけないので名前をつけて解決する

URL名の名前空間

  • 複数アプリで同名のURL名(例:detail)があると衝突する

  • 解決策:各アプリの urls.pyapp_name を設定して名前空間を付けるとテンプレートや reverse() では アプリ名:URL名(例:polls:detail)で一意に指定できる

polls/urls.py
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"),
]
polls/templates/polls/index.html
<li><a href="{% url 'detail' question.id %}">{{ question.question_text }}</a></li>

↓ 変更

polls/templates/polls/index.html
<li><a href="{% url 'polls:detail' question.id %}">{{ question.question_text }}</a></li>

簡単なフォームを書く

polls/templates/polls/detail.html
<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>

投票フォーム

polls/views.py
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 を付けて送信。
  • methodpost(更新系は POST が原則。GETは取得用途)

CSRF対策

  • {% csrf_token %}自サイト内に送るPOSTフォームすべてに入れる。

ラジオボタンの構成

  • ループ:{% for choice in question.choice_set.all %}
    QuestionChoice逆参照で選択肢一覧を取得。
  • 各入力の属性
    • name="choice":同名で1つだけ選択できるグループ
    • value="{{ choice.id }}":送信される実値(選んだChoiceのID)
    • id="choice{{ forloop.counter }}"<label for="...">:ラベル紐づけ
  • 送信されるPOST例choice=3

ループカウンタ

  • forloop.counter:テンプレート for1始まりの周回数。連番ID付けに使用。

エラーメッセージ

  • {% if error_message %}...{% endif %}:未選択などのエラー表示に使用。

見出しと構造

  • <fieldset><legend>:フォーム内容の意味的グルーピングと見出し表示({{ question.question_text }})。
polls/views.py
def results(request, question_id):
    question = get_object_or_404(Question, pk=question_id)
    return render(request, "polls/results.html", {"question": question})
polls/templates/polls/results.html
<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>

スクリーンショット 2025-09-28 21.02.27.png

汎用ビューを使う:コードは少ない方がいい

  • URLconf を変換する。
  • 古い不要なビューを削除する。
  • 新しいビューに Djangoの汎用ビューを設定する。

URLconfの修正

polls/urls.py

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"),
]
polls/views.py
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

自動テストの導入

自動テストとは

自動テストとは、一度テストを作成するとアプリを変更するたびに自動で意図した通りにコードが動作するかを確認することができる。

自動テストを導入する意味

  • 手動でやらないので時間の節約になる
  • 問題点の検出だけでなく、問題の発生を防ぐことができる
  • テストがないコードは信用されない
  • チーム開発の際にメンバーからコードを壊されるのを防ぐ

基本的なテストの方針

テストは実施にコードを書く前に書くというのが原則。もし先にコードを書いてしまっていても新しい機能の追加やバグ修正を行う際にテスト書く

テスト作成

バグを見つけた時

terminal
>>> 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 で始まる名前のファイルの中から、自動的にテストを見つけてくれる

polls/tests.py
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)

テスト実行

terminal
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 を返していることを見つける

バグを修正する

polls/models.py
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)
polls/models.py
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)

より包括的なテスト

polls/tests.py
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 でも使うことができる
terminal
>>> 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&#x27;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
    ]

新しいビューをテストする

polls/test.py
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のテスト

polls/views.py
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などのファイル

アプリの構造をカスタマイズする

polls/static/polls/style.css
li a {
    color: green;
}

polls/templates/polls/index.html
{% 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) に画像への参照を追加。

plls/static/polls/style.css
body {
    background: white url("images/background.png") no-repeat;
}

はじめてのDjangoアプリ作成、その7

adminフォームのカスタマイズ

polls/admin.py
from django.contrib import admin

from .models import Question

class QuestionAdmin(admin.ModelAdmin):
    fields = ["pub_date", "question_text"]


admin.site.register(Question, QuestionAdmin)

polls/admin.py
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)

リレーションを張ったオブジェクトの追加

polls/admin.py
from django.contrib import admin

from .models import Choice, Question

# ...
admin.site.register(Choice)

polls/admin.py
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)

polls/admin.py
class ChoiceInline(admin.TabularInline):
    ...

管理サイトのチェンジリストページをカスタマイズする

polls/admin.py
class QuestionAdmin(admin.ModelAdmin):
    # ...
    list_display = ["question_text", "pub_date"]
polls/admin.py
class QuestionAdmin(admin.ModelAdmin):
    # ...
    list_display = ["question_text", "pub_date", "was_published_recently"]
polls/models.py
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 に次に行を追加します。

polls/admin.py
list_filter = ["pub_date"]
search_fields = ["question_text"]

管理サイトのルック&フィールをカスタマイズする

mysite/settings.py
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特有のクラスやメソッドの理解を深めて使えるようになっていきたいです!

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?