🖊️

【Django】ブログアプリを作ろう!

2024/03/20に公開

はじめに

この記事を書くきっかけは、Djangoの勉強を始めて、いざブログアプリでも作ってみようかな!としたらどうにもうまくいかなかったことにあります。
初学者なので間違っている部分、効率の悪い部分などあるかもしれませんがご了承ください。
勉強しながら、ブログアプリを作りながらこれを書いているので、おそらくなぞっていけば私と同じように進められるはずです。

要件

今回作りたいブログアプリのページ・機能は以下の通りです。

  • 記事の一覧画面( / と /articles)
    • ヘッダーの表示
      • トップへのリンク
      • 記事作成ページへ遷移できるリンク
    • タイトル/本文/最終更新日時/作成日時を表示
    • 記事は最終更新日時の新しいものから順に並べる
    • 詳細画面へ遷移できるリンク
  • 記事の登録画面(/articles/create)
    • 入力フォーム
      • 全要素必須
      • 文字数
    • 投稿内容確認ボタン
  • 記事の登録内容確認画面(/articles/create_confirm)
    • 入力内容の表示
    • 文字数の表示
    • 戻るボタン
    • 投稿ボタン
  • 記事の編集画面(/articles/{id}/edit)
    • 編集フォーム
      • 編集対象のレコードが初期値で入力されている
    • バリデーション
      • 全要素必須
      • 文字数
    • 投稿内容確認ボタン
    • 戻るボタン
    • 投稿ボタン
  • 記事の編集内容確認画面(/articles/{id}/edit_confirm)
    • 入力内容の表示
    • 戻るボタン
    • 投稿ボタン
  • 記事の詳細画面(/articles/{id})
    • タイトル/本文/最終更新日時/作成日時を表示
    • 編集画面へのリンク
    • 削除確認画面へのリンク
  • 記事の削除確認画面(/articles/{id}/delete_confirm)
    • 削除対象のタイトル/本文/最終更新日時/作成日時を表示
    • 戻るボタン
    • 削除ボタンを押下で削除実行
  • 404ページ
    • 存在しないidをURL指定すると表示

投稿される記事は以下の条件とします。

  • タイトル:max 50文字
  • 記事本文:max 10,000文字

仮想環境

私はDockerでやっているので、他の環境をお使いの方は、適宜そちらに合わせて読み替えてください。

ディレクトリ構成

開発の下準備をしましょう。
最終的な構成は以下のようになるはずです。

Django
    ┣ articles
    ┃  ├ migrations
    ┃  │  ├ __init__.py
    ┃  │  └ 0001_initial.py
    ┃  ├ templates
    ┃  │  └ articles
    ┃  │     ├ base.html
    ┃  │     ├ articles_list.html
    ┃  │     ├ article_create.html
    ┃  │     ├ article_create_confirm.html
    ┃  │     ├ article_detail.html
    ┃  │     ├ article_edit.html
    ┃  │     ├ article_edit_confirm.html
    ┃  │     └ article_delete.html
    ┃  ├ __init__.py
    ┃  ├ admin.py
    ┃  ├ apps.py
    ┃  ├ forms.py
    ┃  ├ models.py
    ┃  ├ urls.py
    ┃  └ views.py
    ┣ blog
    ┃  ├ __init__.py
    ┃  ├ asgi.py
    ┃  ├ settings.py
    ┃  ├ urls.py
    ┃  └ wsgi.py
    ┣ manage.py
    ┗ requirements.txt

下準備として、以下のようなディレクトリ構成にしておきます。

Django
    ┣ articles
    ┃  ├ migrations
    ┃  ├ templates
    ┃  │  └ articles
    ┃  └ __init__.py
    ┣ blog
    ┃  ├ __init__.py
    ┃  ├ asgi.py
    ┃  ├ settings.py
    ┃  ├ urls.py
    ┃  └ wsgi.py
    ┣ manage.py
    ┗ requirements.txt

足りないファイルはここから作っていきましょう!

設定まわり

ライブラリのインストール

フォームにCSSなどを適用するために必要なライブラリをインストールしましょう!

