Djangoチュートリアル5-CRUD操作-

Djangoチュートリアル5-CRUD操作-

Djangoチュートリアル5-CRUD操作-

この記事は部内での講習会用に作成された記事です。

前回のチュートリアルでは記事のモデルをDBに登録し、記事の表示機能を実装しました。
今回では記事の投稿・更新・削除などDB操作の基本であるCRUD機能を実装していきます。

前回のチュートリアルからの続きです!

Djangoチュートリアル4-モデルの定義-

DjangoでDBに記事を保存するためのモデルの定義方法について紹介します。


CRUDとは?

まずコードを書いていく前にCRUD操作について簡単に説明します。
簡単に言えばCRUD操作とはデータを扱うシステムにおける基本的な操作のことです。

CRUDは以下の4つの操作の頭文字を取ったものです。

  • Create(作成): 新しいデータを作成する操作
  • Read(読み取り): 既存のデータを取得する操作
  • Update(更新): 既存のデータを変更する操作
  • Delete(削除): 既存のデータを削除する操作

データベースの管理画面からこれらの操作は簡単に行うことができます。 ですが、利用するユーザーが直接管理画面を操作することはないです。

そのため記事などのデータを扱うWebアプリケーションでは、CRUDの実装は必須になります。

Djangoでは、CRUD操作を実装するための専用のビューやフォームが用意されています。
ですが今回はよりDB操作について理解するために、直接ORMを使ってCRUD操作を実装していきます。

Django側が用意している組み込みのビューを利用すれば記事のような単純なモデルであればもちろん簡単にCRUD操作を実現できます。 しかし、組み込みのビューではカスタマイズ性が低く、また他の言語やフレームワークとの共通項が少ないため、汎用的な理解にはあまり向いていません。 (サクッと実装したいときにはめちゃくちゃ便利です!)

CRUD操作の実装について

では実際にCRUD操作を実装していきます。
ですが、実は前回のチュートリアルですでに1つは実装しています。

前回のチュートリアルでは記事データの一覧・詳細表示機能を実装しました。
これはCRUD操作の中のRead操作にあたりますね。

よって今回は残りのCreateUpdateDeleteを実装していきます。


Create操作の実装

まずはCreateを実装していきます。
今回のCreateでは、記事の投稿機能を実装します。

DjangoでCreateを実装するには、一般的に以下2つの要素が必要です。

  1. フォーム: ユーザーが入力するためのモデル専用のフォーム
  2. ビュー: フォームの内容チェックやデータベースとのやり取り

これに加えて今回は投稿用のページが必要になるので、いつものページ追加の手順も行います。
ページの追加は以下の流れでしたね。(ビューは上のビューと同じものなので1つの作業になります)

    1. HTMLファイルの作成
    1. ビューの作成
    1. URLの設定

では、順番に実装していきましょう!

ちなみに今回のCRUDの実装は記事アプリの基本機能のため、appディレクトリに実装していきます。


HTMLファイルの用意

まずは記事投稿用ページ用のHTMLファイルを作成しましょう。

app/templates/appフォルダの中にcreate.htmlを作成します。(index.htmlと同じ位置です)

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <link
    href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css"
    rel="stylesheet"
    />
</head>
<body>
 
    <nav class="navbar bg-body-tertiary">
        <div class="container">
            <a class="navbar-brand" href="/">ブログサイト</a>
 
            <ul class="navbar-nav  flex-row gap-3">
                <li class="nav-item"><a class="nav-link" href="{% url 'signup' %}">新規登録</a></li>
                {% if user.is_authenticated  %}
                <li class="nav-item"><a class="nav-link" href="{% url 'mypage' user.username %}">マイページ</a></li>
                <li class="nav-item">
                    <form action="{% url 'logout' %}" method="post">
                        {% csrf_token %}
                        <button type="submit" class="btn btn-link nav-link">ログアウト</button>
                    </form>
                </li>
                
                {% else %}
                <li class="nav-item"><a class="nav-link" href="{% url 'login' %}">ログイン</a></li>
                {% endif %}
            </ul>
        </div>
    </nav>
    
 
    <div class="container mt-5 px-5">
        <h1>新しい記事を投稿する</h1>
    </div>
</body>
</html>

とりあえずは、簡単な骨組みだけ用意しました。
本格的な実装は後で行います。


ビューの用意

表示するためのページが準備できたので、次はビューを作成していきます。

ビューはapp/views.pyに記述します。
記事投稿ページ用の関数をapp/views.py一番下に新しく追加しましょう。

from django.shortcuts import render, get_object_or_404
from django.contrib.auth.models import User
from .models import Article
 
 
def index(request):
    return render(request, "app/index.html")
 
def mypage(request, username):
    profile_user  = get_object_or_404(User, username=username)
    
    context = {"profile_user": profile_user }
    return render(request, "app/mypage.html", context=context)
 
def all_read(request):
    articles = Article.objects.all().order_by("-created_at")
 
    context = {"articles": articles}
    return render(request, "app/all_read.html", context=context)
 
def read(request, article_id):
    article = get_object_or_404(Article, id=article_id)
    context = {"article": article}
 
    return render(request, "app/read.html", context=context)
 
def create(request): 
    return render(request, "app/create.html") 

とりあえず、ページを表示するだけのビューを作成しました。
追加したビューにはこの後でDBの操作やフォームの処理を追加していきます。


URLの設定

HTMLとビューによるページの準備はできました。
よって最後にURLを設定してページにアクセスできるようにしましょう。

URLの設定はapp/urls.pyに記述します。
URLのリストであるurlpatternsの中に投稿用ページのURLを追加します。

from django.urls import path
from . import views
 
