4-3. DjangoのStaticファイルの扱い

今回のテーマは「DjangoのStaticファイルの扱い」です。ここまでローカルの開発サーバでDjangoアプリを開発してきた方はstaticファイルの置き場所は既に設定済みという認識だと思います。しかし、本番環境にデプロイする際にはstaticファイルの参照先を変える必要があります。今回はデプロイ時を想定してStaticファイルの取り扱いについて見ていきたいと思います。


DjangoにおけるStaticファイルの扱い

Djangoに限らずWEBアプリケーション全般の話ですが、実行プログラムとStatic(静的)ファイルの配置場所を分ける傾向にあります。これはStaticファイルへのリクエストを切り離すことでアプリケーション側の負荷を減らしStaticファイルの読み込み効率UPを考慮したものです。尤も近年はキャッシュを用いて高速にStaticファイルを配信する技術もあるで、古いノウハウとなりつつありますがロジックを担当するアプリケーションと読み込まれるだけのStaticファイルを分離するという考え方はまだ生きていると思います。

しかし開発時にアプリケーションとStaticファイルを分離してしまうと開発が困難(というか面倒)となり作業効率が悪くなります。そこでDjangoは開発時に取り扱ったStaticなファイルを本番環境用のディレクトリにそっくりコピーするという戦略を取っています。開発時にはsettings.pyのSTATICFILES_DIRSに登録されたディレクトリを参照しています。

公式ドキュメントでは
https://docs.djangoproject.com/ja/2.1/howto/static-files/
が該当ページとなります。

設定パラメータとファイルコピーのためのコマンド

Staticファイルに関するパラメータ

ここでsettings.pyで設定するStaticファイルに関する設定パラメータを見ていきましょう。

  • STATICFILES_DIRS: 開発時のStaticファイルを置くディレクトリ
  • STATIC_ROOT: 本番環境用のStaticファイルを置くディレクトリ
  • STATIC_URL: テンプレート上でstaticモジュールを使用した時のURL

STATICFILES_DIRSとSTATIC_URLに関しては1-5. staticファイルを扱うで設定している通り、開発中のStaticファイルを置く場所です。DjangoはStaticファイルにアクセスするように指示を受けると開発時であればSTATICFILES_DIRSにあるファイルを探しに行きます。この時、それらのディレクトリにアクセスするURLはSTATIC_URLで設定したURLとなります。

問題はSTATIC_ROOTです。これが本番時にファイルを設置する場所となります。このパラメータを設定しておくことで後述のコマンドで開発で使用したファイルをコピーする先を指定することになります。

ファイルコピー用のコマンド

では本番環境用にSTATIC_ROOTにファイルをコピーするにはどうすれば良いのでしょうか?DjangoにはSTATIC_ROOTへファイルをコピーするコマンドが用意されています。


(venv)$ ./manage.py collectstatic

このコマンドでSTATIC_ROOTで設定したディレクトリにファイルがコピーされます。

NginxやuWSGIでのStaticファイル置き場の設定

ここまでStaticファイルの置き場所について見てきました。DjangoにとってSTATIC_ROOTで指定されたディレクトリは単にコピー先であって、ルーティングの対象外となります。においてCSSが適用されたなかった理由がお分かり頂けたと思います。

では、本番環境でStaticファイルにアクセスするにはどうしたら良いのでしょうか?ここから先の話はDjangoとはあまり関係のない話になります。要はユーザーからSTATIC_URLにアクセスがあった時にSTATIC_ROOTのディレクトリにアクセスするように設定をすれば良いということです。今回はNginx + uWSGIで構成を考えていますのでNginxで設定する場合とuWSGIで設定する場合の2通りを見ていきましょう。

Nginxで設定する場合

ネットで多く紹介されている方法がNginxでディレクトリを設定する方法です。これは単純にSTATIC_URLにアクセスされた場合のルートディレクトリを指定するというものです。


server {
    ... 省略

        location /static/ {
            root   STATIC_ROOTディレクトリへのpath;
        }
    
    ... 省略
}

uWSGIで設定する場合

NginxにアクセスされたリクエストをすべてuWSGIに流して、StaticファイルへのアクセスはuWSGIで処理するよういうことも出来ます。今回はで作成したuwsgi.iniに”static-map”の項目を追加しましょう。このuwsgi.iniはローカルで動かすための設定ですが、本番環境でも基本は変わりません。


[uwsgi]
chdir = {Djangoアプリのプロジェクトルート}
module = mysite.wsgi:application
static-map = /static={STATIC_ROOTのディレクトリパス}

http = 0.0.0.0:8000

最後に

uWSGIでDjangoアプリを動かしたり、サーバーにデプロイした際にStaticファイルが反映されず焦る方もいるかも知れません。この記事がなにかのお役に立てば幸いです。次回はサーバーにDjangoアプリをデプロイする作業を見ていこうと思います。

Sponsored Link


4-2. uWSGIでDjangoアプリを動かす基礎

今回のテーマは「uWSGIでDjangoアプリを動かす基礎」です。前回の「4-1. Djangoアプリケーションのデプロイの概要」でnginxとuWSGIでDjangoアプリを動かす概要を見てきました。今回はuWSGIでDjangoを動かしていきたいと思います。


前提

まずディレクトリ構成を確認しておきましょう。以下のようなディレクトリである前提で話を進めたいと思います。


venv #mysiteの仮想環境用
mysite #プロジェクトルート
│└mysite
│─base
│─thread
│─search
│─static
└─media

uWSGIの導入

まずはuWSGIを導入しましょう。uWSGIのインストールにはpythonとC言語のコンパイラが必要です。今回はUbuntu環境にインストールする前提で話を進めていきます。他の環境にインストールする場合はhttps://uwsgi-docs.readthedocs.io/en/latest/Install.htmlを参考にしてください。


$ sudo apt install build-essential python
$ source venv/bin/activate
(venv)$ pip install uwsgi

これでuWSGIのインストールは完了です。

今回はプロジェクトのvenv環境にuWSGIをインストールしましたが、OSのグローバルな環境や異なるvenv環境にインストールしても構いません。Djangoプロジェクトと異なるvenv環境にインストールした場合には動作させるためのオプションが変わりますのでご注意ください。

uWSGIでDjangoアプリケーションを起動する

ではuWSGIでDjangoアプリを起動してみましょう。今回は8081ポートからブラウザでアクセスできるように起動します。nginxとの連携については別の機会に触れることとし、まずはuWSGI単体でDjangoアプリ起動することを考えます。

Djangoプロジェクトと同じvenv環境にインストールした場合

このケースではuWSGIとDjangoアプリの環境が同じなので以下のように起動すればOKです。


$ cd {venvディレクトリのある階層}
$ source venv/bin/activate
(venv)$ uwsgi --http :8000 --module mysite.wsgi --chdir {Djagoプロジェクトルートパス}

chdirオプションはuwsgiコマンドの実行場所がDjangoプロジェクトのルートであれば必要ありません。また、mysite/mysite/wsgi.pyを直接指定する場合はmoduleオプションではなくwsgi-fileオプションでファイル名を指定する点もご注意ください。

Djangoプロジェクトと異なる環境にuWSGIをインストールした場合

この場合はuWSGI起動時にhomeオプションを指定して、どのpythonを使ってDjangoアプリを起動するか指定しなければいけません。

例えばOSに直接uWSGIをインストールしている状況では以下のようなコマンドで起動します。


$ cd {プロジェクトルート}
$ uwsgi --http :8000 --module mysite.wsgi \
  --home {Djangoプロジェクトのvenvディレクトリパス} \
  --chdir {Djagoプロジェクトルートパス}

uWSGIの動作確認

この状態でWEBブラウザでlocalhost:8000にアクセスして確認してみましょう。以下のような図となるはずです。
fig. 4-2

CSSが適用されていませんね。どういうことでしょうか?実はstaticなファイルの置き場所はuWSGIかNginxどちらかで別途設定してあげる必要があります。詳細は次回の「Staticファイルを扱う」で見ていきます。

uwsgi.iniの設定

これまでuWSGIの起動にはコマンドにオプションをつけて起動してきました。毎回この長いオプションをつけて起動するのは大変です。起動用の設定ファイルを作成しましょう。uWSGIを起動するための設定はuwsgi.iniファイルに書きます。このファイルを起動時に読み込むことでコマンドにオプションを渡すのと同等のことが出来るようになります。


[uwsgi]
chdir = {Djangoアプリのプロジェクトルート}
module = mysite.wsgi:application

http = 0.0.0.0:8000

上記のuwsgi.iniは起動する最低限のオプションに留めました。本番環境では上記設定では不足であることに注意してください。uWSGIはとても多機能なサーバであるため多くのオプションがあります。このオプションはuWSGI Optionsで確認することができます。

では設定ファイルを使ってuWSGIを起動しましょう。以下コマンドで起動します。今回はvenv環境にインストールしたuWSGIを用いるためアクティベートした状態で起動します。


(venv)$ uwsgi --ini uwsgi.ini

最後に

今回はuWSGIの基本的な使用について見てきました。次回はStaticファイルの扱いについて見ていきたいと思います。一歩一歩確実にマスターしていきましょう。

TOPへ戻る

Sponsored Link


4-1. Djangoアプリケーションのデプロイの概要

今回のテーマは「Djangoアプリケーションのデプロイの概要」です。いよいよ第4章が始まりました。この章ではdjangoアプリケーションのデプロイの基礎を見ていきます。一般的にウェブアプリケーションのデプロイ方法は手法が沢山あり唯一無二の正解があるわけではありません。インフラや規模によっても選択するツールや手法が異なりますので本章ではできるだけ原理的な話を中心にしようと思います。原理が理解できていれば便利なデプロイツールや自動化ツールを使うのは難しくないはずです。


Webアプリケーションのデプロイ

この「Django学習帳」はDjangoで初めてWebアプリケーションを作成する方も対象としているためWebアプリケーションのデプロイについても簡単に触れておきます。一般的にWEBアプリケーションは開発者がローカル環境で開発しただけではユーザーは使えないため、公開するためのサーバーに設置する必要があります。簡単に言うとデプロイ作業とはローカルで開発していたWebアプリケーションをサーバに反映させ公開できるようにする設置作業のことです。

静的なHTMLとCSS、javascriptのみで構成されたWEBページであればデプロイ作業はファイルを適当な構成で配置するだけの簡単な作業です。(SCSSやjavascript等のフロントエンド関連のビルドもデプロイ作業に含まれますが、本質的な話ではないので一旦横に置きます。)しかし、サーバーサイドプログラムが動作する一般的なウェブアプリケーションではユーザーからのリクエストをプログラムに伝え動作させるためのミドルウェアの設定や、ソースコードの変更を反映させる方法、データベースのマイグレーションなどを行う必要があります。

現在では様々な自動化ツールが開発され手軽にデプロイできるようになっています。またDocker等のWEBアプリが動作する仮想環境をそのまま本番環境にアップロードするだけという手法も生まれており多様化しています。しかし、基本的な部分は変わりませんので一度オンプレミスやVPSで環境構築からデプロイまでを一通り経験しておくと良い経験になると思います。

Djangoアプリケーションのデプロイ

おそらくDjangoでアプリケーションを作成してきた方はDjangoに付随している開発用サーバを起動してローカル環境で開発をしてきたと思います。(Docker環境という方も居るかと思いますが・・・)この開発用サーバはあくまで開発用なので公開には使用できません。間違っても本番サーバーで./manage runserverで開発サーバを起動して外部へ公開してはいけません。

DjangoはWSGIをサポートするフレームワークです。ですのでDjangoに限らず、WSGIアプリケーションをデプロイした経験があればさほど難しくは感じない筈です。(動作環境やデプロイ方法の共通化がWSGIの大きな目的でもあります。)今回はDjangoアプリケーションを動作させる方法の1つとしてNginx + uWSGIで動作させる方法を中心に見ていきたいたいと思います。

