0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Django Viewの応用・ログイン機能・AbstructBaseUser(自分用メモ)

Last updated at Posted at 2023-08-04

Django自分用メモになります

今回はViewの応用についてまとめます

開発環境

OS:mac
エディタ:vscode
python:3.10.9
django:4.1.0

admin画面へのmodelの登録

admin.py
from django.contrib import admin
from .models import Items

admin.site.register(Items) #Itemsはmodel名

リダイレクト処理

views.py
from django.shortcuts import redirect
def one_item(request):
    return redirect('store:item_detail',id=1)
    #リダイレクト時にidを渡すことも可

def item_detail(request,id):
    item = Items.objects.filter(pk=id).first()
    #detail/15のようにidがないURLを指定されたらリストに飛ばすなどの処理もこのように可能
    if item is None:
        return redirect('store:item_list')
    return render(request,'store/item_detail.html',context={
        'item':item
    })

エラーハンドリング

・setting.pyのDEBUGをFalseにしないとエラーハンドリングはできない
・ALLOWED_HOSTSに自身のホストを追加する
ALLOWED_HOSTS=['127.0.0.1']
・プロジェクトのurls.pyに設定したいステータスコードのハンドラーを追加する
handler404=views.関数名(404エラーの場合に実行したい関数(引数は2つ))
handler500=views.関数名(500エラーの場合に実行したい関数(引数は1つ))

・意図的に404エラーを発生させる方法

from django.http import Http404
#if id ==0: この条件の時に404エラーを発生させるなど
raise Http404

意図しないURLが入力されたときに404エラー画面を表示させる

#views.py
def page_not_found(request,exception):
    #statusに404を設定しないとターミナルのアクセスログに404ではなく200(通常アクセス)と認識される
    return render(request,'store/404.html',status=404)
#   return redirect('store:item_list') またはホームに飛ばすなんかもあり

#プロジェクトのurls.py
from store import views
handler404 = views.page_not_found

#テンプレートに404.htmlを作成で完了

500番エラーとは

サーバーの処理に問題がある時に起こるエラー
記述ミスなどDjangoを書いている段階ではエラーを確認できずサーバーと接続した際にわかるエラー
こちらのエラーが発生したページに例えばメールアドレスなど記載しておくとユーザーがエラー遭遇時に何が起こってるのか、どう対処すればいいのかを把握しやすくなる。

from django.shortcuts import get_object_or_404
get_object_or_404
#指定したモデルを呼び出し、getを行う。値を取得できなかった場合raise Http404を送出する
#例)item = get_object_or_404(Items,pk=id)

get_list_or_404
#指定したモデルを呼び出し、filterを行う。同上
#リストで取り出すためリストの中身がない状態で取り出すと404を返す
#例)items = get_list_or_404(Items,id__gt=2) idが2より大きいものを取り出す

ログイン機能の実装編

ユーザー情報を保存する際は必ずパスワードをハッシュ化して保存する。その際、ハッシュ化する関数(ハッシュアルゴリズム)を指定する場合にはsetting.pyのPASSWORD_HASHERS変数を追加する。
ハッシュ化する関数は公式に色々とある。
Argon2やBCryptが強いらしい

バリデーションのオプションを設定するにはsettings.pyのAUTH_PASSWORD_VALIDATORSのオプションを設定する。
詳しくはこちら

settings.py
#追加
PASSWORD_HASHERS=[#上から順に実行される
    'django.contrib.auth.hashers.Argon2PasswordHasher',
    'django.contrib.auth.hashers.BCryptSHA256PasswordHasher',
    'django.contrib.auth.hashers.BCryptPasswordHasher',
    'django.contrib.auth.hashers.PBKDF2PasswordHasher',
    'django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher',

]
AUTH_PASSWORD_VALIDATORS = [
    {
        'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
        #追加
        'OPTIONS':{
            'min_length':8,
        }
    },
#(略)
]

argon2を使う前に仮想環境にpip installする必要があるが

ターミナル
$pip install django\[argon2\]

とすることでインストールできた。
zshでコマンドを実行する場合[]はエラーで引っかかるのでエスケープすると無事完了した。

DjangoデフォルトのUserクラスをつかってログイン周りの機能を実装する(現場ではあまりつかわないからメモ程度)

model.py
from django.db import models
from django.contrib.auth.models import User

class Profile(models.Model):
    #デフォルトのUserモデルとOneToOneで結びつけて追加したい項目を追加
    user = models.OneToOneField(User,on_delete=models.CASCADE)
    website = models.URLField(blank=True)
    picture =models.FileField(upload_to='user/',blank=True)
    
    def __str__(self):
        return self.user.username #管理画面でみた時にわかりやすく

