7K4B blog

猫でも分かる何か

django 5-14 本棚アプリケーション2

トップ画面の作成

from django.shortcuts import render
(略)
def index_view(request):
    object_list = Book.objects.order_by('category')
    return render(request, 'book/index.html', {'object_list': object_list})

test/bookproject5/bookproject/book/views.py を書く
def index_view(request): を追加
Book.objects.order_by('category') と書くことでカテゴリでソートする
django は request を受け取り response を返すのが基本の流れで render は基本の流れを実現させる基本関数
第一引数は request を受け取るのが定跡
第二引数は 表示する html を指定する( settings.py の DIR からの相対パスであり BASE_DIR はmanage.py のあるディレクトリや各アプリのディレクトリ両方を表すので BASE_DIR/templates は 任意アプリ/templates などを表す)
第三引数は object_list を 'object_list' という名前で参照できるようにしている
なお object_list は book アプリの views.py で定義している index_view 関数で定義しているメンバ object_list = Book.objects.order_by('-id')
また Book は book アプリの models.py で定義しているクラス(イメージはC++の構造体に近い)

{% extends 'base.html' %}
{% block title %} 書籍一覧 {% endblock %}
{% block h1 %} 書籍一覧 {% endblock %}

{% block content %}
    {% for item in object_list %}
    <div class="p-4 m-4 bg-light border border-success rounded">
        <h2 class="text-success">{{ item.title }}</h2>
        <h6> カテゴリ:{{ item.category }} </h6>
        <div class="mt-3">
            <a href="{% url 'detail-book' item.pk %}">詳細へ</a>
        </div>
    </div>
   {% endfor %}
{% endblock content %}

test/bookproject5/bookproject/book/templates/book/index.html を書く
test/bookproject5/bookproject/templates/base.html を継承する形で実装する
したがって、具体的な実装は {% block content %}{% endblock content %} 内になる
div class p-4 m-4 はパディング・マージン設定
django で <a> タグの記述方法は {% url 'A:B' *C %} になる
A は namespace(今回は未使用)、B は name、C は 値
item.pk について
{% for item in object_list %}
<a href="{% url 'detail-book' item.pk %}">詳細へ</a>
test/bookproject5/bookproject/book/templates/book/index.html の a タグと
path('book//detail/', views.DetailBookView.as_view(), name='detail-book'),
test/bookproject5/bookproject/book/urls.py の urlpatterns がリンクしてることを確認すると分かりやい
また object_list は Book.objects の参照型
(これは test/bookproject5/bookproject/book/views.py の index_view(request): で定義している)
参考 https://qiita.com/kotayanagi/items/b97fe4a85b03cc6880ac


ログイン機能の実装

from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path('admin/', admin.site.urls),
    path('accounts/', include('django.contrib.auth.urls')),
    path('', include('book.urls')),
]

test/bookproject5/bookproject/bookproject/urls.py を書く
path('accounts/', include('django.contrib.auth.urls')), を追加
ログイン機能 view は django デフォルトで用意されている(便利)

{% extends 'base.html' %}
{% block content %}
<h1>ログイン</h1>
<form method="post" class="p-4 m-4 bg-light border border-success rounded form-group">
    {% csrf_token %}
    {% for error in form.errors.values %}
        {{ error }}
    {% endfor %}
    <label>ユーザID</label>
    <input class="form-control" name="username">
    <label>パスワード</label>
    <input type="password" class="form-control" name="password">
    <button type="submit" class="btn btn-success mt-4">ログインする</button>
</form>
{% endblock %}

test/bookproject5/bookproject/templates/registration/login.html を書く
なお django デフォルトのログイン view を使うには html を置くディレクトリが決められている
なので面倒だが templates/registration/ ディレクトリに login.html を自作する必要がある

from pathlib import Path
(略)
LOGIN_REDIRECT_URL = 'list-book'
(略)

test/bookproject5/bookproject/settings.py を書く
適当な場所に LOGIN_REDIRECT_URL = 'list-book' を追加
デフォルト設定は LOGIN_REDIRECT_URL='/accounts/profile/' だが用意してないので 'list-book' URL にリダイレクトするよう設定
list-book は test/bookproject5/bookproject/book/urls.py に定義されている
path('book/', views.ListBookView.as_view(), name='list-book'),
参考 https://qiita.com/momomo_rimoto/items/7edb608d85fda53644db


ログアウト機能の実装(なお、この機能は後でアカウント管理アプリに統合する)

(略)
urlpatterns = [
(略)
    path('logout/', views.logout_view, name='logout'),
]

test/bookproject5/bookproject/book/urls.py を書く
path('logout/', views.logout_view, name='logout'), を追加

from django.shortcuts import render, redirect
from django.urls import reverse_lazy
from django.views.generic import ListView, DetailView, CreateView, DeleteView, UpdateView
from django.contrib.auth import logout
from .models import Book
(略)
def index_view(request):
    object_list = Book.objects.order_by('category')
    return render(request, 'book/index.html', {'object_list': object_list})