WSGIについて

ご存知の方も多いかと思いますがWSGIについて見ていきましょう。WSGIはWeb Server Gateway Interfaceの頭文字を取って名付けられています。PythonにおいてWebアプリケーションとWebサーバーを結びつけるための標準化された規格と考えれば良いと思います。Python製のWEBフレームワークはDjangoに限らずWSGIをサポートしているものが多く、共通の手法でWebサーバーで動作することが出来ます。

WSGIはWSGIサーバ(WSGIアプリケーションコンテナ)で上で動作します。この実装としてはuWSGI, guniconなどの他にApache用のモジュールであるmod_wsgiやmod_pythonなどがあります。Apache上で動作させる際にはmod_wsgiを使用する方法がメジャーだと思います。今回はNginxで動作させるため、uWSGIを用いて動作させようと思います。

uWSGIについて

uWSGIは前項で説明したWSGIサーバの1つです。pythonのライブラリ管理ツールpipでインストール可能です。(ただしC言語で書かれているためGCC等のC言語コンパイラが必須です)

Nginx + uWSGIでDjangoアプリを動かす環境の概要

それでは実際にNginx + uWSGIでDjangoアプリケーションを動作させる概念図を見ながら概要を見ていきましょう。冒頭に書いたようにあくまで構成例であり、唯一の正解ではないことに注意してください。ユーザーがWEBサーバーにアクセスしてからNginxを経由してuWSGIに接続され、Djangoアプリケーションを呼び出すところがイメージできれば良いと思います。当然ですが、WEBサーバにApacheを使用した場合は異なる図となります。

fig. 4-1

NginxとuWSGIの通信

uWSGIがDjangoを動作させるサーバとなり、Nginxはリバースプロキシとしてユーザーからのリクエスト受けることになります。
NginxとuWSGIの通信はポートを指定する場合とunixドメインソケットを使用する場合があります。同サーバー上でNginxとuWSGIを動かす場合にはunixドメインソケットの方が良いかと思います。

尚、Djangoアプリのソースコードを更新した場合はuWSGIを再起動する必要があるのでシステムのinit(多くの場合はSystemd)と管理できるようにしておくと自動化が非常に楽になります。

venv仮想環境でアプリごとに独立した環境を構築

Djangoアプリケーションはvenv仮想環境下で動かすことを想定しました。同一サーバーに複数のDjangoアプリが存在した場合にvenv仮想環境下で動作させておくとアプリケーションごとにライブラリの独立性を保つことができます。これによりDjango2.0のアプリとDjango2.1のアプリが同一サーバーに共存するなどということも可能となります。

ソースコードの更新

WEBアプリケーションが公開された後のデプロイ作業はソースコードの更新作業とDBマイグレーションが主な作業となると思います。ソースコードの更新方法は様々な手法が考えられますが、多くの場合はGitサーバからpullすることで更新することが主流ではないでしょうか?近年Githubを始めGitのホスティングサービスは充実しているので活用しているプロジェクトも多いと思います。プロジェクトによってはSFTPによるアップロードなども考えられます。ただしGitはあくまでソースのバージョン管理ツールなのでデプロイに必須ではありません。

ただし近年Gitのhookを契機にビルドツールやデプロイツールが走るようになっているプロジェクトは多く、GitやSVN等のバージョン管理ツールとデプロイ作業は密接な関わりを持っていることは確かです。

マイグレーション

ソースコードの更新に合せてデータベースのマイグレーションを行いますが、マイグレーションはDjangoのマイグレーションツールを使っても良いですし、Flyway等の独自のマイグレーションツールを活用しても問題ありません。本ページではDjangoのマイグレーションツールを利用することを前提として説明していきたいと思います。

最後に

近年はより簡単にWEBアプリケーションをデプロイできるホスティングサービスが増え、自らオンプレミス環境で一から環境構築する機会はあまりないかも知れません。しかし自らサーバーを立て、Webアプリケーションを公開した経験は無駄にならないと思っています。是非一度挑戦してみてください。

Sponsored Link


3-12. ログイン状態に応じて動的に入力フォームを変化させる

今回のテーマは「ログイン状態に応じて動的に入力フォームを変化させる」です。せっかくログイン機能を付与したので、ログインユーザーは名前やメールアドレスを入力しなくても投稿出来るように、しましょう。また、未ログインユーザーはゲストとしてメールアドレスと名前を入力すればスレッド作成やコメント投稿が出来るように修正していきましょう。

※本ページは「3-11. 認証バックエンドをカスタマイズしてログイン方法を変更する」まで読まれた方を対象としています。そのためサンプルソースコードが省略されている場合があります。


モデルの修正

ログインユーザーとTopicモデル、Commentモデルを結びつけましょう。また、ゲストユーザー用にメールアドレスも追加しましょう。

thread/forms.py(一部抜粋)


class TopicModelForm(forms.ModelForm):
    # ゲストユーザー用入力項目
    user_name = forms.CharField(
        label='ゲストユーザー名',
        required=True,
    )
    email = forms.EmailField(
        label='メールアドレス',
        required=True,
    )

    class Meta:
        model=Topic
        fields=[
            'title',
            # 'user_name',
            # 'email',
            'category',
            'message',
        ]
        widgets = {
            'title' : forms.TextInput(attrs={'class': 'hoge'}),
            # 'user_name' : forms.TextInput(attrs={'value': '名無し'}),
        }

    def __init__(self, *args, **kwargs):
        kwargs.setdefault('label_suffix', '')
        super().__init__(*args, **kwargs)
        self.fields['category'].empty_label = '選択して下さい'
        self.fields['user_name'].widget.attrs['value'] = 'anonymous'

    def save(self, commit=True):
        topic = super().save(commit=False)
        topic.user_name = self.cleaned_data['user_name']
        topic.email = self.cleaned_data['email']
        if commit:
            topic.save()
        return topic

        # self.fields['user_name'].widget.attrs['value'] = '匿名'

ゲストユーザー用のスレッド作成フォームの作成

ゲストユーザーがスレッドを作成出来るようにTopicModelFormを改修しましょう。thread/forms.pyを以下のように修正します。

thread/forms.py(一部抜粋)


class TopicModelForm(forms.ModelForm):
    # ゲストユーザー用入力項目
    user_name = forms.CharField(
        label='ゲストユーザー名',
        required=True,
    )
    email = forms.EmailField(
        label='メールアドレス',
        required=True,
    )

    class Meta:
        model=Topic
        fields=[
            'title',
            # 'user_name',
            # 'email',
            'category',
            'message',
        ]
        widgets = {
            'title' : forms.TextInput(attrs={'class': 'hoge'}),
            # 'user_name' : forms.TextInput(attrs={'value': '名無し'}),
        }

    def __init__(self, *args, **kwargs):
        kwargs.setdefault('label_suffix', '')
        super().__init__(*args, **kwargs)
        self.fields['category'].empty_label = '選択して下さい'
        self.fields['user_name'].widget.attrs['value'] = 'anonymous'

    def save(self, commit=True):
        topic = super().save(commit=False)
        topic.user_name = self.cleaned_data['user_name']
        topic.email = self.cleaned_data['email']
        if commit:
            topic.save()
        return topic

        # self.fields['user_name'].widget.attrs['value'] = '匿名'

敢えて、user_nameとemailをクラス変数として追加しています。これはuser_nameとemailについては管理画面では任意項目として、ゲストユーザーの入力フォームのみで必須項目としたいためです。管理画面においても必須項目とする場合にはMetaクラス内のfieldsで指定しても構わないと思います。

ログインユーザー用のスレッド作成フォームの作成

次にログインユーザーが入力するフォームを作成していきます。thread/forms.pyに以下のように追記します。

thread/forms.py(一部抜粋)


class LoginedUserTopicModelForm(forms.ModelForm):
    class Meta:
        model=Topic
        fields=[
            'title',
            'category',
            'message',
        ]
        widgets = {
            'title' : forms.TextInput(attrs={'class': 'hoge'}),
        }

    def __init__(self, *args, **kwargs):
        kwargs.setdefault('label_suffix', '')
        super().__init__(*args, **kwargs)
        self.fields['category'].empty_label = '選択して下さい'
        # self.fields['user_name'].widget.attrs['value'] = '匿名'
    
    def save(self, user, commit=True, **kwargs):
        topic = super().save(commit=False)
        topic.user = user
        if commit:
            topic.save()
        return topic

特に解説は不要かと思います。save関数はオーバーライドしてカスタムユーザーを指定して保存するようにしています。

ゲストユーザー用のコメント投稿フォームの作成

次にゲストユーザーがコメント投稿するためのフォームを用意するためにCommentModelFormを改修します。thread/forms.pyを修正します。

thread/forms.py(一部抜粋)


class CommentModelForm(forms.ModelForm):
    # ゲストユーザー用入力項目
    user_name = forms.CharField(
        label='お名前',
        required=True,
    )
    email = forms.EmailField(
        label='メールアドレス',
        required=True,
    )

    class Meta:
        model = Comment
        fields = [
            'message',
            'image',
        ]

    def __init__(self, *args, **kwargs):
        kwargs.setdefault('label_suffix', '')
        super().__init__(*args, **kwargs)
        self.fields['user_name'].widget.attrs['value'] = 'anonymous'

    def save_with_topic(self, topic_id, commit=True, **kwargs):
        comment = self.save(commit=False)
        comment.topic = Topic.objects.get(id=topic_id)
        comment.no = Comment.objects.filter(topic_id=topic_id).count() + 1
        comment.user_name = self.cleaned_data['user_name']
        comment.email = self.cleaned_data['email']
        if commit:
            comment.save()
        return comment

改修方針はTopicModelFormと同じですね。save_with_topic関数でもuser_nameとemailをUserオブジェクトに渡していますので忘れないようにして下さい。

ログインユーザー用のコメント作成フォームの作成

フォームの準備の最後はログインユーザーがコメント投稿するためのフォームの準備です。thread/forms.pyに追記していきます。

thread/forms.py(一部抜粋)


class LoginedUserCommentModelForm(forms.ModelForm):
    class Meta:
        model = Comment
        fields = [
            'message',
            'image',
        ]

    def __init__(self, *args, **kwargs):
        kwargs.setdefault('label_suffix', '')
        super().__init__(*args, **kwargs)

    def save_with_topic(self, topic_id, user, commit=True, **kwargs):
        comment = self.save(commit=False)
        comment.topic = Topic.objects.get(id=topic_id)
        comment.no = Comment.objects.filter(topic_id=topic_id).count() + 1
        comment.user = user
        if commit:
            comment.save()
        return comment

特に難しい部分はないと思います。save_with_topic関数には仮引数にuserがあるので注意して下さい。

ログイン状態に応じてフォームを変更するようにビューを修正する

さて、ここまで準備したフォームをログイン状態によって動的に使い分けていきます。ゲストユーザーの状態(未ログイン状態)ではCommentModelFormとTopicModelFormを使用します。そしてログイン状態ではLoginedUserCommentModelFormとLoginedUserTopicModelFormを使用します。この切り替えを動的に行うためにビューの中でget_form_class関数をオーバライドします。ではthread/views.pyの改修を見ていきましょう。

thread/views.py(一部抜粋)


#importはファイル文頭で
from . forms import (
    TopicModelForm, TopicForm, CommentModelForm,
    LoginedUserTopicModelForm, LoginedUserCommentModelForm
)
from . models import Topic, Category, Comment