ターミナルで以下を実行してください。

pip install django-widgets-improved

続いて、requirements.txtに以下を追記してください。

django-widgets-improved==1.5.0

settings.py

まずsettings.pyを開きましょう。いろいろ書いてありますが、まずはINSTALLED_APPSに追記します。

INSTALLED_APPS = [
    'articles.apps.ArticlesConfig',
    'widget_tweaks',
    'django.contrib.sites',
        ・
        ・
        ・
]

これから作るアプリと、先ほどインストールしたライブラリ、管理者用サイトを記述してあげます。

次はテンプレートファイルの居場所を指示します。TEMPLATESに追記します。

TEMPLATES = [
    {
        'DIRS': [BASE_DIR / 'articles' / 'templates'],
            ・
            ・
            ・
    },
]

BASE_DIRmanage.pyのあるディレクトリを指しています。

apps.py

articlesディレクトリにapps.pyを作成します。

from django.apps import AppConfig


class ArticlesConfig(AppConfig):
    default_auto_field = 'django.db.models.BigAutoField'
    name = 'articles'

この後作成するmodels.pyの内容をデータベースに反映するため、読み込む対象を記述します。

modelの定義

まずmodels.pyを作りましょう。
作成ディレクトリはarticlesです。

データベースの作成準備みたいなものだと理解しておいてください。

カラム

ブログアプリのために必要なカラムは以下としましょう。

カラム 概要 オプション
title タイトル 文字数制限:50
article_body 本文 文字数制限:10,000
created_at 作成日時 現在日時を入れる
updated_at 更新日時 現在日時を入れる

models.py

上記のカラムを実現するために、models.pyは以下のように書きます。

from django.db import models


class Articles(models.Model):
    title = models.CharField(verbose_name='タイトル', max_length=50)
    article_body = models.TextField(verbose_name='本文', max_length=10000)
    created_at = models.DateTimeField(verbose_name='作成日時', auto_now_add=True)
    updated_at = models.DateTimeField(verbose_name='更新日時', auto_now=True)

マイグレーション

以下のコマンドを実行して、models.pyに書いた内容を反映させましょう!

python3 manage.py makemigrations articles
python3 manage.py migrate

OKがたくさん出てきたら完了です!migrationsディレクトリ内にいくつかファイルが生成されているはずです。models.pyは一旦閉じて大丈夫です。

ルーティング

blog/urls.py

URLの紐付けをしてあげましょう。
blogディレクトリ内にあるurls.pyを開きましょう。コメントアウトされている部分は邪魔なので削除しちゃいます。

必要なものをインポートします。以下を追記してください。

from django.urls import include
from articles.views import ArticlesListView

1行目は、すでにある

from django.urls import path

に追記する形で

from django.urls import path, include

としても良いでしょう。

2行目は、articlesディレクトリ下にあるviews.pyからArticlesListViewクラスを読み込むよ、ということが書いてあります。

ArticlesListViewはまだ作っていないのでエラーを吐かれると思いますが、気にしなくて大丈夫です。

続いて、urlpatternsに追記をします。以下のようになっていれば問題ありません。

urlpatterns = [
    path('admin/', admin.site.urls),
    path('articles/', include('articles.urls')),
    path('', ArticlesListView.as_view(), name="home"),
]

2つ目のパスにあるarticles.urlsからurlを読み込みたいので、次はarticlesディレクトリ内にurls.pyを作りましょう。

articles/urls.py

from django.urls import path
from . import views

app_name = "articles"

2つ目のインポートは、views.pyから全部読み込むよ、としています。

後々楽なので、app_nameと名前空間を設定しておきます。

さらに以下を追記しましょう。