def logout_view(request):
    logout(request)
    return redirect('index')

test/bookproject5/bookproject/book/views.py を書く
from django.shortcuts import render, redirect を追加
from django.contrib.auth import logout を追加
def logout_view(request): を追加

<!doctype html>
<html lang="en">
  <head>
    <!-- Required meta tags -->
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">

    <!-- Bootstrap CSS -->
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">

    <title>{% block title%}{% endblock title %}本棚アプリ</title>
  </head>
  <body>
    <nav class="navbar navbar-dark bg-success sticky-top">
        <div class="navbar-nav d-flex flex-row">
          <a class="nav-link mx-3" href="{% url 'list-book' %}">書籍一覧</a>
          <a class="nav-link mx-3" href="{% url 'create-book' %}">書籍登録</a>
        </div>
        <div class="navbar-nav d-flex flex-row">
          {% if request.user.is_authenticated %}
          <a class="nav-link mx-3" href="{% url 'logout' %}">ログアウト</a>
          {% else %}
          <a class="nav-link mx-3" href="{% url 'login' %}">ログイン</a>
          {% endif %}
        </div>
    </nav>
    <div class='p-4'>
        <h1>{% block h1 %}{% endblock %}</h1>
        {% block content %}{% endblock content %}
    </div>
  </body>
</html>

test/bookproject5/bookproject/templates/base.html を書く
動作確認は http://127.0.0.1:8000/logout/http://127.0.0.1:8000/admin/ で出来る
(ログアウトしていると管理画面で入力を要求される)
<div class='p-4'> 以外は上部メニュー(なので使いまわせるので継承する形で利用していく)

アカウント登録機能の実装(アカウント管理アプリの作成)

ここからアカウント関連の専用アプリを新規に作っていく
新しくアプリを作るため urls や views の中身を1から書き直していく

(venv) test/bookproject5$ cd bookproject
(venv) $ python3 manage.py startapp accounts


(略)
INSTALLED_APPS = [
(略)
    'accounts.apps.AccountsConfig',
]
(略)

test/bookproject5/bookproject/bookproject/settings.py を書く
'accounts.apps.AccountsConfig', を追加
django はアプリを作ったら必ず INSTALLED_APPS に登録する必要がある

from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path('admin/', admin.site.urls),
    path('accounts/', include('accounts.urls')),
    path('', include('book.urls')),
]

test/bookproject5/bookproject/bookproject/url.s.py を書く
path('accounts/', include('accounts.urls')), に修正

from django.urls import path
from . import views

urlpatterns = [
    path('', views.index_view, name='index'),
    path('book/', views.ListBookView.as_view(), name='list-book'),
    path('book/<int:pk>/detail/', views.DetailBookView.as_view(), name='detail-book'),
    path('book/create/', views.CreateBookView.as_view(), name='create-book'),
    path('book/<int:pk>/delete/', views.DeleteBookView.as_view(), name='delete-book'),
    path('book/<int:pk>/update/', views.UpdateBookView.as_view(), name='update-book'),
]

test/bookproject5/bookproject/book/url.s.py を書く

from django.shortcuts import render, redirect
from django.urls import reverse_lazy
from django.views.generic import ListView, DetailView, CreateView, DeleteView, UpdateView
from .models import Book

class ListBookView(ListView):
    template_name = 'book/book_list.html'
    model = Book

class DetailBookView(DetailView):
    template_name = 'book/book_detail.html'
    model = Book

class CreateBookView(CreateView):
    template_name = 'book/book_create.html'
    model = Book
    fields = ('title', 'text', 'category')
    success_url = reverse_lazy('list-book')

class DeleteBookView(DeleteView):
    template_name = 'book/book_confirm_delete.html'
    model = Book
    success_url = reverse_lazy('list-book')

class UpdateBookView(UpdateView):
    model = Book
    fields = (['title', 'text', 'category'])
    template_name = 'book/book_update.html'
    success_url = reverse_lazy('list-book')

def index_view(request):
    object_list = Book.objects.order_by('category')
    return render(request, 'book/index.html', {'object_list': object_list})

test/bookproject5/bookproject/book/views.py を書く

from django.urls import URLPattern, path
from django.contrib.auth.views import LoginView, LogoutView
from .views import SignupView

app_name = 'accounts'

urlpatterns = [
    path('login/', LoginView.as_view(), name='login'),
    path('logout/', LogoutView.as_view(), name='logout'),
    path('signup/', SignupView.as_view(), name='signup'),
]

test/bookproject5/bookproject/accounts/urls.py を書く
django が最初から用意している class LoginView, class LogoutView, class SignupView を使う
url の名前(例えば'login'や'logout')を複数つけてしまったときに対応させるために app_name を定義しておく
href = "{% url 'accounts:login' %}" のように html タグで url を指定できるようになる( C++ namespace のような役割)

(略)
LOGIN_REDIRECT_URL = 'index'
LOGOUT_REDIRECT_URL = 'index'
(略)

