【Django】ログイン機能の実装

既存システムからDjangoへのリプレースで、ログイン機能をどう実装するかを検討して、実装した時のメモ。

0.事前検討

(前提)

  • 既存システムDBには、既にID、パスワードソルト、パスワードハッシュ値がある。
  • 既存ユーザーには、ログイン方法の変更の通知はできる。

(方針)

  • この際、IDをメールアドレスに変更する。
  • なるべく、Djangoのフレームに乗っ取る形で実装する。
  • パスワードも再設定が可能なので、DjangoのUserモデル認証に切り替える。
  • 将来は、メールベースの二段階認証にする。

(参考ページ)

Djangoの認証システムを使用する

1.【ログイン】LoginViewで実装(1)

ログイン部分については、LoginViewを使用して実装する。

LoginView

基にしたVisual Studio のサンプルが、admin機能で利用しているので、利用者向けのログイン機能を追加する。

1-1)forms.py:ログインフォームの定義

forms.pyにユーザー用のログインフォームを定義する。Visual Studio のサンプルで既に定義されていたので、それをコピーして修正した。

変更点としては、usernameをメールアドレスで入力させるようにする。Userモデルにはusernameのほかにもemailも持っているがとりあえず。

class UserAuthenticationForm(AuthenticationForm):
    """Authentication for User Login."""
    username = forms.CharField(max_length=254,
                               widget=forms.EmailInput({
                                  'class': 'form-control',
                                  'placeholder': 'Email アドレス'}))
    password = forms.CharField(label=_("Password"),
                               widget=forms.PasswordInput({
                                   'class': 'form-control',
                                   'placeholder':'パスワード'}))

基底のAutheticationFormについては、以下を参照した。

class AuthenticationForm 

(GitHub)django.contrib.auth.forms.py

※今回は、従来のDBを使ったりとかしてカスタマイズすることも可能だとは思うが、手抜きで行くことに。

1-2)template:ログインページの定義

app/login.htmlというファイルで、テンプレートを作成。内容は、LoginViewを参考に。

1-3)url.pyの定義

urlpatternsに、ログインとログアウトを追加。

 :
from django.contrib.auth.views import LoginView, LogoutView
from app import forms, views
 :
urlpatterns = [
   :
    path('login/',
         LoginView.as_view
         (
             template_name='app/login.html',
             authentication_form=forms.UserAuthenticationForm,
             extra_context=
             {
                 'title': 'Login',
             },
         ),
         name='login'),
    path('logout/', LogoutView.as_view(next_page='/'), name='logout'),
   :
]
 :

1-4)setting.pyの定義

ログイン後の最初のページを「LOGIN_REDIRECT_URL」で定義する。

LOGIN_REDIRECT_URL

2.【ログイン】LoginViewで実装(2)

(1)では、Userのusernameにメールアドレスを入れるという強引な方法にしたが、ユーザー名も使いたい場合は、認証でUser.emailをチェックするように修正する。

Django の認証方法のカスタマイズ

具体的には、認証バックエンドを拡張して、カスタマイズすればいい。

認証バックエンドの実装

(GitHub)django.contrib.auth.backends.py

ベースのModelBackend.authenticateでは、以下のように取得している。

class ModelBackend(BaseBackend):
    :
    def authenticate(self, request, username=None, password=None, **kwargs):
        :
        user = UserModel._default_manager.get_by_natural_key(username)
        :

拡張クラスでは、DBのemailからusernameを検索するようにすればいいはず。とりあえずは、今回の運用は(1)の実装で問題ないので確認はしてないが、たぶん。

3.ログイン画面にお知らせ表示

ログイン画面の下に、DBから取得したお知らせ情報を表示するようにしたいので、修正する。

3-1)views.py:login関数の追加

1.ではLoginViewをurl.pyから直接呼び出すことで、views.pyの修正はなかったが、DBデータを取得するためにviews.pyに関数を追加する。

def login(request):
    """Renders the login page."""
    assert isinstance(request, HttpRequest)

    # news list
    news_list = Information.news.order_by('-from_time') # DBからお知らせ情報を取得

    view_obj = LoginView()
    view_obj.request = request
    view_obj.form_class = UserAuthenticationForm
    view_obj.template_name = 'app/login.html'
    view_obj.extra_context = {
        'title':'Login',
        'news_list':news_list,         # 追加のお知らせ情報リスト
    }

    return view_obj.dispatch(request)