urlpatterns = [
    path("", views.index, name="index"),
    path("mypage/<str:username>/", views.mypage, name="mypage"),
    path("all_read/", views.all_read, name="all_read"),
    path("read/<int:article_id>/", views.read, name="read"),
    path("create/", views.create, name="create"), 
]
リスト型なので追加するときは各データの間にカンマ(,)を忘れないようにしてください。

これでページの追加は完了です🚀  

正しくページが追加されているかサーバーを起動して確認してみましょう。

python manage.py runserverhttp://127.0.0.1:8000/create/にアクセスしましょう!

最後にホームのページ(index.html)からアクセスできるようにしましょう!
app/templates/app/index.htmlを以下のように修正します。

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1" />
  <title>BlogSite - ホーム</title>
  <link
    href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css"
    rel="stylesheet"
  />
</head>
<body>
 
    <nav class="navbar bg-body-tertiary">
        <div class="container">
            <a class="navbar-brand" href="/">ブログサイト</a>
 
            <ul class="navbar-nav  flex-row gap-3">
                <li class="nav-item"><a class="nav-link" href="{% url 'signup' %}">新規登録</a></li>
                {% if user.is_authenticated  %}
                <li class="nav-item"><a class="nav-link" href="{% url 'mypage' user.username %}">マイページ</a></li>
                <li class="nav-item">
                    <form action="{% url 'logout' %}" method="post">
                        {% csrf_token %}
                        <button type="submit" class="btn btn-link nav-link">ログアウト</button>
                    </form>
                </li>
                
                {% else %}
                <li class="nav-item"><a class="nav-link" href="{% url 'login' %}">ログイン</a></li>
                {% endif %}
            </ul>
        </div>
    </nav>
 
    <div class="container mt-5">
        <div class="d-flex flex-column align-items-center text-center gap-4">
            <h1 class="fw-bold display-3 ">ブログサイトへようこそ</h1>
            <p class="fs-5 text-muted">このブログサイトはDjangoで作られているブログサイトです。<br/>ユーザーごとに自分の記事を投稿して全ユーザーに共有することができます。</p>
            <div class="d-flex gap-4">
                <a href="{% url 'all_read' %}" class="btn btn-dark mt-4">記事の一覧をみる</a>
                <a href="{% url 'create' %}" class="btn btn-light  mt-4">記事を投稿する</a>
            </div>
        </div>
    </div>
 
</body>
</html>

記事投稿用のボタンを追加しました!
aタグはインライン要素なので横並びになりますが、余白を調節するためにflexをかけています。

本当は記事欄ページなどにもボタンを追加したいですが、今回はとりあえず省略します。 実装する場合は上記のようにボタンをリンク化したものをHTMLに追加するだけで簡単にできます!ぜひ練習でやってみてください

フォームについて

ユーザーからの入力を受け取るには一番メジャーな方法はフォームです。
フォームはDjangoだけの特別な機能ではなく、Webアプリケーション全般で使われるものです。

実装する場合はよくHTMLのformタグが使われます。
そのformタグの中にinputタグやselectタグなどをHTMLで記述してフォームが作れます。

受け取りたいデータを考えてHTMLでフォームを作成するのも良いですが、Djangoではフォームをより便利に扱えます。

Djangoでフォームを実装する利点をまとめると以下のようになります。

  • 入力値のチェック: ユーザーが入力した値が正しいかどうかを自動でチェックしてくれる。
  • エラーメッセージの表示: 入力値が不正な場合に、エラーメッセージを表示してくれる。
  • 型の定義: 入力できるデータの型や形式を定義できる。
  • HTMLの自動生成: 受け取りたいデータを定義したらあとは自動でHTMLを生成してくれる。
  • モデルフォーム: モデルフォームを使えば、モデルから自動でフォームを生成できる。

これ以外にもカスタマイズ性やセキュリティの面でたくさん利点があります!

Djangoのフォームはそれ自体で強力ですが、特にDBに登録するためのフォームとは相性が抜群です
Djangoの機能のモデルフォームを使うことで、簡単にモデル定義に合わせたフォームを作成してくれます!


モデルフォームの実装

Djangoのフォームを使うときはまずフォームを定義する必要があります。
フォームはforms.pyというファイルに定義します。

ですがforms.pyはデフォルトでは存在しないので用意しましょう!
appディレクトリの中にforms.pyを作成します。(models.pyと同じ位置)

作成出来たら以下のコードをforms.pyに記述します。

from django import forms
from .models import Article
 
class ArticleForm(forms.ModelForm):
    class Meta:
        model = Article
        fields = ['title', 'content']
  • from django import forms: Djangoのフォーム機能を読み込んでいます。
  • from .models import Article: Articleモデルをこのファイルで使うために読み込んでいます。
  • class ArticleForm(forms.ModelForm): forms.ModelFormを渡すことでモデルフォームになります。
  • class Meta: モデルフォームの定義を行うためのおまじない。
  • model = Article: モデルフォームがどのモデル用のフォームかを指定しています。
  • fields = ['title', 'content']: モデルの中のどのデータをユーザに入力してもらうかを設定しています。

これだけでモデルフォームの定義は完了です!

モデルフォームではなく、通常のフォームを作成したい場合はforms.Formを渡せば通常のフォームを定義できます。 その場合はモデルから自動で入力欄が作られるわけでないので、自分で入力欄の定義をする必要があります。それでも通常のHTMLより簡単です。なにより入力値チェックやエラー表示が自動で行われるのが便利です。

今回は紹介しないのですがもちろん通常フォームもDjangoを使う上で覚えておいたほうが便利です。
通常のフォームについては以下の記事がわかりやすく解説してくれています。