urlpatterns = [
    # 記事の一覧画面 /articles
    path('', views.ArticlesListView.as_view(), name='articles_list'),
    # 記事の登録画面 /articles/create
    path('create/', views.ArticleCreateView.as_view(), name='article_create'),
    # 記事の登録内容確認画面 /articles/create_confirm
    path('create_confirm', views.ArticleCreateConfirm.as_view(), name='article_create_confirm'),
    # 記事の詳細画面 /articles/{id}
    path('<int:pk>/', views.ArticleDetailView.as_view(), name='article_detail'),
    # 記事の編集画面 /articles/{id}/edit
    path('<int:pk>/edit/', views.ArticleUpdateView.as_view(), name='article_edit'),
    # 記事の編集内容確認画面 /articles/{id}/edit_confirm
    path('<int:pk>/edit_confirm', views.ArticleUpdateConfirmView.as_view(), name='article_edit_confirm'),
    # 記事の削除確認画面 /articles/{id}/delete_confirm
    path('<int:pk>/delete_confirm', views.ArticleDeleteView.as_view(), name='article_delete'),
]

pkprimary keyの略です。よく使われる略語だそうです。

views.py

ルーティングでインポートしたいクラスたちを作りましょう。

まずviews.pyを作ります。作成ディレクトリはarticlesです。

記事の一覧画面

from django.views.generic.list import ListView

from .models import Articles


class ArticlesListView(ListView):
    # 表示するページのhtmlの指定
    template_name = 'articles/articles_list.html'
    # modelの指定
    model = Articles
    # オブジェクト名の指定
    context_object_name = 'articles_list'
    # リストの表示順序の指定
    ordering = ['-updated_at']

ListViewをインポートして、クラスの引数に入れてあげると、いい感じのリスト表示を実装してくれます。

template_nameで指定したhtmlは後ほど作ります。

modelは最初に作ったデータベースです。

記事の登録画面

views.pyに追記をしていきます。

from django.urls import reverse_lazy
from django.views.generic.edit import CreateView
class ArticleCreateView(CreateView):
    # 表示するページのhtmlの指定
    template_name = 'articles/article_create.html'
    # modelの指定
    model = Articles
    # form_classの指定
    form_class = ArticleForm
    # 登録成功時のアクション
    success_url = reverse_lazy("articles:create_confirm")

ArticleFormはこれから作成しますので、未定義のままで問題ありません。

forms.py

他のviewクラスを作る前に、formを作ってしまいましょう。

まずforms.pyを作ります。作成ディレクトリはarticlesです。

from django import forms

from .models import Articles


class ArticleForm(forms.ModelForm):
    class Meta:
        # 対象のモデルを指定
        model = Articles
        # 登録するカラムの指定
        fields = ['title', 'article_body']
        # ラベルを設定
        labels = {
            'title': 'タイトル',
            'article_body': '記事本文',
        }
        # プレースホルダーを設定
        widgets = {
            'title': forms.TextInput(attrs={
                'placeholder': '50字以内で入力してください'
            }),
            'article_body': forms.Textarea(attrs={
                'placeholder': '1万字以内で入力してください',
                'cols': '30',
                'rows': '10',
            }),
        }

widgetsの設定は任意です。なくても問題ありません。

views.py (2回目)

views.pyに戻って、追記をしていきます。

記事の登録内容確認画面

from django.shortcuts import redirect, render
from .forms import ArticleForm
class ArticleCreateConfirm(View):
    # HTTPの要求への応答を処理する。
    def post(self, request, *args, **kwargs):
        form = ArticleForm(request.POST or None)
        # テンプレートに渡すのに必要
        context = {'form': form}
        if 'confirm' in request.POST:
            return render(request, 'articles/article_create_confirm.html', context)
        if 'back' in request.POST:
            return render(request, 'articles/article_create.html', context)
        if 'create' in request.POST:
            # フォームのバリデーションを実行
            if form.is_valid():
                form.save()
                return redirect('home')
            else:
                return render(request, 'articles/article_create.html', context)

記事の登録内容確認画面で、

  • confirm = 確認 のPOSTがあれば、確認画面をレンダリングする
  • back = 戻る のPOSTがあれば、記事登録画面に戻る
  • create = 作成 のPOSTがあったときは、
    • バリデーションの結果、正常値(True)なら、保存して、ホームにリダイレクトする
    • バリデーションの結果、異常値(False)なら、記事登録画面に戻る

という分岐が設定されています。

記事の詳細画面