3-2)url.pyの定義

LoginViewを直接呼び出していたのを、login関数に変更。

 :
from django.contrib.auth.views import LogoutView
from app import forms, views
 :
urlpatterns = [
   :
    path('login/', views.login, name='login'),     # 変更部分
    path('logout/', LogoutView.as_view(next_page='/'), name='logout'),
   :
]
 :

4.ログインが必要なページ

ログインが必要なページについては、以下でログインしていない場合、ログイン画面にリダイレクトさせる。

login_required デコレータ

クラスビューを使っている場合は、MixInを使う。

LoginRequired mixin

5.二段階認証の追加

これは、別記事で後日。

DjangoのTips

自分のためのメモ。リンクは、基本的にDjangoの3.1ドキュメント。

(Django公式)

Django3.1 ドキュメント

Django3.1 APIリファレンス

(Djangoソース)

https://github.com/django/django/tree/master/django

(DBに関してはこっち)

1.テンプレート

1.1 include

テンプレートの中から、テンプレートを呼び出せる。

https://docs.djangoproject.com/ja/3.1/ref/templates/builtins/#include

方針としては、templates/app/commonというようなフォルダを作って共通のテンプレート部品を置いて、includeして利用する。

1.2 オーバライドしたblockで、基底の呼び出し

https://docs.djangoproject.com/ja/3.1/howto/overriding-templates/#extending-an-overridden-template

以下を呼び出せばいい。注意点として基底側で、「static」を使っている場合、「load static」を呼ばないとエラーになる。

{{ block.super }}

1.3 改行のBRタグへの変更

「linebreaksbr」というフィルタを使えばいい。

https://docs.djangoproject.com/ja/3.1/ref/templates/builtins/#linebreaksbr

{{ value|linebreaksbr }}

1.4 フィルタ

「|」は、テンプレート言語のフィルタというもの。

https://docs.djangoproject.com/ja/3.1/ref/templates/language/#filters

組み込みフィルタリファレンス

自分でフィルタを登録することもできるらしい。

https://docs.djangoproject.com/ja/3.1/howto/custom-template-tags/#writing-custom-template-filters

Djangoのアドオンのhumanizeも使える。

https://docs.djangoproject.com/ja/3.1/ref/contrib/humanize/

桁区切りを入れる、intcommaだが、自分のアプリではうまくいかずにソースを見たところ、言語設定の関係がうまく動作しなかったよう。とりあぜう、第二引数をFalseを設定して回避できた。

1.5 footerのコピーライトの年号

Visual Studioのサンプルだと、ビューで年を変数にしてテンプレートに渡しているが、テンプレートの「now」を使えばわざわざ渡す必要は無くなる。

https://docs.djangoproject.com/ja/3.1/ref/templates/builtins/#now

コピーライト部分は、以下のような感じで書けばいい。

<p class="text-center">
 &copy; gijutsu.com 2019-{% now "Y" %}
</p>

1.6 メニューのアクティブ制御

共通メニューの、ページ毎での”active”制御を、どう実装するか。以下の回答が参考になる。

https://stackoverflow.com/questions/46617375/how-do-i-show-an-active-link-in-a-django-navigation-bar-dropdown-list?answertab=votes#tab-top

ドキュメントはこちら。

ResolverMatch

urls.pyのurlpatternsで定義したnameでチェックすることで実現。

    {% with request.resolver_match.url_name as url_name %}
      <ul class="navbar-nav mr-auto">
        <li class="nav-item {% if url_name == 'home' %}active{% endif %}">
          <a class="nav-link" href="{% url 'home' %}">Home</a>
        </li>
        <li class="nav-item {% if url_name == 'contact' %}active{% endif %}">
          <a class="nav-link" href="{% url 'contact' %}">お問合せ</a>
        </li>
      </ul>
    {% endwith %}

2.セッション

https://docs.djangoproject.com/ja/3.1/topics/http/sessions/

具体的なViewでの使い方は、以下。

ビューでセッションを使う

3.モデル

https://gijutsu.com/2021/01/12/django-db/

バリデータの設定は以下に

https://docs.djangoproject.com/ja/3.2/ref/validators/

4.ビュー

ビルトインのクラスベースビュー API

1)ログインが必要なページ

ログインが必要なページについては、以下でログインしていない場合、ログイン画面にリダイレクトさせる。

login_required デコレータ

クラスビューを使っている場合は、MixInを使う。

LoginRequired mixin

