モデルとかテンプレートの作成
省略
この人のフォルダ構成は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と違って順番が保持されないために順番を保持するために使用できるメソッド
順序月の辞書が作れる
消費税率などの見られてもよい環境変数 にしたい情報は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)
決済機能
決済機能についての簡単な説明がある
https://github.com/TakumaFujimoto/vegeket_project/blob/main/sec10/README.md
ここではStripeを利用する
Stripeで開発者向けの開発用の決済機能を無料で試すことができる
Stripe: 世界で最もシェアを獲得するクレジットカード決済代行サービス
各種セキュリティ・法規制に対応しているため決済代行サービスを使わないで課金が必要な自作サービスを実装する選択肢はほぼ皆無だと思われる(特に個人)
使用時の手数料3%以外の費用がないため個人でマッチングアプリ を実装したい場合(年齢確認用)などで利用できる
Stripeのドキュメント
Stripe のドキュメント
Stripe使用準備
アカウントの作成
上記ストライプの公式サイトからアカウントを作成する(メール認証があるので認証を済ませる)
アカウントを作成するとダッシュ ボードに移動する
最初から開発環境を提供してくれている模様
本番環境を利用する場合は別途申請が必要となる
ここではすべて開発環境だけを使用する
Stripeパッケージのインストール
pip install stripe
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_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.api_key = settings.STRIPE_API_SECRET_KEY
class PaySuccessView (TemplateView):
template_name = 'pages/success.html'
def get (self, request, *args, **kwargs):
del request.session['cart' ]
return super ().get(request, *args, **kwargs)
class PayCancelView (TemplateView):
template_name = 'pages/cancel.html'
def get (self, request, *args, **kwargs):
return super ().get(request, *args, **kwargs)
tax_rate = stripe.TaxRate.create(
display_name='消費税' ,
description='消費税' ,
country='JP' ,
jurisdiction='JP' ,
percentage=settings.TAX_RATE * 100 ,
inclusive=False ,
)
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(
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
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
これで基本的な決済機能を実装できるようになった
カスタムユーザモデルを作成する
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?"
return True
def has_module_perms (self, app_label):
"Does the user have permissions to view the app `app_label`?"
return True
@ property
def is_staff (self):
"Is the user a member of 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
@ 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):
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):
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
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 )
)
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)
画面遷移せずにクライアント側からjQuery を用いてサーバサイドにリクエス トを投げてデータをやり取りする技術
クライアント側
$. ajax ({
url : “create_post/ ”,
type : “POST”,
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, ])
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' )
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を使用してキャッシュデータを削除しておくことも忘れないようにしておく
ログイン処理(クラスベースView)
ログインログアウト処理
クラスベースのログインビューが存在するためそれを利用する
from django.views.generic import View
class LoginView (View):
def post (self, request, *args, **kwargs):
username = request.POST[‘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):
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):
ログイン実装例(クラスベース)
例: ユーザー登録
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' )
例: ログイン制約
class UserView (LoginRequiredMixin, TemplateView):
template_name = 'user.html'
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 ,
‘disable_existing_loggers’: 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 例(プロジェクトフォルダに作成)
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):
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' # リクエストでは最後・レスポンスでは最初に実行される
]
offlineアクセスのオフラインとはネットワークにつながっていないということではなくユーザーがいない状態のことを差す
必要なモジュール
django -allauth
pip install django-allauth
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
エディタに戻って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’)
parser.add_argument(‘second’)
parser.add_argument(‘--option1’)
def handler (self, *args, **options):
print (f’{options[“first”]}’)
print (f’{options[“option1”]}’)
を使うとマップ型の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 ='名前' )
parser.add_argument('age' , type =int )
parser.add_argument('--birthday' , default='2020-01-01' )
parser.add_argument('three_words' , nargs=3 )
parser.add_argument('--active' , action='store_true' )
parser.add_argument('--color' , choices=['Blue' , 'Red' , 'Yellow' ])
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 %}
この中でのクラスに{{ msg.tags }}があるが、これはsettings.pyに定義しておくメッセージの重要度に応じたCSS である
次に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 }} </ 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 データから値を取得している