views.pyに追記をしていきます。

from django.views.generic.detail import DetailView
class ArticleDetailView(DetailView):
    # 表示するページのhtmlの指定
    template_name = 'articles/article_detail.html'
    # modelの指定
    model = Articles
    # オブジェクト名の指定
    context_object_name = 'article'

記事の編集画面

views.pyに追記をしていきます。

from django.views.generic.edit import UpdateView
class ArticleUpdateView(UpdateView):
    # 表示するページのhtmlの指定
    template_name = 'articles/article_edit.html'
    # modelの指定
    model = Articles
    # form_classの指定
    form_class = ArticleForm

    # 取得するデータに関する部分
    def get_context_data(self, **kwargs):
        kwargs['article_id'] = self.object.pk
        return super().get_context_data(**kwargs)

記事の編集内容確認画面

views.pyに追記をしていきます。

from django.shortcuts import get_object_or_404
class ArticleUpdateConfirmView(View):
    # HTTPの要求への応答を処理する。
    def post(self, request, *args, **kwargs):
        article_instance = get_object_or_404(Articles, pk=kwargs['pk'])
        form = ArticleForm(request.POST or None, instance=article_instance)
        # テンプレートに渡すのに必要
        context = {'form': form, 'article': article_instance, 'article_id': kwargs['pk']}
        if 'confirm' in request.POST:
            return render(request, 'articles/article_edit_confirm.html', context)
        if 'back' in request.POST:
            return render(request, 'articles/article_cedit.html', context)
        if 'create' in request.POST:
            # フォームのバリデーションを実行
            if form.is_valid():
                form.save()
                return redirect('home')
            else:
                return render(request, 'articles/article_edit.html', context)

記事の登録内容確認画面の部分と似た感じになっています。

記事の削除確認画面

views.pyに追記をしていきます。

from django.views.generic.edit import DeleteView
class ArticleDeleteView(DeleteView):
    # 表示するページのhtmlの指定
    template_name = 'articles/article_delete.html'
    # modelの指定
    model = Articles
    # 登録成功時のアクション
    success_url = reverse_lazy("articles:create_list")
    # オブジェクト名の指定
    context_object_name = 'article_delete'

テンプレート

テンプレートに渡すデータの準備ができたので、テンプレートを用意しましょう!

articles/templates/articles内に、空のhtmlファイルを一気に作っておきましょう。
ファイル名はviews.pyに書き入れたものと一致させてください。

Django
    ┗ articles
       └ templates
          └ articles
            ├ base.html
            ├ articles_list.html
            ├ article_create.html
            ├ article_create_confirm.html
            ├ article_detail.html
            ├ article_edit.html
            ├ article_edit_confirm.html
            └ article_delete.html

ベースファイル base.html

まずbase.htmlを編集します。

<!DOCTYPE html>
<html lang="ja">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Blogアプリ</title>
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet"
        integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">
</head>

<body>
</body>

</html>

今回は、CSSにBootstrapを利用します。

<body>タグの中身の一例です。

<nav class="navbar navbar-expand-lg navbar-light bg-light">
    <div class="container-fluid">
        <a href="{% url 'articles:articles_list' %}" class="navbar-brand">Blogアプリ</a>
        <button class="navbar-toggler" type="button" data-bs-toggle="collapse"
            data-bs-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 me-auto mb-2 mb-lg-0">
                <li class="nav-item">
                    <a class="nav-link" href="{% url 'articles:article_create' %}">記事を追加</a>
                </li>
            </ul>
        </div>
    </div>
</nav>
<div class="container">{% block content %} {% endblock %}</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.bundle.min.js"
    integrity="sha384-MrcW6ZMFYlzcLA8Nl+NtUVF0sA7MsXsP1UyJoMp4YLEuNSfAP+JcXn/tWtIaxVXM"
    crossorigin="anonymous"></script>

動的にしたい部分は{% %}で囲めば大丈夫です。

articles:articles/urls.pyで設定した名前空間を示しています。

base.htmlで記述しているのは、ヘッダー部分と、メインコンテンツの枠組みのみです。