test/bookproject5/bookproject/bookproject/settings.py を書く
ログイン・ログアウト機能はリダイレクトURLを必ず設定しなければならない
LOGIN_REDIRECT_URL = 'index' に修正
LOGOUT_REDIRECT_URL = 'index' を追加
'index' URL つまり トップページにリダイレクトさせる
django 標準クラス LoginView は setting.py の LOGIN_REDIRECT_URL / LOGOUT_REDIRECT_URL を参照するように定義されている)

<!doctype html>
<html lang="en">
  <head>
    <!-- Required meta tags -->
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">

    <!-- Bootstrap CSS -->
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">

    <title>{% block title%}{% endblock title %}本棚アプリ</title>
  </head>
  <body>
    <nav class="navbar navbar-dark bg-success sticky-top">
        <div class="navbar-nav d-flex flex-row">
          <a class="nav-link mx-3" href="{% url 'list-book' %}">書籍一覧</a>
          <a class="nav-link mx-3" href="{% url 'create-book' %}">書籍登録</a>
        </div>
        <div class="navbar-nav d-flex flex-row">
          {% if request.user.is_authenticated %}
          <a class="nav-link mx-3" href="{% url 'accounts:logout' %}">ログアウト</a>
          {% else %}
          <a class="nav-link mx-3" href="{% url 'accounts:login' %}">ログイン</a>
          <a class="nav-link mx-3" href="{% url 'accounts:signup' %}">会員登録</a>
          {% endif %}
        </div>
    </nav>
    <div class='p-4'>
        <h1>{% block h1 %}{% endblock %}</h1>
        {% block content %}{% endblock content %}
    </div>
  </body>
</html>

test/bookproject5/bookproject/templates/base.html を書く
<a class="nav-link mx-3" href="{% url 'accounts:logout' %}">ログアウト</a> に修正
<a class="nav-link mx-3" href="{% url 'accounts:login' %}">ログイン</a> に修正

from django.shortcuts import render
from django.contrib.auth.models import User
from django.urls import reverse_lazy
from django.views.generic import CreateView

from .forms import SignupForm

class SignupView(CreateView):
    model = User
    form_class = SignupForm
    template_name = 'accounts/signup.html'
    success_url = reverse_lazy('index')

test/bookproject5/bookproject/accounts/view.py を書く
なお CreateView は ModelFormMixin や FormMixin などを継承している
基底クラス ModelFormMixin は model, template_name, success_url を持つ
基底クラス FormMixin は form_class を持つ
自作クラス SignupForm を指定(なお、指定しなかった場合はmodelで指定したモデルに基づいてフォームが作成される)
参考 https://docs.djangoproject.com/ja/4.0/ref/class-based-views/generic-editing/
参考 https://docs.djangoproject.com/ja/4.0/ref/class-based-views/mixins-editing/#django.views.generic.edit.ModelFormMixin

from django.contrib.auth.forms import UserCreationForm
from django.contrib.auth.models import User

class SignupForm(UserCreationForm):
    class Meta:
        model = User
        fields = ('username',)

test/bookproject5/bookproject/accounts/forms.py を書く
パスワードの定義は標準 UserCreationForm の中に password1, password2 という変数で定義されているので書く必要なし
本当はユーザー名も同上だが練習のため自分で定義した
なお class Meta: の中で定義するのは非本質を定義するときの定跡らしい

{% extends 'base.html' %}
{% block title %}アカウント作成{% endblock %}
{% block h1 %}アカウント作成{% endblock %}
{% block content %}
<form method="post" class="p-4 m-4 bg-light border border-success rounded form-group">
    {% csrf_token %}
    <input type="text" name='username' class="form-control my-4" placeholder="ユーザーID">
    <input type="password" name='password1' class="form-control mt-4" placeholder="パスワード">
    <input type="password" name='password2' class="form-control mt-4" placeholder="パスワード確認用">
    <small class="mb-2 d-block text-start">パスワードは8文字以上で設定してください。</small>
    {% if form.errors %}
    <span class="mb-2 small text-danger d-block text-start">利用できないユーザーIDやパスワードの可能性があります。入力内容を再度確認してください。</span>
    {% endif %}
    <button type="submit" class="btn btn-success m-2">アカウント作成</button>
</form>
{% endblock %}

test/bookproject5/bookproject/accounts/signup.html を上記のように書く
name='password1' にすることで標準クラス UserCreationForm で定義されている password1 に対応する
http://127.0.0.1:8000/accounts/signup/ にアクセスして例えば tanaka(任意のパスワード)を登録してみる
http://127.0.0.1:8000/admin/ にアクセスして登録できているか確認


レビュー機能の追加

from django.urls import path
from . import views

urlpatterns = [
(略)
    path('book/<int:book_id>/review/', views.CreateReviewView.as_view(), name='review'),
]

test/bookproject5/bookproject/book/urls.py を書く
path('book//review/', views.CreateReviewView.as_view(), name='review'), を追加
自作クラス CreateReviewView は BaseView を継承している CreateView を継承しているので as_view() を呼び出すことが出来る
なお as_view() はリクエストを受け取ってレスポンスを返す機能
参考 https://docs.djangoproject.com/ja/4.0/ref/class-based-views/base/#django.views.generic.base