【Python Django】初心者プログラマーのWebアプリ#4 【フォーム送信】

https://qiita.com/Bashi50/items/93da7a071b2e02dc21ed


ビューでモデルフォームを使う

モデルフォームは定義するだけでは使えません。

モデルフォームを使う場所は主に2か所あります。

  • ビュー: フォームの内容チェックやデータベースとのやり取りを行う場所
  • HTML: ユーザーが入力するためのフォームを表示する場所

まずはビューでのモデルフォームの扱いを見ていきましょう。

app/views.pyに新しく以下のコードを追加します。

from django.shortcuts import render, get_object_or_404, redirect 
from django.contrib.auth.models import User
from .models import Article
from .forms import ArticleForm 
 
 
def index(request):
    return render(request, "app/index.html")
 
def mypage(request, username):
    profile_user  = get_object_or_404(User, username=username)
    
    context = {"profile_user": profile_user }
    return render(request, "app/mypage.html", context=context)
 
def all_read(request):
    articles = Article.objects.all().order_by("-created_at")
 
    context = {"articles": articles}
    return render(request, "app/all_read.html", context=context)
 
def read(request, article_id):
    article = get_object_or_404(Article, id=article_id)
    context = {"article": article}
 
    return render(request, "app/read.html", context=context)
 
def create(request): 
    if request.method == "POST": 
        form = ArticleForm(request.POST) 
        if form.is_valid(): 
            article = form.save(commit=False) 
            article.author = request.user 
            article.save() 
            return redirect("read", article_id=article.id) 
        return render(request, "app/create.html", {"form": ArticleForm()}) 
    elif request.method == "GET": 
        form = ArticleForm() 
        context = {"form": form} 
        return render(request, "app/create.html", context=context)   

よく見ると、ユーザー登録と同じようなことをしていますね!!(accounts.views.pyを参照)


データの流れと共にコードを見ていきましょう。

def create(request):
    if request.method == "POST":
        ‥‥
    
    elif request.method == "GET":
        ‥‥

ページにアクセスされたらまずこの条件分岐が始めに見られます!

  • if request.method == "POST": POSTメソッド(データ送信用)でアクセスされたか
  • elif request.method == "GET": GETメソッド(ページ表示用)でアクセスされたか

一番最初にアクセスされたらページ表示のはずですよね。
よってelif request.method == "GET"以下が実行されます。

def create(request):
        ‥‥
    
    elif request.method == "GET":
        form = ArticleForm()
        context = {"form": form}
 
        return render(request, "app/create.html", context=context)

ここはやっていることはシンプルです。
用意したフォームは表示するためにはHTMLに渡す必要があります。
そのためビューでフォームを生成してHTMLに渡しています。

  • form = ArticleForm(): モデルフォームを生成しています。

最後にフォームを渡した投稿用ページを表示しています。
この後は、ユーザー側でデータが入力され、データが送信されてきます。

データ送信(POSTメソッド)されてページが呼ばれるのでif request.method == "POST"以下が実行されます。

def create(request):
    if request.method == "POST":
        form = ArticleForm(request.POST)
 
        if form.is_valid():
          ‥‥
        return render(request, "app/create.html", {"form": ArticleForm()})
 
        ‥‥

ここでは、ユーザーが入力したデータを受け取って、データをチェックしています

  • form = ArticleForm(request.POST): ユーザーが入力したデータを一時的に受け取っています。
  • if form.is_valid(): 受けったデータが正しいかどうかをチェックしています。
  • return render ‥: データが不正だったので、エラーをフォームに渡して再度ページを表示しています。

エラーがなかった場合はif form.is_valid():内の処理が実行されます。

def create(request):
    if request.method == "POST":
        form = ArticleForm(request.POST)
 
        if form.is_valid():
            article = form.save(commit=False)
            article.author = request.user
            article.save()
              
            return redirect("read", article_id=article.id)
        ‥‥

if form.is_valid():内の処理はデータをDBに保存する処理です。

  • article = form.save(commit=False): 入力データを変数(正しくはオブジェクト)に保存しています。
  • form.save(commit=False): DBにはまだ保存したくのでcommit=Falseを指定しています。
  • article.author = request.user: ログインユーザーを筆者に設定しています。(入力欄にはない)
  • article.save(): すべてのデータがそろったのでDBに保存しています。
  • return redirect("read", article_id=article.id): 投稿が完了したので記事の詳細ページに移動してます。

これでビュー側の処理は完了です。いきなりコードが長くなりましね‥
ですがDBに保存する流れは毎回同じということに気づけるとぐっと理解が深まります!


ログインしているユーザーのみが投稿できるようにする

記事登録のコードを見てもらえるとわかるのですが、ログインユーザーを筆者に設定しています。

なのでこのままではログインしていないユーザーが投稿するとエラーが発生します。  

そもそも記事の投稿機能はログインしているユーザーのみが使える機能であってほしいですよね。
一般的にもそうなっているはずです(ツイートは見えるけどツイートするにはログインが必要など)

よってこのページはログインしているユーザーのみがアクセスできるように制限をかけます。

制限をかけるapp/views.pyの投稿用ビューに以下のコードを追加します。

from django.shortcuts import render, get_object_or_404, redirect
from django.contrib.auth.models import User
from .models import Article
from .forms import ArticleForm
from django.contrib.auth.decorators import login_required 
 
 
def index(request):
    return render(request, "app/index.html")
 
def mypage(request, username):
    profile_user  = get_object_or_404(User, username=username)
    
    context = {"profile_user": profile_user }
    return render(request, "app/mypage.html", context=context)
 