5.フォーム

https://docs.djangoproject.com/en/3.1/topics/forms/

Form fields

ビルトインの Field クラス

モデルからフォームを作成する

6.役に立つパッケージ

※国内限定なら以下で十分

https://qiita.com/xKxAxKx/items/86bdf0bc4c7dc9ee65d9

7.ログ

https://docs.djangoproject.com/ja/3.1/topics/logging/#examples

8.URL ディスパッチャ

https://docs.djangoproject.com/ja/3.1/topics/http/urls/

コントローラの中で、URLを取り出す場合は、reverseを使う。

https://docs.djangoproject.com/ja/3.1/topics/http/urls/#reverse-resolution-of-urls

from django.urls import reverse

:

return HttpResponseRedirect(reverse('news-year-archive', args=(year,)))

9.メッセージ

https://docs.djangoproject.com/ja/3.1/ref/contrib/messages/

10.リクエストとレスポンスのオブジェクト

https://docs.djangoproject.com/ja/3.1/ref/request-response/

(処理後に呼び出しページに戻る例)

    return HttpResponseRedirect(request.META['HTTP_REFERER'])

同じnameの入力値を配列で受け取るのは以下。

https://docs.djangoproject.com/ja/3.1/ref/request-response/#django.http.QueryDict.getlist

Pythonコーディング

0)ドキュメント

Pythonでのコーディングする時の参考ページ。

(ゼロからのパイソン講座)

https://www.python.jp/train/index.html

(Pythonドキュメント)

https://docs.python.org/ja/3/

https://docs.python.org/ja/3/contents.html

4.8. 間奏曲: コーディングスタイル

(コーディング規約:PEP 8)

https://pep8-ja.readthedocs.io/ja/latest/#

https://www.python.org/dev/peps/pep-0008/

1)Nullチェック

Pythonでは、NullではなくてNone。

None オブジェクト

チェックには、「is」演算子がある。

6.10.3. 同一性の比較

2)例外

4.3. 例外

8.4. try

例外を投げるのは、throwではなくて、raiseとのこと。

7.8. raise 文

組み込み例外

3)可変長引数

4.7. 関数定義についてもう少し

4.7.4. 任意引数リスト

argsについては記載されているが、kwargsの記載は見つけれらず。完全な文法仕様を見ると定義されている。

10. 完全な文法仕様

arguments:
    | args [','] &')' 
args:
    | ','.(starred_expression | named_expression !'=')+ [',' kwargs ] 
    | kwargs 
kwargs:
    | ','.kwarg_or_starred+ ',' ','.kwarg_or_double_starred+ 
    | ','.kwarg_or_starred+
    | ','.kwarg_or_double_starred+
starred_expression:
    | '*' expression 
kwarg_or_starred:
    | NAME '=' expression 
    | starred_expression 
kwarg_or_double_starred:
    | NAME '=' expression 
    | '**' expression 

4)型(class)チェック

class type(object)

型オブジェクト

isinstance(object, classinfo)

isinstance を使って、以下のように判断できる。

# 文字かのチェック
if isinstance(value, str):
    :

# 整数型か文字かのチェック
if isinstance(value, [int, str]):
    :

5)文字列の操作

文字列メソッド

6)辞書型

マッピング型 — dict

7)クラスについて

https://docs.python.org/ja/3/tutorial/classes.html

7-1) 静的メソッドとクラスメソッド

クラスメソッド:classmethod

静的メソッド:staticmethod

共に、「@」を付けてデコレータとして使うらしいが、普通に関数として@を付けずに使うこともできるらしい。クラスメソッドと静的メソッドの違いは、継承された場合に出てくる。

クラスメソッドが派生クラスから呼び出される場合は、その派生クラスオブジェクトが暗黙の第一引数として渡されます。

Pythonドキュメント:@classmethod
  • クラスメソッドは派生(継承)したクラスで動くので、派生クラスに影響される。
  • 静的メソッドは派生クラスに影響されない。

7-2) クラス名からクラスの取得

klass = globals()[classname]

8)多重継承

9.5.1. 多重継承

9.5.1. Multiple Inheritance ※一応、原文で確認したほうがいい

検索の基本は以下。

  1. 深さ優先
  2. 左から右

9)日付型

datetime

10)列挙型

https://docs.python.org/ja/3/library/enum.html#module-enum

11)複数の配列を同時処理する場合

zipで配列をまとめればいい。

