はじめに
DjangoのCRUD処理について勉強した内容を記録に残しました。Djangoビギナーズブックを参考人させていただきました。余談ですが、本記事を書いている時点でプレミア価格になってました…!大変お世話になっているので納得です。電子版が欲しいなあ。
プロジェクトファイルを作成
下準備です。
$ django-admin startproject conf .
# 変更した部分のみ抜粋
LANGUAGE_CODE = 'ja'
TIME_ZONE = 'Asia/Tokyo'
homeアプリを作成
CRUD処理に入る前にホーム画面を表示するアプリを作ります。
$ python manage.py startapp home
INSTALLED_APPS = [
'home.apps.HomeConfig', # これを追加
# 省略
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [os.path.join(BASE_DIR, 'templates')], #ここを修正
ルーティングします。
from django.contrib import admin
from django.urls import path, include # 追加
urlpatterns = [
path('', include('home.urls')), # 追加
path('admin/', admin.site.urls),
]
from django.urls import path
from . import views
app_name = 'home'
urlpatterns = [
path('', views.top, name='top')
]
from django.shortcuts import render
def top(request):
return render(request, 'home/top.html')
htmlを作成します。まずはベースとなるファイルを作ります。
{% load static %}
<!doctype html>
<html lang="ja">
<head>
<!-- Required meta tags -->
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<!-- 互換表示の解除 -->
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<!-- Bootstrap CSS -->
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">
<title>ユーザー登録・更新</title>
</head>
<body>
<!-- ナビバー -->
<nav class="navbar navbar-expand-lg navbar-dark bg-primary">
<a class="navbar-brand" href="{% url 'home:top' %}">テストサイト</a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarSupportedContent">
<ul class="navbar-nav mr-auto">
<li class="nav-item">
<a class="nav-link" href="{% url 'home:top' %}">Home</a>
</li>
</ul>
</div>
</nav>
<!-- コンテンツ -->
{% block content %}{% endblock %}
<!-- Optional JavaScript -->
<!-- jQuery first, then Popper.js, then Bootstrap JS -->
<script src="https://code.jquery.com/jquery-3.3.1.slim.min.js" integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.7/umd/popper.min.js" integrity="sha384-UO2eT0CpHqdSJQ6hJty5KVphtPhzWj9WO1clHTMGa3JDZwrnQq4sF86dIHNDz0W1" crossorigin="anonymous"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js" integrity="sha384-JjSmVgyd0p3pXB1rRibZUAYoIIy6OrQ6VrjIEaFf/nJGzIxFDsf4x0xIM+B07jRM" crossorigin="anonymous"></script>
</body>
</html>
{% extends "base.html" %}
<!-- コンテンツ -->
{% block content %}
<div class="container">
<h1>home</h1>
</div>
{% endblock %}
開発用サーバーを起動して、ブラウザからサーバーにアクセスすると、以下のようなページが表示されるはずです。
レビューアプリを作成
前置きが長くなりましたが、CRUD処理のためのアプリを作成します。ご飯屋さんをレビューするアプリをモチーフにします。
$ python manage.py startapp reviews
INSTALLED_APPS = [
'home.apps.HomeConfig',
'reviews.apps.ReviewsConfig', # これを追加
]
urlpatterns = [
path('', include('home.urls')),
path('reviews/', include('reviews.urls')), # 追加
]
from django.urls import path
from . import views
app_name = 'reviews'
urlpatterns = [
]
モデルを作成します。店名とタイトルとレビュー本文、星の数、作成日時をフィールドとします。
from django.db import models
from django.utils import timezone
class Review(models.Model):
STARS = (
(1, '★'),
(2, '★★'),
(3, '★★★'),
(4, '★★★★'),
(5, '★★★★★'),
)
store_name = models.CharField('店名', max_length=255)
title = models.TextField('タイトル', max_length=255)
text = models.TextField('口コミテキスト', blank=True)
stars = models.IntegerField('星の数', choices=STARS)
created_at = models.DateTimeField('作成日', default=timezone.now)
def __str__(self):
return self.title
モデルの定義が終わったら、マイグレーションします。
$ python manage.py makemigrations reviews
$ python manage.py migrate
管理サイトで表示できるようにします。
from django.contrib import admin
from .models import Review
admin.site.register(Review)
データを追加
スーパーユーザーを作成します。
$ python manage.py createsuperuser
ブラウザから管理サイト(http://***/admin)を開き、ログインします。
一覧と詳細ページ(Read)
一覧ページ(/)と詳細ページ(/detail/int)を作成します。int:pkは後でreview_list.htmlで指定することになります。
urlpatterns = [
path('', views.review_list, name='review_list'),
path('detail/<int:pk>/', views.review_detail, name='review_detail'),
]
views.pyを作成します。detail/intのintにデータベースのPrimary Keyに存在しない数字を入れたら、Not Foundになるようにしています。
from django.shortcuts import render, get_object_or_404
from .models import Review
def review_list(request):
context = {
'review_list': Review.objects.all().order_by('-created_at'),
}
return render(request, 'reviews/review_list.html', context)
def review_detail(request, pk): # pkはreview_idのこと
#review = Review.objects.get(id=pk) # pkはreview_idのこと
context = {
#'review': review,
'review': get_object_or_404(Review, pk=pk),
}
return render(request, 'reviews/review_detail.html', context)
htmlを作成します。
<div class="collapse navbar-collapse" id="navbarSupportedContent">
<ul class="navbar-nav mr-auto">
<li class="nav-item">
<a class="nav-link" href="{% url 'home:top' %}">Home</a>
</li>
<!-- レビューページへのリンク -->
<li class="nav-item">
<a class="nav-link" href="{% url 'reviews:review_list' %}">Reviews</a>
</li>
</ul>
</div>
review_list.html(一覧)を作成します。int:pkはurlタグで指定します。'reviews:review_detail'の後に半角スペースを入れて、review.pkのように埋め込みたいデータを書きます。
{% extends "base.html" %}
<!-- コンテンツ -->
{% block content %}
<div class="container">
<h1>Reviews</h1>
<hr>
{% for review in review_list %}
<article>
<h3><a href="{% url 'reviews:review_detail' review.pk %}">{{ review.store_name }}</a></h3>
<p>{{ review.get_stars_display }} - {{ review.title }}</p>
</article>
{% endfor %}
</div>
{% endblock %}
詳細ページのhtmlを作成します。
{% extends "base.html" %}
<!-- コンテンツ -->
{% block content %}
<div class="container">
<article>
<h1>{{ review.store_name }}</h1>
<p>{{ review.get_stars_display }} - {{ review.title }}</p>
{{ review.text | linebreaks }}
</article>
</div>
{% endblock %}
開発用サーバーを起動し、ブラウザでアクセスすると、以下のページが表示されます。
レビュー作成ページ(Create)
レビュー作成ページ(create/)を追加します。
urlpatterns = [
path('', views.review_list, name='review_list'),
path('detail/<int:pk>/', views.review_detail, name='review_detail'),
path('create/', views.review_create, name='review_create'), # 追加:作成ページと送信処理
#path('create/send/', views.review_create_send, name='review_create_send'), # 参考:送信処理
]
レビュー作成と送信処理を一つのビューに書くと以下のようになります。
from .forms import ReviewCreateForm # フォーム機能
from .models import Review
from django.shortcuts import render, get_object_or_404, redirect
# 省略
'''追加:作成&送信'''
def review_create(request):
form = ReviewCreateForm(request.POST or None)
# request.method == 'POST' : Submitボタンが押された場合
# form.is_valid() : 入力内容が問題ない
# 上記を両方満たせば、データを保存して一覧ページへリダイレクトする。
if request.method == 'POST' and form.is_valid():
form.save()
return redirect('reviews:review_list')
# 通常のページアクセス時はmethodがGETになる
# 入力内容に問題がある場合は、form.is_validがFalseになり、ReviewCreateFormにエラー情報が付与される
context = {
'form': ReviewCreateForm()
}
return render(request, 'reviews/review_form.html', context)
分かりやすくするため、レビュー作成ページの表示と送信後の処理を分けて書くと以下のようになります。
'''作成'''
def review_create(request):
context = {
'form': ReviewCreateForm()
}
return render(request, 'reviews/review_form.html', context)
'''送信ボタンを押したときの処理'''
def review_create_send(request):
#name = request.POST.get('store_name')
#print('送信されたデータ→{}'.format(name))
form = ReviewCreateForm(request.POST)
# 入力内容に問題が無いかチェック
if form.is_valid():
form.save()
return redirect('reviews:review_list')
#return render(request, 'reviews/review_list.html') # renderを使うと二重送信の恐れあり。
# 問題があれば、入力画面のテンプレートファイルにエラーメッセージ入りのformオブジェクトを渡す
else:
context = {
'form': form,
}
return render(request, 'reviews/review_form.html', context) # saveしてないので、renderでも二重送信の恐れはない。
forms.pyを作成します。フォームの役割は大まかに二つあります。
- form内の各入力欄、選択欄を自動的に生成する
- 送信されてきたデータをまとめる
from django import forms
from . models import Review
class ReviewCreateForm(forms.ModelForm):
class Meta:
model = Review
fields = '__all__'
#fields = ('store_name',)
#exclude = ('created_at',)
# bootstrap対応のため、classを指定する。
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
for field in self.fields.values():
field.widget.attrs['class'] = 'form-control'
review_list.htmlに作成ページへのリンクを追加します。
{% extends "base.html" %}
<!-- コンテンツ -->
{% block content %}
<div class="container">
<!-- 省略 -->
<a href="{% url 'reviews:review_create' %}">to create page</a>
</div>
{% endblock %}
レビュー作成ページのhtmlを作成します。action属性を空文字にした場合、「現在のURL=review/create」がクリック後のアクセス先になります。こうすることでコードが再利用しやすくなります。
{% extends "base.html" %}
<!-- コンテンツ -->
{% block content %}
<div class="container">
<!-- フォーム利用。 -->
<form class="form-group mt-2" action="" method="POST">
{{ form.as_p }}
{% csrf_token %}
<button type="submit" class="btn btn-primary ml-1">Submit</button>
</form>
</div>
{% endblock %}
実際の作成ページは以下のようになります。一覧ページに作成ページへのリンクが追加されています。
作成ページはこのような形です。(本当は作成日は入力可能です。。)
更新ページ(Update)
作成(Create)ができるようになったので、次は更新(Update)を実装します。
urlpatterns = [
# 省略
path('update/<int:pk>/', views.review_update, name='review_update'), # 更新
]
関数ビューを追加します。ポイントは、作成(Create) で作ったreview_form.htmlを再利用している点です。また、3行目でReviewCreateFormをインスタンス化する際に、instance引数に、Reviewモデルインスタンスを渡しています。これによって、入力欄に現在の値(店名やレビュー本文など)が入った状態で表示されます。
def review_update(request, pk):
review = get_object_or_404(Review, pk=pk)
form = ReviewCreateForm(request.POST or None, instance=review)
if request.method == 'POST' and form.is_valid():
form.save()
return redirect('reviews:review_list')
context = {
'form': form
}
return render(request, 'reviews/review_form.html', context)
更新ページへのリンクを、詳細ページに追加します。
<a href="{% url 'reviews:review_update' review.pk %}">更新</a>
forms.pyを少し変更します。作成日フィールド(created_at)を除外します。
class ReviewCreateForm(forms.ModelForm):
class Meta:
model = Review
#fields = '__all__'
exclude = ('created_at',)
作成日は表示はしたいので、review_form.htmlを修正します。
<form class="form-group mt-2" action="" method="POST">
{{ form.as_p }}
<p>作成日 : {{ form.instance.created_at }}</p>
{% csrf_token %}
<button type="submit" class="btn btn-primary ml-1">Submit</button>
</form>
詳細ページに更新ページへのリンクが追加されています。
リンクをクリックすると、更新ページになります。作成ページと同様にreview_form.htmlを使用しているのでデザインは同じですが、店名など現在の値が入っています。
削除(Delete)
読み込み(Read)、作成(Create)、更新(Update)ができたので、最後に削除(Delete)を実装します。まずはURL定義から。どのレビューを削除するか指定する必要があるので、pkが要ります。
urlpatterns = [
# 省略
path('delete/<int:pk>/', viwes.review_delete, name='review_delete'), # 削除
]
続いて関数ビューを定義します。削除ボタンを押すと、リクエストメソッドはPOSTになるので、ifの中に入流ので、データを削除し、一覧ページにリダイレクトします(二重送信防止のため)。通常のアクセスでは削除確認用のページに遷移します。その際、Reviewモデルインスタンスをテンプレートファイルに渡します。
def review_delete(request, pk):
review = get_object_or_404(Review, pk=pk)
if request.method == 'POST':
review.delete()
return redirect('reviews:review_list')
context = {
'review': review
}
return render(request, 'reviews/review_confirm_delete.html', context)
テンプレートファイルを作成します。削除するだけなので、ユーザーが入力する欄はありませんが、CSRF対策とリクエストメソッドをPOSTにするためform要素を使っています。
{% extends "base.html" %}
<!-- コンテンツ -->
{% block content %}
<div class="container">
<p>{{ review.store_name }}のレビューを削除します。よろしいですか?</p>
<form action="", method='POST'>
{% csrf_token %}
<button type="submit">削除</button>
</form>
</div>
{% endblock %}
詳細ページから削除確認ページへのリンクを貼ります。
<a href="{% url 'reviews:review_delete' review.pk %}">削除</a>
削除ページへのリンクが追加になっています。
削除リンクをクリックすると、削除確認ページへジャンプします。