class TopicCreateView(CreateView):
    template_name = 'thread/create_topic.html'
    form_class = LoginedUserTopicModelForm
    model = Topic
    success_url = reverse_lazy('base:top')

    def get_form_class(self):
        '''
        ログイン状態によってフォームを動的に変更する
        '''
        if self.request.user.is_authenticated:
            return LoginedUserTopicModelForm
        else:
            return TopicModelForm

    def form_valid(self, form):
        ctx = {'form': form}
        if self.request.POST.get('next', '') == 'confirm':
            ctx['category'] = form.cleaned_data['category']
            return render(self.request, 'thread/confirm_topic.html', ctx)
        elif self.request.POST.get('next', '') == 'back':
            return render(self.request, 'thread/create_topic.html', ctx)
        elif self.request.POST.get('next', '') == 'create':
            form.save(self.request.user)
            # メール送信処理
            template = get_template('thread/mail/topic_mail.html')
            user_name = self.request.user.username if self.request.user else form.cleaned_data['user_name']
            mail_ctx={
                'title': form.cleaned_data['title'],
                'user_name': user_name,
                'message': form.cleaned_data['message'],
            }
            EmailMessage(
                subject='トピック作成: ' + form.cleaned_data['title'],
                body=template.render(mail_ctx),
                from_email='hogehoge@example.com',
                to=['admin@example.com'],
                cc=['admin2@example.com'],
                bcc=['admin3@example.com'],
            ).send()
            # return super().form_valid(form)
            return redirect(self.success_url)
        else:
            # 正常動作ではここは通らない。エラーページへの遷移でも良い
            return redirect(reverse_lazy('base:top'))

class TopicAndCommentView(FormView):
    template_name = 'thread/detail_topic.html'
    form_class = LoginedUserCommentModelForm

    def get_form_class(self):
        '''
        ログイン状態によってフォームを動的に変更する
        '''
        if self.request.user.is_authenticated:
            return LoginedUserCommentModelForm
        else:
            return CommentModelForm
    
    def form_valid(self, form):
        # comment = form.save(commit=False)
        # comment.topic = Topic.objects.get(id=self.kwargs['pk'])
        # comment.no = Comment.objects.filter(topic=self.kwargs['pk']).count() + 1
        # comment.save()

        # Comment.objects.create_comment(
        #     user_name=form.cleaned_data['user_name'],
        #     message=form.cleaned_data['message'],
        #     topic_id=self.kwargs['pk'],
        #     image=form.cleaned_data['image']
        # )
        kwargs = {}
        if self.request.user.is_authenticated:
            kwargs['user'] = self.request.user

        form.save_with_topic(self.kwargs.get('pk'), **kwargs)
        response = super().form_valid(form)
        return response

    def get_success_url(self):
        return reverse_lazy('thread:topic', kwargs={'pk': self.kwargs['pk']})
    
    def get_context_data(self):
        ctx = super().get_context_data()
        ctx['topic'] = Topic.objects.get(id=self.kwargs['pk'])
        ctx['comment_list'] = Comment.objects.filter(
                topic_id=self.kwargs['pk']).annotate(vote_count=Count('vote')).order_by('no')
        return ctx

ポイントはrequest.user.is_authenticated()メソッドによって使用するフォームを使い分けているところですね。TopicオブジェクトやCommentオブジェクトの保存処理は各フォームオブジェクトの同一メソッドを呼び出すことで共通化しています。

念のため、上記のビュークラスを使用するようにURLも確認しておきましょう。

thread/urls.py


from django.urls import path

from . import views
app_name = 'thread'


urlpatterns = [
    # path('create_topic/', views.TocicCreateViewBySession.as_view(), name='create_topic'),
    path('create_topic/', views.TopicCreateView.as_view(), name='create_topic'),
    # path('create_topic/', views.topic_create, name='create_topic'),
    path('/', views.TopicAndCommentView.as_view(), name='topic'),
    # path('category//', views.CategoryView.as_view(), name='category'),
    path('category//', views.show_catgegory, name='category'),
]

テンプレートの修正

ログインしている場合とゲストユーザーの場合で表示を切り替えるため、テンプレートにも修正を加えましょう。テンプレートにはuserが渡されていますので、このuserで判断をしていきます。

スレッド作成画面
templates/thread/create_topic.html.py



{% extends 'base/base.html' %}
{% block title %}スレッド作成 - {{ block.super }}{% endblock %}
{% block content %}
<div class="ui grid stackable">
    <div class="eleven wide column">
        <div class="ui breadcrumb">
            <a href="{% url 'base:top' %}" class="section">TOP</a>
            <i class="right angle icon divider"></i>
            <a class="active section">スレッド作成</a>
        </div>
        <div class="ui segment">
            <div class="content">
                <div class="header"><h3>スレッド作成</h3></div>
                {% if not user.is_authenticated %}
                <div class="ui warning message">
                    <p>ゲストユーザーでスレッド作成しますか?ログインする場合は以下のボタンでログインして下さい。</p>
                    <a class="ui button orange" href="/accounts/login?next=/thread/create_topic/">ログイン</a>
                </div>
                {% endif %}
                <form class="ui form" action="{% url 'thread:create_topic' %}" method="POST">
                    {% csrf_token %}
                    <!-- {{form.as_p}} -->
                    {% for field in form %}
                    <div class="field">{{field.label_tag}}{{field}}</div>
                        {% for error in field.errors%}
                        <p style="color: red;">{{error}}</p>
                        {% endfor%}
                    {% endfor %}
                    <button type="submit" class="ui button" name="next" value="confirm">作成</button>
                </form>
            </div>
        </div>
    </div>
    {% include 'base/sidebar.html' %}
</div>
{% endblock %}

templates/thread/confirm_topic.html.py



{% extends 'base/base.html' %}
{% block title %}スレッド作成 - {{ block.super }}{% endblock %}
{% block content %}
<div class="ui grid stackable">
    <div class="eleven wide column">
        <div class="ui breadcrumb">
            <a href="{% url 'base:top' %}" class="section">TOP</a>
            <i class="right angle icon divider"></i>
            <a class="active section">スレッド作成</a>
        </div>
        <div class="ui segment">
            <div class="content">
                <div class="header"><h3>スレッド作成</h3></div>
                <p>内容を確認してください</p>
                <table  class="ui celled table table table-hover" >
                    <tr><td>タイトル</td><td>{{form.title.value}}</td></tr>
                    {% if user.is_authenticated %}
                    <tr><td>お名前</td><td>{{user.username}}</td></tr>
                    {% else %}
                    <tr><td>ゲスト名</td><td>{{form.cleaned_data.user_name}}</td></tr>
                    {% endif %}
                    <tr><td>カテゴリー</td><td>{{form.cleaned_data.category}}</td></tr>
                    <tr><td>本文</td><td><pre>{{form.message.value}}</pre></td></tr>
                </table>
                <form class="ui form" action="{% url 'thread:create_topic' %}" method="POST">
                    {% csrf_token %}
                    {% for field in form %}
                        {{field.as_hidden}}
                    {% endfor %}
                    <button class="ui button grey" type="submit" name="next" value="back">戻る</button>
                    <button class="ui button orange" type="submit" name="next" value="create">作成</button>
                </form>
            </div>
        </div>
    </div>
    {% include 'base/sidebar.html' %}
</div>
{% endblock %}

templates/thread/detail_topic.html



{% extends 'base/base.html' %}
{% block title %}トピック作成 - {{ block.super }}{% endblock %}
{% block content %}
{% load threadfilters %}
{% load static %}
<div class="ui grid stackable">
    <div class="eleven wide column">
        <div class="ui breadcrumb">
            <a href="{% url 'base:top' %}" class="section">TOP</a>
            <i class="right angle icon divider"></i>
            <a href="{% url 'thread:category' url_code=topic.category.url_code %}" class="section">{{topic.category.name}}</a>
            <i class="right angle icon divider"></i>
            <a class="active section">{{topic.title}}</a>
        </div>
        <div class="ui segment">
            <div class="content">
                <div class="header"><h3>{{topic.title}}</h3></div>
                <p>{% if topic.user %}{{topic.user}}{% else %}{{topic.user_name}}(Guest){% endif %} - {{topic.created}}</p>
                <div class="ui segment">
                    <p><pre>{{topic.message}}</pre></p>
                </div>
            </div>
        </div>
        <!--コメント表示-->
        <div class="ui segment">
            {% if comment_list %}
            {% for comment in comment_list %}
            <div class="ui segment secondary">
                <p>{{comment.no}}. {% if comment.user %}{{comment.user.username}}{% else %}{{comment.user_name}}(Guest){% endif %}
                <br>{{comment.created}}</p>
                {% if comment.pub_flg %}
                <p>{{comment.message | comment_filter | safe}}</p>
                    {% if comment.image %}
                    <a href="{{comment.image.url}}" target="_blank" rel="noopener noreferrer"><img decoding="async" src="{{comment.image.url}}" width=200px></a>
                    {% endif %}
                <div class="ui right aligned vertical segment">
                    <div class="vote_button" style="cursor: pointer;"
                        data-comment-id="{{comment.id}}" data-count="{{comment.vote_count}}">
                        <i class="heart outline icon"></i>
                        <span class="vote_counter">
                            {% if comment.vote_count > 0 %}{{comment.vote_count}}{% endif %}
                        </span>
                    </div>
                </div>
                {% else %}
                <p style="color: grey">コメントは非表示とされました</p>
                {% endif %}
            </div>
            {% endfor %}
            {% else %}
            <div class="ui warning message"><p>まだコメントはありません</p></div>
            {% endif %}
        </div>
        <!--//コメント表示-->
        <!--コメント投稿-->
        <h4>コメント投稿</h4>
        {% if not user.is_authenticated %}
        <div class="ui warning message">
            <p>ゲストユーザーで投稿しますか?ログインする場合は以下のボタンでログインして下さい。</p>
            <a class="ui button orange" href="/accounts/login?next=/thread/{{topic.id}}/">ログイン</a>
        </div>
        {% endif %}
        <div class="ui segment">
            <form class="ui form" action="" method="POST" enctype="multipart/form-data">
                {% csrf_token %}
                {{form.as_p}}
                <button class="ui button orange" type="submit">コメント投稿</button>
            </form>
        </div>
        <!--//コメント投稿-->
    </div>
    {% include 'base/sidebar.html' %}
</div>
{% endblock %}
{% block js %}
<script src="{% static 'js/vote.js' %}" type='text/javascript'></script>
{% endblock %}

基本的にはuser.is_authenticatedで場合分けしていくことでログインしている場合としていない場合を区別することが出来ます。

動作確認

では、動作確認をしてみましょう。

まずはゲストユーザーでスレッド作成をしてみましょう。

ゲストユーザーでのコメント作成も出来ますね。

次にログインユーザーでスレッド作成してみましょう。

ログインユーザーでのコメント投稿画面です。

最後に

Sponsored Link


3-11. 認証バックエンドをカスタマイズしてログイン方法を変更する

今回のテーマは「3-11. 認証バックエンドをカスタマイズしてログイン方法を変更する」です。これまで、Django標準のログイン方法としてユーザー名とパスワードでログインするログイン画面でした。しかしメールアドレスでログインする方法に変更したい場合もあると思います。今回はDjangoのユーザー認証について見ていきましょう。

※本ページは「3-10. 独自カスタマイズのユーザーを使用する」まで読まれた方を対象としています。そのためサンプルソースコードが省略されている場合があります。


Djangoのログインの仕組み

djangoの認証は認証用のバックエンドによって行われます。このバックエンドはsettings.pyのAUTHENTICATION_BACKENDSに設定します。AUTHENTICATION_BACKENDSのデフォルト値は’django.contrib.auth.backends.ModelBackend’1つですが、バックエンドは配列として追加することが出来ます。

Djangoの認証はdjango.contrib.auth.authenticate関数で行われます。このauthenticate関数が呼ばれるとAUTHENTICATION_BACKENDSに記載されたバックエンドのauthenticate関数を順番に呼び出します。認証に失敗すれば次のバックエンドを呼び出します。もし認証に成功すれば、認証されたユーザーを返す仕組みです。

今回は独自に実装したフォームの中のclean関数をオーバーライドしてauthenticate関数を呼び出し認証処理を行います。もし認証に失敗した場合はバリデーションエラーを返す仕組みです。

認証バックエンドをカスタマイズする