管理画面で見れるようにadmin.pyも記述

admin.py
from django.contrib import admin
from .models import Profile

admin.site.register(Profile)

それぞれのモデルへ登録するフォームをforms.pyに設定
ModelFormを継承してmodelと紐づける

forms.py
from django import forms
from django.contrib.auth.models import User
from user.models import Profile

class UserForm(forms.ModelForm):
    username = forms.CharField(label='名前')
    email = forms.EmailField(label='メールアドレス')
    password = forms.CharField(label='パスワード',widget=forms.PasswordInput())
    
    class Meta:
        model = User #紐付け
        fields = ('username','email','password')
        
class ProfileForm(forms.ModelForm):
    website = forms.URLField(label='ホームページ')
    picture = forms.FileField(label='写真')
    
    class Meta:
        model = Profile #紐付け
        fields = ('website','picture')
        
#ログイン用のフォーム
class LoginForm(forms.Form):
    username=forms.CharField(label='名前',max_length=150)
    password=forms.CharField(label='パスワード',widget=forms.PasswordInput())
    confirm_password=forms.CharField(label='パスワード再入力',widget=forms.PasswordInput())
    
    def clean(self): #バリデーションを設定
        cleaned_data=super().clean()
        password = cleaned_data['password']
        confirm_password = cleaned_data['confirm_password']
        if password != confirm_password:
            raise forms.ValidationError('パスワードが一致しません')

次はviews.py

特別に新しくでてきたこと

user=user_form.save()をしたら
userにたいしてuser.set_password(user.password)で暗号化してセットして
そのあとuser.save()で保存する
OneToOneで紐づけているprofileのほうは一度
profile_form.save(commit=False)として仮置きで保存した後
profileのフィールドであるuserをprofile.user=userとセットした後に
profile.save()でまとめて保存する。

ログインの関数では
authenticateでユーザが存在するかを確認する

@login_requiredデコレータをつけることで
ログインしてる時だけ指定した関数が実行できるようにできる

{% if user.is_authenticated %}:ログインしている場合だけ実行されたり表示する

views.py
from django.shortcuts import render
from user.forms import UserForm,ProfileForm,LoginForm
from django.contrib.auth import authenticate,login,logout
from django.http import HttpResponse
from django.shortcuts import redirect
from django.contrib.auth.decorators import login_required

def user_list(request):
    return render(request,'user/user_list.html')

def index(request):
    return render(request,'user/index.html')

#登録用
def register(request):
    user_form=UserForm(request.POST or None)
    profile_form=ProfileForm(request.POST or None,request.FILES or None)
    if user_form.is_valid() and profile_form.is_valid():
        user = user_form.save()
        user.set_password(user.password) #パスワードを暗号化してセット
        user.save() #保存
        profile = profile_form.save(commit=False) #仮置きで保存
        profile.user=user
        profile.save() #ここで保存
    return render(request,'user/registration.html',context={
        'user_form':user_form,
        'profile_form':profile_form
    })
    
#ログイン用ビュー
def user_login(request):
    login_form=LoginForm(request.POST or None)
    if login_form.is_valid():
        username=login_form.cleaned_data.get('username')
        password = login_form.cleaned_data.get('password')
        user = authenticate(username=username,password=password) #そんなユーザがいるかどうか確かめる
        if user: #userが存在する場合
            if user.is_active: #そしてuserがactiveの場合
                login(request,user) #login関数は第一引数にrequest,第二引数にそのユーザにするとログインができる
                return redirect('user:index')
            else:
                return HttpResponse('アカウントがアクティブではないです')
        else: #userが存在しない時
            return HttpResponse('ユーザが存在しません')
    return render(request,'user/login.html',context={
        'login_form':login_form
    })
    
#ログアウト
@login_required #loginしていないとこの関数のURLにいっても起動しない
def user_logout(request):
    logout(request)
    return redirect('user:index')

@login_required
def info(request):
    return HttpResponse('ログインしています')

ユーザとパスワードのバリデーション

パスワードが妥当なものかを判断するには

from django.contrib.auth.password_validation import validate_password,
from django.contrib.auth.password_validation import password_change

を用いる。

validate_password(password,user)

ユーザのパスワードが適切か(短すぎないか、ありきたりすぎないか、ユーザ名から容易に推測できるものか)をチェックする。
第一引数にパスワードをとり、第二引数にユーザをとる