def all_read(request):
    articles = Article.objects.all().order_by("-created_at")
 
    context = {"articles": articles}
    return render(request, "app/all_read.html", context=context)
 
def read(request, article_id):
    article = get_object_or_404(Article, id=article_id)
    context = {"article": article}
 
    return render(request, "app/read.html", context=context)
 
@login_required
def create(request):
    if request.method == "POST":
        form = ArticleForm(request.POST)
 
        if form.is_valid():
            article = form.save(commit=False)
            article.author = request.user
            article.save()
 
            return redirect("read", article_id=article.id)
        return render(request, "app/create.html", {"form": ArticleForm()})
    
    elif request.method == "GET":
        form = ArticleForm()
        context = {"form": form}
 
        return render(request, "app/create.html", context=context)

ログインしていないユーザーが投稿ページにアクセスした場合は、ログインページにリダイレクトされるようになっています
そのため、ログインページのURLを設定しておく必要があります。

settings.pyの一番下に以下のコードを追加します。

LOGIN_URL = '/accounts/login/'

これでログインしていないユーザーが投稿ページにアクセスした場合は、ログインページにリダイレクトされます。 また、リダイレクトされたユーザーがログイン後は投稿ページに戻されます。

ちなみに、ログインページのデフォルトで設定されているURLも/accounts/login/なので、実はチュートリアルコードだとsettings.pyに追加しなくても動きます。

HTMLでモデルフォームを使う

次はHTML側でモデルフォームを使っていきます。

先ほどのビューで生成したフォームをHTMLに渡しています。
そのため、HTML側ではフォームを表示するだけでOKです!

app/templates/app/create.htmlに投稿フォームを追加しましょう!

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <link
    href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css"
    rel="stylesheet"
    />
</head>
<body>
 
    <nav class="navbar bg-body-tertiary">
        <div class="container">
            <a class="navbar-brand" href="/">ブログサイト</a>
 
            <ul class="navbar-nav  flex-row gap-3">
                <li class="nav-item"><a class="nav-link" href="{% url 'signup' %}">新規登録</a></li>
                {% if user.is_authenticated  %}
                <li class="nav-item"><a class="nav-link" href="{% url 'mypage' user.username %}">マイページ</a></li>
                <li class="nav-item">
                    <form action="{% url 'logout' %}" method="post">
                        {% csrf_token %}
                        <button type="submit" class="btn btn-link nav-link">ログアウト</button>
                    </form>
                </li>
                
                {% else %}
                <li class="nav-item"><a class="nav-link" href="{% url 'login' %}">ログイン</a></li>
                {% endif %}
            </ul>
        </div>
    </nav>
    
 
    <div class="container mt-5 px-5">
        <h1>新しい記事を投稿する</h1>
        <form method = "post"> 
            {% csrf_token %} 
            {{ form }} 
            <button type="submit" class="btn btn-dark">投稿する</button> 
        </form> 
    </div>
</body>
</html>

これだけで、モデルフォームをHTMLに表示することができます!

  • form method = "post": フォームの送信方法をPOSTに設定しています。
  • {% csrf_token %}: CSRFトークンを追加しています。
  • {{ form }}: モデルフォームをHTMLに変換して表示しています。
  • button type="submit": フォームを送信するためのボタンを追加しています。
セキュリティのためにCSRFトークンを忘れずに追加してください。追加しないとDjango側からエラーが出て怒られます。

これで記事の内容を入力して投稿してみてください!
記事の詳細ページや記事一覧にも表示されるはずです。


フォームのデザインを整える

今のままだとデザインがあまり良くないので、Bootstrapを使ってデザインを整えましょう。

Djangoのフォームのデザインをカスタマイズする方法はたくさんあります。
ですが今回はHTMLファイルの内容を変えずに、デザインを追加する方法を紹介します!

フォームの定義であるapp/forms.pyを編集していきます。

from django import forms
from .models import Article
 
class ArticleForm(forms.ModelForm):
    class Meta:
        model = Article
        fields = ['title', 'content']
 
        widgets = { 
            'title': forms.TextInput(attrs={'class': 'form-control mt-2 mb-3', 'placeholder': 'タイトル'}), 
            'content': forms.Textarea(attrs={'class': 'form-control mt-2 mb-5', 'rows': 8, 'placeholder': '記事の内容'}), 
        } 

これを追加するだけでHTML側のフォームのデザインが整います!
おおもとのフォームを変更するだけで使用されているフォームのデザインが変わるので、変更するところも少なく済みます。

今回はwidgetsという機能を使っています。
これは各フィールドの属性を変更してデザインを整えるためのものです。

  • widgets: 各フィールドのデザインを変更するための辞書型のオブジェクト
  • 'title': forms.TextInput(attrs=): タイトルフィールドのデザインを変更しています。
  • 'content': forms.Textarea(attrs=): コンテンツフィールドのデザインを変更しています。
  • attrs: HTMLの属性を設定するための辞書型のオブジェクト
  • class: CSSクラスを指定してBootstrapのスタイルを適用しています。
  • placeholder: 入力欄のプレースホルダーを設定しています。
  • rows: テキストエリアの最小表示行数を指定しています。

これ以外にもデザインを整える方法はたくさんあります。
エラー表示のカスタマイズや各フィールドの表示位置などをよりカスタマイズすることもできます。

詳しくは以下のドキュメントを参照してみてください!
このドキュメントでは、各フィールドやエラーごとにHTMLに変換する方法が書いてあります。

フォームを使う

https://docs.djangoproject.com/ja/5.2/topics/forms/


Update操作の実装

記事の追加機能が実装できました!
次はこの記事の内容を変更する機能を実装していきます。