デフォルトのModelBackendに加えてカスタマイズしたバックエンドを追加しましょう。accounts/backends.pyを新規作成します。ファイル名はこの名前でなくても大丈夫です。

accounts/backends.py


from django.contrib.auth.backends import ModelBackend
from .models import User

class EmailAuthBackend(ModelBackend):
    def authenticate(self, request, email=None, password=None, **kwargs):
        try:
            user = User.objects.get(email=email)
        except User.DoseNotExist:
            return None
        else:
            if user.check_password(password) and self.user_can_authenticate(user):
                return user

作成したバックエンドを設定に追加する
作成したバックエンドを追加するためにはmysite/settings.pyに追加します。インポートが多いのでEmailAuthenticationForm以外のクラスも表示していますが、追加するのはEmailAuthenticationFormのみです。このEmailAuthenticationFormはdjango.contrib.auth.form.AuthenticationFormをベースとしてメールアドレス用に修正しています。

mysite/settings.py


+ AUTHENTICATION_BACKENDS = [
+     'django.contrib.auth.backends.ModelBackend',
+     'accounts.backends.EmailAuthBackend',    
+ ]

メールアドレスでのログイン用フォームを用意する

メールアドレスで認証するためのフォームを作成していきます。

accounts/forms.py


from django.contrib.auth import (
    authenticate, get_user_model
)
from django.forms import ModelForm, Form
from django.forms.fields import EmailField
from django import forms

# from django.contrib.auth.models import User
# from django.contrib.auth import get_user_model
from django.contrib.auth.forms import (
    AuthenticationForm, PasswordChangeForm,
    PasswordResetForm, SetPasswordForm,
    UserChangeForm, UserCreationForm
)
from django.utils.translation import gettext_lazy as _
from django.utils.text import capfirst

from .models import User

UserModel = get_user_model()

class UserInfoChangeForm(ModelForm):
    class Meta:
        model = User
        fields = [
            'email',
            # 'last_name',
            # 'first_name',
        ]

    def __init__(self, email=None, first_name=None, last_name=None, *args, **kwargs):
        kwargs.setdefault('label_suffix', '')
        super().__init__(*args, **kwargs)
        # ユーザーの更新前情報をフォームに挿入
        if email:
            self.fields['email'].widget.attrs['value'] = email
        if first_name:
            self.fields['first_name'].widget.attrs['value'] = first_name
        if last_name:
            self.fields['last_name'].widget.attrs['value'] = last_name

    def update(self, user):
        user.email = self.cleaned_data['email']
        user.first_name = self.cleaned_data['first_name']
        user.last_name = self.cleaned_data['last_name']
        user.save()

class EmailChangeForm(ModelForm):
    class Meta:
        model = User
        fields = ['email']

    def __init__(self, email=None, *args, **kwargs):
        kwargs.setdefault('label_suffix', '')
        super().__init__(*args, **kwargs)
        # ユーザーの更新前情報をフォームに挿入
        if email:
            self.fields['email'].widget.attrs['value'] = email

    def update(self, user):
        user.email = self.cleaned_data['email']
        user.save()


class CustomAuthenticationForm(AuthenticationForm):
    def __init__(self, *args, **kwargs):
        kwargs.setdefault('label_suffix', '')
        super().__init__(*args, **kwargs)

class CustomPasswordChangeForm(PasswordChangeForm):
    def __init__(self, *args, **kwargs):
        kwargs.setdefault('label_suffix', '')
        super().__init__(*args, **kwargs)

class CustomPasswordResetForm(PasswordResetForm):
    def __init__(self, *args, **kwargs):
        kwargs.setdefault('label_suffix', '')
        super().__init__(*args, **kwargs)

class CustomSetPasswordForm(SetPasswordForm):
    def __init__(self, *args, **kwargs):
        kwargs.setdefault('label_suffix', '')
        super().__init__(*args, **kwargs)

class AdminUserCreationForm(UserCreationForm):
    class Meta:
        model = User
        fields = ('username', 'email')
    
    # def save(self, commit=True):
    #     user = User.objects.create_user(
    #         self.cleaned_data["name"],
    #         self.cleaned_data["email"],
    #         self.cleaned_data["password1"],
    #     )
    #     return user

class CustomUserChangeForm(UserChangeForm):
    class Meta:
        model = User
        fields = '__all__'
        
class CustomUserCreationForm(UserCreationForm):
    class Meta:
        model = User
        fields = ('username', 'email')

    def __init__(self, *args, **kwargs):
        kwargs.setdefault('label_suffix','')
        super().__init__(*args, **kwargs)

class EmailAuthenticationForm(Form):
    """
    django.contrib.auth.form.AuthenticationFormをベースに改変
    """
    email = EmailField(
        label=_('Email'),
        widget=forms.EmailInput(attrs={'autofocus': True,})
    )
    password = forms.CharField(
        label=_("Password"),
        strip=False,
        widget=forms.PasswordInput,
    )

    error_messages = {
        'invalid_login': _(
            "Please enter a correct %(username)s and password. Note that both "
            "fields may be case-sensitive."
        ),
        'inactive': _("This account is inactive."),
    }

    def __init__(self, request=None, *args, **kwargs):
        """
        The 'request' parameter is set for custom auth use by subclasses.
        The form data comes in via the standard 'data' kwarg.
        """
        self.request = request
        self.user_cache = None
        kwargs.setdefault('label_suffix', '')
        super().__init__(*args, **kwargs)

        # Set the max length and label for the "username" field.
        self.email_field = UserModel._meta.get_field(UserModel.USERNAME_FIELD)
        self.fields['email'].max_length = self.email_field.max_length or 254
        if self.fields['email'].label is None:
            self.fields['email'].label = capfirst(self.email_field.verbose_name)
        for field in self.fields.values():
            field.widget.attrs["class"] = "form-control"
            field.widget.attrs["placeholder"] = field.label

    def clean(self):
        email = self.cleaned_data.get('email')
        password = self.cleaned_data.get('password')

        if email is not None and password:
            self.user_cache = authenticate(self.request, email=email, password=password)
            if self.user_cache is None:
                raise self.get_invalid_login_error()
            else:
                self.confirm_login_allowed(self.user_cache)

        return self.cleaned_data

    def confirm_login_allowed(self, user):
        """
        Controls whether the given User may log in. This is a policy setting,
        independent of end-user authentication. This default behavior is to
        allow login by active users, and reject login by inactive users.

        If the given user cannot log in, this method should raise a
        ``forms.ValidationError``.

        If the given user may log in, this method should return None.
        """
        if not user.is_active:
            raise forms.ValidationError(
                self.error_messages['inactive'],
                code='inactive',
            )

    def get_user(self):
        return self.user_cache

    def get_invalid_login_error(self):
        return forms.ValidationError(
            self.error_messages['invalid_login'],
            code='invalid_login',
            params={'username': _('Email')},
        )

最初の解説でも触れましたが、clean関数内でauthenticate関数を呼び認証処理を行っています。こうすることで各バックエンドを呼び出して認証することが出来ます。

ビューの変更

作成したEmailAuthenticationFormを使用するようにビューを変更しましょう。
accounts/views.py(一部抜粋)


  from .forms import (
      UserInfoChangeForm,
      CustomAuthenticationForm, CustomPasswordChangeForm,
      CustomPasswordResetForm, CustomSetPasswordForm,
      CustomUserChangeForm, CustomUserCreationForm, EmailChangeForm,
+     EmailAuthenticationForm
  )

#一部省略・・・

  class CustomLoginView(LoginView):
-     form_class = CustomAuthenticationForm
+     # form_class = CustomAuthenticationForm
+     form_class = EmailAuthenticationForm

urlに関しては変更ないですが、念の為表示しておきます。
accounts/urls.py(一部抜粋)



urlpatterns = [
    path('login/', views.CustomLoginView.as_view(), name='login'),
    path('logout/', views.CustomLogoutView.as_view(), name='logout'),

    path('password_change/', views.CustomPasswordChangeView.as_view(), name='password_change'),
    path('password_change/done/', views.CustomPasswordChangeDoneView.as_view(), name='password_change_done'),

    path('password_reset/', views.CustomPasswordResetView.as_view(), name='password_reset'),
    path('password_reset/done/', views.CustomPasswordResetDoneView.as_view(), name='password_reset_done'),
    path('reset/<uidb64>/<token>/', views.CustomPasswordResetConfirmView.as_view(), name='password_reset_confirm'),
    path('reset/done/', views.CustomPasswordResetCompleteView.as_view(), name='password_reset_complete'),

    path('create/', views.UserCreateView.as_view(), name="create"),
    path('profile/', views.UserProfileView.as_view(), name="profile"),
    path('change/', views.EmailChangeView.as_view(), name="change"),
]

動作確認

では動作確認をしていきましょう。これまでユーザー名でログインする画面でしたがメールアドレスでログイン出来るようになればOKです。

最後に

Djangoにおける認証の仕組みについて理解が深まれば幸いです。次回はログインユーザーのみが投稿出来るように修正していきます。


3-10. 独自カスタマイズのユーザーを使用する

今回のテーマは「独自カスタマイズのユーザーを使用する」です。ここまではDjango標準のUserを使用してきました。しかし標準のUserを使用すると不都合が多い場合も多いです。ユーザーのカスタマイズ方法を見ていきましょう。

※本ページは「3-9. LogoutViewで作成したログアウト画面をカスタマイズする」まで読まれた方を対象としています。そのためサンプルソースコードが省略されている場合があります。


usernameの説明文とバリデーションの齟齬の問題

既存ユーザーのusernameのバリデーションはUnicodeUserValidatorが使われいてます。usernameの説明文には「半角アルファベット、半角数字、@/./+/-/_ で150文字以下にしてください。」となっています。しかし、実際には漢字やひらがな、カタカナ等の全角文字でもユーザー登録出来てしまいます。これはDjango1.10からpython3のみでusernameでunicodeが許容される仕様に変更されたことによるものです。(参考:リリース1.10)よって既存のユーザーモデルのusernameの説明文は「全角、半角アルファベット、半角数字、@/./+/-/_ で150文字以下にしてください。」が正しいということになります。この問題を解決する方法は二つです。1つは説明文やエラーメッセージを変更する方針。もう一つは説明文に合せて半角英数と@/./+/-/_ のみを許容するバリデーションを使用するバリデータに切り替える方針です。今回はバリデータをASCIIUsernameValidatorに変更します。

ユーザーカスタマイズの2つの方法

ユーザーのカスタマイズには大きく分けて二つの方法があります。1つは既存のユーザーモデルの拡張です。例えばプロフィールモデルなどをユーザーのIDとリレーションさせた1対1関係のモデルを用意することで既存ユーザーにない情報を付与することができます。もう一方は独自のユーザーモデルを構築する方法です。この方法の中でもAbstructUserを継承したユーザーモデルを使用する方法と、AbstructBaseUserを継承したユーザーモデルを構築する方法があります。AbstructBaseUserを継承したモデルのほうがカスタマイズ性が高く融通がききます。今回はAbstructBaseUserを継承した独自ユーザーを作成していきます。

データベースの初期化

独自ユーザーモデルを使用する場合は最初のデータベースマイグレーションの前に行う必要があります。そうしないとユーザーに関連する多くのモデルがDjangoの既存ユーザーモデルとリレーションした状態でデータベースが構築されてしまうからです。ここまで、学習の都合で既存Userモデルを使用してきたので、一度データベースを破棄して新規に作成しましょう。

$ mysql -u root mysql
[mysql] drop database forum_data;
[mysql] create database forum_data;
[mysql] grant all on forum_data.* to 'forum_user'@'localhost';

独自カスタマイズユーザーの実装

今回は独自ユーザーモデルはaccountsアプリケーション内に作成していきます。
accounts/models.py