from sre_constants import CATEGORY
from unicodedata import category
from django.db import models
from .consts import MAX_RATE

RATE_CHOICES = [(x, str(x)) for x in range(0, MAX_RATE + 1)]
(略)
class Review(models.Model):
    book = models.ForeignKey(Book, on_delete=models.CASCADE)
    title = models.CharField(max_length=100)
    text = models.TextField()
    rate = models.IntegerField(choices=RATE_CHOICES)
    user = models.ForeignKey('auth.User', on_delete=models.CASCADE)
    def __str__(self):
        return self.title

test/bookproject5/bookproject/book/models.py を書く
from .consts import MAX_RATE を追加
RATE_CHOICES = [(x, str(x)) for x in range(0, MAX_RATE + 1)] を追加
class Review(models.Model): を追加
レビューをDBでやりとりするため models.py を書く必要がある
book = models.ForeignKey(...) は Review モデルが自身以外のモデル(例えば Book モデル)を利用するときに書く
models.ForeignKey(...) の第二引数は Book モデルを削除したときの自身の挙動を指定
on_delete=models.CASCADE で Book モデルを削除したとき自身も削除する指定になる
なお models.ForeignKey(...) の第二引数は必ず指定しなければならない
Field(choices=...) はペア型[A,B]で指定する(今回は IntegerField なので第一引数は int 型)
なお choices= の第二引数は表示名
user = models.ForeignKey('auth.User', on_delete=models.CASCADE) は
user = models.ForeignKey(get_user_model(), on_delete=models.CASCADE) と同じ
参考 https://docs.djangoproject.com/en/4.0/ref/models/fields/
参考 https://djangobrothers.com/blogs/referencing_the_user_model/

MAX_RATE = 5

test/bookproject5/bookproject/book/consts.py を書く
定数は consts.py に書くのが定跡

(venv)$ python3 manage.py makemigrations
(venv)$ python3 manage.py migrate

model を定義したので反映させておく

from django.contrib import admin
from .models import Book, Review

admin.site.register(Book)
admin.site.register(Review)

test/bookproject5/bookproject/book/admin.py を書く
管理画面からReviewモデルを追加削除修正できるようにする

{% extends 'base.html' %}

{% block title %}レビュー投稿{% endblock %}
{% block h1 %}レビュー投稿{% endblock %}
{% block content %}
<form method="post" class="p-4 m-4 ng-light border border-success rounded form-group">
    {% csrf_token %}
    <label>対象書籍</label>
    <input class="form-control" value="{{ book.title }}" readonly>
    <label>タイトル</label>
    <input class="form-control" name="title">
    <label>本文</label>
    <textarea class="form-control" name="text" rows="3"></textarea>
    <label>星の数</label>
    <select class="form-control" name="rate">
        <option value="0">0(最低)</option>
        <option value="1">1</option>
        <option value="2">2</option>
        <option value="3">3(普通)</option>
        <option value="4">4</option>
        <option value="5">5(最高)</option>
    </select>
    <input type="hidden" name='book' value="{{ book.id }}">
    <button type="submit" class="btn btn-success mt-4">投稿する</button>
</form>
{% endblock %}

test/bookproject5/bookproject/book/review_form.html を書く
fields で4個のフォームを指定したので4個の input または select を書く
name には fields の値を指定する
入力しない情報をフォームに渡す場合は input type = "hidden" を使用する

from django.shortcuts import render, redirect
from django.urls import reverse, reverse_lazy
from django.views.generic import ListView, DetailView, CreateView, DeleteView, UpdateView
from .models import Book, Review

class CreateReviewView(CreateView):
    model = Review
    fields = ('book', 'title', 'text', 'rate')
    template_name = 'book/review_form.html'
    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context['book'] = Book.objects.get(pk=self.kwargs['book_id'])
        return context
    def form_valid(self, form):
        form.instance.user = self.request.user
        return super().form_valid(form)
    def get_success_url(self):
        return reverse('detail-book', kwargs={'pk':self.object.book.id})

