アプリ開発ナレッジ

アプリ開発のナレッジを掲載します

【開発ナレッジ】Django実践アプリ機能実装ナレッジ~環境変数設定・決済機能・カスタムユーザー・ログイン処理・非同期処理・キャッシュ制御・ログ・OAuth認証・バッチ処理~

Udemyの学習で学習した Django の実践的な機能の実装方法のナレッジをまとめます

2種類の講座の学習まとめ

講座1: 【中級者向け】Django3でショッピングアプリ開発 - ECサイト構築で行うPython・WEBアプリケーション開発
https://github.com/TakumaFujimoto/vegeket_project

講座2: 最新版:Python + Django3 Djangoを基礎から応用まで、アプリケーション開発マスターpython付き


できること(抜粋)

講座1

講座2

  • OAuth認証
  • バッチ処理
  • ログ出力
  • ログイン処理
  • AJAX非同期処理とキャッシュ制御
  • Python基本文法
  • 関数ベースビュー
  • クラスベースビュー
  • Form
  • エラーハンドリング


目次

自環境

  • OS:Windows 10
  • シェル:powershell
  • Python:3.8系
  • エディター:PyCharm
  • 仮想環境:Anaconda Navigator

ECサイトの構築

仮想環境の準備は以下参照
https://shinseidaiki.hatenablog.com/entry/2021/11/07/232602


使うパッケージ requirements.txt

asgiref==3.4.1
certifi==2021.5.30
charset-normalizer==2.0.3
Django==3.2.5
django-environ==0.4.5
idna==3.2
Pillow==8.3.2
pytz==2021.1
requests==2.26.0
sqlparse==0.4.2
stripe==2.60.0
urllib3==1.26.6

requirements.txtをインストールする場合は上記ファイルを任意のフォルダに配置して、
仮想環境になっていることを確認して
以下のコマンドを仮想環境で実行する

pip install -r requirements.txt

なお記事投稿者が記事記入時はDjangoが4にアップグレードされていたため、上記の方法で環境を合わせた

初期準備

django-admin startproject udemyec
django-admin startapp base

フォルダの作成
templates, secrets, static

Tips!

setting.pyのBASE_DIRはpathlibの組み込み変数でプロジェクトのルートパスを差す
BASE_DIR / 'templates' の書き方でもOSに応じて臨機応変にパスを作ってくれる

Secretsフォルダの利用

GitHubにおおよそのことは書いていた
https://github.com/TakumaFujimoto/vegeket_project/blob/main/sec03/README.md

環境変数を扱うためにインストールする
django-environ

環境変数を作成する

secretsフォルダ直下に以下を作成

.env.dev
.env.prod

環境に応じた環境変数を用意しておくことで開発を円滑に行うことが出来るようにする
.env.devは開発環境用
.env.prodは本番環境用(productの略)

環境変数に格納する値はクオテーションなしで登録

.env.dev(開発環境)

SECRET_KEY=django-xxxxxxxxxxxxxx
DEBUG=True
ALLOWED_HOSTS=*

.env.prod(本番環境 - デプロイする場合)

SECRET_KEY=django-xxxxxxxxxxxxxx
DEBUG=false
ALLOWED_HOSTS=[特定のホスト]

setting.pyに環境変数を適用

import environ

env = environ.Env()
root = environ.Path(BASE_DIR / 'secrets')

# 本番環境
# env.read_env(root('.env.prod'))

# 開発環境
env.read_env(root('.env.dev'))


SECRET_KEY = env.str('SECRET_KEY')

DEBUG = env.bool('DEBUG')

ALLOWED_HOSTS = env.list('ALLOWED_HOSTS')

rootはプロジェクト直下に.envファイルを置く場合は記述する必要はない
本番環境と開発環境で自動で分ける場合はif文を使って条件分岐をする必要がある模様
具体的な例はわからなかったが、以下が参考になるかもしれない
https://ikura-lab.hatenablog.com/entry/2019/06/16/142339

.gitignoreに関してはこちらの記事などを参照にする
どこかでまとめる必要がある
https://leading-tech.jp/wiseloan/gitignore/

このあたりを記述

*.log
*.pyc
__pycache__/
secrets/
db.sqlite3

モデルとかテンプレートの作成

省略

この人のフォルダ構成はtemplatesフォルダ内に以下のフォルダとファイルがある
base.html
pagesフォルダ
snippetsフォルダ

modelsフォルダを作成してその直下に__init__.pyファイルを配置すればフォルダがmodelsとして認識される
modelsが増えてきた場合に有効
フォルダ化にはmodelごとに自由なファイル名のモデルを定義することができる
なお、initiは初回に読み込まれるので、以下のような記述を行っておくとモデルが読み込まれる
__init__.py

from .item_models import *

画像を別のストレージで使用する場合については別途調査が必要

Itemの外部キーのcategoryのondeleteについてはon_delete=models.SET_NULLにした方がよい
なぜならカテゴリ削除と同時にItemを削除したくはないから
カテゴリが削除された場合はNullを詰める

ManyToManyでリレーションをとる場合はtagsのような複数形の名前にする方が慣例の模様
ManyToManyの場合は中間テーブルが内部的に作成されて直接つながっていないためondeleteは設定する必要がない

ここではカスタムユーザーモデルは先に作ってない
https://github.com/TakumaFujimoto/vegeket_project/blob/main/sec06/README.md

admin画面はunregisterを使用すると非表示にすることもできる

クラスベースビューと関数ベースビューの違い

クラスベース

class IndexListView(ListView):
    model = Item
    template_name = 'pages/index.html'


関数ベースで書いた場合

def index(request):
  object_list = Item.objects.all()
  context = { 'object_list': object_list, }
  return render(request, 'index.thml', context )
Tips!

for文の中で使える便利なものが存在する
{{ forloop }}
for文が何回回ったかなどを出力できる

Tips! get post

def postやdef getをクラスベースビューで記述するとPOSTの時やGETメソッドの時に実行される関数を記述することができる

class AddCartView(View):
    def post(self, request):
Tips! OrderdDict

OrderedDict()はPython標準ライブラリで搭載されている
辞書の場合、追加するときにListと違って順番が保持されないために順番を保持するために使用できるメソッド
順序月の辞書が作れる

Tips! プチ環境変数

消費税率などの見られてもよい環境変数にしたい情報はsetting.pyに直接記入できる

TAX_RATE = 0.1
Tips! get_queryset, get_context_data

get_querysetについて

データをDBなどから取得するメソッド
ListViewがget_querysetをもともと持っており、デフォルトではモデルのすべてのアイテムを返すようになっている
カスタマイズする場合はget_querysetをオーバーライドする
また、obj.quantityやobj.subtotal のようにmodelsに定義されていないquantityのような値はviewにおいて新たに定義することのできるインスタンス変数となり、DBには保存されないがプログラムで自由に使用することができるようになる
あらかじめモデルで定義していなくても自由にインスタンス変数を追加することができる
self.queryset = []で定義してフォー分で回して、self.queryset.append(obj)したりしてquerysetを作成するようにして作る

get_context_data

テンプレートに値を渡すメソッド
context["total"] = self.total のような記述をすることでテンプレート上でtotalというキーで値にアクセスすることができるようになる
また以下のようにjsonから辞書型に変換して画面に値を渡すことも可能
context["items"] = json.loads(obj.items)

Tips!

管理画面者の編集
以下のadminの中のファイルローカルにコピーして上書きすることで編集することが可能(元ファイルは当然ながら編集しない)
https://github.com/django/django/tree/main/django/contrib/admin/templates

例えば、base_site.htmlをtemplateルートフォルダ下(setting.pyのDIRSのところで設定している場所、基本はプロジェクト直下にテンプレートを指定することが一般的)にadminフォルダを作成し、その下にgithub上のdjangoテンプレートと同じフォルダ構成でファイルを配置する
base_site.htmlの内容を上書きするとadminサイトに変更が反映される

CSSを書き換えたい場合は 管理画面はstatic: admin/css/base.cssを継承しているためこのファイルを作成して継承すればいい(CSSなのでstaticフォルダ以下であるところに注意)
Github上の場所は以下
https://github.com/django/django/tree/main/django/contrib/admin/static/admin/css


決済機能

決済機能についての簡単な説明がある
https://github.com/TakumaFujimoto/vegeket_project/blob/main/sec10/README.md

ここではStripeを利用する
Stripeで開発者向けの開発用の決済機能を無料で試すことができる
Stripe: 世界で最もシェアを獲得するクレジットカード決済代行サービス
各種セキュリティ・法規制に対応しているため決済代行サービスを使わないで課金が必要な自作サービスを実装する選択肢はほぼ皆無だと思われる(特に個人)
使用時の手数料3%以外の費用がないため個人でマッチングアプリを実装したい場合(年齢確認用)などで利用できる

Stripeのドキュメント
Stripe のドキュメント

Stripe使用準備

アカウントの作成
上記ストライプの公式サイトからアカウントを作成する(メール認証があるので認証を済ませる)
アカウントを作成するとダッシュボードに移動する
最初から開発環境を提供してくれている模様
本番環境を利用する場合は別途申請が必要となる
ここではすべて開発環境だけを使用する

Stripeパッケージのインストール

pip install stripe
Stripeに使用するテンプレート画面の作成

Stripeドキュメントに記載されるテンプレートを使用する
https://stripe.com/docs/payments/checkout
こちらにサンプルプロジェクトが記載されている
https://stripe.com/docs/checkout/quickstart
講座の時とは内容が更新されている模様
フィルタリングがあるので、HTML PYTHONを選択して今回はPythonのテンプレートを表示して模倣する

テンプレートとしては以下がある

  • server.py
  • checkout.html チェックアウト、ECサイトにおいてはショッピングカートページを差す画面の見本
  • success.html 決済が成功した後に表示される画面
  • cancel.html 決済が失敗した場合や戻るボタンを押された場合に表示する画面