https://docs.python.org/ja/3/library/functions.html#zip

ちなみに、ループカウンタを同時に使いたい場合は、以下を使う。

https://docs.python.org/ja/3/library/functions.html#enumerate

12)タプルについて

タプルの場合、リストの内包表記のように書くとGenaratorとなるので、以下のように「tuple」で明示的に変換する必要がある。

names= tuple((f.name for f in keys))

名前付きのタプルを使うことで、プログラムが見やすくなる。

https://docs.python.org/ja/3/library/collections.html#collections.namedtuple

名前付きタプルの一部値を変更した新しいタプルの作成は以下。

https://docs.python.org/3/library/collections.html#collections.somenamedtuple._replace

13)メタクラス

https://docs.python.org/ja/3/reference/datamodel.html#metaclasses

14)ソート

https://docs.python.org/ja/3/howto/sorting.html

15)イテレータ型

https://docs.python.org/ja/3/library/stdtypes.html?highlight=dictionary#iterator-types

・最後の要素は、インデックスが-1で取得できる。後ろから3要素なら、data[-3]で取得できる。

Django開発ではまったこと

はじめてのDjango開発で、ひっかかったところ、はまったところの記録。

1)拡張テンプレート側のstaticのロードエラー

ベース側で「{% load static %}」しているので、なんとなく拡張側は要らないような気でいたら、以下エラーが出た。

Invalid block tag on line **: 'static', expected 'endblock'. Did you forget to register or load this tag?

拡張ファイル側でも「static」を使う場合は、「{% load static %}」する必要がある。

※「{{ block.super() }} 」した場合に、その中で「static」を使われている場合も、「{% load static %}」する必要がある。

2)Django Debug Toolbarが表示されない

Visual Studio からDjangoの開発用サーバーで実行すると、ツールバーが表示されない。Chromeのエラーを見ると、toolbar.jsの読み込みがはじかれている。

Failed to load module script: The server responded with a non-JavaScript MIME type of "text/plain".

とりあえず、setting.pyに以下を追加することで解決。

# For Debug Toolbar
if DEBUG:
    import mimetypes
    mimetypes.add_type("text/javascript", ".js", True)

3)Visual Studioのサンプルの文字コード

サンプルのDjangoのWebアプリのテンプレートの文字コードがSJISのようで、修正しながら、テンプレート内に日本語を書くと、実行時にエラーになった。他のエディタで一度UTF-8で一度保存してから修正する必要あり。

4)アドオンhumanizeのintcomma

自分のプロジェクトの言語設定の問題か、桁区切りをつけるintcommaがうまく動作しなかった。

https://docs.djangoproject.com/ja/3.1/ref/contrib/humanize/#intcomma

ソースを見ると、第二引数で言語設定を無視できるよう。

(GitHubソース)contrib/humanize/templatetags/humanize.py

以下のように、Falseを設定することで回避できた。

<div class="text-right>{{ report.count_visit|intcomma:False }}</div>

DjangoでWebアプリ

既存システムのDBを使って、Djangoで新たなWebアプリを作っている作業メモ。

1.開発環境と既存DBの取り込み

とりあえず、Visual Studio Community 2019で、既存DBのモデルに取り込みまでは、以下で行った。

2.アプリ開発

チュートリアルを参考に改造していく。

(Django公式)はじめての Django アプリ作成、その 1 | Django ドキュメント

2.1 DBの内容をページに表示

まずは、既存DBのモデルにてデータを取得し、ページに表示する。流れとしては、Viewでmodelを使ってデータを取得し、それをテンプレートに渡して表示させる。

1)ビュー

以下を参考に、モデルを取り込んで、データをテンプレートに渡すようにする。

(Django公式)実際に動作するビューを書く

app/views.pyを修正していく。

①読み込みたいmodelをimport
from .models import DBモデル
②modelを操作してデータを取得

データベース操作について、公式サイトのリンクが以下にまとめてくれてあるので、ありがたく参考にさせてもらう。

(Qiita)Django データベース操作 についてのまとめ

2)テンプレート

app/templates/app以下のテンプレートを修正していく。テンプレートの記述言語についての説明は以下ページ。

(Django公式)

テンプレート

The Django Template Language

Viewから渡されたデータは、「{{ 変数名 }}」と記述して使えるので、それをページ内に表示させる。

2.2 カスタムマネージャーの作成

DB取得等のロジックをViewの中に色々書くのもいまいちなので、カスタムマネージャーを作成する。