変更機能と言っていますが、やっていることはもう一度DBに保存するだけです!
なので実はCreateとほとんど同じです。

それではやっていきましょう!

まずは更新用のページを作成していきます。(さっきと同じ手順です)

もしできる人は、前回のCreateのコードを参考にして自分で新しいページを用意してみてください!


HTMLファイルの用意

app/templates/appフォルダの中にupdate.htmlを作成します。(index.htmlと同じ位置です)

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <link
    href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css"
    rel="stylesheet"
    />
</head>
<body>
 
    <nav class="navbar bg-body-tertiary">
        <div class="container">
            <a class="navbar-brand" href="/">ブログサイト</a>
 
            <ul class="navbar-nav  flex-row gap-3">
                <li class="nav-item"><a class="nav-link" href="{% url 'signup' %}">新規登録</a></li>
                {% if user.is_authenticated  %}
                <li class="nav-item"><a class="nav-link" href="{% url 'mypage' user.username %}">マイページ</a></li>
                <li class="nav-item">
                    <form action="{% url 'logout' %}" method="post">
                        {% csrf_token %}
                        <button type="submit" class="btn btn-link nav-link">ログアウト</button>
                    </form>
                </li>
                
                {% else %}
                <li class="nav-item"><a class="nav-link" href="{% url 'login' %}">ログイン</a></li>
                {% endif %}
            </ul>
        </div>
    </nav>
    
 
    <div class="container mt-5 px-5">
        <h1>記事の内容を修正する</h1>
    </div>
</body>
</html>

とりあえずは、簡単な骨組みだけ用意しました。

ビューの用意

次はapp/views.pyに更新用のビューを作成していきます。

app/views.pyの一番下に新しく以下のコードを追加しましょう。

from django.shortcuts import render, get_object_or_404, redirect
from django.contrib.auth.models import User
from .models import Article
from .forms import ArticleForm
from django.contrib.auth.decorators import login_required
 
 
def index(request):
    return render(request, "app/index.html")
 
def mypage(request, username):
    profile_user  = get_object_or_404(User, username=username)
    
    context = {"profile_user": profile_user }
    return render(request, "app/mypage.html", context=context)
 
def all_read(request):
    articles = Article.objects.all().order_by("-created_at")
 
    context = {"articles": articles}
    return render(request, "app/all_read.html", context=context)
 
def read(request, article_id):
    article = get_object_or_404(Article, id=article_id)
    context = {"article": article}
 
    return render(request, "app/read.html", context=context)
 
@login_required
def create(request):
    if request.method == "POST":
        form = ArticleForm(request.POST)
 
        if form.is_valid():
            article = form.save(commit=False)
            article.author = request.user
            article.save()
 
            return redirect("read", article_id=article.id)
        return render(request, "app/create.html", {"form": ArticleForm()})
    
    elif request.method == "GET":
        form = ArticleForm()
        context = {"form": form}
 
        return render(request, "app/create.html", context=context)
    
@login_required
def update(request, article_id): 
    return render(request, "app/update.html") 

更新用のupdateビューを追加しました。
このページもログインしているユーザーのみがアクセスするので@login_requiredをつけています。

また、詳細表示などと同様に記事ごとにページの内容を変更する必要があります。
よって動的URLを設定するためにarticle_idを引数にとっています。


URLの設定

app/urls.pyにURLを追加します。

from django.urls import path
from . import views
 
urlpatterns = [
    path("", views.index, name="index"),
    path("mypage/<str:username>/", views.mypage, name="mypage"),
    path("all_read/", views.all_read, name="all_read"),
    path("read/<int:article_id>/", views.read, name="read"),
    path("create/", views.create, name="create"),
    path("update/<int:article_id>/", views.update, name="update"), 
]

動的URLを設定するためにURL内に変数(パラメータ)を入れています。

これでページの準備は完了です! 正しくページが追加されているかサーバーを起動して確認してみましょう。

http://127.0.0.1:8000/update/1/のように適当なidでアクセスしてみてください。

もしページが表示されれば成功です!


記事の更新機能を実装する

Update操作のやることはCreate操作とほとんど同じです。

  • HTMLでフォームを表示する
  • ビューでフォームを受け取る
  • DBに保存する

これに加えて、どの記事を更新するかを指定する必要があります。

まずはビューを実装していきます!

from django.shortcuts import render, get_object_or_404, redirect
from django.contrib.auth.models import User
from .models import Article
from .forms import ArticleForm
from django.contrib.auth.decorators import login_required
 
 
def index(request):
    return render(request, "app/index.html")
 
def mypage(request, username):
    profile_user  = get_object_or_404(User, username=username)
    
    context = {"profile_user": profile_user }
    return render(request, "app/mypage.html", context=context)
 
def all_read(request):
    articles = Article.objects.all().order_by("-created_at")
 
    context = {"articles": articles}
    return render(request, "app/all_read.html", context=context)
 
def read(request, article_id):
    article = get_object_or_404(Article, id=article_id)
    context = {"article": article}
 
    return render(request, "app/read.html", context=context)
 
@login_required
def create(request):
    if request.method == "POST":
        form = ArticleForm(request.POST)
 
        if form.is_valid():
            article = form.save(commit=False)
            article.author = request.user
            article.save()
 
            return redirect("read", article_id=article.id)
        return render(request, "app/create.html", {"form": ArticleForm()})
    
    elif request.method == "GET":
        form = ArticleForm()
        context = {"form": form}
 
        return render(request, "app/create.html", context=context)
    
