todoリストを作る

はじめに

今回はDjangoを使ってtodoリストのアプリケーションを作成しました。

以下のようにやることを表示させ、詳細一覧から詳細を見れるようなアプリケーションを作成しました。
また、更新や削除のボタンを設け、そこからデータの変更をできるようにします。


アプリケーションの作成

まず作業するディレクトリを作り、以下のコードで仮想環境を作成します。

python3 -m venv myvenv

次に仮想環境を立ち上げ、Djangoをインストールします。
更にプロジェクトを作成します。

django-admin startproject todoproject

作成したプロジェクト内に移動し、アプリケーションを作成します。

python3 manage.py startapp todo

このtodoprojectというプロジェクトでアプリケーションを作っていきます。


アプリケーションの機能

作成したtodoアプリケーションでできる機能は次のものです。
作成(create)
削除(delete)
更新(update)
読み取り、一覧の表示(read)
ログイン


機能の説明

やることの登録は127.0.0.1:8000/todo/createにアクセスするとできるようにします。

削除は127.0.0.1:8000/todo/id/dereteにアクセスするようにします。
また、更新は127.0.0.1:8000/todo/id/updateにアクセスするようにします。
また、やることリストを作り、127.0.0.1:8000/todo/にアクセスするとデータを得られるようにします。


それぞれの遷移先のページではbootstrapを使ってhtmlファイルの記述をしました。

以上のページのhtmlを作成し、遷移させるためのtodo/urls.pyとviews.pyを載せておきます。

todo/urls.py

from django.urls import path
from . import views

urlpatterns = [
    path('', views.index_view, name='index'),
    path('todo/', views.ListtodoView.as_view(), name='list-todo'),
    path('todo/create/', views.CreatetodoView.as_view(), name='create-todo'),
    path('todo/<int:pk>/delete/', views.DeletetodoView.as_view(), name='delete-todo'),
    path('todo/<int:pk>/update/', views.UpdatetodoView.as_view(), name='update-todo'),
]

todo/views.py

from django.contrib.auth.mixins import LoginRequiredMixin
from django.shortcuts import render, redirect
from django.urls import reverse, reverse_lazy
from django.views.generic import (ListView, CreateView, DeleteView, UpdateView)
from .models import todo

# Create your views here.
def index_view(request):
    object_list = todo.objects.all

    #paginator = Paginator(ITEM_PER_PAGE)
    #page_number = request.GET.get('page',1)
    #page_obj = paginator.page(page_number)

    return render(
        request,
        'todo/index.html',
        {'object_list': object_list}#, 'page_obj': page_obj},
    )

class ListtodoView(LoginRequiredMixin, ListView):
    template_name = 'todo/todo_list.html'
    model = todo
    #paginate_by = ITEM_PER_PAGE


class CreatetodoView(LoginRequiredMixin, CreateView):
    template_name = 'todo/todo_create.html'
    model = todo
    fields = {'title', 'text', 'category'}
    success_url = reverse_lazy('list-todo')

    def form_valid(self, form):
        form.instance.user = self.request.user

        return super().form_valid(form)

class DeletetodoView(LoginRequiredMixin, DeleteView):
    template_name = 'todo/todo_confirm_delete.html'
    model = todo
    success_url = reverse_lazy('list-todo')

    def get_object(self, queryset=None):
        obj = super().get_object(queryset)

        if obj.user != self.request.user:
            raise PermissionDenied

        return obj

class UpdatetodoView(LoginRequiredMixin, UpdateView):
    template_name = 'todo/todo_update.html'
    model = todo
    fields = {'title', 'text', 'category'}
    success_url = reverse_lazy('list-todo')

    def get_object(self, queryset=None):
        obj = super().get_object(queryset)

        if obj.user != self.request.user:
            raise PermissionDenied

        return obj

おわりに

今回はtodoリストの作成を行いました。
CRUDの実装をし、簡単な操作が一覧からできるようにしました。