登録段階でパスワードのバリデーションをかける

views.pyを編集

views.py
from django.core.exceptions import ValidationError
from django.contrib.auth.password_validation import validate_password
#登録用
def register(request):
    user_form=UserForm(request.POST or None)
    profile_form=ProfileForm(request.POST or None,request.FILES or None)
    if user_form.is_valid() and profile_form.is_valid():
        #下記から追加した部分。(commit=False)によってまだ保存しないで持っておく
        user = user_form.save(commit=False)
        #パスワードのバリデーションを行う
        try:
            validate_password(user_form.cleaned_data.get('password'),user)
            #forms.pyでvalidate_passwordをしている場合はuser_form.save()でいい
        except ValidationError as e:
            user_form.add_error('password',e) #add_errorでフォームにエラーを追加できる
            return render(request,'user/registration.html',context={
                'user_form':user_form, #再表示させる
                'profile_form':profile_form
            })
        #以下同じ。ValidationError発生後は下記は行われない
        user.set_password(user.password) #パスワードを暗号化してセット
        user.save() #保存
        profile = profile_form.save(commit=False) #仮置きで保存
        profile.user=user
        profile.save() #ここで保存
    return render(request,'user/registration.html',context={
        'user_form':user_form,
        'profile_form':profile_form
    })

バリデータの自作

公式
自作してsettings.pyでAUTH_PASSWORD_VALIDATORSにバリデータとして追加すればOK

プロジェクト直下にutils/validators.pyファイルを作成。
こちらにパスワードバリデート処理を書いていく

validators.py
from django.core.exceptions import ValidationError
import re #正規表現ライブラリ

class CustomPasswordValidator():
    def __init__(self):
        pass
    
    def validate(self,password,user=None):
        #if allは全てがTrueの場合Trueを返して中の処理を実行
        if all((re.search('[0-9]',password),re.search('[a-z]',password),re.search('[A-Z]',password))):
            return
        raise ValidationError('パスワードには0-9,a-z,A-Zを一文字ずつ含む必要があります')
    
    #ヘルプテキストも追加しておく
    def get_help_text(self):
        return 'パスワードには0-9,a-z,A-Zを一文字ずつ含む必要があります'

settings.pyにも設定

settings.py
AUTH_PASSWORD_VALIDATORS = [
    {
        #自作のパスワードバリデータを追加
        'NAME':'utils.validators.CustomPasswordValidator'
    },
]

AbstractBaseuserを使ったログイン機能の実装

AbstractUserはすでに存在するフィールドをそのまま流用して,usernameフィールドを削除したい場合用いると良い
AbstractBaseUserは初めからUserクラスを作り替えたい時に便利。

手順
1.カスタムマネージャーとカスタムユーザのクラスを作成
2.settings.pyを修正して、ユーザはカスタムのクラスを指すようにする
3.マイグレーションを行う
4.フォームやAdminを作成する

またカスタムのユーザにsuperuserなどの一般的なDjangoのパーミッションを取り入れるにはPermissionsMixinを用いるといい

一般的なユーザがもつフィールドや属性を確認したければ公式

カスタマイズ系の内容はこちら

では実際に書いてみる

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) #passwordをセットして
        user.save(using=self._db) #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 User(AbstractBaseUser,PermissionsMixin):
    username=models.CharField(max_length=150)
    email = models.EmailField(max_length=255,unique=True)
    is_active = models.BooleanField(default=False) #このユーザが有効かどうか
    is_staff = models.BooleanField(default=False) #このユーザが管理画面にアクセスできるか
    website = models.URLField(null=True)
    picture = models.FileField(null=True,upload_to='picture/') #upload_toでファイルの保存フォルダを指定
    #passwordフィールドはAbstractBaseUserに定義されてるのでここで定義する必要はなし
    #is_superuserフィールドもPermissionsMixinに定義されているので定義しないでOK
    
    USERNAME_FIELD='email' #USERNAME_FIELDはユーザをユニークと識別するフィールドを指定する
    REQUIRED_FIELDS = ['username'] #superuser作成時に入力する項目。USERNAME_FIELDとパスワードは必須になるのでそれ以外にあれば設定
    
    objects=UserManager() #UserManagerとの紐付け。objectsに対して作ったマネージャーを設定
    
    def __str__(self): #管理画面でみやすくなる
        return self.email

settings.pyに追加

settings.py
#作成したUserクラスを指定
AUTH_USER_MODEL='accounts.User' #accountsはアプリ名

AbstructBaseUserを使ったユーザー登録おさらい