from django.db import models
from django.contrib.auth.models import PermissionsMixin
from django.contrib.auth.base_user import AbstractBaseUser, BaseUserManager
from django.contrib.auth.validators import UnicodeUsernameValidator, ASCIIUsernameValidator
from django.core.mail import send_mail
from django.utils.translation import gettext_lazy as _
from django.utils import timezone

# Create your models here.

class UserManager(BaseUserManager):
    """
    Create and save user with email
    """
    use_in_migrations = True

    def _create_user(self, username, email, password, **extra_fields):
        """
        Create and save a user with the given username, email, and password.
        """
        if not username:
            raise ValueError('The given username must be set')

        if not email:
            raise ValueError('The given email must be set')

        email = self.normalize_email(email)
        username = self.model.normalize_username(username)
        user = self.model(username=username, email=email, **extra_fields)
        user.set_password(password)
        user.save(using=self._db)
        return user

    def create_user(self, username, email=None, password=None, **extra_fields):
        extra_fields.setdefault('is_staff', False)
        extra_fields.setdefault('is_superuser', False)
        return self._create_user(username, email, password, **extra_fields)

    def create_superuser(self, username, email, password, **extra_fields):
        extra_fields.setdefault('is_staff', True)
        extra_fields.setdefault('is_superuser', True)

        if extra_fields.get('is_staff') is not True:
            raise ValueError('Superuser must have is_staff=True.')
        if extra_fields.get('is_superuser') is not True:
            raise ValueError('Superuser must have is_superuser=True.')

        return self._create_user(username, email, password, **extra_fields)


class User(AbstractBaseUser, PermissionsMixin):
    """
    Django標準のUserをベースにカスタマイズしたUserクラス
    """
    username_validator = UnicodeUsernameValidator()
    # python3で半角英数のみ許容する場合はASCIIUsernameValidatorを用いる
    # username_validator = ASCIIUsernameValidator()

    username = models.CharField(
        _('username'),
        max_length=50,
        unique=True,
        # help_text=_('Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.'),
        help_text='この項目は必須です。全角文字、半角英数字、@/./+/-/_ で50文字以下にしてください。',
        validators=[username_validator],
        error_messages={
            'unique': _("A user with that username already exists."),
        },
    )
    # first_name = models.CharField(_('first name'), max_length=30, blank=True)
    # last_name = models.CharField(_('last name'), max_length=150, blank=True)
    email = models.EmailField(
        _('email address'),
        help_text='この項目は必須です。メールアドレスは公開されません。',
        blank=False
    )
    is_staff = models.BooleanField(
        _('staff status'),
        default=False,
        help_text=_('Designates whether the user can log into this admin site.'),
    )
    is_active = models.BooleanField(
        _('active'),
        default=True,
        help_text=_(
            'Designates whether this user should be treated as active. '
            'Unselect this instead of deleting accounts.'
        ),
    )
    date_joined = models.DateTimeField(_('date joined'), default=timezone.now)

    objects = UserManager()

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

    class Meta:
        verbose_name = _('user')
        verbose_name_plural = _('users')
        # abstract = True
        abstract = False

    def clean(self):
        super().clean()
        self.email = self.__class__.objects.normalize_email(self.email)

    # first_nameとlast_nameに関する部分はコメントアウト
    # def get_full_name(self):
    #     """
    #     Return the first_name plus the last_name, with a space in between.
    #     """
    #     full_name = '%s %s' % (self.first_name, self.last_name)
    #     return full_name.strip()

    # def get_short_name(self):
    #     """Return the short name for the user."""
    #     return self.first_name

    def email_user(self, subject, message, from_email=None, **kwargs):
        """Send an email to this user."""
        send_mail(subject, message, from_email, [self.email], **kwargs)

基本的にはdjango.contrib.auth.models.UserManagerとdjango.contrib.auth.models.AbstructUserのコピーです。ただし今回はfirst_nameとlast_nameフィールドを削ったので、その部分に関しては変更を加えています。また、バリデータに関してはデフォルトのままUnicodeUserValidatorを使用していますが、もしpython3環境で半角英数を使用したい場合はASCIIUsernameValidatorに変更をしましょう。ただヘルプメッセージは実態に即したメッセージに変更しないと整合性が失われるので変更しています。

独自ユーザー用のフォームの作成

独自のユーザーモデルを作成したので各フォームもそれに従って変更します。多くのものはimportを変更するだけで問題ないですが、ユーザー作成、ユーザー情報変更用のフォームは追加が必要になります。

accounts/forms.py


from django.forms import ModelForm
# from django.contrib.auth.models import User
# from django.contrib.auth import get_user_model
from django.contrib.auth.forms import (
    AuthenticationForm, PasswordChangeForm,
    PasswordResetForm, SetPasswordForm,
    UserChangeForm, UserCreationForm
)

from .models import User

# UserModel = get_user_model()

class UserInfoChangeForm(ModelForm):
    class Meta:
        model = User
        fields = [
            'email',
            # 'last_name',
            # 'first_name',
        ]

    def __init__(self, email=None, first_name=None, last_name=None, *args, **kwargs):
        kwargs.setdefault('label_suffix', '')
        super().__init__(*args, **kwargs)
        # ユーザーの更新前情報をフォームに挿入
        if email:
            self.fields['email'].widget.attrs['value'] = email
        if first_name:
            self.fields['first_name'].widget.attrs['value'] = first_name
        if last_name:
            self.fields['last_name'].widget.attrs['value'] = last_name

    def update(self, user):
        user.email = self.cleaned_data['email']
        user.first_name = self.cleaned_data['first_name']
        user.last_name = self.cleaned_data['last_name']
        user.save()

class EmailChangeForm(ModelForm):
    class Meta:
        model = User
        fields = ['email']

    def __init__(self, email=None, *args, **kwargs):
        kwargs.setdefault('label_suffix', '')
        super().__init__(*args, **kwargs)
        # ユーザーの更新前情報をフォームに挿入
        if email:
            self.fields['email'].widget.attrs['value'] = email

    def update(self, user):
        user.email = self.cleaned_data['email']
        user.save()


class CustomAuthenticationForm(AuthenticationForm):
    def __init__(self, *args, **kwargs):
        kwargs.setdefault('label_suffix', '')
        super().__init__(*args, **kwargs)

class CustomPasswordChangeForm(PasswordChangeForm):
    def __init__(self, *args, **kwargs):
        kwargs.setdefault('label_suffix', '')
        super().__init__(*args, **kwargs)

class CustomPasswordResetForm(PasswordResetForm):
    def __init__(self, *args, **kwargs):
        kwargs.setdefault('label_suffix', '')
        super().__init__(*args, **kwargs)

class CustomSetPasswordForm(SetPasswordForm):
    def __init__(self, *args, **kwargs):
        kwargs.setdefault('label_suffix', '')
        super().__init__(*args, **kwargs)

class AdminUserCreationForm(UserCreationForm):
    class Meta:
        model = User
        fields = ('username', 'email')
    
    # def save(self, commit=True):
    #     user = User.objects.create_user(
    #         self.cleaned_data["name"],
    #         self.cleaned_data["email"],
    #         self.cleaned_data["password1"],
    #     )
    #     return user

class CustomUserChangeForm(UserChangeForm):
    class Meta:
        model = User
        fields = '__all__'
        
class CustomUserCreationForm(UserCreationForm):
    class Meta:
        model = User
        fields = ('username', 'email')

    def __init__(self, *args, **kwargs):
        kwargs.setdefault('label_suffix','')
        super().__init__(*args, **kwargs)

少々複雑なので、不要な部分はコメントアウトしています。import部分で先程作成したUserモデルをimportしています。EmailChangeFormはこれまでのUserInfoChangeFormの代わりに使います。ここは大きな問題はないと思います。

ビューの変更

accounts/views.py(一部抜粋)


from django.shortcuts import render, redirect
from django.views.generic import TemplateView, FormView, UpdateView
# from django.contrib.auth.forms import UserCreationForm
# from django.contrib.auth.models import User
from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.auth.views import (
    LoginView, LogoutView, PasswordChangeView, PasswordChangeDoneView,
    PasswordResetView, PasswordResetDoneView, PasswordResetConfirmView, PasswordResetCompleteView
)
from django.urls import reverse_lazy
from django.contrib.auth import login, authenticate

from .models import User
from .forms import (
    UserInfoChangeForm,
    CustomAuthenticationForm, CustomPasswordChangeForm,
    CustomPasswordResetForm, CustomSetPasswordForm,
    CustomUserChangeForm, CustomUserCreationForm, EmailChangeForm
)

# Create your views here.

class UserCreateView(FormView):
    # form_class = UserCreationForm
    form_class = CustomUserCreationForm
    template_name = 'registration/create.html'
    success_url = reverse_lazy('accounts:profile')
    def form_valid(self, form):
        print(self.request.POST['next'])
        if self.request.POST['next'] == 'back':
            return render(self.request, 'registration/create.html', {'form': form})
        elif self.request.POST['next'] == 'confirm':
            return render(self.request, 'registration/create_confirm.html', {'form': form})
        elif self.request.POST['next'] == 'regist':
            form.save()
            # 認証
            user = authenticate(
                username=form.cleaned_data['username'],
                password=form.cleaned_data['password1'],
            )
            # ログイン
            login(self.request, user)
            return super().form_valid(form)
        else:
            # 通常このルートは通らない
            return redirect(reverse_lazy('base:top'))

class UserProfileView(LoginRequiredMixin, TemplateView):
    template_name = 'registration/profile.html'
    def get_queryset(self):
        return User.objects.get(id=self.request.user.id)

class EmailChangeView(LoginRequiredMixin, FormView):
    template_name = 'registration/change.html'
    form_class = EmailChangeForm
    success_url = reverse_lazy('accounts:profile')
    
    def form_valid(self, form):
        #formのupdateメソッドにログインユーザーを渡して更新
        form.update(user=self.request.user)
        return super().form_valid(form)

    def get_form_kwargs(self):
        kwargs = super().get_form_kwargs()
        # 更新前のユーザー情報をkwargsとして渡す
        kwargs.update({
            'email' : self.request.user.email,
        })
        return kwargs

class UserChangeView(LoginRequiredMixin, FormView):
    '''
    Django組込みのUserを利用する場合のユーザー情報変更ビュー
    カスタムユーザーでは使用しない
    '''
    template_name = 'registration/change.html'
    form_class = UserInfoChangeForm
    success_url = reverse_lazy('accounts:profile')
    
    def form_valid(self, form):
        #formのupdateメソッドにログインユーザーを渡して更新
        form.update(user=self.request.user)
        return super().form_valid(form)

    def get_form_kwargs(self):
        kwargs = super().get_form_kwargs()
        # 更新前のユーザー情報をkwargsとして渡す
        kwargs.update({
            'email' : self.request.user.email,
            'first_name' : self.request.user.first_name,
            'last_name' : self.request.user.last_name,
        })
        return kwargs

class CustomLoginView(LoginView):
    form_class = CustomAuthenticationForm

class CustomLogoutView(LogoutView):
    template_name = 'registration/logged_out.html'
    next_page = '/'

class CustomPasswordChangeView(PasswordChangeView):
    form_class = CustomPasswordChangeForm
    template_name = 'registration/password_change_form.html'
    success_url = reverse_lazy('accounts:password_change_done')

class CustomPasswordChangeDoneView(PasswordChangeDoneView):
    template_name = 'registration/password_change_done.html'

class CustomPasswordResetView(PasswordResetView):
    email_template_name = 'registration/password_reset_email.html'
    form_class = CustomPasswordResetForm
    from_email = 'info@example.com'
    subject_template_name = 'registration/password_reset_subject.txt'
    success_url = reverse_lazy('accounts:password_reset_done')
    template_name = 'registration/password_reset_form.html'

class CustomPasswordResetDoneView(PasswordResetDoneView):
    template_name = 'registration/password_reset_done.html'