@login_required
def update(request, article_id):
    article = get_object_or_404(Article, id=article_id) 
 
    if article.author != request.user: 
        return redirect("read", article_id=article.id) 
 
    if request.method == "POST": 
        form = ArticleForm(request.POST, instance=article) 
 
        if form.is_valid(): 
            form.save() 
            return redirect("read", article_id=article.id) 
        else: 
            return render(request, "app/update.html", {"form": form}) 
 
    elif request.method == "GET": 
        form = ArticleForm(instance=article) 
        context = {"form": form} 
 
        return render(request, "app/update.html", context=context) 

またややこしく書いてありますが、よく見るとCreateの時と同じです。

よってここではUpdate特有の処理について紹介します。

  • get_object_or_404(Article, id=article_id):URLのIDから更新する記事を取得しています。
  • if article.author != request.user:編集できるのは自分の記事だけなので、チェックをしています。
  • ArticleForm(instance=article):更新前の入力欄の内容を渡しています。(これが編集される)
  • form = ArticleForm(request.POST, instance=article):入力されたデータで変更しています。
  • form.save():入力されたデータをDBに保存しています。

これでUpdate操作のビューは完成です!


HTMLでフォームを表示する

次はHTMLでフォームを表示していきます。
といってもこれはCreateの時と本当に同じです!

これで適当な記事の更新ページにアクセスしてみてください。
正しく実装できていれば更新ぺージが表示されるはずです。

このままではアクセスがしづらいのです。
よって記事の詳細ページから更新ページにアクセスできるようにしましょう。

app/templates/app/read.htmlに更新ページへのリンクを追加します。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <link
    href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css"
    rel="stylesheet"
    />
</head>
<body>
 
    <nav class="navbar bg-body-tertiary">
        <div class="container">
            <a class="navbar-brand" href="/">ブログサイト</a>
 
            <ul class="navbar-nav  flex-row gap-3">
                <li class="nav-item"><a class="nav-link" href="{% url 'signup' %}">新規登録</a></li>
                {% if user.is_authenticated  %}
                <li class="nav-item"><a class="nav-link" href="{% url 'mypage' user.username %}">マイページ</a></li>
                <li class="nav-item">
                    <form action="{% url 'logout' %}" method="post">
                        {% csrf_token %}
                        <button type="submit" class="btn btn-link nav-link">ログアウト</button>
                    </form>
                </li>
                
                {% else %}
                <li class="nav-item"><a class="nav-link" href="{% url 'login' %}">ログイン</a></li>
                {% endif %}
            </ul>
        </div>
    </nav>
    
 
<div class="container mt-5 px-3">
    <div class="mx-auto" style="max-width: 800px;">
        <div class="small"><a class=" link-secondary link-offset-2 link-underline-opacity-0" href="{% url 'all_read' %}">記事一覧に戻る</a></div> 
        <div class="d-flex align-items-center gap-3 mb-3 justify-content-between"> 
            <h1 class="mb-0">{{ article.title }}</h1> 
            {% if user.is_authenticated and user == article.author %} 
            <a href="{% url 'update' article.id %}" class="btn btn-dark mt-4">編集する</a> 
            {% endif %} 
        </div> 
 
 
        <div class="text-muted small mb-4 d-flex flex-wrap gap-3">
            <div>作成日: {{ article.created_at|date:"Y年m月d日 H:i" }}</div>
            <div>投稿者: {{ article.author.username }}</div>
        </div>
 
        <hr />
 
        <div class="article-content fs-5 lh-lg">
            {{ article.content|linebreaksbr }}
        </div>
    </div>
</div>
 
</body>
</html>

ここでは、記事のタイトルの横に編集ボタンを追加しています。

  • {% if user.is_authenticated and user == article.author %}: ログインしているユーザーが記事の筆者である場合にのみ、編集ボタンを表示します。

ついでに、記事一覧ページに戻るリンクも追加しました。


Delete操作の実装

記事の更新機能が実装できました!
次は記事の削除機能を実装していきます。

Delete操作もUpdate操作と同じように、やっていることはDBから削除するだけです!
ですが、削除するだけなのでHTMLでフォームを表示する必要はありません。

やることは以下の通りです。

  • ビューで削除処理を実装する
  • URLを設定する

これだけです!!


ビューの実装

まずはビューを実装していきます。

app/views.pyの一番下に以下のコードを追加します。

from django.shortcuts import render, get_object_or_404, redirect
from django.contrib.auth.models import User
from .models import Article
from .forms import ArticleForm
from django.contrib.auth.decorators import login_required
from django.views.decorators.http import require_POST 
 
 
def index(request):
    return render(request, "app/index.html")
 
def mypage(request, username):
    profile_user  = get_object_or_404(User, username=username)
    
    context = {"profile_user": profile_user }
    return render(request, "app/mypage.html", context=context)
 
def all_read(request):
    articles = Article.objects.all().order_by("-created_at")
 
    context = {"articles": articles}
    return render(request, "app/all_read.html", context=context)
 
def read(request, article_id):
    article = get_object_or_404(Article, id=article_id)
    context = {"article": article}
 
    return render(request, "app/read.html", context=context)
 
@login_required
def create(request):
    if request.method == "POST":
        form = ArticleForm(request.POST)
 
        if form.is_valid():
            article = form.save(commit=False)
            article.author = request.user
            article.save()
 
            return redirect("read", article_id=article.id)
        return render(request, "app/create.html", {"form": ArticleForm()})
    
    elif request.method == "GET":
        form = ArticleForm()
        context = {"form": form}
 
        return render(request, "app/create.html", context=context)
    