要するにforms.pyでcleanやsaveをどこまで定義するのかによってviews.pyで記述する内容がかわってくる。forms.pyでバリデーションやsaveメゾットを定義してしまう方が楽かも
下記は演習でのforms.pyとviews.py

forms.py
from django import forms
from django.contrib.auth import get_user_model
from .models import Users
from django.core.exceptions import ValidationError
from django.contrib.auth.password_validation import validate_password


User=get_user_model()

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)
    # picture = forms.FileField(label='写真')
    
    class Meta:
        model=Users
        fields=('username','age','email','password')
        
    def clean(self):
        cleaned_data = super().clean()
        password = cleaned_data.get('password')
        confirm_password = cleaned_data.get('confirm_password')
        if password != confirm_password:
            raise ValidationError('パスワードが一致しません')
        
    def save(self,commit=False):
        user = super().save(commit=False)
        validate_password(self.cleaned_data.get('password'),user)
        user.set_password(self.cleaned_data.get('password'))
        user.save()
        return user

つぎにviews.py

views.py
from django.shortcuts import render,redirect
from .forms import RegistForm
from django.contrib.auth.password_validation import validate_password
from django.core.exceptions import ValidationError

def home(request):
    return render(request,'accounts/home.html')


def regist(request):
    form = RegistForm(request.POST or None)
    if form.is_valid():
        # user = form.save(commit=False) 不要
        #パスワードのバリデーションを行う
        try:
            # validate_password(form.cleaned_data.get('password'),user) 
            # forms.pyでvalidate_passwordをすでにしているので今回はform.save()をtry文で実行する
            form.save()
            return redirect('accounts:home')
        except ValidationError as e:
            form.add_error('password',e) #add_errorでフォームにエラーを追加できる
        # user.set_password(user.password) 不要
        # user.save() 不要
    
    return render(request,'accounts/regist.html',context={
        'form':form
    })

管理画面から作成したり編集できるようにするには

流れとしてはforms.pyにて管理画面で使うフォームを定義(バリデーションの設定なんかも)してadmin.pyでUserAdminを継承したクラスにforms.pyで作成した設定を読み込んだり設定を記述する

理解はしたけどこういうものなんだなくらいの認識。

forms.py
from django import forms
from django.contrib.auth.forms import ReadOnlyPasswordHashField
from django.contrib.auth import get_user_model
from django.core.exceptions import ValidationError

User = get_user_model() #今回作成した利用するUserクラスを指定する

#管理画面でつかうフォーム
class UserCreationForm(forms.ModelForm):
    password=forms.CharField(label='password',widget=forms.PasswordInput)
    confirm_password = forms.CharField(label='password再入力',widget=forms.PasswordInput)
    
    class Meta:
        model =User
        fields = ('username','email','password')
        
    def clean(self):
        cleaned_data = super().clean()
        password = cleaned_data.get('password')
        confirm_password = cleaned_data.get('confirm_password')
        if password != confirm_password:
            raise ValidationError('パスワードが一致しません')
        
    def save(self,commit=False):
        user = super().save(commit=False)
        #下記のようにforms.pyでvalidate_passwordした場合はviews.pyにてtryで実行するのはform.save()
        validate_password(self.cleaned_data.get('password'),user)
        user.set_password(self.cleaned_data.get('password'))
        user.save()
        return user


class UserChangeForm(forms.ModelForm):
    password = ReadOnlyPasswordHashField() #ハッシュ化されたパスワードを表示
    website = forms.URLField(required=False)
    picture = forms.FileField(required=False)
    
    class Meta:
        model = User
        fields = ('username','email','password','is_staff','is_active','is_superuser','website','picture')
        
    def clean_password(self):
        #initial すでに登録されているパスワードを返す
        return self.initial['password']

admin.pyで読み込む

admin.py
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin
from django.contrib.auth import get_user_model
from .forms import UserCreationForm,UserChangeForm

User=get_user_model()

class CustomizeUserAdmin(UserAdmin):
    #管理画面の表示の仕方を変えたい時にUserAdminを継承して変えていく
    form = UserChangeForm #ユーザ編集画面で使うFormを定義
    add_form = UserCreationForm #ユーザ作成画面でつかうFormを定義
    
    #ユーザ一覧画面で表示する要素の定義
    list_display = ('username','email','is_staff')
    
    #ユーザ編集画面で表示する要素の定義
    fieldsets=(
        ('ユーザ情報',{'fields':('username','email','password','website','picture')}),
        ('パーミッション',{'fields':('is_staff','is_active','is_superuser')}),
    )
    
    #ユーザ作成画面で表示する要素の定義
    add_fieldsets=(
        ('ユーザ情報',{
            'fields':('username','email','password','confirm_password')
        }),
    )