class CustomPasswordResetConfirmView(PasswordResetConfirmView):
    form_class = CustomSetPasswordForm
    post_reset_login = False
    post_reset_login_backend = None
    success_url = reverse_lazy('accounts:password_reset_complete')
    template_name = 'registration/password_reset_confirm.html'

class CustomPasswordResetCompleteView(PasswordResetCompleteView):
    template_name = 'registration/password_reset_complete.html'

ビューに関しては基本的にはUserモデルのインポートとフォームを変更するだけで問題ありません。ただしEmailChangeViewは新設しています。

テンプレートの変更

ユーザーからfirst_nameとlast_nameを削除したため、テンプレートにも多少修正が必要です。以下変更したテンプレートのみ表示します。

templates/accounts/profile.html(一部抜粋)



{% extends 'base/base.html' %}
{% block title %}ITについて切磋琢磨する掲示板 - {{ block.super }}{% endblock %}
{% block content %}
<div class="ui grid stackable">
    <div class="eleven wide column">
        <div class="ui breadcrumb">
            <a href="{% url 'base:top' %}" class="section">TOP</a>
            <i class="right angle icon divider"></i>
            <a class="active section">ユーザー情報</a>
        </div>
        <div class="ui segment">
            <div class="content">
                <div class="header"><h3>ユーザー情報</h3></div>
                <div class="ui divided bulleted list">
                    <div class="item">ログインID:{{user.username}}</div>
                    <div class="item">E-mail:{% if user.email %}{{user.email}}{% else %}未設定{% endif %}</div>
                    <!-- <div class="item">名字:{% if user.last_name %}{{user.last_name}}{% else %}未設定{% endif %}</div>
                    <div class="item">名前:{% if user.first_name %}{{user.first_name}}{% else %}未設定{% endif %}</div> -->
                </div>
            </div>
        </div>
        <a class="ui button" href="{% url 'accounts:change' %}">登録情報変更</a>
        <a class="ui button" href="{% url 'accounts:password_change' %}">パスワード変更</a>
    </div>
    {% include 'base/sidebar.html' %}
</div>
{% endblock %}

templates/accounts/create_confirm.html(一部抜粋)



{% extends 'base/base.html' %}
{% block title %}ITについて切磋琢磨する掲示板 - {{ block.super }}{% endblock %}
{% block content %}
<div class="ui grid stackable">
    <div class="eleven wide column">
        <div class="ui breadcrumb">
            <a href="{% url 'base:top' %}" class="section">TOP</a>
            <i class="right angle icon divider"></i>
            <a class="active section">ユーザー作成</a>
        </div>
        <div class="ui segment">
            <div class="content">
                <div class="header"><h3>ユーザー作成</h3></div>
                <table  class="ui celled table table table-hover" >
                    <tr><td>お名前</td><td>{{form.cleaned_data.username}}</td></tr>
                    <tr><td>メールアドレス</td><td>{{form.cleaned_data.email}}</td></tr>
                    <tr><td>パスワード</td><td>********</td></tr>
                </table>
                <form class="ui form" action="" method="POST">
                    {% csrf_token %}
                    {% for field in form %}
                    {{field.as_hidden}}
                    {% endfor %}
                    <button class="ui button" name="next" value="back" type="submit">修正</button>
                    <button class="ui orange button" name="next" value="regist" type="submit">登録</button>
                </form>
            </div>
        </div>
    </div>
    {% include 'base/sidebar.html' %}
</div>
{% endblock %}

独自ユーザーを管理画面で表示する設定

独自に作成したユーザーは登録しないと管理画面で表示・修正することは出来ません。accounts/admin.pyに追記します。

accounts/admin.py


from django.contrib import admin
from django.contrib.auth.admin import UserAdmin
from django.utils.translation import gettext_lazy as _

from .models import User
from .forms import AdminUserCreationForm, CustomUserChangeForm

class CustomUserAdmin(UserAdmin):
    fieldsets = (
        (None, {'fields': ('username', 'password')}),
        (_('Personal info'), {'fields': ('email',)}),
        (_('Permissions'), {'fields': ('is_active', 'is_staff', 'is_superuser', 'groups', 'user_permissions')}),
        (_('Important dates'), {'fields': ('last_login', 'date_joined')}),
                                                )
    add_fieldsets = ((None, {'classes': ('wide',), 'fields': ('username', 'email', 'password1', 'password2'), }),)
    form = CustomUserChangeForm
    add_form = AdminUserCreationForm
    list_display = ('username', 'email','is_staff')
    search_fields = ('username', 'email')
    ordering = ('username',)

admin.site.register(User, CustomUserAdmin)

ここも基本的にはdjango.contrib.auth.admin.UserAdminを継承したクラスを作成してパラメータを上書きしただけです。特にformとadd_formに作成したフォームを指定することを忘れないで下さい。add_formはUserモデルを作成するためのフォームです。間違えないようにしましょう。

URL設定

ビューに変更を加えているのでURL設定も変更しましょう。

accounts/urls.py


from django.urls import path, include
from django.contrib.auth import views as av
from . import views
from .forms import (
    CustomAuthenticationForm, CustomPasswordChangeForm
)

app_name = 'accounts'

urlpatterns = [
    # path('', include('django.contrib.auth.urls')),
    # copy from django.contrib.auth.urls.py
    # path('login/', av.LoginView.as_view(form_class=CustomAuthenticationForm
    #                                     ), name='login'),
    path('login/', views.CustomLoginView.as_view(), name='login'),
    path('logout/', views.CustomLogoutView.as_view(), name='logout'),
    # path('logout/', av.LogoutView.as_view(
    #     template_name='regstration/logged_out.html',
    #     next_page='/'
    # ), name='logout'),

    # path('password_change/', av.PasswordChangeView.as_view(
    #     form_class=CustomPasswordChangeForm,
    #     template_name='registration/password_change_form.html',
    #     success_url='accounts/password_change/done/'
    # ), name='password_change'),
    # path('password_change/done/', av.PasswordChangeDoneView.as_view(
    #     template_name = 'registration/password_change_done.html',
    # ), name='password_change_done'),

    path('password_change/', views.CustomPasswordChangeView.as_view(), name='password_change'),
    path('password_change/done/', views.CustomPasswordChangeDoneView.as_view(), name='password_change_done'),
    

    # path('password_reset/', av.PasswordResetView.as_view(), name='password_reset'),
    # path('password_reset/done/', av.PasswordResetDoneView.as_view(), name='password_reset_done'),
    # path('reset///', av.PasswordResetConfirmView.as_view(), name='password_reset_confirm'),
    # path('reset/done/', av.PasswordResetCompleteView.as_view(), name='password_reset_complete'),

    path('password_reset/', views.CustomPasswordResetView.as_view(), name='password_reset'),
    path('password_reset/done/', views.CustomPasswordResetDoneView.as_view(), name='password_reset_done'),
    path('reset///', views.CustomPasswordResetConfirmView.as_view(), name='password_reset_confirm'),
    path('reset/done/', views.CustomPasswordResetCompleteView.as_view(), name='password_reset_complete'),


    path('create/', views.UserCreateView.as_view(), name="create"),
    path('profile/', views.UserProfileView.as_view(), name="profile"),
    path('change/', views.EmailChangeView.as_view(), name="change"),
]

基本的にはEmailChangeViewを設定するだけです。

独自ユーザーを使用する設定

独自ユーザーを使用するためにはAUTH_USER_MODELを指定する必要があります。この値はデフォルトでは’auth.User’となっているのですが、この値を{アプリケーション名}.{ユーザーモデル名}に変更します。
mysite/settings.py(一部抜粋)


+ AUTH_USER_MODEL = 'accounts.User'

カスタムユーザーモデルをThreadアプリのモデルに適用する

ここまで学習のためThreadアプリのモデルにはDjangoのユーザーを適用させてきました。カスタムユーザーを作成したのでThreadモデルにもカスタムユーザーを紐付ける修正をしましょう。
thread/models.py(一部抜粋)

from accounts.models import User
・・・省略・・・

class Topic(models.Model):
    ・・・省略・・・
    user = models.ForeignKey(
        User,
        on_delete=models.SET_NULL,
        null=True,
        blank=True,
    )
    email = models.EmailField(
        verbose_name='メールアドレス',
        null=True,
        blank=True,
    )
    ・・・省略・・・

class Comment(models.Model):
    ・・・省略・・・
    user = models.ForeignKey(
        User,
        on_delete=models.SET_NULL,
        null=True,
        blank=True,
    )
    email = models.EmailField(
        verbose_name='メールアドレス',
        null=True,
        blank=True,
    )
    ・・・省略・・・

データベースマイグレーション

では、新規作成したデータベースにマイグレーションを行いましょう。以下コマンドを入力してマイグレーションを行います。


$ ./manage.py makemigrations
$ ./manage.py migrate

動作確認

では動作確認をしていきましょう。

ユーザー登録画面

ユーザープロフィール画面

ユーザー情報変更画面

最後に

今回はほとんど、Django既存ユーザーのコピーを追加した感じでしたが、このユーザーをベースにより自分好みのモデルにカスタマイズしていくことが出来ます。次回はログイン方式を変更していきましょう。

Sponsored Link


3-6. ユーザー情報変更画面を作成する

今回のテーマは「ユーザー情報変更画面を作成する」です。ここまでログイン、ログアウト、パスワード変更機能を見てきました。このままパスワードリセット機能を実装したいところですが、ユーザーにEmailを登録する必要がありますので、一度標準のビューから離れてユーザー情報変更画面を作成しましょう。

※本ページは「3-5. PasswordChangeViewを使用してパスワード変更画面を作成する」まで読まれた方を対象としています。そのためサンプルソースコードが省略されている場合があります


ユーザー情報変更画面のフォーム作成

まずはフォームを作成しましょう。accounts/forms.pyを作成します。

accounts/forms.py


from django.forms import ModelForm
from django.contrib.auth.models import User

class UserChangeForm(ModelForm):
    class Meta:
        model = User
        fields = [
            'email',
            'last_name',
            'first_name',
        ]

    def __init__(self, email=None, first_name=None, last_name=None, *args, **kwargs):
        kwargs.setdefault('label_suffix', '')
        super().__init__(*args, **kwargs)
        # ユーザーの更新前情報をフォームに挿入
        if email:
            self.fields['email'].widget.attrs['value'] = email
        if first_name:
            self.fields['first_name'].widget.attrs['value'] = first_name
        if last_name:
            self.fields['last_name'].widget.attrs['value'] = last_name

    def update(self, user):
        user.email = self.cleaned_data['email']
        user.first_name = self.cleaned_data['first_name']
        user.last_name = self.cleaned_data['last_name']
        user.save()

UserChangeFormのインスタンス生成時にユーザー情報を渡してフォームに予め表示するよう__init__関数をオーバーライドしています。また、ユーザー情報更新用の関数としてupdate関数を定義しています。このupdate関数はModelFormの関数ではないことに注意して下さい。

ユーザー情報変更画面のビュー作成

では次にビューを作成していきましょう。

accounts/views.py(一部抜粋)


from django.views.generic import FormView

class UserChangeView(LoginRequiredMixin, FormView):
    template_name = 'registration/change.html'
    form_class = UserChangeForm
    success_url = reverse_lazy('accounts:profile')
    
    def form_valid(self, form):
        #formのupdateメソッドにログインユーザーを渡して更新
        form.update(user=self.request.user)
        return super().form_valid(form)

    def get_form_kwargs(self):
        kwargs = super().get_form_kwargs()
        # 更新前のユーザー情報をkwargsとして渡す
        kwargs.update({
            'email' : self.request.user.email,
            'first_name' : self.request.user.first_name,
            'last_name' : self.request.user.last_name,
        })
        return kwargs

普通のFormViewの使い方なので特段解説は不要かと思いますが、get_form_kwargs関数をオーバーライドしている部分に触れておきます。get_form_kwargs関数はdjango.views.generic.edit.FormMixinクラスの関数です。この関数が返すdict型オブジェクトがフォームクラス生成時にコンストラクタの実引数として投入されます。今回のケースですとUserChangeFormの__init__関数に渡されることになります。これにより現在のユーザー情報をフォームに表示することが可能となります。