@login_required
def update(request, article_id):
    article = get_object_or_404(Article, id=article_id)
 
    if article.author != request.user:
        return redirect("read", article_id=article.id)
 
    if request.method == "POST":
        form = ArticleForm(request.POST, instance=article)
 
        if form.is_valid():
            form.save()
            return redirect("read", article_id=article.id)
        else:
            return render(request, "app/update.html", {"form": form})
    
    elif request.method == "GET":
        form = ArticleForm(instance=article)
        context = {"form": form}
 
        return render(request, "app/update.html", context=context)
 
@require_POST
@login_required
def delete(request, article_id): 
    article = get_object_or_404(Article, id=article_id) 
    return redirect("all_read") 

ここで重要なのはページが許可しているメソッドです。

簡単に削除ページにアクセスできてしまうと、悪意のあるユーザーが簡単に記事を削除してしまいます。
よって特定のページからしかアクセスできないように制限をかけます。
POSTメソッドにすることで、特定のアクセスのみを許可しています。

  • @require_POST: このビューはPOSTメソッドでのみアクセスできるように制限しています。
  • @login_required: ログインしているユーザーのみがアクセスできるように制限しています。

削除後は記事一覧ページにリダイレクトするようにしています。

また、削除する記事を取得するためにarticle_idを引数にとっています。

  • get_object_or_404(Article, id=article_id): URLのIDから削除する記事を取得しています。
このままでもツールを使えばPOSTメソッドでアクセスできてしまいます。 よって、後ほどでログイン中のユーザーと筆者が一致している時でないと削除できないようにします。

URLの設定

次はURLを設定していきます。

app/urls.pyに以下のコードを追加します。

from django.urls import path
from . import views
 
urlpatterns = [
    path("", views.index, name="index"),
    path("mypage/<str:username>/", views.mypage, name="mypage"),
    path("all_read/", views.all_read, name="all_read"),
    path("read/<int:article_id>/", views.read, name="read"),
    path("create/", views.create, name="create"),
    path("update/<int:article_id>/", views.update, name="update"),
    path("delete/<int:article_id>/", views.delete, name="delete"), #[!code ++
]

これでURLの設定は完了です!
記事ごとに削除ページを作るためにarticle_idを引数にとって動的URLを設定しています。


記事の削除機能を実装する

では最後に記事の削除機能を実装していきます。

これはビューの方で実装していきます。
app/views.pydeleteビューを以下のように変更します。

from django.shortcuts import render, get_object_or_404, redirect
from django.contrib.auth.models import User
from .models import Article
from .forms import ArticleForm
from django.contrib.auth.decorators import login_required
from django.views.decorators.http import require_POST
from django.contrib import messages 
 
 
def index(request):
    return render(request, "app/index.html")
 
def mypage(request, username):
    profile_user  = get_object_or_404(User, username=username)
    
    context = {"profile_user": profile_user }
    return render(request, "app/mypage.html", context=context)
 
def all_read(request):
    articles = Article.objects.all().order_by("-created_at")
 
    context = {"articles": articles}
    return render(request, "app/all_read.html", context=context)
 
def read(request, article_id):
    article = get_object_or_404(Article, id=article_id)
    context = {"article": article}
 
    return render(request, "app/read.html", context=context)
 
@login_required
def create(request):
    if request.method == "POST":
        form = ArticleForm(request.POST)
 
        if form.is_valid():
            article = form.save(commit=False)
            article.author = request.user
            article.save()
 
            return redirect("read", article_id=article.id)
        return render(request, "app/create.html", {"form": ArticleForm()})
    
    elif request.method == "GET":
        form = ArticleForm()
        context = {"form": form}
 
        return render(request, "app/create.html", context=context)
    
@login_required
def update(request, article_id):
    article = get_object_or_404(Article, id=article_id)
 
    if article.author != request.user:
        return redirect("read", article_id=article.id)
 
    if request.method == "POST":
        form = ArticleForm(request.POST, instance=article)
 
        if form.is_valid():
            form.save()
            return redirect("read", article_id=article.id)
        else:
            return render(request, "app/update.html", {"form": form})
    
    elif request.method == "GET":
        form = ArticleForm(instance=article)
        context = {"form": form}
 
        return render(request, "app/update.html", context=context)
 
@require_POST
@login_required
def delete(request, article_id):
    article = get_object_or_404(Article, id=article_id)
    if article.author != request.user: 
        return redirect("all_read") 
 
    article.delete() 
    messages.success(request, "記事を削除しました。") 
 
    return redirect("all_read") 

これが削除機能の実装です。
やっていることは表示の代わりに消しているRead操作みたいなものです。

  • if article.author != request.user: 削除できるのは自分の記事だけなので、チェックをしています。
  • article.delete(): 記事を削除しています。
  • messages.success(request, "記事を削除しました。"): 削除が成功したことをユーザーに通知するためのメッセージを格納しています。
  • return redirect("all_read"): 記事一覧ページにリダイレクトしています。

これで削除のロジック自体は完成なのであとはHTML側で削除ボタンを表示するだけです。


HTMLで削除ボタンを表示する

次はHTML側で削除ボタンを表示していきます。

記事の詳細ページであるapp/templates/app/read.htmlに削除ボタンを追加します。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <link
    href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css"
    rel="stylesheet"
    />
</head>
<body>
 
    <nav class="navbar bg-body-tertiary">
        <div class="container">
            <a class="navbar-brand" href="/">ブログサイト</a>
 
            <ul class="navbar-nav  flex-row gap-3">
                <li class="nav-item"><a class="nav-link" href="{% url 'signup' %}">新規登録</a></li>
                {% if user.is_authenticated  %}
                <li class="nav-item"><a class="nav-link" href="{% url 'mypage' user.username %}">マイページ</a></li>
                <li class="nav-item">
                    <form action="{% url 'logout' %}" method="post">
                        {% csrf_token %}
                        <button type="submit" class="btn btn-link nav-link">ログアウト</button>
                    </form>
                </li>
                
                {% else %}
                <li class="nav-item"><a class="nav-link" href="{% url 'login' %}">ログイン</a></li>
                {% endif %}
            </ul>
        </div>
    </nav>
    
 