最後に作成したアプリケーションをGitHubに上げたのでリンクを貼っておきます。
https://github.com/1hayato1/todoproject

VSCodeとDockerでLaTeX環境を整える

はじめに

以下の記事を参考にVSCodeとDockerでLaTeX環境を整えました。
zenn.dev

VSCodeとWSL2を接続してWSL2で作業をしていきます。

前提

  • VSCodeが使える状態
  • WSL2が使える状態
  • VSCodeとWSL2が繋がっている状態
  • Git, GitHubが使える状態

Dockerからイメージを持ってくる

以下のコードでイメージをpullします。

docker pull ghcr.io/being24/latex-docker:latest

テンプレートのclone

(https://github.com/being24/latex-template-ja)にアクセスし、以下のボタンを押す。

そして、forkする場所(Owner)を選択し、名前を付ける

できればforkしたもののCodeをクリックし、urlをコピーする。

コピーができれば、以下のコードでローカルにcloneする。

git clone コピーしたurl

buildの設定

buildするには、VSCode拡張機能からLaTeX-workshopをインストールします。
また、Ctrl + Shift + Pで以下のものを選択します。

すると左にTEXというものが表示され、選択し、View LaTeX PDFを実行してbuildできるようになります。

buildにはDockerを実行可能にしておく必要があります。以下のコードにより、docker daemonを起動します。

sudo service docker start

また、Docker imageも起動します。

docker run -it ghcr.io/being24/latex-docker

保存時に自動でbuildすることもできます。VSCodeは自動保存を行う設定があります。その設定をするには、作成したリポジトリの.vscode/settings.jsonを以下のように編集します。編集にはviを使います。

"latex-workshop.latex.autoBuild.run": "onSave"

main.texの編集

LaTeXで編集してbuildするためにmain.texを編集します。このmain.texに書かれた内容がbuildされます。
まず、main.texを編集するために、cloneしたディレクトリ内で以下のコードにより、VSCode上でエクスプローラーを開きます。

code .

viで編集しても同じですが、VSCodeの方が編集しやすいです。エクスプローラーが開けたらmain.texを開きます。buildはmain.texを開いた状態でしかできません。
main.texは試しで、cloneしてきたものの\titleと\authorを編集します。

\documentclass{classes/resume}

\title{
	\gt{LateX 環境の構築}\\
}

\author{
	\gt{  大学   学科}\\
	\gt{学籍番号 氏名}
}

\date{}

\begin{document}
\maketitle

% ここに本文をかく
% exampleフォルダを参考にしてください

\end{document}

LaTeX-workshopでpdfを表示させる

VSCodeでmain.texを開いた状態でView LaTeX PDFを押すとVSCodeでpdfが表示されます。

表示されたpdfは以下のようになりました。

VSCodeで編集しながら横で見られるのがよくて、こちらの方が好みですが、webブラウザ上で表示させることもできます。

このView in web browserを押すとwebブラウザが立ち上がり、同じように確認できます。

おわりに

今回は参考の記事を見ながら環境を整え、VSCodeとDockerでLaTeX環境を作成しました。これでVSCode上でpdfで表示ができるようになりました。

本棚アプリケーションまとめ

アプリケーションをHerokuで公開

はじめに

今回は今まで作成してきた本棚アプリケーションをHerokuで公開します。
過去記事
urhayataro.hatenablog.com

前提

  • githbのアカウントがあり、使える状態
  • gitをインストールしていて使える状態

Herokuアカウントを作成して、少し本棚アプリケーションのソースコードを編集して公開するまでを行います。


Herokuアカウントの作成

Herokuのアカウントの作成には、Herokuのウェブサイトにアクセスします。(https://jp.heroku.com/)
それから以下の手順で作成を進めます。

  1. 右上の新規登録のボタンを押す
  2. 氏名やメールアドレス等の情報を入力する
  3. 登録したメールアドレスに送られてきたメールで認証をする
  4. アカウントのパスワードを設定する
  5. 作成したアカウントでHerokuにログインして利用規約に同意する
  6. Welcome to Herokuという画面が表示されると完了

アプリケーションの作成

Herokuにログインしてアプリケーションの作成を行います。ここでアプリケーションを作成することで、Djangoで作成した本棚アプリケーションをHerokuのサーバーにアップロードすることができます。

アプリケーションの作成は、Herokuにログインし、以下の画面のCreate new appをクリックします。

それから適当なアプリケーションの名前を付けます。これは他の名前と重複してはいけないみたいですので、許可される適当な名前にしましょう。
また、regionは日本は選択できません。アメリカのままにしておきます。

アプリケーションの作成ができると以下の画面が表示されます。

この画面を下にスクロールしていくとHerokuにデプロイする手順が載っています。これに則って進めていきます。

まずはHeroku CLIのインストールを行います。上の写真の黄色いところを押して、使用しているosによってインストールを行いましょう。
vscode上でWSLを使って進める場合はここは無視して、ターミナルに以下を入力しましょう。

curl https://cli-assets.heroku.com/install-ubuntu.sh | sh

インストールができれば次のようになります。

次に、インストールができているかを確認します。以下の入力をします。

heroku login

出力が以下のようになっていればHeroku CLIのインストールが正常に完了しており、適当なキーを押すとHeorkuのサイトに移行します。ここでアカウントのログインをしましょう。

Heroku: Press any key to open up the browser to login or q to exit:

デプロイのための本棚アプリケーションの編集

次からはデプロイするための操作をしていきます。作成した本棚アプリケーションにある、settings.pyにはSECRET_KEYがあります。これは第三者に知られてはいけないものなので隠す必要があります。

ただ、デプロイするにはSECRET_KEYも外部からのアクセスを受け付けるサーバーに認識させる必要があります。この問題の解決をしていきます。

まずはHerokuでアプリケーションを選択します。そしてsettingsを押します。
以下の画面のReval Config Varsをクリックし、Addボタンを押します。

そこでKEYにはSECRET_KEYと入力し、VALUEにはsettings.pyにあるSECRET_KEYを入力します。
これでHerokuにSECRET_KEYを認識させることができました。


できれば次は、SECRET_KEYを環境変数として設定したい値をファイルに保存し、ファイルを呼び出して環境変数として扱うことができるようにします。これには以下のコードにより、dotenvをインストールします。

pip install python-dotenv

インストールができれば.envファイルを作成します。

touch .env

この作成した.envファイルにSECRET_KEYを記述します。

SECRET_KEY = プロジェクトのシークレットキー

また、settings.pyも合わせて以下のように変更します。

import os # 追加
from dotenv import load_dotenv # 追加
load_dotenv() # 追加

SECRET_KEY = os.getenv('SECRET_KEY') # 変更

これでSECRET_KEYを.envファイルに隠して、呼び出して使うことができます。

最後に、せっかく隠すために用意した.envファイルをサーバーにアップロードしては意味が無くなります。そのため、.gitignoreというファイルを作成し、編集します。
.gitignoreというファイルはgitの管理から特定のファイルを除外し、外部のリポジトリにアップロードしないようにするファイルです。
まずは以下のコードより、ファイルを作成します。

touch .gitignore

作成したファイルを以下のようにしておきます。

# Pythonのキャッシュが自動生成されているので除外
__pycache__/

# 環境変数は除外
.env

# 環境変数は除外
/myvemv/

# mediaディレクトリに.gitleepというファイルを作成し、それ以外は除外
/media/*
!/media/.gitkeep

# staticディレクトリに.gitleepというファイルを作成し、それ以外は除外
/static/*
!/static/.gitkeep

# DBは除外
db.sqlite3

staticファイルの設定

これまではbookアプリケーションの中にstyle.cssなどのstaticファイルを格納していました。ただ、デプロイする場合staticファイルはsettings.pyに定義したディレクトリに格納されている必要があります。

はじめに、staticファイルをまとめて移動して保存する場所を指定します。staticファイルをまとめるには、STATIC_ROOTという変数を、settings.pyで以下のように指定します。

STATIC_URL = '/static/'

STATIC_ROOT = BASE_DIR / 'static' # 追加

MEDIA_URL = '/media/'

次に、以下のコードにより、staticディレクトリへCSSなどのstaticファイルをコピーします。

python3 manage.py collectstatic

これでstaticの設定ができました。



デプロイのための準備

ここからはデプロイするために必要なもののインストールやファイルの作成を行います。

まずはアプリケーションサーバーのインストールを行います。
以下のコードにより、Gunicornをインストールします。

pip install gunicorn

これをHerokuでデプロイするには、インストールした上でHerokuにGunicornを使うことを伝える必要があります。
そのため、次のコードによりファイルを作成し、書き込みをしていきます。

touch Procfile
web: python manage.py migrate && gunicorn プロジェクト名.wsgi

これでHerokuでGunicornを使うと伝えることができます。


次に、requirements.txtファイルを作成します。これはアプリケーションを動かす上でインストールが必要なライブラリを指定するために使用されます。
手元の環境とHerokuのサーバーでインストールしているものが違う場合に自動的に必要なライブラリをインストールしているものがrequirements.txtです。
では以下のコードにより、ファイルを作成します。

touch requirements.txt

次に、以下のコードによりインストールされているライブラリを確認します。

pip freeze

すると以下のような出力になります。

これらがpipを使ってインストールしたライブラリの一覧となっています。あとはこれをrequirements.txtに書き出します。以下のコードを実行します。

pip freeze > requirements.txt

これでrequirements.txtの書き込みができました。


最後にDEBUGとALLOWED_HOSTSの設定を行います。設定にはsettings.pyの以下の部分を変更します。

# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = False # 編集

ALLOWED_HOSTS = ['ドメイン'] # 編集

DEBUGをFalseにするとブラウザ上で動作確認をした時にエラーが出ても、エラーの詳細が表示されなくなります。
また、ALLOWED_HOSTSも設定しておきます。これは自分のドメインがあるならそれでも構いませんが、Herokuのアプリケーションでsettingsを選択して出てくる下の画像の黄線を設定しても構いません。djangoからcomまでです。

これでデプロイする準備が整いました。


デプロイする

デプロイにはHerokuのアプリケーション→Deployから手順が確認できました。これに基いて進めます。
まずはターミナルでコードをアップロードするためのコマンドを実行します。

git init
heroku git:remote -a Herokuのアプリケーション名
git add .
git commit -m 'deploy'
git push heroku master

これでHerokuにアプリケーションがアップロードされます。
初めてgitを使う場合は出力に従って設定してください。

アップロードができれば次のような出力がされます。

表示されたリンクにアクセスすると次のように本棚アプリケーションのトップページが表示されます。

ログインして使うことができるので、アプリケーションの公開ができたことがわかります。

これでアプリケーションの作成から公開を終わります。
現在の設定ではAWSを利用していないので、Herokuの仕様により、本の写真をアップロードしたり、表示することができません。また、デプロイ時などで定期的にサーバーが再起動され、ブラウザから追加したデータが定期的に消えてしまうことを知っておきましょう。


おわりに

今回は作成していた本棚アプリケーションをHerokuで公開する設定や準備を行い、公開しました。
これで本棚アプリケーションの作成と公開までが全て終了しました。
最後に今回作成したファイルの場所の確認のため写真を載せておきます。

全体のまとめ↓
urhayataro.hatenablog.com

ページの改良

はじめに

前回に引き続きトップページを見やすい形にしていきます。やることは、本を登録した新着順、レビューの平均点順に並び替えをします。それからページネーションをつけて1ページに2冊ずつ本を見れるようにします。


トップページのデータの並び替え

新着順に並び替え

新着順に並び替えるにはbook/views.pyを以下のように編集します。

def index_view(request):
    object_list = Book.objects.order_by('-id') # 修正


    return render( request, 'book/index.html', {'object_list': object_list})

修正した-idとすることで新着順にしています。本の追加した順にidは1,2,3...と増えていくのでidの古いものからとすると、新しい本からというようになります。

レビューの平均点順に並び替え

平均点順にはbook/views.pyを以下のようにします。

from django.db.models import Avg

def index_view(request):
    object_list = Book.objects.all
    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},
    )

追加したAvg('review__rate')を使うことでデータの平均値を取ります。どのデータの平均値をとるのかは、'review__rate'で指定します。つまりreviewが対象とするモデル、rateがそのフィールドを示しています。

できればbook/templates/book/index.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">
        {% 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.title }}</h3>
                <img src="{{ ranking_book.thumbnail.url }}" class="img-thumbnail" />
                <h6>評価:{{ ranking_book.avg_rating}点</h6>
                <a href="{% url 'detail-book' ranking_book.id %}">評価を見る</a>
            </div>
        {% endfor %}
    </div>
</div>
</div>
{% endblock content %}

これで新着順、評価順に並び替えることができました。少し見た目を整えるために評価の小数点を修正します。
book/templates/book/index.htmlを以下のように修正します。

        {% 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.title }}</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 %}

追加した|floatformat:で小数点以下を四捨五入することができます。その後ろに2とすることで小数点以下第2位まで見れるようにしました。


書籍の詳細にレビューを追加する

ここでは複数のレビューを追加、閲覧できるようにします。やりかたはbook/templates/book/detail.htmlに以下を追加します。

<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 'list-book' %}" class="btn btn-primary">一覧へ</a>

これで一度に複数のレビューに対応することができます。
以下のように複数のレビューが表示されるでしょう。

ページネーションの作成

本棚アプリケーションで本が何百冊となった場合、10冊ずつの閲覧ができたほうが見栄えが良いでしょう。ここでは2冊ずつでページネーションを作成していきます。
今回はfunciton-based viewとclass-based viewの場合の実装をしていきます。

funciton-based viewでのページネーションの作成

まずはbook/views.pyとbook/consts.pyに以下を追加します。

from django.core.paginator import Paginator # 追加
from .consts import ITEM_PER_PAGE # 追加

def index_view(request):
    object_list = Book.objects.all
    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}, # 追加
    )
MAX_RATE = 5
ITEM_PER_PAGE = 2 # 追加

from .consts import ITEM_PER_PAGEで一つのページに表示する本の数を指定するために定義しておき、ITEM_PER_PAGE = 2と2つずつ表示させるようにしています。ここを違う数にすると10冊ずつのように変更ができます。


次はhtmlファイルの中にページネーションの仕組みをいれていきます。そのため、以下のコマンドで新たなファイルを作成しましょう。

mkdir book/templates/book/components
mkdir book/templates/book/components/paginations.html

作成したファイルに以下の記述をします。

{% 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 %}

基本的にはpage_objを使って条件分岐を行っています。

<a href="?page={{ page_obj.previous_page_number }}">&lt;&lt;前へ</a>

の部分で前のページがある場合は"前へ"と表示し、前のページのリンクを挿入することでページネーションの実装を行っています。
次への部分も基本的には同じです。

あとは記述したcomponentsファイルをhtmlファイルに組み込みます。book/templates/book/index.htmlを以下のように変更します。

<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.title }}</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>

これで一度確認してみましょう。

前へのリンクははじめのページなのでありませんが、次へはあります。これでページネーションの作成ができました。

class-based viewでのページネーションの作成

次は、class-based viewでページネーションの作成をします。
book/views.pyに以下の記述を追加します。

class ListBookView(LoginRequiredMixin, ListView):
    template_name = 'book/book_list.html'
    model = Book
    paginate_by = ITEM_PER_PAGE # 追加

実はこれだけでできます。あとはhtmlにページネーションに関する記述を追加します。book/templates/book/book_list.htmlに以下の記述を追加します。

{% 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 %}
    {% include 'book/components/pagination.html' %} # 追加
{% endblock content %}

これで同じように確認してみると良いでしょう。class-based viewでもページネーションの作成ができました。


おわりに

今回は新しい順、評価順に並び替えて表示するような変更を行いました。また、2冊ずつでページネーションをつけて見やすいページになるようにしました。

これで本棚アプリケーションの作成はおおよそ終了しました。
次回は作成した本棚アプリケーションをHerokuで公開します。

画像を利用した本の投稿

はじめに

前回の続きで、画像を利用することができるようになったので、それを利用してユーザーが本の投稿をできるようにします。


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

ログインしている状態でないと本の投稿やデータの閲覧ができないようにします。
まずbook/views.pyに以下を追加します。

from django.contrib.auth.mixins import LoginRequiredMixin # 追加

class ListBookView(LoginRequiredMixin, ListView): # 追加
    template_name = 'book/book_list.html'
    model = Book
    paginate_by = ITEM_PER_PAGE

追加した継承のLoginRequiredMixinは一番左に記述しましょう。
ログインしていない状態ではaccounts/login/に遷移させることができます。

また、他のclassの継承にもLoginRequiredMixinを追記しておきましょう。


自分で投稿したデータのみ編集可能にする

まずはbook/models.pyに以下を追加します。

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

サーバーを立ち上げ、127.0.0.1:8000/admin/にアクセスします。それから適当なユーザーを選択します。

この赤い部分がユーザーのidとなっています。これを確認すれば、以下のコマンドを実行します。

python3 manage.py makemigrations
python3 manage.py migrate

再度選択しろと言われたら今度は1を選択します。


ではここからログインしているユーザーしか編集できないようにするための実装をしていきます。まずはbook/views.pyに以下の記述を追加します。

from django.core.exceptions import PermissionDenied # 追加

class UpdateBookView(LoginRequiredMixin, UpdateView):
    template_name = 'book/book_update.html'
    model = Book
    fields = {'title', 'text', 'category', 'thumbnail'}
 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 # 追加

追加した if obj.user != self.request.userについて見ていきます。obj.userはUpdateBookViewで呼び出された書籍の登録をしたユーザーを表しており、self.request.userは現在ログイン中のユーザーを表しています。もし一致していなければraise PermissionDeniedの処理を行うといったものです。
ここでraiseは例外を出すときに利用します。また、PermissionDeniedはDjangoが用意している例外です。

サーバーを立ち上げて登録したユーザーと別ユーザーで編集してみましょう。
以下のように例外がスローされました。

また、書籍登録したユーザーが編集を行ってデータを更新した際のページ遷移先を設定します。book/views.pyを以下のように編集します。

class UpdateBookView(LoginRequiredMixin, UpdateView):
    template_name = 'book/book_update.html'
    model = Book
    fields = {'title', 'text', 'category', 'thumbnail'}
 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}) # 追加

これで編集についてはできたので、削除についても以下のようにしましょう。

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

また、Bookモデルにuserを追加したので、formにユーザーの情報を追加するため、book/views.pyに以下を追加します。

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) # 追加

これで本を投稿したユーザーしか編集することができないようにすることができました。


おわりに

今回は本を投稿したユーザーのみが編集、削除できる仕組みを実装しました。また、登録した人と別のユーザーが編集しようとすると例外を発生させることも行いました。
次回はトップページを新しい順、評価の高い順に並び替えること、ページネーションをつけて見やすいトップページにしていきます。

本に合わせた画像を表示させる

はじめに

前回の続きで、今回は本に合わせて画像を表示させることを行います。
urhayataro.hatenablog.com

やることはBookモデルで画像を扱えるようにしてモデルを作成します。その後、cssでサイトの見た目を整えます。


本の画像を表示させる

まずはBookモデルに画像を扱うためのフィールドを追加します。フィールドの追加はbook/models.pyに以下を追加しますが、その前にIntegerFieldを使用するために、以下でPillowをインストールします。

pip install pillow

できれば以下を追加します。

class Book(models.Model):
    title = models.CharField(max_length=100)
    text = models.TextField()
    thumbnail = models.ImageField() # 追加
    category = models.CharField(
        max_length=100,
        choices = CATEGORY
        )
    user = models.ForeignKey('auth.User', on_delete=models.CASCADE)

    def __str__(self):
        return self.title

次に以下のコマンドを実行します。

python3 manage.py makemigrations

すると以下のメッセージが表示されます。
内容はthumbnailフィールドを追加する時に今まで作成したデータはどのような画像を使うかというものです。

ここでは2を選択して追加した場所を以下のように変更しましょう。

thumbnail = models.ImageField(null=True, blank=True)

nullはデータベースにデータが入っていないことを許容するか、blankはフォームに入力されたデータが空でも許容するかを表しています。

変更すれば、もう一度モデルの作成をしましょう。

python3 manage.py makemigrations
python3 manage.py migrate

画像ファイルを保存する場所の指定と画像の結びつけ

モデルで画像を扱う際、画像を保存する必要があります。まずbookproject/settings.pyに以下を追加します。

MEDIA_URL = '/media/'

MEDIA_ROOT = BASE_DIR / 'media'

また次のコマンドでbookやbookprojectと同じ階層にディレクトリを作成します。

mkdir media

この作成したmediaという場所に本の画像が保存されることになります。

ここからは画像とURLを結びつけます。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)

追加したsettings.MEDIA_URLはsettings.pyの中のMEDIA_URLを示しています。リクエストされたURLとMEDIA_URLで指定した文字列が同じ場合にsettings.MEDIA_ROOTを呼び出します。
settings.MEDIA_ROOTはsettings.pyで指定した画像が保存される場所を示しています。

ここまでできれば、サーバーを立ち上げ、127.0.0.1:8000/admin/にアクセスします。
以下のように画像の選択ができます。

合わせて画像が表示されるか確認してみると良いでしょう。


画像の表示ができれば、トップページじ画像を表示させましょう。
book/templates/book/index.htmlに以下を追加します。

{% 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 %}

トップページに画像が表示されていることを確認しましょう。

最後にfieldsにthumbnailを追加します。まずはbook/views.pyに以下を追加します。

class CreateBookView(LoginRequiredMixin, CreateView):
    template_name = 'book/book_create.html'
    model = Book
    fields = {'title', 'text', 'category', 'thumbnail'} # 追加

class UpdateBookView(LoginRequiredMixin, UpdateView):
    template_name = 'book/book_update.html'
    model = Book
    fields = {'title', 'text', 'category', 'thumbnail'} # 追加

また、フォームで画像を扱うためにbook/templates/book/book_create.htmlを以下のように修正します。

{% extends 'base.html' %}

{% block title %}書籍作成{% endblock %}

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

合わせてbook/templates/book/book_update.htmlを以下のように修正します。

{% extends 'base.html' %}

{% block title %}書籍修正{% endblock %}

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

cssの設定

cssを適用するために、bookproject/templates/base.htmlに以下を記述を追加します。

{% 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' %}"> # 追加
    </head>

cssの利用には{% load static %}と{% static 'book/css/style.css' %}でbookproject/book/static/book/css/style.cssにあるcssファイルを呼び出して使います。
ではその場所にstyle.cssを作成します。

mkdir book/static
mkdir book/static/book
mkdir book/static/book/css
mkdir book/static/book/css/style.css

作成したstyle.cssに以下の記述をします。これはaタグの下線を消すコードです。

a {
    text-decoration: none;
}

これでcssの設定ができました。


おわりに

今回は、本に合わせた画像の表示とcssの設定を行いました。
次回は画像を利用してユーザーが本の投稿や削除をできるようにしていきます。