test/bookproject5/bookproject/book/views.py を上記のように書く
from .models import Book, Review を追加
class CreateReviewView(CreateView): を追加
処理は urls.py で url をパターンマッチして自作クラス CreateReviewView を呼び出し html を返す流れになる
fields ではモデルのうちどれをフォームとして作成するか指定する
get_context_data() は基底クラス CreateView で定義されている関数でオーバーライドする(引数 self, **kwargs は定跡、詳細は省略)
python では * はタプル型の可変長引数 ** は辞書型の可変長引数を表す
kwargs はキーワード引数で url 'book/<int:book_id>/review/' の book_id 情報を持っている
kwargs は標準クラス CreateView に定義されている
kwargs は辞書型なので self.kwargs['book_id'] で url の book_id を取り出すことが出来る
context は辞書型で html にて book.nyaa (nyaa は任意の Book モデルのメンバ変数名)で情報を取得できるようになる
標準クラス Model は object 変数が定義されており持っていて色々と役立つ(ぇ
form_valid() は基底クラス CreateView で定義されている関数でオーバーライドする(引数 self, form は定跡、詳細は省略)
また form_valid() はフォームの送信内容が正常なときに呼び出される関数
基底クラス CreateView はメンバ変数 request を持ち request には user が定義されている
ユーザ情報はユーザーに入力させないので自作クラス CreateReviewView で直接情報を得るようにする
get_success_url() は処理が成功したときにリダイレクトを設定する関数
参考 https://codor.co.jp/django/about-context

{% extends 'base.html' %}
{% block title %}{{ object.title }}{% endblock %}
{% block h1 %}書籍詳細{% endblock %}

{% block content %}
<div class="p-4 m-4 bg-light border border-success rounded">
    <h2 class="text-success">{{ object.title }}</h2>
    <p>{{ object.text }}</p>
    <a href="{% url 'review' object.pk %}" class="btn btn-primary">レビューする</a>
    <a href="{% url 'list-book' %}" class="btn btn-primary">一覧へ</a>
    <a href="{% url 'update-book' object.pk %}" class="btn btn-primary">編集する</a>
    <a href="{% url 'delete-book' object.pk %}" class="btn btn-primary">削除する</a>
    <h6 class="card-title">{{ object.category }}</h6>
</div>
{% endblock content %}

test/bookproject5/bookproject/book/templates/book/book_detail.html を書く
http://127.0.0.1:8000/book/1/review/ にアクセスして下図

レビューを投稿したら http://127.0.0.1:8000/admin/ で確認できる

画像の表示

from sre_constants import CATEGORY
(略)
class Book(models.Model):
    title = models.CharField(max_length = 100)
    text = models.TextField()
    thumbnail = models.ImageField(null=True, blank=True)
    category = models.CharField(max_length = 100, choices = CATEGORY)
    def __str__(self):
        return self.title

class Review(models.Model):
(略)

test/bookproject5/bookproject/book/models.py を書く
thumbnail = models.ImageField() を追加
null は データベースに何もデータが入ってないことを許容するかどうか
blank はフォームに入力されたデータが空でも許容するかどうか
両方とも設定しておくのが定跡

(略)
STATIC_URL = '/static/'
MEDIA_URL = '/media/'
MEDIA_ROOT = BASE_DIR / 'media'
(略)

test/bookproject5/bookproject/setting.py を書く
MEDIA_URL = '/media/' を追加
MEDIA_ROOT = BASE_DIR / 'media' を追加
画像ファイルの保存先は setting.py に書く必要がある
保存先を設定しなかった場合(デフォルト)は BASE_ROOT (manage.py) があるディレクト

(venv)$ pip install pillow
(venv)$ python3 manage.py makemigrations
(venv)$ mkdir media

modelsを変更したのでデータベースに反映する
pillow は python の画像ライブラリ
画像ファイル保存先のフォルダを作っておく

from django.conf import settings
from django.conf.urls.static import static
from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path('admin/', admin.site.urls),
    path('accounts/', include('accounts.urls')),
    path('', include('book.urls')),
]
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

test/bookproject5/bookproject/urls.py を書く
from django.conf import settings を追加
from django.conf.urls.static import static を追加
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) を追加
アプリだけでなく画像ファイルなど静的ファイルも urls を登録する必要がある
静的ファイルの url は urlpatterns の外に書くのが定跡(?)
settings.MEDIA_URL は setting.py の MEDIA_URL を指す
参考 https://just-python.com/document/django/project-setting/static-media

{% extends 'base.html' %}
{% block title %} 書籍一覧 {% endblock %}
{% block h1 %} 書籍一覧 {% endblock %}

{% block content %}
    {% for item in object_list %}
    <div class="p-4 m-4 bg-light border border-success rounded">
        <h2 class="text-success">{{ item.title }}</h2>
        <img src="{{ item.thumbnail.url }}" class="img-thumbnail" />
        <h6> カテゴリ:{{ item.category }} </h6>
        <div class="mt-3">
            <a href="{% url 'detail-book' item.pk %}">詳細へ</a>
        </div>
    </div>
   {% endfor %}
{% endblock content %}

test/bookproject5/bookproject/book/templates/book/index.html を書く
<img src="{{ item.thumbnail.url }}" class="img-thumbnail" /> を追加
object_list は view.py の index_view() で定義してあり Book モデル型
また Book モデルに thumbnail が定義されている
http://127.0.0.1:8000/admin/ から画像付き書籍情報を登録するとトップ画面(http://127.0.0.1:8000/)に表示される


ついでにCreateBookViewなども修正しておく

(略)
class CreateBookView(CreateView):
    template_name = 'book/book_create.html'
    model = Book
    fields = ('title', 'text', 'category', 'thumbnail')
    success_url = reverse_lazy('list-book')
(略)
class UpdateBookView(UpdateView):
    model = Book
    fields = (['title', 'text', 'category', 'thumbnail'])
    template_name = 'book/book_update.html'
    success_url = reverse_lazy('list-book')
