1
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

DjangoのCRUD処理について

Posted at

はじめに

DjangoのCRUD処理について勉強した内容を記録に残しました。Djangoビギナーズブックを参考人させていただきました。余談ですが、本記事を書いている時点でプレミア価格になってました…!大変お世話になっているので納得です。電子版が欲しいなあ。

プロジェクトファイルを作成

下準備です。

$ django-admin startproject conf .
conf/settings.py
# 変更した部分のみ抜粋
LANGUAGE_CODE = 'ja'
TIME_ZONE = 'Asia/Tokyo'

homeアプリを作成

CRUD処理に入る前にホーム画面を表示するアプリを作ります。

$ python manage.py startapp home
conf/settings.py
INSTALLED_APPS = [
    'home.apps.HomeConfig', # これを追加

# 省略
TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [os.path.join(BASE_DIR, 'templates')], #ここを修正

ルーティングします。

conf/urls.py
from django.contrib import admin
from django.urls import path, include # 追加

urlpatterns = [
    path('', include('home.urls')), # 追加
    path('admin/', admin.site.urls),
]

home/urls.py
from  django.urls import path
from . import views

app_name = 'home'

urlpatterns = [
    path('', views.top, name='top')
]
home/views.py
from django.shortcuts import render


def top(request):
    return render(request, 'home/top.html')

htmlを作成します。まずはベースとなるファイルを作ります。

templates/base.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>
home/top.html
{% extends "base.html" %}

<!-- コンテンツ -->
{% block content %}
<div class="container">
    <h1>home</h1>
</div>
{% endblock %}

開発用サーバーを起動して、ブラウザからサーバーにアクセスすると、以下のようなページが表示されるはずです。
img_1.png

レビューアプリを作成

前置きが長くなりましたが、CRUD処理のためのアプリを作成します。ご飯屋さんをレビューするアプリをモチーフにします。

$ python manage.py startapp reviews
conf/settings.py
INSTALLED_APPS = [
    'home.apps.HomeConfig',
    'reviews.apps.ReviewsConfig', # これを追加
]
conf/urls.py
urlpatterns = [
    path('', include('home.urls')),
    path('reviews/', include('reviews.urls')), # 追加
]
reviews/urls.py
from django.urls import path
from . import views


app_name = 'reviews'

urlpatterns = [
]

モデルを作成します。店名とタイトルとレビュー本文、星の数、作成日時をフィールドとします。

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

管理サイトで表示できるようにします。

reviews/admin.py
from django.contrib import admin
from .models import Review


admin.site.register(Review)

データを追加

スーパーユーザーを作成します。

$ python manage.py createsuperuser

ブラウザから管理サイト(http://***/admin)を開き、ログインします。
img_2.png

適当にデータを追加します。
img_3.png

一覧と詳細ページ(Read)

一覧ページ(/)と詳細ページ(/detail/int)を作成します。int:pkは後でreview_list.htmlで指定することになります。

reviews/urls.py
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になるようにしています。

reviews/views.py
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を作成します。

templates/base.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のように埋め込みたいデータを書きます。

templates/review_list.html
{% 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を作成します。

templates/review_detail.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 %}

開発用サーバーを起動し、ブラウザでアクセスすると、以下のページが表示されます。
img_4.png

img_5.png

レビュー作成ページ(Create)

レビュー作成ページ(create/)を追加します。

reviews/urls.py
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'), # 参考:送信処理
]

レビュー作成と送信処理を一つのビューに書くと以下のようになります。

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

分かりやすくするため、レビュー作成ページの表示と送信後の処理を分けて書くと以下のようになります。

reviews/views.py

'''作成'''
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内の各入力欄、選択欄を自動的に生成する
  • 送信されてきたデータをまとめる
reviews/forms.py
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に作成ページへのリンクを追加します。

templates/reviews/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」がクリック後のアクセス先になります。こうすることでコードが再利用しやすくなります。

templates/reviews/review_form.html
{% 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 %}

実際の作成ページは以下のようになります。一覧ページに作成ページへのリンクが追加されています。

img_6.png

作成ページはこのような形です。(本当は作成日は入力可能です。。)

img_7.png

更新ページ(Update)

作成(Create)ができるようになったので、次は更新(Update)を実装します。

reviews/urls.py
urlpatterns = [
    # 省略
    path('update/<int:pk>/', views.review_update, name='review_update'), # 更新
]

関数ビューを追加します。ポイントは、作成(Create) で作ったreview_form.htmlを再利用している点です。また、3行目でReviewCreateFormをインスタンス化する際に、instance引数に、Reviewモデルインスタンスを渡しています。これによって、入力欄に現在の値(店名やレビュー本文など)が入った状態で表示されます。

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

更新ページへのリンクを、詳細ページに追加します。

templates/reviews/review_detail.html
<a href="{% url 'reviews:review_update' review.pk %}">更新</a>

forms.pyを少し変更します。作成日フィールド(created_at)を除外します。

reviews/forms.py
class ReviewCreateForm(forms.ModelForm):

    class Meta:
        model = Review
        #fields = '__all__'
        exclude = ('created_at',)

作成日は表示はしたいので、review_form.htmlを修正します。

templates/reviews/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>

詳細ページに更新ページへのリンクが追加されています。

img_8.png

リンクをクリックすると、更新ページになります。作成ページと同様にreview_form.htmlを使用しているのでデザインは同じですが、店名など現在の値が入っています。

img_9.png

削除(Delete)

読み込み(Read)、作成(Create)、更新(Update)ができたので、最後に削除(Delete)を実装します。まずはURL定義から。どのレビューを削除するか指定する必要があるので、pkが要ります。

reviews/urls.py
urlpatterns = [
    # 省略
    path('delete/<int:pk>/', viwes.review_delete, name='review_delete'), # 削除
]

続いて関数ビューを定義します。削除ボタンを押すと、リクエストメソッドはPOSTになるので、ifの中に入流ので、データを削除し、一覧ページにリダイレクトします(二重送信防止のため)。通常のアクセスでは削除確認用のページに遷移します。その際、Reviewモデルインスタンスをテンプレートファイルに渡します。

review/views.py
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要素を使っています。

templates/reviews/review_confirm_delete.html
{% 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 %}

詳細ページから削除確認ページへのリンクを貼ります。

templates/reviews/review_detail.html
<a href="{% url 'reviews:review_delete' review.pk %}">削除</a>

削除ページへのリンクが追加になっています。

img_10.png

削除リンクをクリックすると、削除確認ページへジャンプします。

img_11.png

1
2
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
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?