<div class="container mt-5 px-3">
    <div class="mx-auto" style="max-width: 800px;">
        <div class="small"><a class=" link-secondary link-offset-2 link-underline-opacity-0" href="{% url 'all_read' %}">記事一覧に戻る</a></div>
        <div class="d-flex align-items-center gap-3 mb-3 justify-content-between">
            <h1 class="mb-0">{{ article.title }}</h1>
            {% if user.is_authenticated and user == article.author %}
            <div class="d-flex align-items-center gap-2"> 
                <a href="{% url 'update' article.id %}" class="btn btn-dark mt-4">編集する</a> 
                <form action="{% url 'delete' article.id %}" method="post" class="mb-0" <!-- [!code ++] -->
                    onsubmit="return confirm('本当に削除してもよろしいですか?');"> 
                    {% csrf_token %} 
                    <button type="submit" class="btn btn-danger mt-4">削除する</button> 
                </form> 
            </div> 
            {% endif %}
        </div>
       
 
 
        <div class="text-muted small mb-4 d-flex flex-wrap gap-3">
            <div>作成日: {{ article.created_at|date:"Y年m月d日 H:i" }}</div>
            <div>投稿者: {{ article.author.username }}</div>
        </div>
 
        <hr />
 
        <div class="article-content fs-5 lh-lg">
            {{ article.content|linebreaksbr }}
        </div>
    </div>
</div>
 
</body>
</html>

ここでは、記事のタイトルの横に削除ボタンを追加しています。

POSTメソッドでページにアクセスするためにformタグを使用しています。

  • onsubmit="return confirm('本当に削除してもよろしいですか?'): 削除ボタンを押した時に確認ダイアログを表示しています。

これで削除機能の実装は完了です!!

これだけでもいいですが削除後にユーザーに通知するメッセージを表示する機能を追加していきます。

記事一覧ページのapp/templates/app/all_read.htmlでメッセージを受け取ります。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <link
    href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css"
    rel="stylesheet"
    />
</head>
<body>
 
    <nav class="navbar bg-body-tertiary">
        <div class="container">
            <a class="navbar-brand" href="/">ブログサイト</a>
 
            <ul class="navbar-nav  flex-row gap-3">
                <li class="nav-item"><a class="nav-link" href="{% url 'signup' %}">新規登録</a></li>
                {% if user.is_authenticated  %}
                <li class="nav-item"><a class="nav-link" href="{% url 'mypage' user.username %}">マイページ</a></li>
                <li class="nav-item">
                    <form action="{% url 'logout' %}" method="post">
                        {% csrf_token %}
                        <button type="submit" class="btn btn-link nav-link">ログアウト</button>
                    </form>
                </li>
                
                {% else %}
                <li class="nav-item"><a class="nav-link" href="{% url 'login' %}">ログイン</a></li>
                {% endif %}
            </ul>
        </div>
    </nav>
    
 
<div class="container mt-5 px-5">
    {% if messages %}
        <div class="mt-3"> 
            {% for message in messages %} 
            <div class="alert alert-{{ message.tags }} alert-dismissible fade show" role="alert"> 
                {{ message }} 
                <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="閉じる"></button> 
            </div> 
            {% endfor %} 
        </div> 
    {% endif %} 
    <div class="d-flex align-items-center gap-3 justify-content-between"> 
        <h1>記事一覧</h1> 
        <div><a href="{% url 'create' %}" class="btn btn-dark">投稿する</a></div> 
    </div> 
    {% for article in articles %}
    <a href="{% url 'read' article.id %}" class="text-decoration-none text-dark">
        <div class="card mb-4 mt-4 shadow-sm cursor-pointer">
            <div class="card-body">
                <h4 class="card-title">{{ article.title }}</h4>
                <p class="card-subtitle text-muted mt-1">
                    投稿者: {{ article.author.username }} |
                    投稿日: {{ article.created_at|date:"Y年m月d日" }}
                </p>
            </div>
        </div>
    </a>
    {% empty %}
        <p>記事が見つかりませんでした</p>
    {% endfor %}
</div>
 
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" integrity="..." crossorigin="anonymous"></script> 
</body>
</html>

これはBootstrapのJavaScriptを使ってメッセージを表示・削除するためのコードです。

  • {% if messages %}: メッセージが存在する場合にのみ表示します。

それとついでに一覧ページから記事を作成するボタンを追加しました。

これでCRUD機能の実装は完了です!!


まとめ

これでDjangoを使ったブログサイトのCRUD機能の実装が完了しました!

今回はDjangoの得意な機能も使って実装してきましたが、
基本的にはどのフレームワークでもCRUD機能は同じように使われるような実装を意識して進めてきました。

なので別の言語の時はすんなり内容が理解できると思います!  

また、Djangoはまだまだ本領を発揮していません!
Djangoの強力な組み込みビュー・検索機能など今回のCRUDをより簡単に実装できるものがあります。

次回以降はそれについてもお話ししていきたいと思います!


📚 参考資料

【Python Django】初心者プログラマーのWebアプリ#4 【フォーム送信】

https://qiita.com/Bashi50/items/93da7a071b2e02dc21ed

フォームを使う

https://docs.djangoproject.com/ja/5.2/topics/forms/

django ModelFormクラスのwidget属性の使い方

https://qiita.com/keishi04hrikzira/items/a6e6820740e1e99d05fe

他のStringを探す