get_form_kwargs関数をオーバーライドするとフォームにパラメータを渡すことができて便利なので覚えておくとフォームの使いみちが広がるかも知れません。

ユーザー情報変更画面のテンプレート作成



{% extends 'base/base.html' %}
{% block title %}ITについて切磋琢磨する掲示板 - {{ block.super }}{% endblock %}
{% block content %}
<div class="ui grid stackable">
    <div class="eleven wide column">
        <div class="ui breadcrumb">
            <a href="{% url 'base:top' %}" class="section">TOP</a>
            <i class="right angle icon divider"></i>
            <a href="{% url 'accounts:profile' %}" class="section">プロフィール</a>
            <i class="right angle icon divider"></i>
            <div class="active section">ユーザー情報の変更</div>
        </div>
        <div class="ui segment">
            <div class="content">
                <div class="header"><h3>ユーザー情報の変更</h3></div>
                <form class="ui form" action="" method="POST">
                    {% csrf_token %}
                    {{form.as_p}}
                    <button class="ui orange button" type="submit">確認</button>
                </form>
            </div>
        </div>
        <a href="{% url 'accounts:profile' %}">プロフィールに戻る</a>
    </div>
    {% include 'base/sidebar.html' %}
</div>
{% endblock %}

formを表示するだけなので、特段解説は不要だと思います。手を抜いてform.as_pで表示しています。

URLの設定

続いてURLの設定を行います。
accounts/urls.py(一部抜粋)


  urlpatterns = [
      # path('', include('django.contrib.auth.urls')),
      path('create/', views.UserCreateView.as_view(), name="create"),
      path('profile/', views.UserProfileView.as_view(), name="profile"),
+     path('change/', views.UserChangeView.as_view(), name="change"),
  ]

プロフィール画面のリンク修正

プロフィール画面のリンクも修正しておきましょう。



- <a class="ui button" href="">登録情報変更</a>
+ <a class="ui button" href="{% url 'accounts:change' %}">登録情報変更</a>

動作確認

では動作確認をしましょう。プロフィール画面から「登録情報変更」を押してユーザー情報変更画面に遷移して、フォームからユーザー情報を変更できればOKです。

最後に

では、次回はパスワードを忘れたユーザーのためのパスワードリセット画面について見ていきたいと思います。

Sponsored Link


3-9. LogoutViewで作成したログアウト画面をカスタマイズする

今回のテーマは「ログアウト画面をカスタマイズする」です。ここまではログイン画面と同様にログアウト画面も修正していきます。今回もLogoutViewを使用するケースとLogoutViewを継承するクラスを作成するケースとで分けたいと思います。

※本ページは「LoginViewで作成したログイン画面をカスタマイズする」まで読まれた方を対象としています。そのためサンプルソースコードが省略されている場合があります。


URLのカスタマイズ

もしURLが修正されていない場合はLoginViewで作成したログイン画面をカスタマイズするの「URLのカスタマイズ」を行って下さい。

ケース1:LooutViewを使用してas_view()でプロパティを渡す

このケースではビューを自作する必要はありません。as_view関数でLogoutViewのクラス変数を書き換える方法です。

accounts/urls.py(一部抜粋)


  urlpatterns = [
      # copy from django.contrib.auth.urls.py
      path('login/', views.CustomLoginView.as_view(), name='login'),
-     path('logout/', av.LogoutView.as_view(), name='logout'),
+     path('logout/', av.LogoutView.as_view(
+         template_name='regstration/logged_out.html',
+         next_page='/'
+     ), name='logout'),
  
      path('password_change/', av.PasswordChangeView.as_view(), name='password_change'),
      path('password_change/done/', av.PasswordChangeDoneView.as_view(), name='password_change_done'),
  
      path('password_reset/', av.PasswordResetView.as_view(), name='password_reset'),
      path('password_reset/done/', av.PasswordResetDoneView.as_view(), name='password_reset_done'),
      path('reset///', av.PasswordResetConfirmView.as_view(), name='password_reset_confirm'),
      path('reset/done/', av.PasswordResetCompleteView.as_view(), name='password_reset_complete'),
  
      path('create/', views.UserCreateView.as_view(), name="create"),
      path('profile/', views.UserProfileView.as_view(), name="profile"),
      path('change/', views.UserChangeView.as_view(), name="change"),
  ]

今回はform_classのみ書き換えましたがtemplate_name等も書き換え可能です。LoginViewの詳細は「」をご覧ください。

ケース2:LogoutViewを継承するビュークラスを作る

次にLoginViewを継承して新しいビュークラスを作る場合を考えましょう。as_view関数でプロパティを書き換える方法よりも応用の効く方法です。accounts/views.pyに書き加えていきます。

accounts/views.py(一部抜粋)


# importはページトップ
- from django.contrib.auth.views import LoginView
+ from django.contrib.auth.views import LoginView, LogoutView

+ class CustomLogoutView(LogoutView):
+     template_name = 'registration/logged_out.html'
+     next_page = '/'

accounts/urlsも書き換えます。
accounts/urls.py(一部抜粋)


  urlpatterns = [
      # copy from django.contrib.auth.urls.py
      path('login/', views.CustomLoginView.as_view(), name='login'),
-     path('logout/', av.LogoutView.as_view(
-         template_name='regstration/logged_out.html',
-         next_page='/'
-     ), name='logout'),
+     path('logout/', views.CustomLogoutView.as_view(), name='logout'),
  
      path('password_change/', av.PasswordChangeView.as_view(), name='password_change'),
      path('password_change/done/', av.PasswordChangeDoneView.as_view(), name='password_change_done'),
  
      path('password_reset/', av.PasswordResetView.as_view(), name='password_reset'),
      path('password_reset/done/', av.PasswordResetDoneView.as_view(), name='password_reset_done'),
      path('reset///', av.PasswordResetConfirmView.as_view(), name='password_reset_confirm'),
      path('reset/done/', av.PasswordResetCompleteView.as_view(), name='password_reset_complete'),
  
      path('create/', views.UserCreateView.as_view(), name="create"),
      path('profile/', views.UserProfileView.as_view(), name="profile"),
      path('change/', views.UserChangeView.as_view(), name="change"),
  ]

ログアウト後の遷移先を変更する

ログアウト後の遷移先はLogoutViewのnext_page変数を指定するかsettigs.pyのLOGOUT_REDIRECT_URLを指定することで設定出来ます。また、GETもしくはPOSTメソッドで’next’キーのパラメータを指定しても遷移します。優先順位としては

  1. GETもしくはPOSTで指定したnextパラメータ
  2. next_page
  3. LOGOUT_REDIRECT_URL

の順番となります。

最後に

ログアウト画面に関してはフォームもないので作業量は少ないです。カスタマイズがすくないのでLogoutViewをそのまま用いても支障は少ないと思います。次回はパスワード変更画面をカスタマイズしていきます。

Sponsored Link


3-8. LoginViewで作成したログイン画面をカスタマイズする

今回のテーマは「LoginViewで作成したログイン画面をカスタマイズする」です。ここまではDjangoに組み込まれたビューを使用して認証機能を実装してきましたが、ここからはカスタマイズを加えていきます。LoginViewをベースとしたカスタマイズをすることでフォームに手を加えたり、テンプレート名を変更するなど自由なカスタマイズが出来るようになります。

※本ページは「PasswordResetViewを使用してパスワードリセット画面を作成する」まで読まれた方を対象としています。そのためサンプルソースコードが省略されている場合があります。


URLのカスタマイズ

ここまではdjango.contrib.auth.urlsのurlpatternsをincludeしただけでした。これではカスタマイズは難しいのでaccounts/urls.pyにURLを設定し直しましょう。

mysite/urls.py(一部抜粋)


  urlpatterns = [
      path('admin/', admin.site.urls),
-     path('accounts/', include('django.contrib.auth.urls')),
      path('accounts/', include('accounts.urls')),
      path('', include('base.urls')),
      path('thread/', include('thread.urls')),
      path('api/', include('api.urls')),
      path('search/', include('search.urls')),
      path('sitemap.xml', sitemap, {'sitemaps': sitemaps}),
  ] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

accounts/urls.py


from django.urls import path, include
from django.contrib.auth import views as av
from . import views

app_name = 'accounts'

urlpatterns = [
    # copy from django.contrib.auth.urls.py
    path('login/', av.LoginView.as_view(), name='login'),
    path('logout/', av.LogoutView.as_view(), name='logout'),

    path('password_change/', av.PasswordChangeView.as_view(), name='password_change'),
    path('password_change/done/', av.PasswordChangeDoneView.as_view(), name='password_change_done'),

    path('password_reset/', av.PasswordResetView.as_view(), name='password_reset'),
    path('password_reset/done/', av.PasswordResetDoneView.as_view(), name='password_reset_done'),
    path('reset///', av.PasswordResetConfirmView.as_view(), name='password_reset_confirm'),
    path('reset/done/', av.PasswordResetCompleteView.as_view(), name='password_reset_complete'),

    path('create/', views.UserCreateView.as_view(), name="create"),
    path('profile/', views.UserProfileView.as_view(), name="profile"),
    path('change/', views.UserChangeView.as_view(), name="change"),
]

avはauth_viewsの頭文字をとった省略形です。この時点では前回まで実装した内容とほとんど変わりありません。これでログイン、ログアウト、パスワード変更画面等、別々に設定可能な準備が整いました。

URLショートコードの修正

以降、URLのショートコードには’accounts’が付くことに注意して下さい。例えばログイン画面へのリダイレクトはredirect(reverse_lazy(‘accounts:login’))となります。テンプレートのurlの表記も全て変わりますので注意してくだい。

templates/base/base.html(一部抜粋)



  <div class="right menu">
      {% if user.is_authenticated %}
      <a class="item" href="{% url 'accounts:profile' %}">ユーザー情報</a>
-     <a class="item" href="{% url 'logout' %}">ログアウト</a>
+     <a class="item" href="{% url 'accounts:logout' %}">ログアウト</a>
      {% else %}
-     <a class="item" href="{% url 'login' %}">ログイン</a>
+     <a class="item" href="{% url 'accounts:login' %}">ログイン</a>
      <a class="item" href="{% url 'accounts:create' %}">ユーザー登録</a>
      {% endif %}
  </div>

templates/registration/login.html(一部抜粋)



  <a class="ui item" href="{% url 'accounts:create' %}">ユーザー登録</a>/
- <a class="ui item" href="{% url 'password_reset' %}">パスワードを忘れた場合</a>
+ <a class="ui item" href="{% url 'accounts:password_reset' %}">パスワードを忘れた場合</a>

templates/registration/profile.html



  <a class="ui button" href="{% url 'accounts:change' %}">登録情報変更</a>
- <a class="ui button" href="{% url 'password_change' %}">パスワード変更</a>
+ <a class="ui button" href="{% url 'accounts:password_change' %}">パスワード変更</a>

カスタムフォームの作成

LoginViewはデフォルトでAuthenticationFormを使っていますが、これでは少々融通が利かないので自分でカスタマイズ可能なフォームクラスを作成しておきましょう。

accounts/forms.py(一部抜粋)


+ from django.contrib.auth.forms import AuthenticationForm
+ 
+ class CustomAuthenticationForm(AuthenticationForm):
+     def __init__(self, *args, **kwargs):
+         kwargs.setdefault('label_suffix', '')
+         super().__init__(*args, **kwargs)

もしフォームにclassを付与したいなどの場合はここで追加します。

ケース1:LoginViewを使用してas_view()でプロパティを渡す

このケースではビューを自作する必要はありません。as_view関数でLoginViewのクラス変数を書き換える方法です。

accounts/urls.py


+ from .forms import CustomAuthenticationForm