記事の一覧画面 articles_list.html

次にarticles_list.htmlを編集しましょう。まずは枠組みです。

{% extends 'articles/base.html' %} {% block content %}

{% endblock %}

base.htmlを継承して、contentと名のつくブロックを上書きするということが書いてあります。

書き方自体は自由ですが、今回はテーブルで作ってみようと思います。上記の内側に以下を追記してください。

<h2 class="m-2 border-bottom">記事一覧</h2>
<table class="table table-striped table-hover">
    <thead>
        <tr>
            <th scope="col">タイトル</th>
            <th scope="col">本文</th>
            <th scope="col">作成日時</th>
            <th scope="col">最終更新日時</th>
            <th scope="col"></th>
        </tr>
    </thead>
    <tbody>

    </tbody>
</table>

tbodyの中に、データベースからデータを持ってきて表示させたいので、ループ処理を入れます。tbodyの中に以下を追記してください。

{% for article in articles_list %}
<tr>
    <td>
        <div class="overflow-hidden" style="height: 70px; max-width: 300px;">{{ article.title }}</div>
    </td>
    <td>
        <div class="overflow-hidden" style="height: 70px; max-width: 300px;">{{ article.article_body }}</div>
    </td>
    <td>
        {{ article.created_at }}
    </td>
    <td>
        {{ article.updated_at }}
    </td>
    <td>
        <a class="btn btn-primary" href="{% url 'articles:article_detail' article.pk %}">詳細</a>
    </td>
</tr>
{% endfor %}

pythonのfor処理と似た感じで書けます。

ここまで書くと、以下のようになっているはずです。

{% extends 'articles/base.html' %} {% block content %}
<h2 class="m-2 border-bottom">記事一覧</h2>
<table class="table table-striped table-hover">
    <thead>
        <tr>
            <th scope="col">タイトル</th>
            <th scope="col">本文</th>
            <th scope="col">作成日</th>
            <th scope="col">最終更新日</th>
            <th scope="col"></th>
        </tr>
    </thead>
    <tbody>
        {% for article in articles_list %}
        <tr>
            <td>
                <div class="overflow-hidden" style="height: 70px; max-width: 300px;">{{ article.title }}</div>
            </td>
            <td>
                <div class="overflow-hidden" style="height: 70px; max-width: 300px;">{{ article.article_body }}</div>
            </td>
            <td>
                {{ article.created_at }}
            </td>
            <td>
                {{ article.updated_at }}
            </td>
            <td>
                <a class="btn btn-primary" href="{% url 'articles:article_detail' article.pk %}">詳細</a>
            </td>
        </tr>
        {% endfor %}
    </tbody>
</table>
{% endblock %}

記事の登録画面 article_create.html

次は記事の登録画面を作りましょう。article_create.htmlを編集します。枠組みはさっきと同じです。

{% extends 'articles/base.html' %} {% load widget_tweaks %} {% block content %}
<h2 class="m-2 border-bottom">新規記事作成</h2>
<form class="m-3" action="{% url 'articles:article_create_confirm' %}" method="post">
    {% csrf_token %}
    <div class="mb-3">
        <label for="titleForm" class="form-label">{{ form.title.label }}</label>
        {{ form.title | add_class:'form-control' }}
    </div>
    <div class="mb-3">
        <label for="bodyForm" class="form-label">{{ form.content.article_body }}</label>
        {{ form.article_body | add_class:'form-control' }}
    </div>
    <input class="btn btn-success" type="submit" name="confirm" value="投稿内容確認">
</form>
{% endblock %}

{% csrf_token %}はセキュリティ上必要な呪文だと思って書きましょう。役割について知りたい方はこちらのnoteを参照してください。

form.pyで指定した内容が反映されます。

記事の登録内容確認画面 article_create_confirm.html

次は記事の登録内容確認画面を作りましょう。article_create_confirm.htmlを編集します。枠組みはこれまでと同じです。