テンプレートはあくまで三本であり、部分的に使用できる場合は、必要な箇所の機能だけを模倣すればよい

success.htmlを作成する

templates/pages/success.html

{% extends 'base.html' %}
 
{% block main %}
<div class="container my-5">
  <div class="row">
    <div class="col-12">
      <h1>Thank you!</h1>
      <!-- 注文履歴確認ページ -->
      <p>注文履歴は<a href="/orders/">こちら</a></p>
    </div>
  </div>
</div>
{% endblock %}
cancel.htmlを作成する

templates/pages/cancel.html

{% extends 'base.html' %}
 
{% block main %}
<div class="container my-5">
  <div class="row">
    <div class="col-12">
      <h1>Cancel</h1>
      <p>うまく処理されませんでした。カートは<a href="/cart/">こちら</a></p>
    </div>
  </div>
</div>
{% endblock %}
.envを編集する

環境変数にSecrets情報を格納しておく
今回は開発環境のみなので、.env.devのみを編集する

secrets/.env.dev

STRIPE_API_KEY=[STRIPEのAPIキー]
MY_URL=http://127.0.0.1:8000

[STRIPEのAPIキー]は公式のダッシュボードからコピーして貼り付ける
https://dashboard.stripe.com/

ホームの真ん中の方に開発者向けという項目があるので、シークレットキーをコピペする
なお、公開可能キーは自分でカスタマイズした決済ページを実装する場合に必要になってくるものになる
Stripeの決済ページにリダイレクトする今回のような方法の場合ではシークレットキーのみでよい
今使用しているシークレットキーは開発環境用のテスト用シークレットキーとなっており、本番環境で使用する場合は、別途申請を行って本番環境用のシークレットキーを入手して、 .env.prod に本番環境用のシークレットキーをコピペして使用する

setting.pyの編集

設定ファイルにシークレットキーの環境変数を読み込ませる

setting.pyに以下を追記

# Stripe API Key
STRIPE_API_SECRET_KEY = env.str('STRIPE_API_SECRET_KEY')

# スキーマ&ドメイン
MY_URL = env.str('MY_URL')
ビューの作成

base/views/pay_views.py

from django.shortcuts import redirect
from django.views.generic import View, TemplateView
from django.conf import settings
from stripe.api_resources import tax_rate
from base.models import Item
import stripe

# Stripeのシークレットキーを読み込む 
stripe.api_key = settings.STRIPE_API_SECRET_KEY
 
 
class PaySuccessView(TemplateView):
    template_name = 'pages/success.html'
 
    def get(self, request, *args, **kwargs):
        # 最新のOrderオブジェクトを取得し、注文確定に変更
 
        # カート情報削除
        del request.session['cart']
 
        return super().get(request, *args, **kwargs)
 
 
class PayCancelView(TemplateView):
    template_name = 'pages/cancel.html'
 
    def get(self, request, *args, **kwargs):
        # 最新のOrderオブジェクトを取得
 
        # 在庫数と販売数を元の状態に戻す
 
        # is_confirmedがFalseであれば削除(仮オーダー削除)
 
        return super().get(request, *args, **kwargs)
 
 
tax_rate = stripe.TaxRate.create(
    display_name='消費税',
    description='消費税',
    country='JP',
    jurisdiction='JP',
    percentage=settings.TAX_RATE * 100, # 100分率整数で渡す必要がある
    inclusive=False,  # 外税(税別)を指定(内税(税込)の場合はTrue)
)
 
 
def create_line_item(unit_amount, name, quantity):
    return {
        'price_data': {
            'currency': 'JPY',
            'unit_amount': unit_amount,
            'product_data': {'name': name, }
        },
        'quantity': quantity,
        'tax_rates': [tax_rate.id]
    }
 
 
class PayWithStripe(View):
 
    def post(self, request, *args, **kwargs):
        cart = request.session.get('cart', None)
        if cart is None or len(cart) == 0:
            return redirect('/')
 
        line_items = []
        for item_pk, quantity in cart['items'].items():
            item = Item.objects.get(pk=item_pk)
            line_item = create_line_item(
                item.price, item.name, quantity)
            line_items.append(line_item)
 
        checkout_session = stripe.checkout.Session.create(
            # customer_email=request.user.email,
            payment_method_types=['card'],
            line_items=line_items,
            mode='payment',
            success_url=f'{settings.MY_URL}/pay/success/',
            cancel_url=f'{settings.MY_URL}/pay/cancel/',
        )
        return redirect(checkout_session.url)

PaySuccessViewは決済が成功した場合のリダイレクトのためのView
PayCancelViewは決済が失敗した場合のリダイレクトのためのViewであり、
実際の決済を行う処理はPayWithStripeとなる
PayWithStripeはpostのみでアクセス可能とする
公式ドキュメントのテンプレートのserver.pyを参考に作成していく

line_itemsはStripeの決済ページに表示させるデータを準備している
載せられるデータは詳しくはドキュメント参考だが、今回載せている'price_data'は価格であり、ドキュメントの'price'よりもより詳しく記載できている
'quantity': quantity, は購入数量
'tax_rates': [tax_rate.id] は任意で載せることができるが、これを載せていると自動で税金計算をしてくれるなど便利になる