(略)

test/bookproject5/bookproject/book/views.py を書く
fields = ('title', 'text', 'category', 'thumbnail') を追加
fields = (['title', 'text', 'category', 'thumbnail']) を追加
フィールドに画像フォームを追加( 'thumbnail' は Book モデルで定義してある)
fields は 基底クラス CreateView のメンバ変数

{% extends 'base.html' %}
{% block title %}書籍作成{% endblock %}

{% block content %}
<form method='POST' enctype="multipart/form-data" class="p-4 m-4 bg-light border border-success rounded form-group">{% csrf_token %}{{form.as_p}}
    <input type='submit' value='作成する'>
</form>
{% endblock content %}

test/bookproject5/bookproject/book/templates/book/book_create.html を書く
<form method='POST' enctype="multipart/form-data" class="p-4 m-4 bg-light border border-success rounded form-group"> を追加
enctype="multipart/form-data" でデータをマルチパート形式で送信する
フォーム内にファイルの送信欄を配置する場合これを指定しなければならない
参考 https://www.tagindex.com/html/form/form_enctype.html

{% extends 'base.html' %}
{% block title %} 書籍修正 {% endblock %}

{% block content %}
<form method="POST" enctype="multipart/form-data" class="p-4 m-4 bg-light border border-success rounded form-group">{% csrf_token %}{{form.as_p}}
    <button type="submit">修正する</button>
</form>
{% endblock content %}

test/bookproject5/bookproject/book/templates/book/book_update.html を書く
<form method='POST' enctype="multipart/form-data" class="p-4 m-4 bg-light border border-success rounded form-group"> を追加

CSSの設定

CSS はデザインを定義する物

{% load static %}
<!doctype html>
<html lang="en">
  <head>
    <!-- Required meta tags -->
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">

    <!-- Bootstrap CSS -->
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">

    <title>{% block title%}{% endblock title %}本棚アプリ</title>
    <link rel="stylesheet" type="text/css" href="{% static 'book/css/style.css' %}">
(略)
</html>

test/bookproject5/bookproject/book/templates/book/book_update.html を書く
先頭に {% load static %} を追加
⁢link rel="stylesheet" type="text/css" href="{% static 'book/css/style.css' %}"> を追加
CSS は static フォルダにファイルを作るので django では load static で CSS を呼び出す

a {
    text-decoration: none;
}

test/bookproject5/bookproject/book/static/book/css/style.css を書く
static を置くディレクトリは setting.py に定義されている
templates と同じで任意のアプリ下に作っても ok
リンクの下線が消えているのが確認できる

ログイン状態の判定をする

from django.shortcuts import render, redirect
from django.urls import reverse, reverse_lazy
from django.views.generic import ListView, DetailView, CreateView, DeleteView, UpdateView
from .models import Book, Review
from django.contrib.auth.mixins import LoginRequiredMixin

class CreateReviewView(LoginRequiredMixin, CreateView):
(略)
class ListBookView(LoginRequiredMixin, ListView):
(略)
class DetailBookView(LoginRequiredMixin, DetailView):
(略)
class CreateBookView(LoginRequiredMixin, CreateView):
(略)
class DeleteBookView(LoginRequiredMixin, DeleteView):
(略)
class UpdateBookView(LoginRequiredMixin, UpdateView):
(略)

test/bookproject5/bookproject/views.py を書く
from django.contrib.auth.mixins import LoginRequiredMixin を追加
クラスの中身は弄らず全てのクラスの引数に LoginRequireMixin を追加する
(継承の順番に注意、Viewクラス処理の前に Mixin 判定をする必要があるため)
LoginRequiredMixin とはその名の通り View にログイン権限をかけることができる Mixin
引数で指定するだけでログイン権限が実現できる
参考 https://just-python.com/document/django/views-basic/views-mixin

自分で投稿したデータしか編集・削除できないようにする

(略)
class Book(models.Model):
    title = models.CharField(max_length = 100)
    text = models.TextField()
    thumbnail = models.ImageField(null=True, blank=True)
    category = models.CharField(max_length = 100, choices = CATEGORY)
    user = models.ForeignKey('auth.User', on_delete=models.CASCADE)
    def __str__(self):
        return self.title
(略)

test/bookproject5/bookproject/book/models.py を書く
user = models.ForeignKey('auth.User', on_delete=models.CASCADE) を追加
とりあえず管理者Userをデフォルト設定にするため、管理者UserIDを確認する

(venv)$ python3 manage.py makemigrations
You are trying to add a non-nullable field 'user' to book without a default; we can't do that (the database needs something to populate existing rows).
Please select a fix:
 1) Provide a one-off default now (will be set on all existing rows with a null value for this column)
 2) Quit, and let me add a default in models.py
Select an option: 1
Please enter the default value now, as valid Python
The datetime and django.utils.timezone modules are available, so you can do e.g. timezone.now
Type 'exit' to exit this prompt
>>> 1
Migrations for 'book':
  book/migrations/0005_book_user.py
    - Add field user to book