{% extends 'articles/base.html' %} {% block content %}
<h2 class="m-2 border-bottom">投稿内容確認</h2>
<p class="m-3">以下の内容で投稿します</p>
<div class="mx-3 card">
    <div class="card-body">
        <h3 class="card-title">{{ form.title.value }}</h3>
        <p class="card-text">{{ form.article_body.value }}</p>
        <small class="card-text text-body-secondary text-muted">文字数: {{ form.article_body.value | length }}</small>
    </div>
</div>
<form method="post" action="{% url 'articles:article_create_confirm' %}" class="m-3">
    {% csrf_token %}
    {% for field in form %} {{ field.as_hidden }} {% endfor %}
    <input class="btn btn-success" type="submit" name="create" value="投稿">
    <input class="btn btn-warning" type="submit" name="back" value="戻る">
</form>
{% endblock %}

forの部分は、隠しパラメータで値を引き継いでいる部分です。

記事の詳細画面 article_detail.html

次は記事の詳細画面を作りましょう。article_detail.htmlを編集します。枠組みはこれまでと同じです。

{% extends 'articles/base.html' %} {% block content %}
<h2 class="m-2 border-bottom">投稿詳細</h2>
<div class="card m-3">
    <div class="card-body">
        <h3 class="card-title">{{ article.title }}</h3>
        <textarea class="card-text form-control-plaintext" readonly>{{ article.article_body }}</textarea>
        <small class="card-text text-body-secondary text-muted">最終更新日時:<time>{{ article.updated_at }}</time></small><br>
        <small class="card-text text-body-secondary text-muted">作成日時:<time>{{ article.created_at }}</time></small>
        <div class="card-text">
            <a href="{% url 'articles:article_edit' article.pk %}" class="btn btn-success">編集</a>
            <a href="{% url 'articles:article_delete' article.pk%}" class="btn btn-danger">削除</a>
        </div>
    </div>
</div>
{% endblock %}

記事の編集画面 article_edit.html

次は記事の編集画面を作りましょう。article_edit.htmlを編集します。枠組みはこれまでと同じです。

{% extends 'articles/base.html' %} {% load widget_tweaks %} {% block content %}
<h2 class="m-2 border-bottom">投稿を編集</h2>
<form class="m-3" action="{% url 'articles:article_edit_confirm' article_id %}" method="post">
    {% csrf_token %}
    <div class="mb-3">
        <label for="titleForm" class="form-label">{{ form.title.label }}</label>
        {{ form.title | add_class:'form-control' }}
    </div>
    <div class="mb-3">
        <label for="bodyForm" class="form-label">{{ form.content.article_body }}</label>
        {{ form.article_body | add_class:'form-control' }}
    </div>
    <input class="btn btn-success" type="submit" name="confirm" value="投稿内容確認">
</form>
{% endblock %}

article_create.htmlの内容をコピーして必要な部分を編集すると楽に書けると思います。

actionarticle_idviews.pyに記述した引数の名前です。

記事の編集内容確認画面 article_edit_confirm.html

次は記事の編集内容確認画面を作りましょう。article_edit_confirm.htmlを編集します。枠組みはこれまでと同じです。

{% extends 'articles/base.html' %} {% block content %}
<h2 class="m-2 border-bottom">編集内容確認</h2>
<p class="m-3">以下の内容に更新します</p>
<div class="mx-3 card">
    <div class="card-body">
        <h3 class="card-title">{{ form.title.value }}</h3>
        <p class="card-text">{{ form.article_body.value }}</p>
        <small class="card-text text-body-secondary text-muted">文字数: {{ form.article_body.value | length }}</small>
    </div>
</div>
<form method="post" action="{% url 'articles:article_edit_confirm' article_id %}" class="m-3">
    {% csrf_token %}
    {% for field in form %} {{ field.as_hidden }} {% endfor %}
    <input class="btn btn-success" type="submit" name="create" value="更新">
    <input class="btn btn-warning" type="submit" name="back" value="戻る">
</form>
{% endblock %}

article_create_confirm.htmlの内容をコピーして必要な部分を編集すると楽に書けると思います。

記事の削除確認画面 article_delete.html

最後に記事の削除確認画面を作りましょう。article_delete.htmlを編集します。枠組みはこれまでと同じです。