checkout_sessionにline_itemsといったデータを追加送信すると決済ページにその情報を表示させることが可能となる
Stripeとの通信に必要な情報はcheckout_sessionを使用するので、これを必ず定義する
checkout_sessionの要素はドキュメント(https://stripe.com/docs/checkout/quickstart)を参照
なお、success_url=f'{settings.MY_URL}/pay/success/',のように{settings.MY_URL}を付与しておかないとStripeの決済画面のURLからの相対パスにリダイレクトしてしまうため、URLにドメインを追加しておく

最後は return redirect(checkout_session.url) でStripe公式がデフォルトで用意している決済画面にリダイレクトする

views.pyではなくviewsフォルダを作成してviewsを実装している場合はviews/__init__.pyのpay_viewsのインポートを忘れないように

from .pay_views import *
ルーティングの設定

'pay/checkout/'のURLエンドポイントはStripeの決済画面にリダイレクトするページとなっている

urls.py

    # Pay
    path('pay/checkout/', views.PayWithStripe.as_view()),
    path('pay/success/', views.PaySuccessView.as_view()),
    path('pay/cancel/', views.PayCancelView.as_view()),
動作確認

サーバーを起動させる
商品を選択してカートからcheckoutボタンを押す

成功させたい場合は4242 4242 4242 4242
支払いに確認を必要とする場合は4000 0025 0000 3155
支払いが拒否される場合は4000 0000 0000 9995
を入力する
カード番号以外は適当な値を入力してかまわない

それぞれのパターンの挙動が正しければ動作確認はOK
これで基本的な決済機能を実装できるようになった

トラブルシュート

以下のようなエラーが発生した場合は、Stripeのアカウント名を変更する必要がある模様(参考https://note.com/daikinishimatsu/n/n029a7ff01f62

InvalidRequestError at /pay/checkout/
In order to use Checkout, you must set an account or business name at https://dashboard.stripe.com/account.

ダッシュボードにアクセスし(https://dashboard.stripe.com/)、プロフィール>アカウントのところで「名称未設定」のアカウントと表示されていればアカウント名を登録する
登録方法は 設定(歯車アイコン)>アカウントの詳細>アカウント名 で好きなアカウント名を入力する
保存を押すことでアカウント名は保存される

エラーハンドリング

公式ドキュメント
https://docs.djangoproject.com/en/4.0/ref/views/#error-views

事前準備
setting.pyのDEBUGをFalseにする
DEBUGでない場合はホストALLOWED_HOSTの追加必要 '127.0.0.1'
プロジェクトのurls.pyにステータスコードを設定したハンドラーを追加する

handler404 = views.関数名()

なお、デフォルトではNot Foundエラーやサーバーエラー画面の味気ない画面が出力される
間違ったURLを正しいURLにリダイレクトなどカスタムしたい場合にエラーハンドリングをカスタマズする

例:間違ったURLの場合はリダイレクトする例

プロジェクト側 urls.py

handler404 = show_error_page

views.py

def show_error_page(request, exception):
    # return render(request, ' 404.html')
    return redirect('store:home')

カスタムユーザモデルを作成する

Djangoは通常models.pyにカスタムユーザーを使用することを明示せずにマイグレーションをすると、初めのマイグレーションでデフォルトユーザーを作成されてしまう
Djangoの公式ドキュメントに記述されている通りデフォルトユーザーを使用してしまうと後からの変更が困難となるので、基本的には最初からカスタムユーザーを作成しておくことが推奨されている

今回は一度マイグレーションを上記でしてしまっているが、この状態からカスタムユーザーを作成することも一応できる
後からカスタムユーザーを作成する大まかな流れとしては、カスタムユーザーをmodels.pyで作成して、DBを削除して、migrationsフォルダのinit以外のファイルを削除して、cacheも削除して、マイグレーションの初期化を行って、その後もう一度マイグレーションを行う(マイグレーションを一からやり直すことでカスタムユーザーを適用させることができる)

カスタムユーザーの作り方としてはAbstractUserを継承する方法と、AbstractBaseUserを継承する方法の2種類がある

  • AbstractUser: すでに存在するフィールドをそのまま流用して、フィールドの増減を行うことができる

ただし、メールアドレスをログインIDにするなどのことができないため、限定的な利用にとどまる

  • AbstractBaseUser: Userモデルをほとんどゼロベースから作成することが可能

自由度が高くこちらをカスタムユーザーとするのが一般的

以下はAbstractBaseUserのカスタム実装例

モデルの作成

models.pyにドキュメントを参考にしたモデルを作成する
https://docs.djangoproject.com/ja/3.2/topics/auth/customizing/#a-full-example
もしくは下の章で紹介しているカスタムモデルの例を参考にする

AbstractBaseUserを使用する場合は、データ挿入取得更新処理をするマネージャークラスを作成する必要がある
以下のコード例ではclass UserManager(BaseUserManager): の部分である
ほぼお決まりの書き方があり、通常ユーザーcreate_userと管理者ユーザーcreate_superuserの2種類のユーザー新規作成メソッドをオーバーライドする
自力で書こうとするとcreate_superuserを忘れがちなため注意が必要

カスタムモデルのログインIDを変更する場合はUSERNAME_FIELDに使用したいフィールドを記載する
ここでは'username'をログインIDにしているが、'email'を指定することもできる
'email'を指定した場合は、ログインIDにEmailを使用することが可能となる
REQUIRED_FIELDSはただの必須項目の設定である

また、ユーザーの生年月日やニックネームなどの情報はUserモデルに直接フィールドを持たせるよりも分離したProfileモデルに持たせるような実装で作成するのが慣習となっている
Userモデルと1to1のリレーションでテーブル分離したProfileモデルを作成する
models.OneToOneFields(User, ....) を使用すると1対1のリレーションを張ることができる

account_models.py

from django.dispatch import receiver
from django.db.models.signals import post_save
from django.db import models
from django.contrib.auth.models import BaseUserManager, AbstractBaseUser
 
 
class UserManager(BaseUserManager):
 
    def create_user(self, username, email, password=None):
        if not email:
            raise ValueError('Users must have an email address')
        user = self.model(
            username=username,
            email=self.normalize_email(email),
        )
        user.set_password(password)
        user.save(using=self._db)
        return user
 
    def create_superuser(self, username, email, password=None):
        user = self.create_user(
            username,
            email,
            password=password,
        )
        user.is_admin = True
        user.save(using=self._db)
        return user
 
 
class User(AbstractBaseUser):
    id = models.CharField(default=get_random_string(22), primary_key=True, max_length=22)
    username = models.CharField(
        max_length=50, unique=True, blank=True, default='匿名')
    email = models.EmailField(max_length=255, unique=True)
    is_active = models.BooleanField(default=True)
    is_admin = models.BooleanField(default=False)
    objects = UserManager()
    USERNAME_FIELD = 'username'
    EMAIL_FIELD = 'email'
    REQUIRED_FIELDS = ['email', ]
 
    def __str__(self):
        return self.email
 
    def has_perm(self, perm, obj=None):
        "Does the user have a specific permission?"
        # Simplest possible answer: Yes, always
        return True
 
    def has_module_perms(self, app_label):
        "Does the user have permissions to view the app `app_label`?"
        # Simplest possible answer: Yes, always
        return True
 
    @property
    def is_staff(self):
        "Is the user a member of staff?"
        # Simplest possible answer: All admins are staff
        return self.is_admin
 
 
class Profile(models.Model):
    user = models.OneToOneField(
        User, primary_key=True, on_delete=models.CASCADE)
    name = models.CharField(default='', blank=True, max_length=50)
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)
    # 住所や年齢などアプリに必要なユーザー情報を設定する
 
    def __str__(self):
        return self.name
 
 
# OneToOneFieldを同時に作成
@receiver(post_save, sender=User)
def create_onetoone(sender, **kwargs):
    if kwargs['created']:
        Profile.objects.create(user=kwargs['instance'])

@receiverの処理はUserが作成されると同時にProfileモデルのフィールドにもデータを作成する処理となっているが、プロフィールはユーザーがアクティブなってから項目を入力されて新規作成されるものだとも考えられるので、この処理はあってもなくてもよい
ただこの処理があるとUserが作成された際にProfileが作成されるためProfileの新規作成処理を別途作成する必要がなくなる

カスタムモデルを使用するという宣言をするためのsetting.pyの編集

カスタムユーザーモデルを使用する宣言を行うために以下のコードを追加する
setting.py

AUTH_USER_MODEL = 'base.User'

※baseの部分はアプリ名
この設定を忘れるとDjangoのデフォルトユーザーモデルが作成されてしまう


またログインのURL情報や、ログイン成功時のリダイレクトURLもsetting.pyに記載することができる
ログアウトも同様
setting.py

LOGIN_URL = '/login/'
 
LOGIN_REDIRECT_URL = '/'
 
LOGOUT_URL = '/logout/'
 
LOGOUT_REDIRECT_URL = '/login/'
ユーザー作成用のフォームを作成する

base/forms.pyを作成する

from django import forms
from django.contrib.auth import get_user_model
 
 
class UserCreationForm(forms.ModelForm):
    password = forms.CharField()
 
    class Meta:
        model = get_user_model()
        fields = ('username', 'email', 'password', )
 
    def clean_password(self):
        password = self.cleaned_data.get("password")
        return password
 
    def save(self, commit=True):
        user = super().save(commit=False)
        user.set_password(self.cleaned_data["password"])
        if commit:
            user.save()
        return user

clean_passwordはバリデーション
不正なパスワードはここで弾かれる

管理者画面にカスタムユーザーを表示させられるように admin.py を編集する

admin.py

from base.forms import UserCreationForm
from django.contrib.auth.admin import UserAdmin

class ProfileInline(admin.StackedInline):
    model = Profile
    can_delete = False


class CustomUserAdmin(UserAdmin):
    fieldsets = (
        (None, {'fields': ('username', 'email', 'password',)}),
        (None, {'fields': ('is_active', 'is_admin',)}),
    )

    list_display = ('username', 'email', 'is_active',)
    list_filter = ()
    ordering = ()
    filter_horizontal = ()

    add_fieldsets = (
        (None, {'fields': ('username', 'email', 'is_active',)}),
    )

    add_form = UserCreationForm

    inlines = (ProfileInline,)

admin.site.register(User, CustomUserAdmin)

ちなみに行を分けると管理画面上の表示が改行されて見やすくなる
fieldsの中身は列挙すると順に横に表示されていく
fieldsets = (
(None.......), # 1行目表示
(None.........), # 2行目表示
)
またこの例ではInlineを利用して、ProfileをUserの管理画面に表示させている
管理者画面でのユーザー追加にはforms.pyで作成したUserCreationFormを指定することで管理者画面でユーザーを作成することができる
またこの例ではadd_fieldsetsにpasswordのフィールド項目が抜けているように思われる

マイグレーション初期化(一度マイグレーションをしてしまった後に後からカスタムユーザーを適用する場合)

アプリ(base)下のmigrationsフォルダ下の__init__.py以外の0001_initial.pyなどのファイルをすべて削除する
同様に、migrationsフォルダ下の__pycache__フォルダ下も同様にinitだけ残して削除する
また、データベースも削除するので、DjangoのデフォルトのSQLiteを使用している場合はdb.sqlite3を削除するだけでよい
別のデータベースを使用している場合はそのデータベースの中身を削除するか、setting.pyのデータベースの接続設定などを変えるなどしてDBを新規作成する

マイグレーションをしてカスタムユーザーを適用する

初めてのマイグレーションかもしくはマイグレーション初期化が終わったら、マイグレーションを行って、先ほど作成したカスタムユーザーを適用する

py .\manage.py makemigrations
py .\manage.py migrate

管理者を作成できるか確認し、管理者画面にログインできるか確認する

py .\manage.py createsuperuser
ログイン・サインアップ画面を作成する

ログイン・新規登録兼用画面
login_signup.html

{% extends 'base.html' %}
 
{% block main %}
<div class="container my-5">
  <div class="row ">
    <div class="col-12">
      <h1>
        {% if 'login' in request.path %}
        Login
        {% elif 'signup' in request.path %}
        Signup
        {% endif %}
      </h1>
      <form method="POST">
        {% csrf_token %}
        <div class="form-row">
          <div class="form-group col-md-4">
            <input type="text" class="form-control" name="username" placeholder="Username">
          </div>
        </div>
        <div class="form-row">
          <div class="form-group col-md-4">
            <input type="email" class="form-control" name="email" placeholder="Email" required>
          </div>
        </div>
        <div class="form-row">
          <div class="form-group col-md-4">
            <input type="password" class="form-control" name="password" placeholder="Password" required>
          </div>
        </div>
        <button class="btn btn-info btm-sm" type="submit">
          {% if 'login' in request.path %}
          Login
          {% elif 'signup' in request.path %}
          Signup
          {% endif %}
        </button>
      </form>
    </div>
  </div>
</div>
 
{% endblock %}
ユーザー情報変更用画面を作成する

ユーザーが自身のアカウント情報を変更することができる画面を作成する
account.html

{% extends 'base.html' %}
 
{% block main %}
 
<div class="container my-5">
  <div class="row">
    <div class="col-12">
      <h1>Account</h1>
      <form method="POST">
        {% csrf_token %}
        <div class="form-row">
          <div class="form-group col-md-4">
            <label>Username</label>
            <input class="form-control" type="text" name="username" placeholder="name" value="{{user.username}}">
          </div>
        </div>
        <div class="form-row">
          <div class="form-group col-md-4">
            <label>Email</label>
            <input class="form-control" type="email" name="email" placeholder="Email" value="{{user.email}}" required>
          </div>
        </div>
        <button type="submit" class="btn btn-primary">Save</button>
      </form>
    </div>
  </div>
</div>
 
{% endblock %}
プロフィール変更用画面を作成する

プロフィールを変更することのできる画面を作成する
profile.html

{% extends 'base.html' %}
 
{% block main %}
 
<div class="container my-5">
  <div class="row">
    <div class="col-12">
      <h1>Profile</h1>
      <form method="POST">
        {% csrf_token %}
        <div class="form-group ">
          <label>Name</label>
          <input class="form-control" type="text" name="name" placeholder="name" value="{{user.profile.name}}">
        </div>
        <div class="form-row">
          <div class="form-group col-md-2">
            <label>フォーム入力値1</label>
            <input class="form-control" type="text" name="zipcode" placeholder="zipcode"
              value="{{user.profile.zipcode}}">
          </div>
        <button type="submit" class="btn btn-primary">Save</button>
      </form>
    </div>
  </div>
</div>
 
{% endblock %}

form-rowで入力項目を増やす場合はform-groupで囲われている箇所を繰り返し記述する

Viewを作成してログイン・サインアップ・アカウント情報更新機能を作成する

アカウント情報を画面に表示するためのViewを作成する
account_views.py

from django.views.generic import CreateView, UpdateView
from django.contrib.auth.views import LoginView
from django.contrib.auth import get_user_model
from base.models import Profile
from base.forms import UserCreationForm
 
 
class SignUpView(CreateView):
    form_class = UserCreationForm
    success_url = '/login/'
    template_name = 'pages/login_signup.html'
 
    def form_valid(self, form):
        return super().form_valid(form)
 
 
class Login(LoginView):
    template_name = 'pages/login_signup.html'
 
    def form_valid(self, form):
        return super().form_valid(form)
 
    def form_invalid(self, form):
        return super().form_invalid(form)
 
 
class AccountUpdateView(UpdateView):
    model = get_user_model()
    template_name = 'pages/account.html'
    fields = ('username', 'email',)
    success_url = '/account/'
 
    def get_object(self):
        # URL変数ではなく、現在のユーザーから直接pkを取得
        self.kwargs['pk'] = self.request.user.pk
        return super().get_object()
 
 
class ProfileUpdateView(UpdateView):
    model = Profile
    template_name = 'pages/profile.html'
    fields = ('name', '..', ......)
    success_url = '/profile/'
 
    def get_object(self):
        # URL変数ではなく、現在のユーザーから直接pkを取得
        self.kwargs['pk'] = self.request.user.pk
        return super().get_object()

SignUpViewは新規アカウント作成のためのView
LoginViewはログインのためのView
Viewはメソッドを二つに分ける模様
AccountUpdateViewはアカウント情報の更新用View
またユーザーアカウント情報の更新の際の注意点は、URLからIDを取得するのではなく、現在のユーザーから直接PKを取得する(self.request.user.pk)
またフィールドはリストでもよいが、タプルを使用するのが一般的らしい
LoginRequiredMixinを使用すると、ログイン制約をViewにつけることができる(ログインしていなければアクセスさせないということが実現できる)
LoginRequiredMixinはViewクラスの第一引数に記載すると自動的に適用される

ルーティングurls.pyを作成する

urls.py

from django.contrib.auth.views import LogoutView

    # Account
    path('login/', views.Login.as_view()),
    path('logout/', LogoutView.as_view()),
    path('signup/', views.SignUpView.as_view()),
    path('account/', views.AccountUpdateView.as_view()),
    path('profile/', views.ProfileUpdateView.as_view()),
ログアウト機能の実装

ログアウト機能は組み込みのViewが存在するため、それを利用する
上記で設定しているこの部分で実装が完了する
urls.py

from django.contrib.auth.views import LogoutView

path('logout/', LogoutView.as_view()),
Tips! Djangoテンプレートエンジン使用時のHTMLでのログイン判定

{% if user.is_authenticated %} でログイン判定することが可能

カスタムユーザーを使用したログイン処理(関数ベースView)

Tips! Modelマネージャー

Modelクラスはテーブル定義を記述するだけでなくデータ挿入取得更新処理をするマネージャーに分ける場合がある
以下のユーザークラスはマネージャクラスを用いている
※ UserManager(BaseUserManager)の処理

ユーザー登録

以下のようなコードを書けばよい

models.py カスタムユーザーを使用する場合

from django.db import models
from django.contrib.auth.models import (
    BaseUserManager, AbstractBaseUser, PermissionsMixin
)

class UserManager(BaseUserManager):

    def create_user(self, username, email, password=None):
        if not email:
            raise ValueError('Enter Email!')
        user = self.model(
            username=username,
            email=email
        )
        user.set_password(password)
        user.save(using=self._db)
        return user
    
    def create_superuser(self, username, email, password=None):
        user = self.model(
            username=username,
            email=email,
        )
        user.set_password(password)
        user.is_staff = True
        user.is_active = True
        user.is_superuser = True
        user.save(using=self._db)
        return user

class Users(AbstractBaseUser, PermissionsMixin):
    username = models.CharField(max_length=255)
    age = models.PositiveIntegerField()
    email = models.EmailField(max_length=255, unique=True)
    is_active = models.BooleanField(default=False)
    is_staff = models.BooleanField(default=False)
    picture = models.FileField(null=True, upload_to='picture/')

    USERNAME_FIELD = 'email'
    REQUIRED_FIELDS = ['username']

    class Meta:
        db_table = 'users'

views.py

def regist(request):
    regist_form = forms.RegistForm(request.POST or None)
    if regist_form.is_valid():
        try:
            regist_form.save()
            return redirect('accounts:home')
        except ValidationError as e:
            regist_form.add_error('password', e)
    return render(
        request, 'accounts/regist.html', context={
            'regist_form': regist_form,
        }
    )

form.py

from django.contrib.auth.password_validation import validate_password


class RegistForm(forms.ModelForm):
    username = forms.CharField(label='名前')
    age = forms.IntegerField(label='年齢', min_value=0)
    email = forms.EmailField(label='メールアドレス')
    password = forms.CharField(label='パスワード', widget=forms.PasswordInput())
    confirm_password = forms.CharField(label='パスワード再入力', widget=forms.PasswordInput())
    
    class Meta():
        model = Users
        fields = ('username', 'age', 'email', 'password')
    
    def clean(self):
        cleaned_data = super().clean()
        password = cleaned_data['password']
        confirm_password = cleaned_data['confirm_password']
        if password != confirm_password:
            raise forms.ValidationError('パスワードが異なります')

    def save(self, commit=False):
        user = super().save(commit=False)
        validate_password(self.cleaned_data['password'], user)
        user.set_password(self.cleaned_data['password'])
        user.save()
        return user
ユーザーを仮登録してその後本登録する処理の実装

方針
Djangoのユーザーの場合はユーザークラスにもともと登録されているis_activateを使用して制御する

Djangoのシグナル機能を利用してユーザー作成時作成後などの特定のイベントに起因するイベント処理を簡単に実行することができる
この機能をDjangoではシグナルと言う
@receiverアノテーションを関数の先頭に付与して使うことができる
@receiver(post_save, sender=User)
def_save_profile(sender, instance, **kwargs):
instance.profile.save()
のような使い方となる
この場合はユーザーモデルが保存されたタイミングで処理が実行される関数となる
他にもpre_save, pre_delete, post_deleteなどがある

このシグナルを利用して、Djangoでユーザーが作成されたタイミングでトークンを発行するような処理を作る
コンソールにトークンを表示する処理を記述する(本番サービスではメール認証にするべき)
models.py

from django.db.models.signals import post_save
from django.dispatch import receiver

class UserActivateTokens(models.Model):

    token = models.UUIDField(db_index=True)
    expired_at = models.DateTimeField()
    user = models.ForeignKey(
        'Users', on_delete=models.CASCADE
    )

    objects = UserActivateTokensManager()

    class Meta:
        db_table = 'user_activate_tokens'

@receiver(post_save, sender=Users)
def publish_token(sender, instance, **kwargs):
    user_activate_token = UserActivateTokens.objects.create(
        user=instance, token=str(uuid4()), expired_at=datetime.now() + timedelta(days=1)
    )
    # 本番サービスではメールでURLを送る実装とする
    print(f'http://127.0.0.1:8000/accounts/activate_user/{user_activate_token.token}')

accounts/activate_user/{user_activate_token.token}
のエンドポイントでアカウントを有効化する処理をviews.pyの中に追記する

models.pyに有効化処理をまず記述してそのあとviews.pyにこの処理を呼び出す処理を追加
models.py

class UserActivateTokensManager(models.Manager):

    def activate_user_by_token(self, token):
        user_activate_token = self.filter(
            token=token,
            expired_at__gte=datetime.now()
        ).first()
        user = user_activate_token.user
        user.is_active = True
        user.save()


エンドポイントから呼び出される処理 (urls.pyのルーティング処理などは基本なので省略)
views.py

def activate_user(request, token):
    user_activate_token = UserActivateTokens.objects.activate_user_by_token(token)
    return render(
        request, 'accounts/activate_user.html'
    )
ログインの処理

models.py
特になし

views.py

def user_login(request):
    login_form = forms.LoginForm(request.POST or None)
    if login_form.is_valid():
        email = login_form.cleaned_data.get('email')
        password = login_form.cleaned_data.get('password')
        user = authenticate(email=email, password=password)
        if user:
            if user.is_active:
                login(request, user)
                messages.success(request, 'ログイン完了しました。')
                return redirect('accounts:home')
            else:
                messages.warning(request, 'ユーザがアクティブでありません')
        else:
            messages.warning(request, 'ユーザかパスワードが間違っています')
    return render(
        request, 'accounts/user_login.html', context={
            'login_form': login_form,
        }
    )

forms.py

class LoginForm(forms.Form):
    email = forms.CharField(label="メールアドレス")
    password = forms.CharField(label="パスワード", widget=forms.PasswordInput())
ログアウト処理 ( & ログアウト完了時のメッセージについてmessageフレームワークを使用する )

django.contrib.messagesのパッケージに各種エラーやデバッグ・ログイン時ログアウト時などのタイミングでメッセージを簡単に表示させることのできるmessagesというパッケージがある

以下のような書き方となる

messages.debug(request, 'メッセージ')
messages.info(request, 'メッセージ')
messages.success(request, 'ログイン完了しました')
messages.warning(request, '注意')
messages.error(request, 'エラー発生')

例:ログアウトの処理などで以下のように書くとよい
views.py

from django.contrib.auth.decorators import login_required

@login_required
def user_logout(request):
    logout(request)
    messages.success(request, 'ログアウトしました')
    return redirect('accounts:home')


テンプレートでは使用するテンプレートごとに (user_login.htmlなど) にmessagesを書いておく
user_login.html

{% if messages %}
  {% for message in messages %}
    <div>{{ message.message }}</div>
  {% endfor %}
{% endif %}
パスワード変更処理

パスワードは変更後にsession情報を更新する必要がある
またパスワード確認フィールドもあることもポイント

フォームの情報でDBの (上書き) 更新は以下のような形になる
第二引数のinstanceに上書きしたい情報が格納される
forms.PasswordChangeForm(request.POST or None, instance=request.user)
この情報のままで保存するのは危険であるので、viewsのtryの処理のようなvalidationチェックを行う
update_session_auth_hash(request, request.user)でセッション情報を更新する

models.py
特になし

views.py

@login_required
def change_password(request):
    password_change_form = forms.PasswordChangeForm(request.POST or None, instance=request.user)
    if password_change_form.is_valid():
        try:
            password_change_form.save()
            messages.success(request, 'パスワード更新完了しました。')
            update_session_auth_hash(request, request.user)
        except ValidationError as e:
            password_change_form.add_error('password', e)
    return render(
        request, 'accounts/change_password.html', context={
            'password_change_form': password_change_form,
        }
    )


forms.py

class PasswordChangeForm(forms.ModelForm):

    password = forms.CharField(label='パスワード', widget=forms.PasswordInput())
    confirm_password = forms.CharField(
        label='パスワード再入力', widget=forms.PasswordInput())

    class Meta():
        model = Users
        fields = ('password', )

    def clean(self):
        cleaned_data = super().clean()
        password = cleaned_data['password']
        confirm_password = cleaned_data['confirm_password']
        if password != confirm_password:
            raise forms.ValidationError('パスワードが異なります')

    def save(self, commit=False):
        user = super().save(commit=False)
        validate_password(self.cleaned_data['password'], user)
        user.set_password(self.cleaned_data['password'])
        user.save()
        return user
ユーザー情報変更処理

models.py
特になし


views.py

@login_required
def user_edit(request):
    user_edit_form = forms.UserEditForm(request.POST or None, request.FILES or None, instance=request.user)
    if user_edit_form.is_valid():
        messages.success(request, '更新完了しました。')
        user_edit_form.save()
    return render(request, 'accounts/user_edit.html', context={
        'user_edit_form': user_edit_form,
    })


forms.py

class UserEditForm(forms.ModelForm):
    username = forms.CharField(label='名前')
    age = forms.IntegerField(label='年齢', min_value=0)
    email = forms.EmailField(label='メールアドレス')
    picture = forms.FileField(label='写真', required=False)

    class Meta:
        model = Users
        fields = ('username', 'age', 'email', 'picture')

AJAX非同期処理とキャッシュ制御(関数ベースView)

Ajax

画面遷移せずにクライアント側からjQueryを用いてサーバサイドにリクエストを投げてデータをやり取りする技術

クライアント側

$.ajax({
   url : “create_post/”, // 実行するURLを指定
   type : “POST”, // HTTPメソッド
   data : { the_post : $(‘#post-text’).val() }, // 送信するデータ
   success : function(json) { // 実行が成功した場合の処理
   },
   error : function(xhr,errmsg,err) { // 実行が失敗した場合の処理
}
});

サーバーサイド側はJSONを返す処理を記述する

from django.http import JsonResponse
from django.core import serializers


if request.is_ajax:
   # 処理
   json_instance = serializers.serialize(‘json’, [ instance, ]) # jsonに変換する
   return JsonResponse({“instance”: json_instance}, status=200) # レスポンスを返す
   または
   return HttpResponse(
      json.dumps(response_data),
      content_type="application/json"
   )
CACHEの種類

1. Memcached: メモリー上キャッシュ、処理が高速、最も一般的に使われ複数サーバで共有も可能
2. Database: データベースに保存するキャッシュ、取得速度遅いが大容量
3. File system: ファイル上に分割して保存するキャッシュ、速度は遅いが管理が楽
4. Local memory: ローカルPCメモリー上に保存するキャッシュ、デフォルトキャッシュ
5. Dummy: 開発環境用ダミーキャッシュ

Djangoのドキュメント Cache
Django’s cache framework | Django documentation | Django


キャッシュの設定例
setting.py

CACHES = {
  'default': {
    'BACKEND': 'django.core.cache.backends.memcached.MemcachedCache',
    'LOCATION': [
      '172.19.26.240:11211',
      '172.19.26.242:11212‘,
    ]
  }
}

キャッシュ操作

from django.core.cache.cache import *

cache.set(‘KEY’, ‘VALUE’)
cache.get(‘KEY’, ‘DEFAULT’) # 存在しない場合はデフォルト値が返ってくる
cache.clear()
cache.delete('KEY')
Ajax実装例

JQuery CDNのインポート
jQuery CDN
uncompressedをクリック
スクリプト部分をコピーしてbase.pyのようなHTMLファイルのheadタグ部分にペーストする


Ajaxの処理を記述

base.pyのbodyタグ内にブロックを記述し、使いたいテンプレートで処理を記述する

<body>

{% block javascript %}{% endblock %}

</body>


例: フォームの一時保存機能を追加する場合

テンプレートのフォームコンポーネントにidを付与してscriptブロックにscriptタグで処理を記述する

<form method="POST">
  {% csrf_token %}
  {{ post_comment_form.as_p }}
  <input type="button" value="一時保存" id="save_comment">
  <input type="submit" value="コメント送信">
</form>


{% block javascript %}
<script>
$("#save_comment").click(function(){
    var comment = $("#id_comment").val();
    $.ajax({
        url: "{% url 'boards:save_comment' %}",
        type: "GET",
        data: {comment: comment, theme_id: "{{ theme.id }}"},
        dataType: "json",
        success: function(json){
            if(json.message){
                alert(json.message);
            }
        }
    });
});
</script>
{% endblock %}

urlでセーブ用のViewの処理のエンドポイントを呼ぶ処理を実装しているので、viewの処理も実装する
urls.pyの実装例は省略

キャッシュを設定する処理
views.py

from django.core.cache import cache
from django.http import JsonResponse

def save_comment(request):
    if request.is_ajax:
        comment = request.GET.get('comment')
        theme_id = request.GET.get('theme_id')
        if comment and theme_id:
            cache.set(f'saved_comment-theme_id={theme_id}-user_id={request.user.id}', comment)
            return JsonResponse({'message': '一時保存しました!'})

saved_comment-theme_id={theme_id}-user_id={request.user.id} は長いがただのKEYである

例: キャッシュを取出す処理
views.py

def post_comments(request, theme_id): # フォームにレンダリングするためのデータを渡す処理
    saved_comment = cache.get(f'saved_comment-theme_id={theme_id}-user_id={request.user.id}', '')
    post_comment_form = forms.PostCommentForm(request.POST or None, initial={'comment': saved_comment})

画面読み込み時にレンダリングするためのデータを渡す処理の先頭にキャッシュされたデータを取得する処理を記述しておく
フォームの第二引数 initial にキャッシュの初期値を設定する
また、フォーム画面でデータをサーバー側に保存した場合はcache.deleteを使用してキャッシュデータを削除しておくことも忘れないようにしておく

クラスベースビューを使用した主にログイン処理

Djangoはクラスベースビューを用いる方が一般的で実践的
ただし別のフレームワークでフロントエンド開発をする場合はDjangoREST APIとして使用するため関数ベースビューで記述する方が一般的かもしれない
Djangoで画面まで作成する場合にはクラスベースビューで作成する方が便利だと思われる

以下のテンプレートがある

View 全てのViewの元になるView
TemplateView テンプレートを表示する。ホーム画面など
CreateVIew データベースにデータを挿入するView。データ作成画面
UpdateView データを更新するView
DeleteView データを削除するView
ListView 特定のテーブルのデータ一覧を表示するView
DetailView テーブルのレコードの詳細を表示するView
FormView Formを表示してデータを送信するView
RedirectView リダイレクトを行うView
View

上記のテンプレートでうまく実装できなさそうな処理を記述するときに継承するViewクラス

リクエストに応じた処理を記述する

class MyView(View):
  def get(self, request, **kwargs):
    # GETの場合の処理
  def post(self, request, **kwargs):
    # POSTの場合の処理
リダイレクトビュー(RedirectView)

urls.pyに直接記述

from django.views.generic.base import RedirectView

urlpatterns = [
  path('/search/<term>/', RedirectView.as_view(url='https://google.co.jp/?q=%(term)s')),
]
SuccessMessageMixin

データ更新削除時等のメッセージ表示に便利なクラス
Pythonは複数クラスの継承ができるため作成するViewの2つ目のクラスとしてSuccessMessageMixinを継承する
例 views.py

class BookUpdateView(UpdateView, SuccessMessageMixin):
  model = Books
  success_message # 成功時メッセージ(静的)
  def get_success_message(self, cleaned_data): # 成功時メッセージ(動的


そのほかのテンプレートビューは使用する際に配布資料参照のこと

ログイン処理(クラスベースView)

ログインログアウト処理

クラスベースのログインビューが存在するためそれを利用する

from django.views.generic import View

class LoginView(View):
  def post(self, request, *args, **kwargs):
    username = request.POST[‘username’] # usernameを取得
    password = request.POST['password’] # passwordを取得
    user = authenticate(username=username, password=password) # userが存在するか確認
    if user is not None:
      if user.is_active:
         login(request, user) # ログイン処理
    return render(request, "index.html")


class LogoutView(View):
  def get(self, request, *args, **kwargs):
    logout(request) # ログアウト処理
    return HttpResponseRedirect(settings.LOGIN_URL)
ログイン制約

LoginRequiredMixin
ログインユーザーのみViewにアクセスできるように制限するクラス
使用するviewに継承する

class MyView(LoginRequiredMixin, View): # ログインが必要なViewに継承させる
  login_url = '/login/'
  redirect_field_name = 'redirect_to’

@method_decorator(login_required)
アノテーションでもログイン制約を付与することができる
Viewの中の一部のメソッドのみログイン制約をつけたい場合に使用できる

class ProtectedView(TemplateView):
  @method_decorator(login_required)
  def dispatch(self, request, *args, **kwargs):
ログインセッション時間について

セッション時間はデフォルトで2週間となっている
変更する場合はsettingにセッション情報を追記する
プログラム中で個別にセッション時間を指定したい場合には request.session.set_expiry(value) を使用する

settings.py

SESSION_COOKIE_AGE: セッションの保存時間(秒)

https://docs.djangoproject.com/ja/3.1/ref/settings/#std:setting-SESSION_COOKIE_AGE

request.session.set_expiry(value): セッションの保存時間を引数の時間(秒)に変更する

※なお、引数が0の場合、ブラウザを閉じるとセッションが破棄される
value に datetime または timedelta オブジェクトを指定すると指定された日時に破棄される
https://docs.djangoproject.com/ja/3.1/topics/http/sessions/#django.contrib.sessions.backends.base.SessionBase.set_expiry

ログイン実装例(クラスベース)
例: ユーザー登録
class RegistUserView(CreateView):
    template_name = 'regist.html'
    form_class = RegistForm
例: ログイン
class UserLoginView(LoginView):
    template_name = 'user_login.html'
    authentication_form = UserLoginForm

    def form_valid(self, form):
        remember = form.cleaned_data['remember']
        if remember:
            self.request.session.set_expiry(1200000)
        return super().form_valid(form)

def form_validはセッション情報保持ボタンを押すとログイン情報を保持することができるような処理にしている
※講座の中でデフォルトのセッション時間を5秒にしているため
実際の実装の場面ではform_validの部分は省いて実装するケースがほとんどだと思う


FormViewで定義する例

class UserLoginView(FormView):
    template_name = 'user_login.html'
    form_class = UserLoginForm

    def post(self, request, *args, **kwargs):
        email = request.POST['email']
        password = request.POST['password']
        user = authenticate(email=email, password=password)
        next_url = request.POST['next']
        if user is not None and user.is_active:
            login(request, user)
        if next_url:
            return redirect(next_url)
        return redirect('accounts:home')
例: ログアウト
class UserLogoutView(LogoutView):
    pass

Viewで定義する例

class UserLogoutView(View):
    
    def get(self, request, *args, **kwargs):
        logout(request)
        return redirect('accounts:user_login')
例: ログイン制約
# @method_decorator(login_required, name='dispatch')
class UserView(LoginRequiredMixin, TemplateView):
    template_name = 'user.html'

    # @method_decorator(login_required)
    def dispatch(self, *args, **kwargs):
        return super().dispatch(*args, **kwargs)

MiddleWareを利用したログ出力の実装(Pythonの基本ログ出力方法)

ログ出力をする意義

以下の理由にて実務で必須

  • エラー解析
  • パフォーマンス解析(レスポンスタイム)
ログレベル

上から順位重大

  • critical 重大エラー
  • error エラー
  • warning 警告
  • info 情報
  • debug デバッグ
出力方法

loggingモジュール(デフォルトでログレベルはwarning)

import logging

# ログの出力
logging.critical(‘エラー内容')
.. rtc
ファイルへの出力
logging.basicConfig(filename=‘app.log’, filemode=‘w’) 
よく使うテクニック
ogging.error(‘%s raised error’, name) # 変数をログに出力
logging.error(f‘{name} raised error’) # 変数をログに出力(3.6以降)
logging.error(f‘{name=} raised error’) # 変数をログに出力(3.8以降)
logging.error(“”, exc_info=True) #スタックトレースの出力

logging.basicConfig(format=‘%(asctime)s-%(process)s-%(levelname)s-%(message)s’) # 出力するログのフォーマットの設定%(asctime): 出力時間、%(process): プロセスID、%(levelname): ログレベル、%(message):メッセージ
datefmt: 時刻のフォーマットを指定

スタックトレースは特によく使う模様
原因特定するために何行目のログ情報かの情報がとても有益となる

ログの一般的な使い方 tryとかで使う

try:
  method()
except Exception as e
  logggin.error(e, exc_info=True)

logging.error(“”, exc_info=True) #スタックトレースの出力
ロガーのインスタンス
logger = logging.getLogger(__name__)

__name__はファイル名を差し、ロガーの識別子となる
使用を推奨されている

handlerを設定する

logger.setLevel(logging.DEBUG) # ログレベルのデフォルトはwarning
ロガーの情報を設定ファイルに記述して使用する方法

confフォルダを作成して、設定ファイルを logger.conf 作る

[loggers] # loggerの一覧を設定
[handlers] # Handlerの一覧を設定
[formatters] # Formatterの一覧を設定
[logger_${logger名}] # 各loggerの設定
[handler_${handler名}] # handlerの設定
[formatter_${formatter名}] # 各formatterの設定

logger.conf 例

[loggers]
keys=root

[handlers]
keys=consolehandler, filehandler

[formatters]
keys=sampleformatter

[logger_root]
level=DEBUG
handlers=consolehandler, filehandler

[handler_consolehandler]
class_StreamHandler
level=INFO
formatter=samleformatter
args=(sys.stdout,)

[handler_filehandler]
class=FileHandler
level=Error
formatter=samleformatter
args=['logs/app.log', 'a', 'utf-8']

[formatter_sampleformatter]
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'

ログのインスタンスを使用してログを出力させる
logging.py

import logging
import logging.config

logging.config.fileConfig(fname='conf/logger.conf')

logger = logging.getLogger(__name__)

logger.debug('')
logger.info('')
logger.warning('')
logger.error('')
logger.critical('')

logsフォルダを作ってログをファイルとして出力させる
今回はinfo以上のログが出力される

複数のloggerを設定して別々のログを出力させることも可能
confファイル

[loggers]
keys=root, samplelogger

[handlers]
keys=consolehandler, filehandler, sampleconsolehandler

[formatters]
keys=sampleformatter

[logger_root]
level=DEBUG
handlers=consolehandler, filehandler

[logger_samplelogger]
level=DEBUG
handlers=filehandler, sampleconsolehandler
qualname=samplelogger
propagate=0

[handler_consolehandler]
class=StreamHandler
level=INFO
formatter=sampleformatter
args=(sys.stdout,)

[handler_filehandler]
class=FileHandler
level=ERROR
formatter=sampleformatter
args=['logs/app.log', 'a', 'utf-8']

[handler_sampleconsolehandler]
class=StreamHandler
level=DEBUG
formatter=sampleformatter
args=(sys.stdout,)

[formatter_sampleformatter]
format=%(asctime)s-%(name)s-%(levelname)s-%(message)s

logging.pyファイル

import logging
import logging.config

logging.config.fileConfig(fname='conf/logger.conf')

logger = logging.getLogger(__name__)

logger.debug('デバッグログ')
logger.info('インフォログ')
logger.warning('ワーニングログ')
logger.error('エラーログ')
logger.critical('クリティカルログ')


logger = logging.getLogger('samplelogger')
logger.debug('デバッグログ')
logger.info('インフォログ')
logger.warning('ワーニングログ')
logger.error('エラーログ')
logger.critical('クリティカルログ')
ローテーティングファイルハンドラーを利用する

一定の条件を設定して、別ファイルにログを新たに切り出す設定
管理上見やすくしたり1ファイルのファイルサイズの肥大化を防ぐことができる

例: logger.confファイルに以下を追記

[handler_fileHandler]
class=handlers.RotatingFileHandler
level=INFO
formatter=sampleFormatter
args=('./logs/log.out', when='S', interval=10, backupCount=5, encoding=‘utf-8’, 
maxBytes=1000)

時間によるローテーションを設定したい場合は以下

[handler_fileHandler]
class=handlers.TimedRotatingFileHandler
level=INFO
formatter=simpleFormatter
args=(‘logger.log’,when=’D’,interval=1,backupCount=3,encoding=‘utf-8’)

MiddleWareを利用したログ出力の実装(Djangoの基本ログ出力方法)

上記はpythonの基本のログ出力方法
以下ではDjangoに設定する方法

Djangoログ出力 setting.py

Djangoのログ出力は setting.py に記述する
ロギング | Django ドキュメント | Django

setting.py

LOGGING = {
  ‘version’: 1, # ロガーバージョン通常は1
  ‘disable_existing_loggers’: False, # デフォルトログの無効化通常はFalse
  'formatters': {
    : # フォーマッターの一覧を記載
  },
  'handlers': {
    : # ハンドラーの一覧を記載
  },
  'loggers': {
    : # ロガーの一覧を記載
  }
}

setting.py 例

LOGGING = {
    'version': 1,
    'disable_existing_loggers': False,
    'formatters': {
        'simple': {
            'format': '%(asctime)s %(levelname)s [%(pathname)s:%(lineno)s] %(message)s',
        }
    },
    'handlers': {
        'console_handler': {
            'level': 'DEBUG',
            'class': 'logging.StreamHandler',
            'formatter': 'simple',
        },
        'timed_file_handler': {
            'level': 'INFO',
            'class': 'logging.handlers.TimedRotatingFileHandler',
            'filename': os.path.join('logs', 'application.log'),
            'when': 'S',
            'interval': 10,
            'backupCount': 10,
            'formatter': 'simple',
            'encoding': 'utf-8',
            'delay': True,
        },
        'timed_error_handler': {
            'level': 'ERROR',
            'class': 'logging.handlers.TimedRotatingFileHandler',
            'filename': os.path.join('logs', 'application_error.log'),
            'when': 'S',
            'interval': 10,
            'backupCount': 10,
            'formatter': 'simple',
            'encoding': 'utf-8',
            'delay': True,
        },
        'timed_performance_handler': {
            'level': 'INFO',
            'class': 'logging.handlers.TimedRotatingFileHandler',
            'filename': os.path.join('logs', 'application_performance.log'),
            'when': 'S',
            'interval': 10,
            'backupCount': 10,
            'formatter': 'simple',
            'encoding': 'utf-8',
            'delay': True,
        }
    },
    'loggers': {
        'application-logger': {
            'handlers': ['console_handler', 'timed_file_handler'],
            'level': 'DEBUG',
            'propagate': False,
        },
        'error-logger': {
            'handlers': ['timed_error_handler'],
            'level': 'ERROR',
            'propagate': False,
        },
        'performance-logger': {
            'handlers': ['timed_performance_handler'],
            'level': 'INFO',
            'propagate': False,
        }
    }
}


ログの出力の処理
views.pyやmodels.pyなど

import logging

# 前処理
application_logger = logging.getLogger('application-logger')
error_logger = logging.getLogger('error-logger')


# メソッド内で記述する
application_logger.debug('エラーメッセージ')
error_logger.error('エラーメッセージ')
ミドルウェアを利用する

リクエスト・レスポンス処理にフックを加えて入力と出力の値を置き換えるフレームワークのことをミドルウェアという
ミドルウェア (Middleware) | Django ドキュメント | Django

django.utils.deprecation.MiddlewareMixinを継承する

Djangoのビューを呼び出す前に実行される処理を記述する場合は
def process_view(self, request, view_func, view_args, view_kwargs):のメソッドを記述する
def process_viewのreturnは通常Noneを返す

ビューで例外が発生した場合に実行される処理を記述する場合は
def process_exception(self, request, exception) のメソッドを追記する
returnはNoneとHttpResponseオブジェクトを返す
Noneをreturnする場合は他のエラーハンドリング処理を記述する
ミドルウェアの定義の逆順に実行される(setting.pyのMIDDLEWAREの下から順番に実行される)

ビューの実行後に呼び出される場合
def process_template_response(self, request, response):


middleware.pyを記述してsetting.pyに記述する

middleware.py 例(プロジェクトフォルダに作成)

# middleware.py
import logging
from django.utils.deprecation import MiddlewareMixin
import time

application_logger = logging.getLogger('application-logger')
error_logger = logging.getLogger('error-logger')
performance_logger = logging.getLogger('performance-logger')

class MyMiddleware(MiddlewareMixin):

    def process_view(self, request, view_func, view_args, view_kwargs): # viewを呼び出す前に実行
        application_logger.info(request.get_full_path())
        
    def process_exception(self, request, exception):
        error_logger.error(exception, exc_info=True)


class PerformanceMiddlware(MiddlewareMixin):

    def process_view(self, request, view_func, view_args, view_kwargs):
        start_time = time.time()
        request.start_time = start_time

    def process_template_response(self, request, response):
        response_time = time.time() - request.start_time
        performance_logger.info(f'{request.get_full_path()}: {response_time}s')
        return response

setting.py 例

MIDDLEWARE = [
    'class_based_view.middleware.PerformanceMiddlware', # リクエストでは最初・レスポンスでは最後に実行される
    # ...
    'class_based_view.middleware.MyMiddleware'  # リクエストでは最後・レスポンスでは最初に実行される
]

OAuth認証の実装

f:id:shinseidaiki:20211220233228p:plain

offlineアクセスのオフラインとはネットワークにつながっていないということではなくユーザーがいない状態のことを差す


必要なモジュール

django-allauth

pip install django-allauth
google OAuth認証を試してみる

settings.pyのINSTALLED_APPに以下のものを追加する
settings.py

'django.contrib.sites',
'allauth',
'allauth.account',
'allauth.socialaccount',
‘allauth.socialaccount.providers.google’, # google認証

ログイン処理時に認証で行うクラスにallauthを追加する
settings.py

AUTHENTICATION_BACKENDS = (
  “django.contrib.auth.backends.ModelBackend”, # デフォルトの認証
  "allauth.account.auth_backends.AuthenticationBackend", # allauthの認証
)

settings.pyの下部に記載

SITE_ID = 1 # django_site テーブル上の認証に用いるサイトの指定 通常1(django_siteは最初のマイグレーションで作成されるテーブル)
ACCOUNT_EMAIL_REQUIRED = True # 認証にメールアドレスが必要か(デフォルトはFalse)
ACCOUNT_USERNAME_REQUIRED = False # 認証にユーザ名が必要か(デフォルトはTrue)
ACCOUNT_AUTHENTICATION_METHOD = ‘email’ # 認証に利用する要素(email or username or 
username_email)
SOCIALACCOUNT_PROVIDERS = { # プロバイダーごとの設定
  'google': {
    ‘SCOPE‘: [ # Google APIで何を取得するか
      'profile',
      'email',
    ],
    'AUTH_PARAMS': {
      ‘access_type’: ‘onine’, #バッチ処理の場合はオフラインで利用する
}}}

アクセスタイプのofflineはネットにつながっていないことでなくユーザーがいない状態例えばバッチetc. のこと

SOCIALACCOUNT_PROVIDERSの中身は以下のドキュメントのProviderの項目を見て記述する
https://django-allauth.readthedocs.io/en/latest/installation.html


google Cloud platformにプロジェクトを登録する

1
google cloud platformで検索してgcpのトップページにアクセス

2
利用規約に同意

3
コンソール画面でプロジェクトの選択>新しいプロジェクト
プロジェクト名と場所(組織なし)を入力して作成

4
プロジェクトを選択をおし
APIサービス>OAuth同意画面
UserTypeを外部に選択して作成

5
アプリ名には先ほどのプロジェクト名を
ユーザーサポート名には適当なメールアドレスを入力する
デベロッパーの連絡先情報は適当なメールアドレスを入力する
保存して次へ

6
スコープはそのままで保存して次へ
テストユーザーもそのままで保存して次へ
概要もそのままでダッシュボードに戻る

7
APIサービス>認証情報作成>認証情報を作成>OAuthクライアントIDを選択
アプリケーションの種類:ウェブアプリケーション
名前:ウェブクライアント1
承認済のJavaScript生成元にローカルのURLを記載
http://127.0.0.1:8000
承認済のリダイレクトURIにも以下のようなURIを記載
http://127.0.0.1:8000/oauth_accounts/google/login/callback/

作成

8
作成されたクライアントIDとシークレットキーをメモなどどこかに保存する

OAuth認証実装 Djangoソースコード側の手順

エディタに戻ってDjangoソースコードを編集していく
マイグレーションを実行する

py manage.py migrations 

これでマイグレーションの履歴を見るとsocialaccount_○○というOAuthで使用されるDBテーブルが4つほど作成されていることがわかる

スーパーユーザーを作成する

py manage.py createsuperuser 

作成した管理者ユーザーで管理者画面にログインする
SOCIAL ACCOUNTSの中に
social accounts
social application tokens
social applications
テーブルがあることがある
この中にOAuthの設定を記載していく

まず、Siteのテーブルの中にexampleのドメイン名があるがこれを127.0.0.1に変更する

次にSocial Applicationsをクリックして以下を追加する
Provider: google
name: OAuth App
クライアントIDは先ほどコピーした値
シークレットキーも先ほどコピーした値
Keyは空のまま
Sitesの127.0.0.1を左から右に移動させる

これで保存する
こうすることでGoogle認証でログインすることが可能となる

この後、実際にOathでログインしたユーザーはsocial accountsテーブルにメースアドレスや名前などが追加される

画面遷移を作成する

urls.pyにOauthの画面への遷移を追加する

urls.py

path('oauth_accounts/', include('allauth.urls’))


ユーザーログインのテンプレート画面にgoogleログインのためのリンクを貼り付ける
user_login.html

{% load socialaccount %}
<a href=“{% provider_login_url ‘google’ %}”>googleログイン</a>

例:user_login.html

{% extends 'base.html' %}
{% load socialaccount %}
{% block content %}
<form method="POST">
   {% csrf_token %}
   {{ form.as_p }}
   <input type="hidden" name="next" value="{{ request.GET.next }}">
   <input type="submit" value="ログイン">
</form>
<a href="{% provider_login_url 'google' %}">Googleログイン</a>
{% endblock %}
動作確認

Djangoサーバーを起動してアプリケーションにアクセス
ユーザーログイン画面からgooglerログインを選択して、よく見るグーグル認証の画面に遷移して、その後認証が通って元の画面にリダイレクトされればよい

DBの中身を確認するとSOCIAL ACCOUNTSテーブルの中にログインされたUserのEmailアドレスなどが格納されていることがわかる
Account Userの中身を見るとユーザーのEmailが見れることが確認できれば良い

画面をきれいにする

http://127.0.0.1/oauth_accounts/signup/
などと指定すると、oauthのライブラリが持っている変な画面が表示されてしまう

これを表示させたくないない場合はurls.pyに記述していた箇所を以下のように書き換える

from allauth.socialaccount.providers.google.urls import urlpatterns as google_url

path('oauth_accounts/', include(google_url)),

こうするとピンポイントにgoogleのurlだけを入れることができる

END

バッチ処理の実装・運用

ドキュメント
https://docs.djangoproject.com/ja/3.1/howto/custom-management-commands/

基本利用方法

アプリケーションフォルダの直下にmanagementフォルダを作成して、commandsフォルダを作成してその中にコマンド(sample.py)を配置

app\management\commands\sample.py

from django.core.management.base import BaseCommand

class Command(BaseCommand):
    
    def handle(self, *args, **options):
        print("バッチ実行")

このバッチファイルを実行する場合は以下のコマンドで実行できる

py manage.py sample

storesアプリケーションフォルダにも同じようなフォルダ階層でバッチファイルを作成することもできる
stores\management\commands\sample.py

from django.core.management.base import BaseCommand

class Command(BaseCommand):

    def handle(self, *args, **options):
        print('storeのバッチを実行')


なお別のアプリケーションに同じ名前のバッチファイルを作成した場合は、setting.pyに記載したアプリの上から順番に実行される(sample.pyが二つあった場合は、py manage.py sampleでは先に登録されているアプリ(app)の方のsample.pyが実行される)

引数の追加

add_argments: コマンド内で利用する引数を追加する

app/management/sample.py

class Command(BaseCommand):
  def add_arguments(self, parser):
    parser.add_argument(‘first’) # 第1引数をキーfirstに格納する
    parser.add_argument(‘second’) # 第2引数をキーsecondに格納する
    parser.add_argument(‘--option1’) # option1で指定した引数をキーoption1に格納する

  def handler(self, *args, **options):
    print(f’{options[“first”]}’) # add_argumentsで格納した値を取り出す
    print(f’{options[“option1”]}’) # add_argumentsで格納した値を取り出す
    • を使うとマップ型のkeyとして定義することができる

呼び出す場合は必ず引数を必要となり、以下のようなコマンドで呼び出すことができる

python manage.py sample A(第1引数) B(第2引数) --option1 C(option1に対する値)
コマンド内で利用する引数の追加

type: 数値型(int)、文字列型(str)で指定する
help: 説明文の追加 「python manage.py help コマンド名」 で使用できる
python manage.py help コマンド名で表示することができる
default: デフォルトの値(格納する引数が存在する場合に指定した値が格納される)
nargs: 格納する値の数を指定してリスト型で格納。呼び出す際はコマンドの引数に配列の数だけ引数を列挙して渡して実行する
action: store_true(引数が存在する場合はTrue)、store_false (引数が存在する場合はFalse)が格納される
choices: 格納できる値の一覧を記述する

例: sapmle.py

from django.core.management.base import BaseCommand


class Command(BaseCommand):
    help='ユーザ情報を表示するバッチです'

    def add_arguments(self, parser):
        parser.add_argument('name', type=str, help='名前') # 1引数
        parser.add_argument('age', type=int) # 2引数
        parser.add_argument('--birthday', default='2020-01-01')
        parser.add_argument('three_words', nargs=3)
        parser.add_argument('--active', action='store_true') # 引数--activeを指定した場合にTrueを格納する
        parser.add_argument('--color', choices=['Blue', 'Red', 'Yellow']) # 例えばBlueを指定すると以下のハンドルの対応する青が表示される

    def handle(self, *args, **options):
        name = options['name']
        age = options['age']
        birthday = options['birthday']
        three_words = options['three_words']
        active = options['active']
        print(
            f'name = {name}, age = {age}, birthday = {birthday}, three_words = {three_words}'
        )
        print(active)
        color = options['color']
        if color == 'Blue':
            print('青')
        elif color == 'Red':
            print('赤')
        elif color == 'Yellow':
            print('黄')
応用

Ordersモデルで定義されたDBの中のOrdersテーブルのデータをファイルに出力するバッチファイルの作成
例: stores/management/commands/export_orders.py

from django.core.management.base import BaseCommand
from stores.models import Orders
from ecsite_project.settings import BASE_DIR
from datetime import datetime
import os
import csv

class Command(BaseCommand):

    def add_arguments(self, parser):
        parser.add_argument('--user_id', default='all')

    def handle(self, *args, **options):
        orders = Orders.objects
        user_id = options['user_id']
        if user_id == 'all':
            orders = orders.all()
        else:
            orders = orders.filter(user_id=user_id)
        file_path = os.path.join(BASE_DIR, 'output', 'orders', f'orders_{datetime.now().strftime("%Y%m%d%H%M%S")}_{user_id}')
        with open(file_path, mode='w', newline='\n', encoding='utf-8') as csvfile:
            fieldnames = ['id', 'user', 'address', 'total_price']
            writer = csv.DictWriter(csvfile, fieldnames=fieldnames)
            writer.writeheader()
            for order in orders:
                writer.writerow({
                    'id': order.id,
                    'user': order.user,
                    'address': order.address,
                    'total_price': order.total_price,
                })
||

こうすることで日繰りでファイルにデータをレポートとして出力させるバッチにしたりすることができる


** メッセージフレームワーク

*** messages.html を作成して、base.htmlに配置してすべての画面でメッセージを出力できるようにしておく

まずはsnippetsに以下のように {% for msg in message %} を使用したメッセージ出力用の画面を作成しておく
messages.html
>|html|
{% for msg in messages %}
<nav class="py-2 mb-0 {{ msg.tags }}">
  <div class="container">
    {{ msg }}
  </div>
</nav>
{% endfor %}

この中で

settings.pyにメッセージの重要度ごとのCSS設定を記述しておく

settings.pyに以下のような記述をしておく

from django.contrib import messages

# messages
MESSAGE_TAGS = {
    messages.ERROR: 'rounded-0 alert alert-danger',
    messages.WARNING: 'rounded-0 alert alert-warning',
    messages.SUCCESS: 'rounded-0 alert alert-success',
    messages.INFO: 'rounded-0 alert alert-info',
    messages.DEBUG: 'rounded-0 alert alert-secondary',
}
メッセージフレームワークをビューの処理の成功時・失敗時のメッセージ制御に使用する

ビューの処理毎にmessagesを付け加えることでメッセージを表示させる

成功した場合の例

from django.contrib import messages

class View(CreateView):
  def form_valid(self, form): 
    messages.success(self.request, '成功しました')
    return super().form_valid(form)

失敗した場合の例

from django.contrib import messages

class Login(LoginView):
    def form_invalid(self, form):
        messages.error(self.request, '失敗しました')
        return super().form_invalid(form)

これで処理毎に想定したメッセージが表示されていればよい

コンテキストプロセッサー

コンテキストプロセッサーとは動的なデータを表示させようと思うと画面毎に個別にViewsで画面にコンテキストを渡す必要があるが、あらゆる画面で共通して表示させたい情報があった場合はview毎に明示して記載すると面倒でかつメンテナンス性も落ちてしまうため、コンテキストプロセッサーとは記載しておけば、全ての画面にコンテキストとしてデータを渡す機能である

コンテキストプロセッサーのファイルを作成する

プロジェクト(config)フォルダの中にcustom_context_processors.pyとして作成することが一般的な模様

例 config/custom_context_processors.py

from django.conf import settings
from base.models import Item
 
def base(request):
    return {
        'TITLE': settings.TITLE,
        'POPULAR_ITEMS': items.order_by('-sold_count')
    }

上はタイトルや人気のアイテムをすべての画面にコンテキストとして渡す処理となっており、こうすることでどの画面でも人気商品をいつでも表示できるようになる
ちなみにコンテキストプロセッサーの変数は大文字で記載するのが慣習なのかもしれない

settings.pyにコンテキストプロセッサーを使用する設定を追記する

主にTEMPLATESにcontext_processorsの項目に先ほど作成したファイルのパスを記述する
configはプロジェクト名になるため、別の名前になっている場合はその名前に合わせる

settings.py

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [BASE_DIR / 'templates'],
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
                'config.custom_context_processors.base',  # 追記
            ],
        },
    },
]

# custom_context_processors
TITLE = 'VegeKet'
テンプレートにおけるコンテキストプロセッサの表示

通常のテンプレートに変数を表示させるのと同じようにブラケット記法で {{ TITLE }} 表示させることができる
例: header.html

<nav>
    <a class="py-2 text-white" href="/">{{ TITLE }}<!--VegeKet--></a>
</nav>
TIPS! バッジの作成と表示

Webコンポーネントで言及されるバッジとは、Webアプリの通知(ベルのマーク)などで何件通知があるかなどを表示してくれるコンポーネントのことである

ヘッダーなどに設定する
コンテキストプロセッサーに登録した変数を使用すると便利
header.html

<a class="py-2 d-none d-md-inline-block text-white" href="/cart/">Cart
      {% if request.session.cart and request.session.cart.items|length != 0 %}
      <span class="badge badge-danger">{{request.session.cart.items|length}}</span>
      {% endif %}
    </a>

表示の方法としてはclass="badge badge-danger"クラスでbadgeを指定するだけでよい(Bootstrapの場合)
今回はコンテキストプロセッサーではなくセッションのcartデータから値を取得している

TIPS! テンプレート構文を使用したときにHTMLのインデントがずれてしまうとき

特にテンプレート内で比較構文を入れるとこれが起きやすい
例 {% if compare < 10 %} の < がHTMLのタグと認識されることが起きたりする
これは文法的にも問題はなく対処法も特にないので、見過ごすしかない模様

Tipa! 404 Not Foundページの実装

templatesフォルダ直下に 404.html を作成して、settings.pyの
DEBUG=False
ALLOWED_HOSTS = ['127.0.0.1']
を設定すると、404.htmlの内容が表示されるようになる
DEBUGがTrueだったり、ALLOWED_HOSTSが設定されていなかったりすると、エラーが返ってくる

500.htmlなど他のステータスコードも同様に実装することができる

404.html

<!--省略-->

<div>404 Not Found アクセスしようとしたページは見つかりません</div>

<!--省略-->