#第一引数をUser,第二引数をCustomizeUserAdminにすることによって
#Userを管理画面から見れるようにして表示方法は自作したCustomizeUserAdminにすることができる
admin.site.register(User,CustomizeUserAdmin)

以下,管理画面画像

一覧画面
スクリーンショット 2023-08-06 19.25.03.png
作成画面
スクリーンショット 2023-08-06 19.25.14.png
編集画面
スクリーンショット 2023-08-06 19.25.27.png

管理画面のカスタマイズ その1

手始めに基本となるモデルの作成とテーブル名とデータのオブジェクト名をみやすくして
admin.pyの登録もする

models.py
#管理画面のカスタマイズ
class Students(models.Model):
    name=models.CharField(max_length=20)
    age = models.IntegerField()
    score = models.IntegerField()
    school = models.ForeignKey(
        'Schools',on_delete=models.CASCADE
    )
    
    class Meta:
        db_table='students'
        verbose_name_plural='生徒' #管理画面でのテーブルの名前を変更
        
    def __str__(self):
        return self.name + ': '+str(self.age) #管理画面でのオブジェクトの名前を変更
        
class Schools(models.Model):
    name= models.CharField(max_length=20)
    class Meta:
        db_table = 'schools'
        verbose_name_plural='学校' #管理画面でのテーブルの名前を変更
        
    def __str__(self):
        return self.name #管理画面でのオブジェクトの名前を変更

admin.pyに登録

admin.py
admin.site.register(Students)
admin.site.register(Schools)

管理画面のカスタマイズ その2

並び替えとか表示項目の設定とか
admin.ModelAdminを継承したクラスを
admin.site.registerの際に第二引数に取ると各モデルのページをカスタマイズできる
または@admin.register(モデル名)をadmin.ModelAdminを継承したクラスの前にデコレーターする

admin.py
#admin.site.register(Students)
#admin.site.register(Schools) 下記を設定するならコメントアウトしておく
#管理画面での表示の並び替えと表示項目を設定
@admin.register(Students)
class StudentAdmin(admin.ModelAdmin):
    fields = ('name','score','age','school') #並び替えたい順に書く
    list_display = ('id','name','age','score','school') #表示項目の指定
    list_display_links = ('name') #nameをクリックすると詳細に飛ぶリンクを設定
    #scoreを高い順に並べたいときはmodels.pyでMetaクラスにorderingを設定
    search_fields=('name','age',) #検索窓を追加。絞り込みたい項目を設定
    list_filter=('name','age','score','school',) #フィルターを追加
    list_editable = ('age','score','school') #一覧から直接数値を変えることができる

#学校一覧に生徒数も表示したい
@admin.register(Schools)
class SchoolsAdmin(admin.ModelAdmin):
    list_display = ('name','student_count',) #student_countは下記で定義する関数
    
    def student_count(self,obj): #objとはSchoolクラスのインスタンスのこと
        # print(type(obj))
        # print(dir(obj)) #メゾットをみるとstudents_setがある
        count = obj.students_set.count()
        return count
    
    student_count.short_description = '生徒数' #カラム名を変える
    #高校名のカラムを変える場合はmodels.pyにverbose_nameを設定

#models.py 並びかえる時はモデルのクラスMetaに設定
class Meta:
        db_table='students'
        verbose_name_plural='生徒' #管理画面でのテーブルの名前を変更
        ordering=('score',) #管理画面での並び順の設定.降順は-scoreとする
#表示カラム名を変更
class Schools(models.Model):
    name= models.CharField(max_length=20,verbose_name='学校名')

管理画面のカスタマイズ その3

django標準のテンプレートを同じ名前のファイルをつくって一部を自分好みに修正することによって簡単に管理画面をカスタマイズすることができる。(githubに対象のファイル名があるからそれを自身のtemplatesフォルダの中に同じ名前でつくって書き換える)
githubのtemplatesページ
スクリーンショット 2023-08-07 11.55.37.png

↑コピペして書き換えるだけ
django/django/contrib/admin/templates/admin/base.htmlは
すべてのtemplatesを読み込んでいるからなにかを編集したいと思った時にこれをみれば
どのファイルを編集すればいいのかがわかる(これをみるとcssはadmin/css/base.cssを読み込んでいるのがわかるからそれを編集すればいい。)

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?