(venv)$ python3 manage.py migrate

今からデフォルト設定をするため Select an option: は 1 と入力
管理者をデフォルトにするため Type 'exit' to exit this prompt は 1 と入力

from django.shortcuts import render, redirect
from django.urls import reverse, reverse_lazy
from django.views.generic import ListView, DetailView, CreateView, DeleteView, UpdateView
from .models import Book, Review
from django.contrib.auth.mixins import LoginRequiredMixin
from django.core.exceptions import PermissionDenied
(略)
class CreateBookView(LoginRequiredMixin, CreateView):
    template_name = 'book/book_create.html'
    model = Book
    fields = ('title', 'text', 'category', 'thumbnail')
    success_url = reverse_lazy('list-book')
    def form_valid(self, form):
        form.instance.user = self.request.user
        return super().form_valid(form)
(略)
class DeleteBookView(LoginRequiredMixin, DeleteView):
    template_name = 'book/book_confirm_delete.html'
    model = Book
    success_url = reverse_lazy('list-book')
    def get_object(self, queryset=None):
        obj = super().get_object(queryset)
        if obj.user != self.request.user:
            raise PermissionDenied
        return obj
(略)
class UpdateBookView(LoginRequiredMixin, UpdateView):
    model = Book
    fields = (['title', 'text', 'category', 'thumbnail'])
    template_name = 'book/book_update.html'
    success_url = reverse_lazy('list-book')
    def get_object(self, queryset=None):
        obj = super().get_object(queryset)
        if obj.user != self.request.user:
            raise PermissionDenied
        return obj
    def get_success_url(self):
        return reverse('detail-book', kwargs={'pk':self.object.id})
(略)

test/bookproject5/bookproject/book/views.py を書く
from django.core.exceptions import PermissionDenied を追加
class CreateBookView(LoginRequiredMixin, CreateView): に def form_valid(self, form): を追加
class DeleteBookView(LoginRequiredMixin, DeleteView): に def get_object(self, queryset=None): を追加
class UpdateBookView(LoginRequiredMixin, UpdateView): に def get_object(self, queryset=None): を追加
class UpdateBookView(LoginRequiredMixin, UpdateView): に def get_success_url(self): を追加
get_object は get_context と似た機能を持つが context と違ってURLで指定した単一のデータを取得する
したがって obj.user は 本を登録したユーザーで self.request.user はログインして編集しようとしているユーザーになる
なお get_success_url を追加することで管理者ユーザーでログインして修正ボタンを押したときに書籍詳細ページに移動するようになる
例えば tanaka hoge123456(登録してある管理者でないユーザー) でログインして編集しようとすると403になることが確認できる


トップページのデータをソートして表示

from django.shortcuts import render, redirect
from django.urls import reverse, reverse_lazy
from django.views.generic import ListView, DetailView, CreateView, DeleteView, UpdateView
from .models import Book, Review
from django.contrib.auth.mixins import LoginRequiredMixin
from django.core.exceptions import PermissionDenied
from django.db.models import Avg
(略)
def index_view(request):
    object_list = Book.objects.order_by('-id')
    ranking_list = Book.objects.annotate(avg_rating=Avg('review__rate')).order_by('-avg_rating')
    return render(request, 'book/index.html', {'object_list': object_list, 'ranking_list': ranking_list},)

test/bookproject5/bookproject/book/views.py を書く
from django.db.models import Avg を追加
def index_view(request): を修正 ('review__rate') アンダーバー2個なので注意
annotate(avg_rating=Avg('review__rate')) で以降 avg_rating でレビューの rating の平均値リストを取得できる
したがって order_by('-avg_rating') で平均値リストを降順に取得していることになる
参考 https://codor.co.jp/django/how-to-use-annotate

{% extends 'base.html' %}
{% block title %} 本棚アプリ {% endblock %}
{% block h1 %} 本棚アプリ {% endblock %}

{% block content %}
<div class="row">
    <div class="col-9">
        {% for item in object_list %}
        <div class="p-4 m-4 bg-light border border-success rounded">
            <h2 class="text-success">{{ item.title }}</h2>
            <img src="{{ item.thumbnail.url }}" class="img-thumbnail" />
            <h6> カテゴリ:{{ item.category }} </h6>
            <div class="mt-3">
                <a href="{% url 'detail-book' item.pk %}">詳細へ</a>
            </div>
        </div>
        {% endfor %}
    </div>
   <div class="col-3">
    <h2>評価順</h2>
    {% for ranking_book in ranking_list %}
        <div class="p-4 m-4 bg-light border border-success rounded">
            <h3 class="text-success h5">{{ ranking_book.tilte }}</h3>
            <img src="{{ ranking_book.thumbnail.url }}" class="img-thumbnail" />
            <h6>評価:{{ranking_book.avg_rating|floatformat:2}}点</h6>
            <a href="{% url 'detail-book' ranking_book.id %}">詳細を見る</a>
        </div>
    {% endfor %}
    </div>
