できること(抜粋)
講座1
講座2
目次
- 自環境
- ECサイトの構築
- Secretsフォルダの利用
- モデルとかテンプレートの作成
- 決済機能
- エラーハンドリング
- カスタムユーザモデルを作成する
- モデルの作成
- カスタムモデルを使用するという宣言をするためのsetting.pyの編集
- ユーザー作成用のフォームを作成する
- 管理者画面にカスタムユーザーを表示させられるように admin.py を編集する
- マイグレーション初期化(一度マイグレーションをしてしまった後に後からカスタムユーザーを適用する場合)
- マイグレーションをしてカスタムユーザーを適用する
- ログイン・サインアップ画面を作成する
- ユーザー情報変更用画面を作成する
- プロフィール変更用画面を作成する
- Viewを作成してログイン・サインアップ・アカウント情報更新機能を作成する
- ルーティングurls.pyを作成する
- ログアウト機能の実装
- Tips! Djangoテンプレートエンジン使用時のHTMLでのログイン判定
- カスタムユーザーを使用したログイン処理(関数ベースView)
- AJAX非同期処理とキャッシュ制御(関数ベースView)
- クラスベースビューを使用した主にログイン処理
- ログイン処理(クラスベースView)
- MiddleWareを利用したログ出力の実装(Pythonの基本ログ出力方法)
- MiddleWareを利用したログ出力の実装(Djangoの基本ログ出力方法)
- OAuth認証の実装
- OAuth認証実装 Djangoソースコード側の手順
- バッチ処理の実装・運用
- コンテキストプロセッサー
- TIPS! テンプレート構文を使用したときにHTMLのインデントがずれてしまうとき
- Tipa! 404 Not Foundページの実装
自環境
- 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! 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はクラスベースビューを用いる方が一般的で実践的
ただし別のフレームワークでフロントエンド開発をする場合はDjangoはREST 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の基本ログ出力方法)
ログ出力をする意義
以下の理由にて実務で必須
- エラー解析
- パフォーマンス解析(レスポンスタイム)
出力方法
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認証の実装
offlineアクセスのオフラインとはネットワークにつながっていないということではなくユーザーがいない状態のことを差す
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 %}
この中で
次にbase.htmlのどこかに{% include 'snippets/messages.html' %}を取り込んでおく
例 base.html
<header> {% block header %} {% include 'snippets/messages.html' %} {% include 'snippets/header.html' %} {% endblock %} </header>
こうすることですべての画面にメッセージが適用されるようになる
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> <!--省略-->