{% extends 'articles/base.html' %} {% block content %}
<h2 class="m-2 border-bottom">投稿削除確認</h2>
<p class="m-3">以下の投稿を削除します</p>
<div class="mx-3 card">
    <div class="card-body">
        <h3 class="card-title">{{ article_delete.title }}</h3>
        <p class="card-text">{{ article_delete.article_body }}</p>
        <small class="card-text text-body-secondary text-muted">
            最終更新日時:<time>{{ article_delete.updated_at }}</time>
        </small><br>
        <small class="card-text text-body-secondary text-muted">
            作成日時:<time>{{ article_delete.created_at }}</time>
        </small>
    </div>
</div>
<form method="post" class="m-3">
    {% csrf_token %}
    <input class="btn btn-danger" type="submit" name="create" value="削除">
    <a class="btn btn-warning" href="{% url 'articles:article_detail' article_delete.pk %}">戻る</a>
</form>
{% endblock %}

admin.py

もう開発は大詰めです。ここからはサイトを管理するページを設定していきます。

articlesディレクトリにadmin.pyを作り、記述していきます。

from django.contrib import admin

from .models import Articles

admin.site.register(Articles)

これでモデルクラスが表示されるようになりました。

管理者ユーザーの作成

localhost/adminにアクセスすると、ログイン画面が開きます。

しかし、まだユーザーを作っていないので入れません。ここから作っていきましょう。

ターミナルにて以下のコマンドを実行します。

python3 manage.py createsuperuser

するとユーザー名、メールアドレス、パスワードの入力を求められます。

今回は適当に、

  • admin
  • <dummy>admin</dummy>@sample.com
  • adminpass

としましょう。パスワードが簡単すぎると怒られますが、yを入力すればそのまま登録できます。

テスト

articles/tests.py

これ結構大事らしいんですが、今回は使わない予定なので、一旦削除しちゃって大丈夫です。

tests.pyでテストを実行したい方はこちらの記事を参考にしてください。

手動によるテスト

手動でのテストは難しくないのでやりましょう。

  1. 実装機能の洗い出し

箇条書きで洗い出しましょう。と言いつつ、冒頭で既に出しているので再掲になります。今回実装した機能は以下の通りです。

  • 記事の一覧画面( / と /articles)
    • ヘッダーの表示
      • トップへのリンク
      • 記事作成ページへ遷移できるリンク
    • タイトル/本文/最終更新日時/作成日時を表示
    • 記事は最終更新日時の新しいものから順に並べる
    • 詳細画面へ遷移できるリンク
  • 記事の登録画面(/articles/create)
    • 入力フォーム
      • 全要素必須
      • 文字数
    • 投稿内容確認ボタン
  • 記事の登録内容確認画面(/articles/create_confirm)
    • 入力内容の表示
    • 文字数の表示
    • 戻るボタン
    • 投稿ボタン
  • 記事の編集画面(/articles/{id}/edit)
    • 編集フォーム
      • 編集対象のレコードが初期値で入力されている
    • バリデーション
      • 全要素必須
      • 文字数
    • 投稿内容確認ボタン
    • 戻るボタン
    • 投稿ボタン
  • 記事の編集内容確認画面(/articles/{id}/edit_confirm)
    • 入力内容の表示
    • 戻るボタン
    • 投稿ボタン
  • 記事の詳細画面(/articles/{id})
    • タイトル/本文/最終更新日時/作成日時を表示
    • 編集画面へのリンク
    • 削除確認画面へのリンク
  • 記事の削除確認画面(/articles/{id}/delete_confirm)
    • 削除対象のタイトル/本文/最終更新日時/作成日時を表示
    • 戻るボタン
    • 削除ボタンを押下で削除実行
  • 404ページ
    • 存在しないidをURL指定すると表示
  1. テストケースの洗い出し

洗い出した機能に求められる要件を洗い出しましょう。スプレッドシートなどでまとめるとリストアップもテストもしやすいと思います。

終わり!

テストも無事終了したら、ブログアプリ作成完了です。

お疲れ様でした!

参考

Discussion