(Django公式)マネージャーのカスタマイズ

上記の日本語版は、翻訳が微妙で間違った解釈になるところがあるので、英語版を読んだ方が良さそう。

(Django公式)Custom managers

拡張クラスにメソッドを定義、あるいはget_querysetを書き換える等をコーディングするなり、QuerySetを定義するなりする。以下に4パターンの方法が記載されている。

(Django公式ドキュメント)

①カスタムMangerにメソッド追加する方法

Adding extra manager methods

②カスタムMangerの初期クエリセットを変更する方法

Modifying a manager’s initial QuerySet

③QuerySetを拡張してメソッド追加する方法

Calling custom QuerySet methods from the manager

④QuerySetを複数拡張する場合に、Managerをクエリセットから定義できる方法

Creating a manager with QuerySet methods

上記から適切な方法で実装していく。

1)カスタムマネージャーの定義

app/managers.pyを追加して、カスタムマネージャーを定義する。

"""
Definition of DB managers.
"""

from django.db import models

# Managerの拡張クラスを定義
class XxxxxxManager(models.Manager):
    :

データの絞り込みは、filterを使う。

(Django公式ドキュメント)

フィルタを用いて特定のオブジェクトを取得する

フィールドルックアップ

2)モデルの修正

app/models.pyの修正して、モデルのobjectsを書き換える、あるいは目的に応じたmanagerを取得するメソッドと追加する。

3)有効期間内のデータを対象とする例

テーブル(Information)に有効期間として、from_timeとto_timeを持っていて、現在有効なデータだけを対象とする場合の例。「②カスタムMangerの初期クエリセットを変更する方法」で実装。

① manager.py
"""
Definition of DB managers.
"""

from django.db import models
from django.db.models import Q
from django.db.models.functions import Now


# Information
class InformationManager(models.Manager):
    def get_queryset(self):
        return super().get_queryset().filter(
            Q(from_time__lte=Now()) & Q(to_time__gt=Now()))

filterに、「Q(from_time__lte=Now()) & Q(to_time__gt=Now())」とすることで、「from_time <= CURRENT_TIMESTAMP AND to_time > CURRENT_TIMESTAMP」という条件になる。

(Django公式ドキュメント)

Q オブジェクトを用いた複雑な検索

Now関数

② models.pyの修正
"""
Definition of models.
"""

from django.db import models
from .managers import InformationManager


# Create your models here.

class Information(models.Model):
    id_information = models.AutoField(primary_key=True)
    from_time = models.DateTimeField(blank=True, null=True)
    to_time = models.DateTimeField(blank=True, null=True)
    title = models.CharField(max_length=128, blank=True, null=True)
    detail = models.TextField(blank=True, null=True)
    created_at = models.DateTimeField()
    updated_at = models.DateTimeField()

    objects = models.Manager() # The default manager.
    news = InformationManager() # Custom manager

    class Meta:
        managed = False
        db_table = 'information'

Viewでは、Information.newsで対象データを取得する。

2.3 デバッグ

Django Debug Toolbarを入れる。

Django Debug Toolbar

ドキュメントに従い設定したが、Visual Studio からDjangoの開発用サーバーで実行すると、ツールバーが表示されない。

Chromeのエラーを見ると、toolbar.jsの読み込みがはじかれている。

Failed to load module script: The server responded with a non-JavaScript MIME type of "text/plain".

とりあえず、setting.pyに以下を追加することで解決。

# For Debug Toolbar
if DEBUG:
    import mimetypes
    mimetypes.add_type("text/javascript", ".js", True)

3.UIデザイン

デザインは苦手なので、Bootstrapのサンプルを参考にする。

https://getbootstrap.jp/docs/4.5/examples/

1)最新のBootstrapを入れる

app/static/app/content,scripts下のBootstarp、JQuery、Popper等の必要なものを入れる。

CDNを利用するのがいいのかもしれないが、とりあえず開発時点ではstaticに取り込む。

2)サンプルのCSS,JSを入れる

使うサンプルのCSS,JSもapp/static/app/content,scripts下に入れる。

3)サンプルを参考にテンプレートを作成していく。

トップページは、Pricingを参考に。管理画面はDashboardを基に作ることにする。

4.セキュリティー設定

1)admin

adminについて、必要ないなら消す。カスタマイズして使っていく予定なら、IP制限を入れる。以下を利用した。

django-admin-ip-restrictor