プロジェクト・アプリケーション操作
プロジェクト作成
新しいDjangoプロジェクトを開始します。
django-admin startproject <プロジェクト名> [ディレクトリ]
例:
# カレントディレクトリにプロジェクトを作成
django-admin startproject myproject .
# 指定したディレクトリにプロジェクトを作成
django-admin startproject myproject /path/to/directory/
アプリケーション作成
プロジェクト内に新しいアプリケーションを作成します。
python manage.py startapp <アプリケーション名>
例:
python manage.py startapp myapp
注意: 作成したアプリケーションは、プロジェクトの settings.py
内の INSTALLED_APPS
に追加する必要があります。
# settings.py
INSTALLED_APPS = [ # ... 他のアプリ 'myapp', # または 'myapp.apps.MyappConfig'
]
開発サーバーの起動
ローカル開発用のウェブサーバーを起動します。
python manage.py runserver [IPアドレス:ポート番号]
例:
# デフォルト (127.0.0.1:8000)
python manage.py runserver
# ポート 8080 で起動
python manage.py runserver 8080
# すべてのIPアドレスで待ち受け (注意: 開発環境以外では非推奨)
python manage.py runserver 0.0.0.0:8000
各種 manage.py コマンド
Djangoプロジェクトの管理に使用する主要なコマンドです。
コマンド | 説明 |
---|---|
check | プロジェクト全体の設定やモデルなどに問題がないかチェックします。 |
migrate | データベーススキーマの変更(マイグレーション)を適用します。 |
makemigrations [アプリ名] | モデルの変更に基づいてマイグレーションファイルを作成します。アプリ名を指定しない場合は全てのアプリが対象。 |
sqlmigrate <アプリ名> <マイグレーション名> | 特定のマイグレーションが実行するSQL文を表示します。 |
showmigrations | プロジェクトのマイグレーションの状態(適用済みか未適用か)を表示します。 |
createsuperuser | 管理サイトにログインできるスーパーユーザーを作成します。 |
shell | Djangoプロジェクトの環境が読み込まれたPythonインタラクティブシェルを起動します。モデル操作のテストなどに便利です。 |
dbshell | プロジェクトで設定されているデータベースのコマンドラインクライアントを起動します。 |
test [アプリ名 or テストクラス or メソッド] | テストを実行します。 |
collectstatic | 各アプリケーションの静的ファイルを設定されたディレクトリ(STATIC_ROOT )に集めます。デプロイ時に使用します。 |
runserver_plus | (django-extensions が必要) Werkzeugデバッガ付きの開発サーバーを起動します。より高機能なデバッグが可能です。 |
shell_plus | (django-extensions が必要) プロジェクトの全てのモデルや設定を自動インポートした状態でインタラクティブシェルを起動します。 |
モデル (Model) とデータベース
モデル定義
models.py
内でデータベースのテーブル構造をPythonクラスとして定義します。
from django.db import models
from django.utils import timezone
from django.contrib.auth.models import User # ユーザーモデルとの連携
class Category(models.Model): name = models.CharField('カテゴリ名', max_length=50, unique=True) def __str__(self): return self.name
class Post(models.Model): title = models.CharField('タイトル', max_length=200) text = models.TextField('本文') created_date = models.DateTimeField('作成日', default=timezone.now) published_date = models.DateTimeField('公開日', blank=True, null=True) category = models.ForeignKey(Category, verbose_name='カテゴリ', on_delete=models.PROTECT) # 外部キー author = models.ForeignKey(User, on_delete=models.CASCADE, related_name='blog_posts') # ユーザー連携 tags = models.ManyToManyField('Tag', verbose_name='タグ', blank=True) # 多対多 def publish(self): self.published_date = timezone.now() self.save() def __str__(self): return self.title class Meta: ordering = ['-created_date'] # デフォルトの並び順 verbose_name = 'ブログ投稿' verbose_name_plural = 'ブログ投稿'
class Tag(models.Model): name = models.CharField('タグ名', max_length=50, unique=True) def __str__(self): return self.name
主なフィールドタイプ
フィールドタイプ | 説明 | 主なオプション |
---|---|---|
CharField | 短い文字列 (VARCHAR ) | max_length (必須), unique , blank , null , db_index , default , verbose_name |
TextField | 長い文字列 (TEXT ) | blank , null , default , verbose_name |
IntegerField | 整数 (INTEGER ) | unique , blank , null , db_index , default , verbose_name |
FloatField | 浮動小数点数 (FLOAT /DOUBLE ) | unique , blank , null , db_index , default , verbose_name |
DecimalField | 固定小数点数 (DECIMAL /NUMERIC )。通貨などに使用。 | max_digits (必須), decimal_places (必須), unique , blank , null , db_index , default , verbose_name |
BooleanField | 真偽値 (BOOLEAN /TINYINT(1) ) | default , null (注意: null許容は非推奨の場合が多い), verbose_name |
DateField | 日付 (DATE ) | auto_now (更新時自動), auto_now_add (作成時自動), default , blank , null , verbose_name |
DateTimeField | 日時 (DATETIME /TIMESTAMP ) | auto_now , auto_now_add , default (timezone.now など), blank , null , verbose_name |
TimeField | 時間 (TIME ) | auto_now , auto_now_add , default , blank , null , verbose_name |
ForeignKey | 他のモデルへのリレーション (多対一) | 関連先モデル (必須), on_delete (必須), related_name , limit_choices_to , blank , null , verbose_name |
OneToOneField | 他のモデルへのリレーション (一対一) | 関連先モデル (必須), on_delete (必須), related_name , parent_link , verbose_name |
ManyToManyField | 他のモデルへのリレーション (多対多) | 関連先モデル (必須), related_name , through (中間テーブル指定), blank , verbose_name |
FileField | ファイルアップロード | upload_to (アップロード先), storage , blank , null , verbose_name |
ImageField | 画像ファイルアップロード (FileField のサブクラス) | upload_to , storage , height_field , width_field , blank , null , verbose_name |
EmailField | メールアドレス形式の文字列 | max_length , unique , blank , null , verbose_name |
URLField | URL形式の文字列 | max_length , unique , blank , null , verbose_name |
UUIDField | UUID | default (uuid.uuid4 など), primary_key , editable , verbose_name |
JSONField | JSONデータ (対応DBのみ) | encoder , decoder , blank , null , default , verbose_name |
on_delete
オプションの値:
models.CASCADE
: 参照先が削除されたら、自身も削除する(カスケード削除)。models.PROTECT
: 参照しているオブジェクトが存在する場合、参照先の削除を禁止 (ProtectedError
が発生)。models.SET_NULL
: 参照先が削除されたら、自身のフィールドをNULL
に設定 (フィールドがnull=True
である必要あり)。models.SET_DEFAULT
: 参照先が削除されたら、自身のフィールドをデフォルト値に設定 (フィールドにdefault
が設定されている必要あり)。models.SET()
: 参照先が削除されたら、指定した値または呼び出し可能オブジェクトの結果を設定。models.DO_NOTHING
: 何もしない (データベースレベルでの制約に依存するため、非推奨の場合が多い)。
マイグレーション
モデルの変更をデータベーススキーマに反映させるプロセスです。
- マイグレーションファイルの作成: モデルに変更を加えた後、以下のコマンドを実行します。
python manage.py makemigrations [アプリ名]
これにより、
migrations
ディレクトリ内に変更内容を記述したPythonファイルが生成されます。 - マイグレーションの適用: 生成されたマイグレーションファイルをデータベースに適用します。
python manage.py migrate [アプリ名] [マイグレーション名]
アプリ名やマイグレーション名を指定しない場合は、未適用の全てのマイグレーションが実行されます。
マイグレーション関連の他のコマンド:
showmigrations
: マイグレーションの状態確認sqlmigrate <アプリ名> <マイグレーション名>
: 実行されるSQLの確認
クエリセット API (QuerySet API)
データベースからデータを取得・操作するためのインターフェースです。
from .models import Post, Category
# 全件取得
all_posts = Post.objects.all()
# 特定の条件でフィルタリング (絞り込み)
published_posts = Post.objects.filter(published_date__isnull=False)
tech_posts = Post.objects.filter(category__name='技術')
recent_posts = Post.objects.filter(published_date__year=2024)
# 条件を除外
non_draft_posts = Post.objects.exclude(published_date__isnull=True)
# 1件取得 (存在しない/複数存在するとエラー)
first_post = Post.objects.get(pk=1) # 主キーで取得
# 1件取得 (存在しない場合は None、複数存在する場合はエラー)
try: post = Post.objects.get(title='特定のタイトル')
except Post.DoesNotExist: print("投稿が見つかりません")
except Post.MultipleObjectsReturned: print("複数の投稿が見つかりました")
# get_object_or_404 (ビューでよく使う)
from django.shortcuts import get_object_or_404
post = get_object_or_404(Post, pk=post_id)
# 並び替え
sorted_posts = Post.objects.order_by('title') # 昇順
reverse_sorted_posts = Post.objects.order_by('-published_date') # 降順
# 件数制限
first_five_posts = Post.objects.all()[:5]
# 件数取得
post_count = Post.objects.count()
published_count = Post.objects.filter(published_date__isnull=False).count()
# 存在確認
has_posts = Post.objects.exists()
has_tech_posts = Post.objects.filter(category__name='技術').exists()
# OR 条件 (Qオブジェクト)
from django.db.models import Q
result = Post.objects.filter(Q(title__startswith='Django') | Q(text__contains='Python'))
# AND 条件 (デフォルトはAND)
result = Post.objects.filter(title__startswith='Django', category__name='技術')
# または Q オブジェクトで明示的に
result = Post.objects.filter(Q(title__startswith='Django') & Q(category__name='技術'))
# NOT 条件 (~Qオブジェクト)
result = Post.objects.filter(~Q(category__name='その他'))
# フィールドルックアップ (条件指定の拡張)
# __exact, __iexact (大文字小文字無視)
Post.objects.filter(title__exact="Django入門")
# __contains, __icontains (含む)
Post.objects.filter(text__icontains="python")
# __startswith, __istartswith (前方一致)
Post.objects.filter(title__startswith="Django")
# __endswith, __iendswith (後方一致)
Post.objects.filter(title__endswith="チュートリアル")
# __in (リスト内のいずれか)
Post.objects.filter(pk__in=[1, 3, 5])
# __gt, __gte (より大きい, 以上)
Post.objects.filter(created_date__gt=some_date)
# __lt, __lte (より小さい, 以下)
Post.objects.filter(published_date__lte=timezone.now())
# __range (範囲)
Post.objects.filter(created_date__range=(start_date, end_date))
# __isnull (NULLかどうか)
Post.objects.filter(published_date__isnull=True)
# __year, __month, __day, __week_day, __hour, __minute, __second (日付/時間要素)
Post.objects.filter(published_date__year=2023)
# __regex, __iregex (正規表現)
Post.objects.filter(title__regex=r'^[A-Z]')
# 関連オブジェクトの取得 (ForeignKey, OneToOneField)
post = Post.objects.get(pk=1)
category_name = post.category.name
author_username = post.author.username
# 逆参照 (ForeignKey, ManyToManyField で related_name を使う)
category = Category.objects.get(name='技術')
posts_in_category = category.post_set.all() # related_name 未指定の場合
user = User.objects.get(username='testuser')
user_posts = user.blog_posts.all() # related_name="blog_posts" を指定した場合
# 多対多 (ManyToManyField) の操作
post = Post.objects.get(pk=1)
tag1 = Tag.objects.get(name='Python')
tag2 = Tag.objects.get(name='Django')
post.tags.add(tag1, tag2) # タグを追加
post.tags.remove(tag1) # タグを削除
post.tags.clear() # 全てのタグを削除
post.tags.set([tag1, tag2]) # タグを指定したものに入れ替え
tags_for_post = post.tags.all() # 投稿についているタグを取得
tag = Tag.objects.get(name='Python')
posts_with_tag = tag.post_set.all() # そのタグがついている投稿を取得 (related_name 未指定)
# select_related (ForeignKey, OneToOneField の結合) : DBアクセス回数を減らす
posts = Post.objects.select_related('category', 'author').all()
for post in posts: print(post.title, post.category.name, post.author.username) # 追加のDBクエリが発生しない
# prefetch_related (ManyToManyField, 逆参照ForeignKey の結合) : DBアクセス回数を減らす
posts = Post.objects.prefetch_related('tags').all()
categories = Category.objects.prefetch_related('post_set').all()
for post in posts: print(post.title) for tag in post.tags.all(): # 追加のDBクエリが発生しない print(f"- {tag.name}")
# 値のリスト取得 (特定のフィールドだけ取得)
titles = Post.objects.values_list('title', flat=True) # flat=True で単一フィールドのリストに
data = Post.objects.values('title', 'author__username') # 辞書のリスト
# アノテーション (集計)
from django.db.models import Count, Avg, Max, Min, Sum
category_counts = Category.objects.annotate(num_posts=Count('post')) # 各カテゴリの投稿数を集計
for category in category_counts: print(f"{category.name}: {category.num_posts}")
# アグリゲーション (全体の集計)
from django.db.models import Avg
average_rating = Post.objects.aggregate(Avg('rating')) # rating フィールドがあると仮定
# Fオブジェクト (モデルのフィールド同士を比較・操作)
from django.db.models import F
# published_date が created_date より後のものを検索
Post.objects.filter(published_date__gt=F('created_date'))
# 全ての投稿の閲覧数を1増やす (競合状態に強い)
Post.objects.update(view_count=F('view_count') + 1)
# オブジェクトの作成
new_post = Post.objects.create( title='新しい投稿', text='本文です。', category=Category.objects.get(name='お知らせ'), author=User.objects.get(username='admin')
)
# または
post = Post(title='別の投稿', text='こちらも本文')
post.category = Category.objects.first()
post.author = User.objects.first()
post.save()
# オブジェクトの更新
post = Post.objects.get(pk=1)
post.title = '更新されたタイトル'
post.save()
# または update() メソッド (複数オブジェクトを一括更新、save() メソッドは呼ばれない)
Post.objects.filter(category__name='古いカテゴリ').update(category=Category.objects.get(name='新しいカテゴリ'))
# オブジェクトの削除
post = Post.objects.get(pk=2)
post.delete()
# または delete() メソッド (複数オブジェクトを一括削除、delete() メソッドは呼ばれない)
Post.objects.filter(published_date__isnull=True).delete()
# get_or_create (取得または作成)
obj, created = Post.objects.get_or_create( title='存在確認タイトル', defaults={'text': 'デフォルト本文', 'category': Category.objects.first(), 'author': User.objects.first()}
)
if created: print('新しい投稿が作成されました')
else: print('既存の投稿が見つかりました')
# update_or_create (更新または作成)
obj, created = Post.objects.update_or_create( title='更新確認タイトル', defaults={'text': '更新された本文', 'published_date': timezone.now()}
)
if created: print('新しい投稿が作成されました')
else: print('既存の投稿が更新されました')
# bulk_create (複数オブジェクトの一括作成)
posts_to_create = [ Post(title='一括作成1', text='...', category=cat1, author=usr1), Post(title='一括作成2', text='...', category=cat2, author=usr1),
]
Post.objects.bulk_create(posts_to_create)
# bulk_update (複数オブジェクトの一括更新) - Django 4.0+
posts_to_update = Post.objects.filter(pk__in=[1, 2])
posts_to_update[0].title = "一括更新1"
posts_to_update[1].title = "一括更新2"
Post.objects.bulk_update(posts_to_update, ['title']) # 更新するフィールドを指定
URL ルーティング
URL パターンをビュー関数またはビュークラスにマッピングします。プロジェクトの urls.py
と各アプリケーションの urls.py
で設定します。
プロジェクトの urls.py
各アプリケーションの urls.py
をインクルードします。
# myproject/urls.py
from django.contrib import admin
from django.urls import path, include
urlpatterns = [ path('admin/', admin.site.urls), path('blog/', include('blog.urls')), # blog アプリケーションの URL をインクルード path('accounts/', include('django.contrib.auth.urls')), # 認証関連の URL をインクルード # 他のアプリケーションの URL ...
]
アプリケーションの urls.py
特定のURLパスとビューを紐付けます。
# blog/urls.py
from django.urls import path, re_path
from . import views # アプリケーションの views.py をインポート
app_name = 'blog' # 名前空間の設定 (推奨)
urlpatterns = [ # 例: /blog/ path('', views.post_list, name='post_list'), # 関数ベースビュー # 例: /blog/post/5/ path('post/<int:pk>/', views.PostDetailView.as_view(), name='post_detail'), # クラスベースビュー + パスコンバータ # 例: /blog/post/new/ path('post/new/', views.post_new, name='post_new'), # 例: /blog/post/5/edit/ path('post/<int:pk>/edit/', views.post_edit, name='post_edit'), # 例: /blog/archive/2024/ path('archive/<int:year>/', views.post_archive_year, name='post_archive_year'), # 例: /blog/category/python/ (スラッグ) path('category/<slug:category_slug>/', views.post_category, name='post_category'), # 正規表現を使用 (re_path) # 例: /blog/articles/2024/03/ re_path(r'^articles/(?P<year>[0-9]{4})/(?P<month>[0-9]{2})/$', views.article_archive, name='article_archive'), # テンプレート内で URL を逆引きする際に name を使用 # {% url 'blog:post_detail' pk=post.pk %}
]
パスコンバータ
URLパターン内で変数をキャプチャし、型を指定できます。
<str:変数名>
: 空でない任意の文字列 (スラッシュを除く)<int:変数名>
: 0 または正の整数<slug:変数名>
: ASCII 文字、数字、ハイフン、アンダースコアで構成されるスラッグ文字列<uuid:変数名>
: UUID<path:変数名>
: 空でない任意の文字列 (スラッシュを含む)
ビュー (View)
リクエストを受け取り、ビジネスロジックを実行し、レスポンス (通常はHTML) を生成する部分です。
関数ベースビュー (FBV)
シンプルなPython関数としてビューを定義します。
# blog/views.py
from django.shortcuts import render, get_object_or_404, redirect
from django.utils import timezone
from .models import Post
from .forms import PostForm
from django.http import HttpResponse, Http404, JsonResponse
def post_list(request): """投稿一覧を表示""" posts = Post.objects.filter(published_date__lte=timezone.now()).order_by('-published_date') # render(requestオブジェクト, テンプレートファイル名, テンプレートに渡すコンテキスト辞書) return render(request, 'blog/post_list.html', {'posts': posts})
def post_detail(request, pk): """投稿詳細を表示""" # post = Post.objects.get(pk=pk) # オブジェクトが存在しない場合に DoesNotExist エラー post = get_object_or_404(Post, pk=pk) # オブジェクトが存在しない場合に 404 エラー if post.published_date is None or post.published_date > timezone.now(): if not request.user.is_staff: # スタッフユーザー以外は非公開記事を見れない raise Http404("Post does not exist") return render(request, 'blog/post_detail.html', {'post': post})
def post_new(request): """新規投稿作成""" if request.method == "POST": form = PostForm(request.POST) if form.is_valid(): post = form.save(commit=False) # まだDBには保存しない post.author = request.user # ログインユーザーを著者とする # post.published_date = timezone.now() # 必要なら公開日を設定 post.save() # DBに保存 return redirect('blog:post_detail', pk=post.pk) # 保存後に詳細ページへリダイレクト else: form = PostForm() # GETリクエストの場合は空のフォーム return render(request, 'blog/post_edit.html', {'form': form})
def post_edit(request, pk): """投稿編集""" post = get_object_or_404(Post, pk=pk) if request.method == "POST": form = PostForm(request.POST, instance=post) # 更新対象のインスタンスを指定 if form.is_valid(): post = form.save(commit=False) # post.author = request.user # 必要なら著者情報を更新 post.save() return redirect('blog:post_detail', pk=post.pk) else: form = PostForm(instance=post) # GETリクエストの場合は既存データで初期化されたフォーム return render(request, 'blog/post_edit.html', {'form': form})
def api_posts(request): """JSONレスポンスを返す例""" posts = Post.objects.filter(published_date__lte=timezone.now()).values('title', 'published_date', 'author__username') data = list(posts) # QuerySetをリストに変換 return JsonResponse(data, safe=False) # safe=False はリストを許可するため
クラスベースビュー (CBV)
ビューをクラスとして定義し、継承を利用して再利用性を高めます。汎用ビューが多数提供されています。
# blog/views.py (CBV の例)
from django.views.generic import ListView, DetailView, CreateView, UpdateView, DeleteView, TemplateView
from django.urls import reverse_lazy
from .models import Post
from .forms import PostForm
from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin # アクセス制御
class IndexView(TemplateView): """静的なテンプレートを表示""" template_name = 'blog/index.html' def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context['latest_posts'] = Post.objects.filter(published_date__lte=timezone.now()).order_by('-published_date')[:5] context['site_message'] = "ようこそ!" return context
class PostListView(ListView): """投稿一覧 (ListView)""" model = Post # 対象モデルを指定 (または get_queryset をオーバーライド) template_name = 'blog/post_list_cbv.html' # デフォルトは /_list.html context_object_name = 'posts' # テンプレートで使う変数名 (デフォルトは object_list) paginate_by = 10 # ページネーションを有効にする (1ページあたりの件数) def get_queryset(self): # 必要に応じてクエリセットをカスタマイズ return Post.objects.filter(published_date__lte=timezone.now()).order_by('-published_date')
class PostDetailView(DetailView): """投稿詳細 (DetailView)""" model = Post template_name = 'blog/post_detail_cbv.html' # デフォルトは /_detail.html context_object_name = 'post' # デフォルトは object def get_queryset(self): # 非公開記事を考慮する場合など queryset = super().get_queryset() if self.request.user.is_staff: return queryset # スタッフは全て閲覧可能 return queryset.filter(published_date__lte=timezone.now())
class PostCreateView(LoginRequiredMixin, CreateView): """新規投稿作成 (CreateView)""" model = Post form_class = PostForm # 使用するフォームクラスを指定 (または fields を指定) # fields = ['title', 'text', 'category', 'tags'] # form_class を使わない場合 template_name = 'blog/post_form.html' # デフォルトは /_form.html success_url = reverse_lazy('blog:post_list') # 成功時のリダイレクト先 (遅延評価) def form_valid(self, form): # フォームが有効な場合の追加処理 form.instance.author = self.request.user # 著者をログインユーザーに設定 return super().form_valid(form)
class PostUpdateView(LoginRequiredMixin, UserPassesTestMixin, UpdateView): """投稿編集 (UpdateView)""" model = Post form_class = PostForm template_name = 'blog/post_form.html' # success_url は get_success_url() で動的に設定可能 # context_object_name = 'post' # デフォルトは object def get_success_url(self): # 編集成功後、編集した投稿の詳細ページにリダイレクト return reverse_lazy('blog:post_detail', kwargs={'pk': self.object.pk}) def test_func(self): # アクセス権限チェック (UserPassesTestMixin) post = self.get_object() return self.request.user == post.author or self.request.user.is_staff
class PostDeleteView(LoginRequiredMixin, UserPassesTestMixin, DeleteView): """投稿削除 (DeleteView)""" model = Post template_name = 'blog/post_confirm_delete.html' # デフォルトは /_confirm_delete.html success_url = reverse_lazy('blog:post_list') context_object_name = 'post' # デフォルトは object def test_func(self): # アクセス権限チェック post = self.get_object() return self.request.user == post.author or self.request.user.is_staff
その他の汎用クラスベースビュー:
FormView
: フォーム処理専用RedirectView
: 特定のURLにリダイレクト- 日付ベースのビュー (
ArchiveIndexView
,YearArchiveView
,MonthArchiveView
,WeekArchiveView
,DayArchiveView
,TodayArchiveView
,DateDetailView
)
Mixins:
LoginRequiredMixin
: ログイン必須にするUserPassesTestMixin
:test_func
メソッドでアクセス権限をチェックPermissionRequiredMixin
: 特定のパーミッションを持つユーザーのみ許可
テンプレート (Template)
HTMLを生成するためのDjangoのテンプレート言語システムです。
基本構文
- 変数:
{{ variable_name }}
- タグ:
{% tag_name [arguments] %}
- フィルター:
{{ variable_name|filter_name:[argument] }}
- コメント:
{# comment #}
または{% comment %} ... {% endcomment %}
変数へのアクセス
<!-- 辞書アクセス -->
{{ my_dict.key }}
<!-- リスト/タプルアクセス -->
{{ my_list.0 }}
<!-- オブジェクトの属性アクセス -->
{{ post.title }}
<!-- オブジェクトのメソッド呼び出し (引数なしのみ) -->
{{ post.get_absolute_url }}
{{ user.is_authenticated }}
よく使うタグ
タグ | 説明 | 例 |
---|---|---|
{% for ... in ... %} {% empty %} {% endfor %} | ループ処理。empty は対象が空の場合に表示。 | ループ変数: forloop.counter , forloop.counter0 , forloop.revcounter , forloop.revcounter0 , forloop.first , forloop.last , forloop.parentloop |
{% if ... %} {% elif ... %} {% else %} {% endif %} | 条件分岐。and , or , not , == , != , < , > , <= , >= , in , not in が使える。 |
|
{% url 'url_name' [arg1] [arg2] ... [kwarg1=value1] ... %} | URL を逆引きして生成。名前空間がある場合は 'app_name:url_name' のように指定。 |
|
{% static 'path/to/file' %} | 静的ファイル (CSS, JS, 画像など) への URL を生成。settings.STATIC_URL が先頭に付与される。 | テンプレートの先頭に {% load static %} が必要。 |
{% csrf_token %} | POST フォーム内に CSRF 対策用の隠しフィールドを挿入。セキュリティ上必須。 |
|
{% extends 'base.html' %} | テンプレートを継承。テンプレートの先頭に記述。 | {% extends 'base.html' %} |
{% block block_name %} {% endblock [block_name] %} | 継承元テンプレートで定義されたブロックを上書きする。 |
|
{% include 'partial.html' [with context_var=value] %} | 他のテンプレートファイルをインクルード(埋め込み)。 |
|
{% load custom_tags %} | カスタムテンプレートタグやフィルターをロードする。 | {% load blog_extras %} |
{% autoescape off/on %} ... {% endautoescape %} | ブロック内の自動 HTML エスケープを制御する (通常は OFF にしないこと)。 |
よく使うフィルター
フィルター | 説明 | 例 |
---|---|---|
date | 日付オブジェクトを指定された形式でフォーマット。 | {{ post.published_date|date:"Y年m月d日 H:i" }} |
time | 時間オブジェクトを指定された形式でフォーマット。 | {{ event_time|time:"H:i" }} |
timesince | 日付からの経過時間を表示 (例: “2時間前”)。 | {{ post.created_date|timesince }} ago |
timeuntil | 指定された日付までの残り時間を表示。 | {{ event_date|timeuntil }} left |
length | リストや文字列の長さを返す。 | {{ posts|length }} 件の投稿 |
lower / upper | 文字列を小文字 / 大文字に変換。 | {{ user.username|lower }} |
capfirst | 文字列の最初の文字を大文字にする。 | {{ title|capfirst }} |
title | 文字列をタイトルケース (各単語の先頭を大文字) にする。 | {{ book_title|title }} |
truncatechars | 文字列を指定した文字数で切り詰め、末尾に “…” を追加。 | {{ post.text|truncatechars:50 }} |
truncatewords | 文字列を指定した単語数で切り詰め、末尾に “…” を追加。 | {{ post.text|truncatewords:20 }} |
default | 変数が偽 (False, None, 空文字列, 空リストなど) の場合にデフォルト値を表示。 | {{ profile.bio|default:"自己紹介がありません" }} |
default_if_none | 変数が None の場合にデフォルト値を表示。 | {{ profile.website|default_if_none:"#" }} |
safe | HTML エスケープを無効にする (信頼できるコンテンツにのみ使用)。 | {{ html_content|safe }} |
escape | HTML 特殊文字をエスケープする (デフォルトで有効)。 | {{ user_input|escape }} |
linebreaks | プレーンテキストの改行を <p> や <br> タグに変換。 | {{ post.text|linebreaks }} |
linebreaksbr | プレーンテキストの改行を <br> タグに変換。 | {{ comment|linebreaksbr }} |
striptags | 文字列から全ての HTML/XML タグを除去。 | {{ html_content|striptags }} |
join | リストの要素を特定の文字列で連結。 | {{ tag_list|join:", " }} |
add | 値に引数を加算 (数値、文字列結合も可能)。 | {{ current_page|add:1 }} |
filesizeformat | バイト数を人間が読みやすい形式 (KB, MB, GB など) に変換。 | {{ file_size_bytes|filesizeformat }} |
pluralize | 値に応じて単語の単数形/複数形を出し分ける。 | {{ num_comments }} comment{{ num_comments|pluralize }} {{ num_apples }} apple{{ num_apples|pluralize:"s" }} / {{ num_boxes }} box{{ num_boxes|pluralize:"es" }} |
テンプレートの継承
共通レイアウトを base.html
などに定義し、各ページでそれを継承して差分のみを記述します。
<!-- base.html -->
<!DOCTYPE html>
<html>
<head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>{% block title %}My Site{% endblock %}</title> {% load static %} <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.4/css/bulma.min.css"> <link rel="stylesheet" href="{% static 'css/style.css' %}"> {% block extra_head %}{% endblock %}
</head>
<body> <nav class="navbar" role="navigation" aria-label="main navigation"> <!-- Navbar content --> </nav> <section class="section"> <div class="container"> {% block content %} <!-- Page specific content goes here --> {% endblock %} </div> </section> <footer class="footer"> <div class="content has-text-centered"> <p> {% block footer %} 2024 My Company. {% endblock %} </p> </div> </footer> {% block extra_js %}{% endblock %}
</body>
</html>
<!-- child_template.html -->
{% extends 'base.html' %}
{% load static %}
{% block title %}ページタイトル | {{ block.super }}{% endblock %}
{% block extra_head %}
<!-- このページ固有のヘッダー要素 -->
<link rel="stylesheet" href="{% static 'css/page_specific.css' %}">
{% endblock %}
{% block content %}
<h1 class="title">ページのメインコンテンツ</h1>
<p>ここに内容を記述します。</p>
{{ block.super }} <!-- 親テンプレートの content ブロックの内容を含める場合 -->
{% endblock %}
{% block extra_js %}
<!-- このページ固有の JavaScript -->
<script src="{% static 'js/page_specific.js' %}"></script>
{% endblock %}
フォーム (Form)
ユーザーからの入力を受け取り、バリデーション(検証)を行うための仕組みです。
フォームクラスの定義 (forms.py
)
forms.Form
または forms.ModelForm
を継承してフォームを定義します。
forms.Form
(汎用フォーム)
# myapp/forms.py
from django import forms
class ContactForm(forms.Form): name = forms.CharField(label='お名前', max_length=100, required=True) email = forms.EmailField(label='メールアドレス', required=True) message = forms.CharField(label='メッセージ', widget=forms.Textarea, required=True) agree = forms.BooleanField(label='規約に同意する', required=True) def clean_message(self): # 特定フィールドのカスタムバリデーション message = self.cleaned_data['message'] if len(message) < 10: raise forms.ValidationError("メッセージは10文字以上で入力してください。") return message def clean(self): # 複数フィールドにまたがるバリデーション cleaned_data = super().clean() # name = cleaned_data.get("name") # email = cleaned_data.get("email") # ... 特定の条件を確認 ... # if some_condition: # raise forms.ValidationError("フォーム全体のエラーメッセージ") return cleaned_data
forms.ModelForm
(モデル連携フォーム)
モデルに基づいてフォームを自動生成します。
# blog/forms.py
from django import forms
from .models import Post, Comment
class PostForm(forms.ModelForm): class Meta: model = Post # 対象のモデル fields = ('title', 'text', 'category', 'tags', 'published_date') # フォームに含めるフィールド # exclude = ('author', 'created_date') # 除外するフィールド widgets = { 'text': forms.Textarea(attrs={'rows': 10, 'cols': 40}), 'published_date': forms.DateTimeInput(attrs={'type': 'datetime-local'}), 'tags': forms.CheckboxSelectMultiple, # 多対多フィールドをチェックボックスで表示 } labels = { 'title': 'ブログタイトル', 'text': '記事本文', } help_texts = { 'tags': 'Ctrlキー(MacではCommandキー)を押しながら選択すると複数選択できます。', } error_messages = { 'title': { 'max_length': "タイトルが長すぎます。", }, } # ModelForm でも clean_<fieldname> や clean メソッドは使用可能 def clean_title(self): title = self.cleaned_data['title'] if '禁止語' in title: raise forms.ValidationError("タイトルに禁止語が含まれています。") return title
class CommentForm(forms.ModelForm): class Meta: model = Comment fields = ('author_name', 'text')
主なフォームフィールド
forms.py
で使用するフィールドタイプ(モデルフィールドと似ていますが、別物です)。
CharField
,EmailField
,URLField
,IntegerField
,FloatField
,DecimalField
,DateField
,DateTimeField
,TimeField
,BooleanField
,ChoiceField
,MultipleChoiceField
,FileField
,ImageField
,UUIDField
,SlugField
,RegexField
,TypedChoiceField
,ModelChoiceField
,ModelMultipleChoiceField
共通の引数:
required
: 必須項目か (デフォルト: True)label
: フォームでの表示名initial
: 初期値widget
: HTMLでの表示形式 (ウィジェット)help_text
: ヘルプテキストerror_messages
: エラーメッセージのカスタマイズvalidators
: バリデータ関数のリストdisabled
: フィールドを無効化するか
主なウィジェット (Widgets)
フォームフィールドのHTML表現を制御します。
TextInput
,NumberInput
,EmailInput
,URLInput
,PasswordInput
,HiddenInput
Textarea
DateInput
,DateTimeInput
,TimeInput
CheckboxInput
,CheckboxSelectMultiple
Select
,SelectMultiple
,RadioSelect
,NullBooleanSelect
FileInput
,ClearableFileInput
SplitDateTimeWidget
,SelectDateWidget
# ウィジェットの指定例
class SampleForm(forms.Form): name = forms.CharField(widget=forms.TextInput(attrs={'class': 'input', 'placeholder': '名前を入力'})) bio = forms.CharField(widget=forms.Textarea(attrs={'rows': 5, 'class': 'textarea'})) birth_date = forms.DateField(widget=forms.DateInput(attrs={'type': 'date', 'class': 'input'})) favorite_color = forms.ChoiceField( choices=[('R', 'Red'), ('G', 'Green'), ('B', 'Blue')], widget=forms.RadioSelect # ラジオボタンで表示 ) hobbies = forms.MultipleChoiceField( choices=[('M', 'Music'), ('S', 'Sports'), ('T', 'Travel')], widget=forms.CheckboxSelectMultiple # チェックボックスで表示 )
ビューでのフォーム処理
# views.py
from django.shortcuts import render, redirect
from .forms import ContactForm, PostForm
from .models import Post
def contact_view(request): if request.method == 'POST': form = ContactForm(request.POST) if form.is_valid(): # フォームデータが有効な場合の処理 name = form.cleaned_data['name'] email = form.cleaned_data['email'] message = form.cleaned_data['message'] # 例: メール送信など # send_mail(...) return redirect('contact_success') # 成功ページへリダイレクト else: form = ContactForm() # GET リクエストの場合、空のフォームを表示 return render(request, 'myapp/contact_form.html', {'form': form})
def post_new_view(request): if request.method == 'POST': # ファイルアップロードがある場合は request.FILES も渡す form = PostForm(request.POST, request.FILES or None) if form.is_valid(): post = form.save(commit=False) post.author = request.user post.save() form.save_m2m() # ManyToMany フィールドを保存 (commit=False の場合) return redirect('blog:post_detail', pk=post.pk) else: form = PostForm() return render(request, 'blog/post_edit.html', {'form': form})
テンプレートでのフォーム表示
<!-- contact_form.html -->
{% extends 'base.html' %}
{% block content %} <h1 class="title">お問い合わせ</h1> <form method="post" enctype="multipart/form-data"> {# ファイルアップロードがある場合は enctype を指定 #} {% csrf_token %} <!-- フォーム全体のエラー --> {% if form.non_field_errors %} <div class="notification is-danger"> {{ form.non_field_errors }} </div> {% endif %} <!-- 各フィールドの表示方法 --> <!-- 1. 自動レンダリング --> <!-- {{ form.as_p }} --> <!-- 各フィールドを <p> タグで囲む --> <!-- {{ form.as_ul }} --> <!-- 各フィールドを <li> タグで囲む (ul タグで囲む必要あり) --> <!-- {{ form.as_table }} --> <!-- 各フィールドを <tr> タグで囲む (table タグで囲む必要あり) --> <!-- 2. 手動レンダリング (Bulma を使う例) --> {% for field in form %} <div class="field"> <label class="label" for="{{ field.id_for_label }}">{{ field.label }}</label> <div class="control"> {{ field }} {# フィールド本体のウィジェットを描画 #} {% if field.field.widget.input_type == 'checkbox' %} {# チェックボックスの場合はラベルを横に配置するなど調整が必要な場合がある #} {% endif %} </div> {% if field.help_text %} <p class="help">{{ field.help_text|safe }}</p> {% endif %} {% if field.errors %} <p class="help is-danger">{{ field.errors }}</p> {% endif %} </div> {% endfor %} <!-- 特定のフィールドだけ表示 --> <!-- <div class="field"> <label class="label" for="{{ form.name.id_for_label }}">{{ form.name.label }}</label> <div class="control"> {{ form.name }} </div> {% if form.name.errors %} <p class="help is-danger">{{ form.name.errors }}</p> {% endif %} </div> --> <!-- 隠しフィールド --> {% for hidden_field in form.hidden_fields %} {{ hidden_field }} {% endfor %} <div class="field is-grouped"> <div class="control"> <button type="submit" class="button is-link">送信</button> </div> </div> </form>
{% endblock %}
管理サイト (Admin Site)
Django が自動生成する高機能なデータ管理インターフェースです。
管理サイトへのモデル登録
各アプリケーションの admin.py
ファイルで、管理サイトに表示・操作したいモデルを登録します。
# blog/admin.py
from django.contrib import admin
from .models import Post, Category, Tag
# シンプルな登録
admin.site.register(Category)
admin.site.register(Tag)
# 表示や機能をカスタマイズする場合 (ModelAdmin を使用)
@admin.register(Post) # デコレータを使う方法
class PostAdmin(admin.ModelAdmin): list_display = ('title', 'category', 'author', 'published_date', 'created_date', 'was_published_recently') # 一覧表示するフィールド list_filter = ('published_date', 'category', 'author') # フィルタリング項目 search_fields = ('title', 'text') # 検索対象フィールド date_hierarchy = 'published_date' # 日付ベースのドリルダウンナビゲーション ordering = ('-published_date',) # デフォルトの並び順 # list_editable = ('category',) # 一覧画面で直接編集可能なフィールド (list_display に含まれている必要あり、最初の列は不可) list_per_page = 20 # 1ページあたりの表示件数 readonly_fields = ('created_date',) # 編集画面で読み取り専用にするフィールド fieldsets = ( # 編集画面のフィールドをグループ化 (None, { 'fields': ('title', 'text') }), ('メタ情報', { 'fields': ('author', 'category', 'tags', 'published_date', 'created_date'), 'classes': ('collapse',) # デフォルトで折りたたむ }), ) filter_horizontal = ('tags',) # ManyToMany フィールドを使いやすい選択インターフェースで表示 # カスタムアクションの追加 actions = ['make_published', 'make_unpublished'] def make_published(self, request, queryset): queryset.update(published_date=timezone.now()) make_published.short_description = "選択された投稿を公開済みにする" def make_unpublished(self, request, queryset): queryset.update(published_date=None) make_unpublished.short_description = "選択された投稿を非公開にする" # list_display でモデルメソッドやカスタム表示を追加 def was_published_recently(self, obj): now = timezone.now() return now - datetime.timedelta(days=1) <= obj.published_date <= now if obj.published_date else False was_published_recently.admin_order_field = 'published_date' # 並び替えを可能にする was_published_recently.boolean = True # アイコン表示 (True/False) was_published_recently.short_description = '最近公開?'
# admin.site.register(Post, PostAdmin) # デコレータを使わない場合の登録方法
settings.py
の INSTALLED_APPS
に 'django.contrib.admin'
が含まれていることを確認してください。
管理サイトの URL は、プロジェクトの urls.py
で設定されています (通常は /admin/
)。
管理サイトにアクセスするには、スーパーユーザーアカウントが必要です (python manage.py createsuperuser
で作成)。
ModelAdmin の主なオプション
オプション | 説明 |
---|---|
list_display | 一覧ページに表示するフィールド名のタプル。モデルメソッド名も指定可能。 |
list_display_links | 一覧ページで変更ページへのリンクとなるフィールド名のタプル。 |
list_filter | 一覧ページに表示するフィルター項目(フィールド名やRelatedOnlyFieldListFilter など)。 |
search_fields | 一覧ページで検索対象とするフィールド名のタプル。関連フィールドも'foreign_key__related_field' のように指定可能。 |
date_hierarchy | 一覧ページ上部に表示される日付ドリルダウンナビゲーションのフィールド名(DateField または DateTimeField)。 |
ordering | 一覧ページのデフォルトの並び順を指定するフィールド名のタプル。 |
list_editable | 一覧ページで直接編集できるフィールド名のタプル。list_display に含まれ、list_display_links に含まれない必要がある。 |
list_per_page | 一覧ページの1ページあたりの表示件数。 |
readonly_fields | 変更ページで読み取り専用にするフィールド名のタプル。モデルメソッド名も指定可能。 |
fields | 変更ページに表示するフィールドの順序を指定するタプル。fieldsets と同時には使用できない。 |
fieldsets | 変更ページでフィールドをグループ化して表示するためのタプル。fields と同時には使用できない。 |
filter_horizontal | ManyToManyフィールドを水平フィルターインターフェースで表示するフィールド名のタプル。 |
filter_vertical | ManyToManyフィールドを垂直フィルターインターフェースで表示するフィールド名のタプル。 |
raw_id_fields | ForeignKeyやManyToManyフィールドを入力欄と参照ポップアップで表示するフィールド名のタプル(大量の選択肢がある場合に便利)。 |
radio_fields | ForeignKeyまたはchoicesが設定されたフィールドをラジオボタンで表示するための辞書。例: {'status': admin.VERTICAL} または admin.HORIZONTAL 。 |
prepopulated_fields | 他のフィールドの値に基づいて自動的に入力されるフィールド(通常はスラッグフィールド)を指定する辞書。例: {'slug': ('title',)} 。 |
inlines | 関連モデルを同じ変更ページ内で編集可能にするためのインラインクラスのリスト(admin.TabularInline または admin.StackedInline )。 |
actions | 一覧ページで選択したオブジェクトに対して実行できるカスタムアクションのリスト。 |
save_on_top | 変更ページの上部にも保存ボタンを表示するかどうか (True/False)。 |
インラインモデル (Inline Models)
関連するモデルを親モデルの編集ページ内で同時に編集できるようにします。
# blog/admin.py
from django.contrib import admin
from .models import Post, Comment
class CommentInline(admin.TabularInline): # または admin.StackedInline model = Comment extra = 1 # 新規追加用の空フォームの数 readonly_fields = ('created_date',)
@admin.register(Post)
class PostAdmin(admin.ModelAdmin): # ... (他の設定) ... inlines = [CommentInline] # Post の編集ページに Comment のインラインを追加
静的ファイルとメディアファイル
静的ファイル (Static Files)
CSS, JavaScript, 画像ファイルなど、アプリケーションに付属する固定ファイル。
設定 (settings.py
)
# settings.py
import os
# BASE_DIR = ... (プロジェクトルート)
# 静的ファイルの URL (テンプレート {% static %} タグで使用)
# 例: /static/css/style.css
STATIC_URL = '/static/'
# `collectstatic` コマンドで静的ファイルを集めるディレクトリ (デプロイ用)
# 本番環境ではウェブサーバーがこのディレクトリを提供することが多い
STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles')
# アプリケーション固有でない静的ファイルを置くディレクトリ (プロジェクト共通のCSSなど)
# (デフォルトでは設定不要なことが多い)
STATICFILES_DIRS = [ os.path.join(BASE_DIR, 'static'), # プロジェクトルートの 'static' ディレクトリ
]
# 静的ファイルを探すためのファインダー (通常はデフォルトでOK)
STATICFILES_FINDERS = [ 'django.contrib.staticfiles.finders.FileSystemFinder', 'django.contrib.staticfiles.finders.AppDirectoriesFinder',
]
# settings.py で DEBUG = True の場合、Django 開発サーバーが STATIC_URL 以下のファイルを提供
# (django.contrib.staticfiles が INSTALLED_APPS に必要)
配置場所
- 各アプリケーション内の
static/<app_name>/
ディレクトリ (例:myapp/static/myapp/style.css
) STATICFILES_DIRS
で指定されたディレクトリ (例:project_root/static/global.css
)
テンプレートでの利用
{% load static %}
<link rel="stylesheet" href="{% static 'myapp/css/style.css' %}">
<script src="{% static 'js/script.js' %}"></script>
<img src="{% static 'images/logo.png' %}" alt="Logo">
デプロイ時の収集
本番環境にデプロイする前に、以下のコマンドを実行して静的ファイルを STATIC_ROOT
に集めます。
python manage.py collectstatic
メディアファイル (Media Files)
ユーザーがアップロードするファイル (例: プロフィール画像、添付ファイル)。
設定 (settings.py
)
# settings.py
import os
# BASE_DIR = ...
# アップロードされたファイルが保存されるファイルシステムの絶対パス
# (書き込み権限が必要)
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
# MEDIA_ROOT に保存されたファイルにアクセスするための URL
# 例: /media/user_uploads/profile.jpg
MEDIA_URL = '/media/'
モデルでの利用 (models.py
)
from django.db import models
class UserProfile(models.Model): user = models.OneToOneField(User, on_delete=models.CASCADE) avatar = models.ImageField(upload_to='avatars/', blank=True, null=True) # 'avatars/' は MEDIA_ROOT 以下のサブディレクトリ resume = models.FileField(upload_to='resumes/%Y/%m/%d/', blank=True, null=True) # upload_to は動的パスも可能
開発環境での配信設定 (プロジェクトの urls.py
)
開発サーバーでメディアファイルを提供するには、以下の設定を追加します (本番環境ではウェブサーバーが担当)。
# myproject/urls.py
from django.conf import settings
from django.conf.urls.static import static
# ... urlpatterns = [...] ...
if settings.DEBUG: urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
テンプレートでの利用
{% if user_profile.avatar %} <img src="{{ user_profile.avatar.url }}" alt="{{ user_profile.user.username }}'s avatar" width="{{ user_profile.avatar.width }}" height="{{ user_profile.avatar.height }}">
{% endif %}
{% if user_profile.resume %} <a href="{{ user_profile.resume.url }}">履歴書をダウンロード</a>
{% endif %}
注意: 本番環境では、MEDIA_ROOT
と MEDIA_URL
をウェブサーバー (Nginx, Apache など) が適切に提供するように設定する必要があります。
認証 (Authentication)
Django に組み込まれているユーザー認証システムを利用する方法です。
settings.py
の INSTALLED_APPS
に 'django.contrib.auth'
と 'django.contrib.contenttypes'
が必要です。
組み込みビューと URL
Django は一般的な認証機能(ログイン、ログアウト、パスワード変更・リセットなど)のためのビューとURLを提供しています。
プロジェクトの urls.py
に以下を追加することで利用できます:
# myproject/urls.py
from django.urls import path, include
urlpatterns = [ # ... 他のURL ... path('accounts/', include('django.contrib.auth.urls')), # これにより以下の URL が有効になる (要テンプレート作成) # accounts/login/ [name='login'] # accounts/logout/ [name='logout'] # accounts/password_change/ [name='password_change'] # accounts/password_change/done/ [name='password_change_done'] # accounts/password_reset/ [name='password_reset'] # accounts/password_reset/done/ [name='password_reset_done'] # accounts/reset/<uidb64>/<token>/ [name='password_reset_confirm'] # accounts/reset/done/ [name='password_reset_complete']
]
settings.py
でリダイレクト先などを設定できます:
# settings.py
LOGIN_URL = '/accounts/login/' # @login_required などでリダイレクトされるデフォルトのログインURL
LOGIN_REDIRECT_URL = '/' # ログイン成功後のリダイレクト先
LOGOUT_REDIRECT_URL = '/' # ログアウト後のリダイレクト先 (デフォルトは /accounts/logout/)
テンプレートの作成
上記の組み込みビューは、特定のパスにテンプレートが存在することを期待します。templates/registration/
ディレクトリ以下に以下のファイルを作成します。
login.html
logged_out.html
password_change_form.html
password_change_done.html
password_reset_form.html
password_reset_done.html
password_reset_email.html
(パスワードリセットメールの本文)password_reset_subject.txt
(パスワードリセットメールの件名)password_reset_confirm.html
password_reset_complete.html
例: templates/registration/login.html
{% extends 'base.html' %}
{% block title %}ログイン{% endblock %}
{% block content %} <h2 class="title">ログイン</h2> {% if form.errors %} <p class="notification is-danger">ユーザー名またはパスワードが正しくありません。</p> {% endif %} {% if next %} {% if user.is_authenticated %} <p class="notification is-warning">アクセス権限がありません。別のアカウントでログインしてください。</p> {% else %} <p class="notification is-info">このページにアクセスするにはログインしてください。</p> {% endif %} {% endif %} <form method="post" action="{% url 'login' %}"> {% csrf_token %} <div class="field"> <label class="label">{{ form.username.label_tag }}</label> <div class="control"> {{ form.username }} </div> </div> <div class="field"> <label class="label">{{ form.password.label_tag }}</label> <div class="control"> {{ form.password }} </div> </div> <div class="field"> <div class="control"> <button type="submit" class="button is-success">ログイン</button> </div> </div> <input type="hidden" name="next" value="{{ next }}"> {# リダイレクト先があれば #} </form> <p><a href="{% url 'password_reset' %}">パスワードをお忘れですか?</a></p> <!-- <p><a href="{% url 'signup' %}">アカウント作成</a></p> --> {# サインアップ機能は別途実装 #}
{% endblock %}
ビューでの利用
ログイン状態の確認や、ログイン必須の制御を行います。
from django.contrib.auth.decorators import login_required, permission_required
from django.contrib.auth.mixins import LoginRequiredMixin, PermissionRequiredMixin
from django.shortcuts import render
# 関数ベースビューでのログイン必須
@login_required # デフォルトでは settings.LOGIN_URL へリダイレクト
# @login_required(login_url='/custom/login/') # ログインURL を指定
def my_protected_view(request): # このビューはログインユーザーのみアクセス可能 return render(request, 'myapp/protected.html')
# 特定のパーミッションが必要なビュー
@permission_required('myapp.can_publish', raise_exception=True) # raise_exception=True は 403 Forbidden を返す
# @permission_required('myapp.can_view_reports', login_url='/accounts/login/')
def reports_view(request): # 'myapp.can_publish' パーミッションを持つユーザーのみアクセス可能 return render(request, 'myapp/reports.html')
# クラスベースビューでのログイン必須
class MyProtectedCBV(LoginRequiredMixin, TemplateView): template_name = 'myapp/protected_cbv.html' # login_url = '/custom/login/' # カスタムログインURL # redirect_field_name = 'next_page' # リダイレクトパラメータ名
# クラスベースビューでのパーミッションチェック
class ReportCBV(PermissionRequiredMixin, ListView): model = Report template_name = 'myapp/reports_cbv.html' permission_required = 'myapp.can_view_reports' # permission_denied_message = 'アクセス権がありません。' # raise_exception = True
テンプレートでの利用
ユーザー情報やログイン状態に応じて表示を切り替えます。
<!-- base.html など -->
{% if user.is_authenticated %} <p>ようこそ, {{ user.username }} さん! (<a href="{% url 'logout' %}">ログアウト</a>)</p> {% if user.is_staff %} <p><a href="/admin/">管理サイト</a></p> {% endif %} {% if perms.myapp.can_publish %} <p><a href="{% url 'myapp:publish' %}">公開権限あり</a></p> {% endif %}
{% else %} <p><a href="{% url 'login' %}?next={{ request.path }}">ログイン</a> | <a href="{% url 'signup' %}">サインアップ</a></p>
{% endif %}
<!-- request オブジェクトから直接アクセス -->
{% if request.user.is_authenticated %} <p>ログイン中です。</p>
{% endif %}
ユーザー登録 (サインアップ)
Django の組み込みにはサインアップビューは含まれていないため、自作する必要があります。UserCreationForm
を使うのが一般的です。
# accounts/forms.py
from django.contrib.auth.forms import UserCreationForm
from django.contrib.auth.models import User
class SignUpForm(UserCreationForm): email = forms.EmailField(max_length=254, help_text='必須。有効なメールアドレスを入力してください。') class Meta(UserCreationForm.Meta): model = User fields = ('username', 'email') # デフォルトは username, password1, password2
# accounts/views.py
from django.shortcuts import render, redirect
from .forms import SignUpForm
from django.contrib.auth import login
def signup(request): if request.method == 'POST': form = SignUpForm(request.POST) if form.is_valid(): user = form.save() login(request, user) # 登録後に自動ログインさせる場合 return redirect('/') # 登録後のリダイレクト先 else: form = SignUpForm() return render(request, 'registration/signup.html', {'form': form})
# accounts/urls.py
from django.urls import path
from . import views
urlpatterns = [ path('signup/', views.signup, name='signup'),
]
# プロジェクトの urls.py で include する
# path('accounts/', include('accounts.urls')), # django.contrib.auth.urls の前に置くなど調整が必要な場合あり
<!-- templates/registration/signup.html -->
{% extends 'base.html' %}
{% block title %}サインアップ{% endblock %}
{% block content %} <h2 class="title">アカウント作成</h2> <form method="post"> {% csrf_token %} {{ form.as_p }} <button type="submit" class="button is-primary">登録する</button> </form>
{% endblock %}
テスト (Testing)
Django アプリケーションのコード品質を保証するためのテスト作成と実行方法です。
テストの基本構造
各アプリケーションの tests.py
ファイル、または tests
ディレクトリ内にテストケースを記述します。
unittest
モジュールベースの django.test.TestCase
クラスを継承するのが一般的です。各テストメソッドは独立したトランザクション内で実行され、テスト後にデータベースはロールバックされます。
# myapp/tests.py
from django.test import TestCase, Client # Client は HTTP リクエストをシミュレート
from django.urls import reverse
from django.utils import timezone
from .models import Question, Choice
from django.contrib.auth.models import User
def create_question(question_text, days): """指定された日数オフセットで Question を作成するヘルパー関数""" time = timezone.now() + datetime.timedelta(days=days) return Question.objects.create(question_text=question_text, pub_date=time)
class QuestionModelTests(TestCase): def test_was_published_recently_with_future_question(self): """未来の日付の Question は was_published_recently() が False を返すか""" time = timezone.now() + datetime.timedelta(days=30) future_question = Question(pub_date=time) self.assertIs(future_question.was_published_recently(), False) def test_was_published_recently_with_old_question(self): """1日以上前の Question は False を返すか""" 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): """最近の Question は True を返すか""" 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)
class QuestionIndexViewTests(TestCase): def test_no_questions(self): """Question がない場合に適切なメッセージが表示されるか""" response = self.client.get(reverse('polls:index')) # URL名を逆引き 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): """過去の Question がインデックスページに表示されるか""" create_question(question_text="Past question.", days=-30) response = self.client.get(reverse('polls:index')) self.assertQuerysetEqual( response.context['latest_question_list'], ['<Question: Past question.>'] ) def test_future_question(self): """未来の Question がインデックスページに表示されないか""" 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): """未来と過去の 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: Past question.>'] ) def test_two_past_questions(self): """複数の過去の Question が表示されるか""" create_question(question_text="Past question 1.", days=-30) create_question(question_text="Past question 2.", days=-5) response = self.client.get(reverse('polls:index')) self.assertQuerysetEqual( response.context['latest_question_list'], ['<Question: Past question 2.>', '<Question: Past question 1.>'] )
class QuestionDetailViewTests(TestCase): def test_future_question(self): """未来の Question の詳細ページは 404 を返すか""" future_question = create_question(question_text='Future question.', days=5) url = reverse('polls:detail', args=(future_question.id,)) response = self.client.get(url) self.assertEqual(response.status_code, 404) def test_past_question(self): """過去の Question の詳細ページが表示されるか""" past_question = create_question(question_text='Past Question.', days=-5) url = reverse('polls:detail', args=(past_question.id,)) response = self.client.get(url) self.assertContains(response, past_question.question_text)
class LoginRequiredViewTests(TestCase): def setUp(self): # テスト用のユーザーを作成 self.user = User.objects.create_user(username='testuser', password='password') self.protected_url = reverse('my_protected_view_url_name') # 保護されたビューのURL名 def test_access_protected_view_without_login(self): """未ログインで保護されたビューにアクセスするとリダイレクトされるか""" response = self.client.get(self.protected_url) self.assertEqual(response.status_code, 302) # リダイレクト self.assertRedirects(response, f"{reverse('login')}?next={self.protected_url}") def test_access_protected_view_with_login(self): """ログイン状態で保護されたビューにアクセスできるか""" self.client.login(username='testuser', password='password') # ログイン response = self.client.get(self.protected_url) self.assertEqual(response.status_code, 200) # 必要ならテンプレートの内容などを assertContains で確認 def test_post_to_protected_view_with_login(self): """ログイン状態で保護されたビューにPOSTリクエストを送れるか""" self.client.login(username='testuser', password='password') response = self.client.post(self.protected_url, {'data': 'value'}) # POST後の挙動に応じてアサーションを追加 (例: リダイレクト、ステータスコード) self.assertEqual(response.status_code, 200) # または 302 など
テストの実行
プロジェクトルートで以下のコマンドを実行します。
# 全てのテストを実行
python manage.py test
# 特定のアプリケーションのテストを実行
python manage.py test myapp
# 特定のテストクラスを実行
python manage.py test myapp.tests.QuestionModelTests
# 特定のテストメソッドを実行
python manage.py test myapp.tests.QuestionModelTests.test_was_published_recently_with_future_question
# カバレッジ計測 (coverage パッケージが必要)
coverage run manage.py test
coverage report
coverage html # HTMLレポート生成
テストクライアント (django.test.Client
)
HTTP リクエスト (GET, POST など) をシミュレートし、ビューの動作をテストします。
client.get(path, data=None, follow=False, **extra)
client.post(path, data=None, content_type='application/octet-stream', follow=False, **extra)
client.put()
,client.patch()
,client.delete()
,client.head()
,client.options()
,client.trace()
client.login(username=..., password=...)
: テストユーザーでログインclient.logout()
: ログアウトclient.session
: セッションデータへのアクセスclient.cookies
: クッキーへのアクセス
主なアサーションメソッド
TestCase
クラスで利用可能なアサーションメソッドの一部です。
assertEqual(a, b)
,assertNotEqual(a, b)
assertTrue(x)
,assertFalse(x)
assertIs(a, b)
,assertIsNot(a, b)
assertIsNone(x)
,assertIsNotNone(x)
assertIn(a, b)
,assertNotIn(a, b)
assertIsInstance(a, b)
,assertNotIsInstance(a, b)
assertRaises(exc, func, *args, **kwds)
: 特定の例外が発生するかassertContains(response, text, status_code=200)
: レスポンスに特定のテキストが含まれるかassertNotContains(response, text, status_code=200)
assertQuerysetEqual(qs, values, transform=repr, ordered=True)
: クエリセットの内容が期待通りかassertTemplateUsed(response, template_name)
: 特定のテンプレートが使用されたかassertRedirects(response, expected_url, status_code=302, target_status_code=200)
: 正しくリダイレクトされるかassertFormError(response, form, field, errors)
: フォームの特定フィールドに特定のエラーがあるか
その他のテストユーティリティ
django.test.RequestFactory
: リクエストオブジェクトを直接作成(ミドルウェアやコンテキストプロセッサを通さない)override_settings
デコレータ/コンテキストマネージャ: テスト中に一時的に設定を変更modify_settings
デコレータ/コンテキストマネージャ: リストや辞書型設定の一部を変更・追加・削除- テスト用のデータベース設定 (
settings.py
のDATABASES['default']['TEST']
) - フィクスチャのロード (
TestCase.fixtures
属性) – 初期データ投入(現在は Factory Boy などのライブラリが推奨されることが多い)
その他
ミドルウェア (Middleware)
リクエスト/レスポンス処理の間にフックを挿入する仕組み。認証、セッション、CSRF保護などがミドルウェアとして実装されています。
settings.py
の MIDDLEWARE
リストで有効化・順序を定義します。
# settings.py
MIDDLEWARE = [ 'django.middleware.security.SecurityMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', # セッション 'django.middleware.common.CommonMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', # CSRF 保護 'django.contrib.auth.middleware.AuthenticationMiddleware', # 認証 (request.user を追加) 'django.contrib.messages.middleware.MessageMiddleware', # メッセージフレームワーク 'django.middleware.clickjacking.XFrameOptionsMiddleware', # ... カスタムミドルウェア ... # 'myapp.middleware.MyCustomMiddleware',
]
カスタムミドルウェアの作成:
# myapp/middleware.py
import time
class TimingMiddleware: def __init__(self, get_response): self.get_response = get_response # One-time configuration and initialization. def __call__(self, request): # Code to be executed for each request before # the view (and later middleware) are called. start_time = time.time() response = self.get_response(request) # Code to be executed for each request/response after # the view is called. duration = time.time() - start_time print(f"View took {duration:.3f} seconds") response['X-View-Timing'] = f"{duration:.3f}" # レスポンスヘッダーに追加 return response
# シンプルな関数ベースミドルウェア
def simple_middleware(get_response): # One-time configuration and initialization. print("Simple Middleware Initialized") def middleware(request): # Code to be executed for each request before # the view (and later middleware) are called. print("Before view in Simple Middleware") response = get_response(request) # Code to be executed for each request/response after # the view is called. print("After view in Simple Middleware") return response return middleware
ミドルウェアフック (クラスベース):
process_request(request)
: (非推奨) ビューが呼ばれる直前 (ルーティング解決後)process_view(request, view_func, view_args, view_kwargs)
: ビューが呼ばれる直前。HttpResponse
を返すと以降の処理を中断。process_template_response(request, response)
: ビューがテンプレートレンダリングを含むレスポンス (TemplateResponse
) を返した場合。process_response(request, response)
: ビューが呼ばれた後、レスポンスがブラウザに返される直前。process_exception(request, exception)
: ビューで例外が発生した場合。HttpResponse
を返すとデフォルトの例外処理を置き換え。
コンテキストプロセッサ (Context Processors)
全てのリクエストでテンプレートコンテキストに自動的に追加される変数を定義する関数。
settings.py
の TEMPLATES
設定内の 'OPTIONS'
> 'context_processors'
リストで定義します。
# settings.py
TEMPLATES = [ { 'BACKEND': 'django.template.backends.django.DjangoTemplates', 'DIRS': [os.path.join(BASE_DIR, 'templates')], 'APP_DIRS': True, 'OPTIONS': { 'context_processors': [ 'django.template.context_processors.debug', 'django.template.context_processors.request', # request オブジェクトをコンテキストに追加 'django.contrib.auth.context_processors.auth', # user, perms を追加 'django.contrib.messages.context_processors.messages', # messages を追加 'django.template.context_processors.static', # STATIC_URL 'django.template.context_processors.media', # MEDIA_URL # ... カスタムコンテキストプロセッサ ... # 'myapp.context_processors.site_settings', ], }, },
]
# myapp/context_processors.py
from .models import SiteSetting
def site_settings(request): # 全テンプレートでサイト設定を使えるようにする例 try: settings = SiteSetting.objects.get(pk=1) # 実際には適切な方法で設定を取得 except SiteSetting.DoesNotExist: settings = None return {'site_settings': settings}
これにより、どのテンプレートでも {{ site_settings.site_name }}
のようにアクセスできます。
シグナル (Signals)
Django 内の特定のアクション(モデルの保存、リクエストの開始/終了など)が発生した際に、他のアプリケーションに通知を送受信する仕組み(デカップリング)。
主な組み込みシグナル:
- モデル関連:
pre_init
,post_init
,pre_save
,post_save
,pre_delete
,post_delete
,m2m_changed
- リクエスト/レスポンス:
request_started
,request_finished
,got_request_exception
- テスト:
setting_changed
,template_rendered
シグナルレシーバー(受信関数)の定義と接続:
# myapp/signals.py
from django.db.models.signals import post_save
from django.dispatch import receiver
from django.contrib.auth.models import User
from .models import UserProfile
@receiver(post_save, sender=User) # User モデルが保存された後 (作成・更新時)
def create_or_update_user_profile(sender, instance, created, **kwargs): if created: UserProfile.objects.create(user=instance) print(f"Profile created for user {instance.username}") else: # 更新時の処理が必要ならここに記述 # instance.userprofile.save() # 関連オブジェクトも更新する場合など print(f"Profile possibly updated for user {instance.username}")
# myapp/apps.py でシグナルをインポートして接続を確立
from django.apps import AppConfig
class MyappConfig(AppConfig): default_auto_field = 'django.db.models.BigAutoField' name = 'myapp' def ready(self): import myapp.signals # シグナル定義ファイルをインポート
シグナルは便利ですが、多用するとコードの流れが追いにくくなることがあるため、注意が必要です。
メッセージフレームワーク (Messages Framework)
リクエスト間でユーザーに一度だけ表示される通知メッセージ(例: 「保存しました」)を管理する仕組み。
settings.py
に 'django.contrib.messages'
が INSTALLED_APPS
に、'django.contrib.messages.middleware.MessageMiddleware'
が MIDDLEWARE
に、'django.contrib.messages.context_processors.messages'
がコンテキストプロセッサに含まれている必要があります。
ビューでのメッセージ追加:
# views.py
from django.contrib import messages
def my_view(request): if request.method == 'POST': # ... 処理 ... messages.success(request, 'プロファイルが正常に更新されました。') # 他のレベル: debug, info, warning, error # messages.info(request, 'これは情報メッセージです。') # messages.warning(request, '注意してください。') # messages.error(request, 'エラーが発生しました。') return redirect('some_view_name') # ... return render(request, 'myapp/template.html')
テンプレートでのメッセージ表示:
<!-- base.html など -->
{% if messages %} {% for message in messages %} <div class="notification {% if message.tags %}is-{{ message.tags }}{% else %}is-info{% endif %}"> {# Bulma の is-* クラスにマッピング #} <button class="delete"></button> {# 閉じボタン (JSが必要) #} {{ message }} </div> {% endfor %}
{% endif %}
<!-- メッセージレベルのマッピング (settings.py) -->
{# from django.contrib.messages import constants as messages
MESSAGE_TAGS = { messages.DEBUG: 'light', messages.INFO: 'info', messages.SUCCESS: 'success', messages.WARNING: 'warning', messages.ERROR: 'danger',
} #}