- path('login/', av.LoginView.as_view(), name='login')
+ path('login/', av.LoginView.as_view(form_class=CustomAuthenticationForm
                                      ), name='login'),
  path('logout/', av.LogoutView.as_view(), name='logout'),

今回はform_classのみ書き換えましたがtemplate_name等も書き換え可能です。LoginViewの詳細は「」をご覧ください。

ケース2:LoginViewを継承するビュークラスを作る

次にLoginViewを継承して新しいビュークラスを作る場合を考えましょう。as_view関数でプロパティを書き換える方法よりも応用の効く方法です。accounts/views.pyに書き加えていきます。

accounts/views.py(一部抜粋)


#importは行頭に追加
+ from .forms import UserChangeForm, CustomAuthenticationForm

+ class CustomLoginView(LoginView):
+     form_class = CustomAuthenticationForm

accounts/urlsも書き換えます。
accounts/urls.py(一部抜粋)


- path('login/', av.LoginView.as_view(form_class=CustomAuthenticationForm
-                                     ), name='login'),
+ path('login/', views.CustomLoginView.as_view(), name='login'),

ログイン後の遷移先を変更する

ログイン後の遷移については以前触れましたが、改めて説明しておきます。ログイン後の遷移先URLは’/accounts/profile/’が指定されています。これを変更するにはmysite/settings.pyでLOGIN_REDIRECT_URLを指定することで設定出来ます。例えば、ログイン後にトップページに遷移したい場合は

mysite/settings.py(一部抜粋)


+ LOGIN_REDIRECT_URL = '/'

のように追加すればOKです。

最後に

カスタマイズと言ってもさほど大変なことはありません。Djangoの予めある機能を活かしてカスタマイズをしていきましょう。次回はログアウト画面をカスタマイズしていきます。

Sponsored Link


3-7. PasswordResetViewを使用してパスワードリセット画面を作成する

今回のテーマは「PasswordResetViewを使用してパスワードリセット画面を作成する」です。パスワードリセットとはパスワードを忘れた場合に再発行する手続きする画面のことを言います。Djangoにはこの機能用の標準ビューがあります。今回はカスタマイズはせず、標準ビューをそのまま使っていきます。

※本ページは「PasswordChangeViewを使用してパスワード変更画面を作成する」まで読まれた方を対象としています。そのためサンプルソースコードが省略されている場合があります。


パスワードリセットの仕組み
パスワードリセットに関しては4つのビューが絡み合うので少々ややこしいです。まずは概要を抑えましょう。パスワードリセットの概念図がコチラです。

この概念を理解した上で実装すると戸惑わないと思います。それでは標準ビューに4つについてそれぞれ説明していきます。

PasswordResetViewについて

パスワードリセットを開始するため、メールを発信するビューです。django.contrib.auth.views.PasswordResetViewには以下のクラス変数があり、この値を継承クラスでオーバライドしたり、as_viewメソッドで設定することで制御することが出来ます。

  • email_template_name: Emailテンプレート。デフォルトは’registration/password_reset_email.html’
  • extra_email_context: Eメールテンプレートに渡す追加コンテキスト。デフォルトはNone
  • form_class: デフォルトはPasswordResetForm
  • from_email: Emeilのfromアドレス。デフォルトはNone
  • html_email_template_name = HTML用のEmailテンプレート。デフォルトはNone
  • subject_template_name: メールタイトル用テンプレート。デフォルトは ‘registration/password_reset_subject.txt’
  • success_url: デフォルトはreverse_lazy(‘password_reset_done’)
  • template_name: デフォルトは’registration/password_reset_form.html’
  • title = デフォルトは’Password reset’の翻訳
  • token_generator = デフォルトはdefault_token_generator

今回はすべてデフォルトの状態で使ってみます。認証機能のカスタマイズは本章の後半で扱うので、それまでお待ち下さい。

PasswordResetDoneViewについて

メール送信後はPasswordResetDoneViewが表示されます。django.contrib.auth.views.PasswordResetDoneViewには以下のクラス変数があり、この値を継承クラスでオーバライドしたり、as_viewメソッドで設定することで制御することが出来ます。

  • template_name: デフォルトは’registration/password_reset_done.html’
  • title: デフォルトは’Password reset sent’の翻訳(言語がjaの場合は日本語訳)

このビューはメール送信したことをユーザーに伝えるだけの画面であり、ほぼ機能はTemplateViewと同様と考えればいいと思います。

PasswordResetConfirmViewについて

送信されたメールに記載されているURLからアクセスした場合にPasswordResetConfirmViewが表示されます。django.contrib.auth.views.PasswordResetConfirmViewには以下のクラス変数があり、この値を継承クラスでオーバライドしたり、as_viewメソッドで設定することで制御することが出来ます。

  • form_class: デフォルトはSetPasswordForm
  • post_reset_login: デフォルトはFalse
  • post_reset_login_backend: デフォルトはNone
  • success_url: デフォルトはreverse_lazy(‘password_reset_complete’)
  • template_name: デフォルトは’registration/password_reset_confirm.html’
  • title: デフォルトは’Enter new password’の翻訳
  • token_generator: デフォルトはdefault_token_generator

このビューに関しても今回はカスタマイズせずそのまま使います。post_reset_loginについて簡単に解説しておきます。この値は新パスワードを登録した時にログインをするかどうかです。この値はデフォルトではFalseですが、Trueにしておくと新パスワードでログインしリダイレクトします。

PasswordResetCompleteViewについて

パスワードのリセット処理が無事に終了した場合にPasswordResetCompleteViewが表示されます。django.contrib.auth.views.PasswordResetCompleteViewには以下のクラス変数があり、この値を継承クラスでオーバライドしたり、as_viewメソッドで設定することで制御することが出来ます。

  • template_name: デフォルトは’registration/password_reset_complete.html’
  • title: デフォルトは’Password reset complete’の翻訳

このビューはあくまでリセットが完了したことを伝える画面ですので、PasswordResetConfirmViewでsuccess_urlを別画面に指定した場合はなくても問題ありません。

パスワードリセット画面用のテンプレート作成

ではパスワードリセット画面用のテンプレートを作成していきます。django.contrib.auth.views.PasswordChangeViewのデフォルトテンプレートは’registration/password_change_form.html’ですので、その名前で作成していきます。

templates/registration/password_reset_form.html



{% extends 'base/base.html' %}
{% block title %}ITについて切磋琢磨する掲示板 - {{ block.super }}{% endblock %}
{% block content %}
<div class="ui grid stackable">
    <div class="eleven wide column">
        <div class="ui breadcrumb">
            <a class="section" href="{% url 'base:top' %}">TOP</a>
            <i class="right angle icon divider"></i>
            <div class="active section">パスワードリセット</div>
        </div>
        <div class="ui segment">
            <div class="content">
                <div class="header"><h3>パスワードリセット</h3></div>
                <form class="ui form" method="POST" action="">
                    {% csrf_token %}
                    {{form.as_p}}
                    <button type="submit" class="ui orange button">メールを送信する</button>
                </form>
            </div>
        </div>
    </div>
    {% include 'base/sidebar.html' %}
</div>
{% endblock %}

次にPasswordResetDoneViewのテンプレートを準備していきます。
templates/registration/password_reset_done.html



{% extends 'base/base.html' %}
{% block title %}ITについて切磋琢磨する掲示板 - {{ block.super }}{% endblock %}
{% block content %}
<div class="ui grid stackable">
    <div class="eleven wide column">
        <div class="ui breadcrumb">
            <a class="section" href="{% url 'base:top' %}">TOP</a>
            <i class="right angle icon divider"></i>
            <div class="active section">パスワード変更</div>
        </div>
        <div class="ui segment">
            <div class="content">
                <div class="content">
                    <div class="header"><h3>パスワードリセット</h3></div>
                    <p>パスワードリセット手続きのためメールを送信しました。メールを確認の上、リセット手続きを行って下さい。</p>
                </div>
            </div>
        </div>
    </div>
    {% include 'base/sidebar.html' %}
</div>
{% endblock %}

次にPasswordResetConfirmView用のテンプレートを作成していきます。
templates/registration/password_reset_confirm.html



{% extends 'base/base.html' %}
{% block title %}ITについて切磋琢磨する掲示板 - {{ block.super }}{% endblock %}
{% block content %}
<div class="ui grid stackable">
    <div class="eleven wide column">
        <div class="ui breadcrumb">
            <a class="section" href="{% url 'base:top' %}">TOP</a>
            <i class="right angle icon divider"></i>
            <div class="active section">パスワードリセット</div>
        </div>
        <div class="ui segment">
            <div class="content">
                <div class="header"><h3>パスワードリセット</h3></div>
                <form class="ui form" method="POST" action="">
                    {% csrf_token %}
                    {{form.as_p}}
                    <button type="submit" class="ui orange button">変更する</button>
                </form>
            </div>
        </div>
    </div>
    {% include 'base/sidebar.html' %}
</div>
{% endblock %}

次にPasswordResetConfirmView用のテンプレートを作成していきます。
templates/registration/password_reset_complete.html



{% extends 'base/base.html' %}
{% block title %}ITについて切磋琢磨する掲示板 - {{ block.super }}{% endblock %}
{% block content %}
<div class="ui grid stackable">
    <div class="eleven wide column">
        <div class="ui breadcrumb">
            <a class="section" href="{% url 'base:top' %}">TOP</a>
            <i class="right angle icon divider"></i>
            <div class="active section">パスワードリセット完了</div>
        </div>
        <div class="ui segment">
            <div class="content">
                <div class="content">
                    <div class="header"><h3>パスワードリセットが完了しました</h3></div>
                    <a href="{% url 'base:top' %}">TOPに戻る</a>
                </div>
            </div>
        </div>
    </div>
    {% include 'base/sidebar.html' %}
</div>
{% endblock %}

URLリンクの修正

ログイン画面のリンクも修正しておきましょう。
templates/registration/login.html(一部抜粋)



  <a class="ui item" href="{% url 'accounts:create' %}">ユーザー登録</a>/
- <a class="ui item" href="">パスワードを忘れた場合</a>
+ <a class="ui item" href="{% url 'password_reset' %}">パスワードを忘れた場合</a>

urlのショートカットが’password_reset’であることに注意して下さい。

動作確認

では動作を確認してみましょう。プロフィール画面から「パスワード変更」を押してパスワード変更画面を表示パスワードを変更してみます。パスワード変更成功の画面が出ればOKです。念の為パスワードが変更されているか再度ログインして確かめてみましょう。

※動作確認前に必ずLoginViewを使用してログイン画面を作成するで紹介した要領でmysite/urls.pyの変更をしておいて下さい。

パスワードリセット開始

メール送信済みの画面表示

送信されたメールを確認

送信されたメールからパスワードリセット画面にアクセスしパスワード変更

パスワードリセット完了画面

メールテンプレートの作成

さて、送信されたメールの文言どうでしょうか?ちょっとこのまま使うのは難しい気がしますよね。ではメールテンプレートを変更していきましょう。
templates/registration/password_reset_complete.html



{{ user.username }} 様

下記URLよりサイトにアクセスの上、パスワードの再設定を行ってください。

再設定用URL
{{ protocol }}://{{ domain }}{% url 'password_reset_confirm' uidb64=uid token=token %}

本メールは{{protocol}}://{{domain}}より自動送信されています。
心当たりのない場合は破棄をお願いします。

あと、メールタイトルも変更しておきましょう。これも組込みテンプレートと同名のファイルを作成することで解決します。拡張子がtxtなことに注意してくださいね。
templates/registration/password_reset_subject.txt


IT学習ちゃんねるのパスワードリセットのお知らせ

これでパスワードリセット処理時に送信されるメールは以下のようになります。

最後に

既存のビューを使用しているのでコーディング作業のほとんどがテンプレートを作るだけの簡単な作業ですね。では次回から組込みの認証用ビューを利用したカスタマイズに入っていきます。

Sponsored Link