</div>
{% endblock content %}

test/bookproject5/bookproject/book/templates/book/index.html を書く
試しにレビューをいくつか投稿して確認

詳細画面にレビューを追加する

{% extends 'base.html' %}
{% block title %}{{ object.title }}{% endblock %}
{% block h1 %}書籍詳細{% endblock %}

{% block content %}
<div class="p-4 m-4 bg-light border border-success rounded">
    <h2 class="text-success">{{ object.title }}</h2>
    <p>{{ object.text }}</p>
    <div class="border p-4 mb-2">
        {% for review in object.review_set.all %}
        <div>
            <h3 class="h4">{{ review.title }}</h3>
            <div class="px-2">
                <span>(投稿ユーザー:{{ review.user.username }}</span>
                <h6>評価:{{ review.rate }}</h6>
                <p>{{ review.text }}</p>
            </div>
        </div>
        {% endfor %}
    </div>
    <a href="{% url 'review' object.pk %}" class="btn btn-primary">レビューする</a>
    <a href="{% url 'list-book' %}" class="btn btn-primary">一覧へ</a>
    <a href="{% url 'update-book' object.pk %}" class="btn btn-primary">編集する</a>
    <a href="{% url 'delete-book' object.pk %}" class="btn btn-primary">削除する</a>
    <h6 class="card-title">{{ object.category }}</h6>
</div>
{% endblock content %}

test/bookproject5/bookproject/book/templates/book/book_detail.html を書く

ページネーションの実装

from django.shortcuts import render, redirect
from django.urls import reverse, reverse_lazy
from django.views.generic import ListView, DetailView, CreateView, DeleteView, UpdateView
from .models import Book, Review
from django.contrib.auth.mixins import LoginRequiredMixin
from django.core.exceptions import PermissionDenied
from django.db.models import Avg
from django.core.paginator import Paginator
from .consts import ITEM_PER_PAGE

def index_view(request):
    object_list = Book.objects.order_by('-id')
    ranking_list = Book.objects.annotate(avg_rating=Avg('review__rate')).order_by('-avg_rating')
    paginator = Paginator(ranking_list, ITEM_PER_PAGE)
    page_number = request.GET.get('page',1)
    page_obj = paginator.page(page_number)
    return render(request, 'book/index.html', {'object_list': object_list, 'ranking_list': ranking_list, 'page_obj':page_obj},)

test/bookproject5/bookproject/book/views.py を書く
from django.core.paginator import Paginator を追加
from .consts import ITEM_PER_PAGE を追加
def index_view(request): を修正
django には paginator という便利な標準クラスがある

MAX_RATE = 5
ITEM_PER_PAGE = 2

test/bookproject5/bookproject/book/consts.py を書く
ITEM_PER_PAGE = 2 を追加

{% if page_obj.has_other_pages %}
    <ul class="list-unstyled m-0 d-flex justify-content-between">
        {% if page_obj.has_previous %}
            <li><a href="?page={{ page_obj.previous_page_number }}">&lt;&lt;前へ</a></li>
        {% else %}
            <li class="text-muted">&lt;&lt;前へ</li>
        {% endif %}
        {% if page_obj.has_next %}
            <li><a href="?page={{ page_obj.next_page_number }}">次へ&gt;&gt;</a></li>
        {% else %}
            <li class="text-muted">次へ&gt;&gt;</li>
        {% endif %}
    </ul>
{% endif %}

test/bookproject5/bookproject/book/templates/book/components/pagination.html を書く

{% extends 'base.html' %}
{% block title %} 本棚アプリ {% endblock %}
{% block h1 %} 本棚アプリ {% endblock %}

{% block content %}
<div class="row">
    <div class="col-9">
        {% for item in object_list %}
        <div class="p-4 m-4 bg-light border border-success rounded">
            <h2 class="text-success">{{ item.title }}</h2>
            <img src="{{ item.thumbnail.url }}" class="img-thumbnail" />
            <h6> カテゴリ:{{ item.category }} </h6>
            <div class="mt-3">
                <a href="{% url 'detail-book' item.pk %}">詳細へ</a>
            </div>
        </div>
        {% endfor %}
    </div>
    <div class="col-3">
        <h2>評価順TOP2</h2>
        {% for ranking_book in page_obj %}
            <div class="p-4 m-4 bg-light border border-success rounded">
                <h3 class="text-success h5">{{ ranking_book.tilte }}</h3>
                <img src="{{ ranking_book.thumbnail.url }}" class="img-thumbnail" />
                <h6>評価:{{ranking_book.avg_rating|floatformat:2}}点</h6>
                <a href="{% url 'detail-book' ranking_book.id %}">詳細を見る</a>
            </div>
        {% endfor %}
        {% include 'book/components/pagination.html' %}
        </div>
</div>
{% endblock content %}

test/bookproject5/bookproject/book/templates/book/index.html を書く
<h2>評価順TOP2</h2> を追加
{% for ranking_book in page_obj %} を追加
{% include 'book/components/pagination.html' %} を追加