Edited at

Djangoを最速でマスターする part2

More than 1 year has passed since last update.


進捗

2017/11/3 現在

第1章 完成

第2章 完成

第3章 完成


今回やること

前回の記事で作ったDjangoアプリを拡張させて行きます。

これからの拡張の具体的な内容としては、

       
(前半) Django自体の説明
(後半) Django自体の説明+DockerやMySQLなどとの連携

1章
Django Debug Toolbarの導入
DjangoアプリをDocker上で動かしてみる

2章
ログイン機能の実装
MySQLを使用する

3章
Hijack機能の実装
SQL文の効率化と高速化(Django組み込み関数を使う)

4章
カスタムtag・カスタムfilter
SQL文の効率化と高速化(自作関数を使う)

5章
Form
Ajaxを使った高度な通信

6章
自動テストを書こう
(追加予定あり)

といった感じで進めて行こうと思います。

前後半かけて、重箱のすみをつついたような部分まで解説することを目指します。

一回目の記事とは違い、随時更新という形で公開していこうと思っています。

なので、説明のリクエストとかを受けますので、希望があればコメントでおしらせください

時間と能力の可能な限り対応します。

また、章の内容によっては作っているサイトのデータ的に合わない場合があるので、その時はドキュメントを詳しく説明します。


1章 Django Debug Toolbarの導入

Django自体の機能とは関係ないですが、これがあるのとないのでは開発の効率が全然違ってくるので導入することをオススメします。

*公式ドキュメント

上の公式ドキュメントに従いながら導入して行きます。

まずはインストールしてきます。

$ pip install django-debug-toolbar

以下をurls.pyに追加してください(前回分とのマージ結果はGithubのレポジトリを参考にしてください!)


urls.py

from django.conf import settings

from django.conf.urls import include, url

if settings.DEBUG:
import debug_toolbar
urlpatterns += [
url(r'^__debug__/', include(debug_toolbar.urls)),
]


settings.pyDEBUG=Trueとなっている時だけDebug Toolbarが現れるようになります。

settings.pyでDjangoにインストールしたアプリを教えてあげましょう。


settings.py

INSTALLED_APPS = [

'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles', # これがあることを確認(なければ追加)
'debug_toolbar', # 追加部分
'manager',
]

Debug Toolbarをミドルウェアの中に設置します。


TODO

おそらくMIDDLEWAREを変更しただけでいいはずなのですが、MIDDLEWARECLASSにも追加しないとDebug Toolbarが現れてくれなかった。

その説明を書きたい(でもなぜかわからないので、分かる方、ご教授ください)。



settings.py

MIDDLEWARE = [

'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.auth.middleware.SessionAuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
'django.middleware.security.SecurityMiddleware',
'debug_toolbar.middleware.DebugToolbarMiddleware', # 追加
]

MIDDLEWARE_CLASSES = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.auth.middleware.SessionAuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
'debug_toolbar.middleware.DebugToolbarMiddleware', # 追加
]


あとは Debug Toolbarが現れるようにsettings.pyに以下を追加します。


settings.py

# Debug Toolbar


DEBUG = True

if DEBUG:
INTERNAL_IPS = ['127.0.0.1', 'localhost']

def custom_show_toolbar(request):
return True

DEBUG_TOOLBAR_PANELS = [
'debug_toolbar.panels.timer.TimerPanel',
'debug_toolbar.panels.request.RequestPanel',
'debug_toolbar.panels.sql.SQLPanel',
'debug_toolbar.panels.templates.TemplatesPanel',
'debug_toolbar.panels.cache.CachePanel',
'debug_toolbar.panels.logging.LoggingPanel',
]

DEBUG_TOOLBAR_CONFIG = {
'INTERCEPT_REDIRECTS': False,
'SHOW_TOOLBAR_CALLBACK': custom_show_toolbar,
'HIDE_DJANGO_SQL': False,
'TAG': 'div',
'ENABLE_STACKTRACES': True,
}


これで設定は完了です。

あとは Debug Toolbarのstaticファイルを落としてきます。

master directory(manage.pyが置いてあるディレクトリを勝手にこう呼んでます。以下同様。)で以下を実行してstaticファイルを落としてください。

$ python manage.py collectstatic

そうすると、assetsというディレクトリができる(僕がみた説明ではstaticの中にできていましたが、そうなるように設定してるっぽかったので、多分デフォルトではassetsの中にできるんじゃないかと思います)ので、その中にあるdebug_toolbarというフォルダをごっそりstatic配下にコピーしましょう。

assetsディレクトリは不要なので、コピーしたあとは消してしまいましょう。

以上でDjango Debug Toolbarの設定が完了しました。

実際に表示されるか見てみましょう。

こんな感じに表示できました。

/worker_list/のページを見ていますが、表示にかなりの時間がかかっていることがわかります。

SQLが1001回も投げられていて、表示に6.48秒もかかっています(クソサイトですね!!笑)

SQLのところをクリックして、実際どんなクエリが投げられているのか見てみましょう。

(多く投げられていそうなところの+をクリックすると中身が見れます)

後半に説明する予定なので、今回は省略しますが、あまりにも遅いので解決策だけ紹介します。

views.pyでクエリを作成するところを以下のように変更してください。


views.py

class WorkerListView(TemplateView):

template_name = "worker_list.html"

def get(self, request, *args, **kwargs):
context = super(WorkerListView, self).get_context_data(**kwargs)

workers = Worker.objects.all().select_related('person') # 変更部分
context['workers'] = workers

return render(self.request, self.template_name, context)


これでリロードして見ましょう。

今度は投げられるクエリの数が1となり、表示までの時間は0.25秒に短縮されましたね!

典型的なN+1問題という感じですね!


2章 ログイン機能の実装

ウェブアプリには欠かせないログイン機能を実装していきます。

*公式ドキュメント

models.pyを根本的に結構いじるので、エラーが出るのを防ぐため、前回作成したマイグレーションファイルとデータベースを消去しておきましょう。

(今回はPersonをログインできるようにして、色々変更しているため、特別です。でも、データベースを一掃したいときなど、たまにあるかもしれないので、やり方を紹介します。)

とは言っても、Djangoの標準ではsqlite3を使っているので、ファイルを削除するだけで大丈夫です!

manager/migrations/配下の__init__.py以外のファイルを全部と、db.sqlite3というファイルを消してください。

これだけで完了です!

早速ログイン機能を実装していきましょう!


概要図

概要は上の図を参考にしてください!


ログイン機能の実装

まずはPersonモデルを変えていきます。

コードは重要なところをピックアップして書いているので、全体が見たい方は僕のGitHubのレポジトリを見てください!

* GitHubのコードはこちら


models.py

from django.contrib.auth.models import AbstractBaseUser

from manager.managers import PersonManager

class Person(AbstractBaseUser): #1
objects = PersonManager() # 2

identifier = models.CharField(max_length=64, unique=True, blank=False) # 3
name = models.CharField(max_length=128)
email = models.EmailField()

is_active = models.BooleanField(default=True) # 必要です!

USERNAME_FIELD = 'identifier' # 4



1) ログイン用に、AbstractBaseUserを継承します

2) Person.objects.create()の時にもちゃんと作れるように定義します(PersonManagerは下で書きます)

3) アカウント名のカラムを追加します

4) usernameというカラムがないので、代わりにidentifierを使ってね、ということをDjangoに伝えます


PersonManagerを書きます。

manager/managers.pyというファイルを作ってください。


managers.py

from django.contrib.auth.models import BaseUserManager

from django.utils import timezone

class PersonManager(BaseUserManager):

def create_user(self, identifier, email, password=None, **extra_fields):
if not email:
raise ValueError('Users must have an email address')

email = PersonManager.normalize_email(email)
person = self.model(
identifier=identifier,
email=email,
**extra_fields
)
person.set_password(password)
person.save(using=self._db)

return person


続いてviews.pyでログイン用のページの処理を書いていきます。


views.py

from django.contrib.auth.views import login

from django.contrib.auth import authenticate

class CustomLoginView(TemplateView):
template_name = "login.html"

def get(self, _, *args, **kwargs):
if self.request.user.is_authenticated():
return redirect(self.get_next_redirect_url())
else:
kwargs = {'template_name': 'login.html'}
return login(self.request, *args, **kwargs)

def post(self, _, *args, **kwargs):
username = self.request.POST['username']
password = self.request.POST['password']
user = authenticate(username=username, password=password) # 1
if user is not None:
login(self.request, user)
return redirect(self.get_next_redirect_url())
else:
kwargs = {'template_name': 'login.html'}
return login(self.request, *args, **kwargs)

def get_next_redirect_url(self):
redirect_url = self.request.GET.get('next')
if not redirect_url or redirect_url == '/':
redirect_url = '/worker_list/'
return redirect_url



1) ここが一番のキモです! userを認証しています。

このauthenticate関数が何をしているか少し説明します。

Djangoのコードを見て見ましょう。

django/contrib/auth/backends.pyauthenticate関数が定義されています。


backends.py

def authenticate(self, request, username=None, password=None, **kwargs):

if username is None:
username = kwargs.get(UserModel.USERNAME_FIELD) # 2
try:
user = UserModel._default_manager.get_by_natural_key(username) # 3
except UserModel.DoesNotExist:
# Run the default password hasher once to reduce the timing
# difference between an existing and a nonexistent user (#20760).
UserModel().set_password(password)
else:
if user.check_password(password) and self.user_can_authenticate(user): # 4
return user

このコード、テクってますよね!笑

ifelseの間にtry-exceptを挟んでますが、どう処理されるかわかりづらいですよね。

簡単に言えば、ifにひかかってもelseの処理は受けるっていうのがここでのテクニックなのですが、自分で簡単な関数を作ったりして確かめてください!

2) usernameのカラムがないときはUSERNAME_FIELDに定義されてるカラムを取りに行く

3) usernameをkeyにしてオブジェクトを取得している

4) パスワードが正しいかcheckしている


これらを有効にするために、setting.pyに以下を追加します!

僕がちゃんと目を通せてないだけかもしれませんが、なんかドキュメントとかstack overflowとかに書かれていなくて、結構困った記憶があります。

そういうときは、djangoの元コードをみて、どういう処理が走っているかを確認すると解決しますよ!


settings.py

# user authentication


AUTHENTICATION_BACKENDS = (
'django.contrib.auth.backends.ModelBackend',
)

AUTH_USER_MODEL = 'manager.Person'


あとはurls.pyにlogin用のurlを定義して、login.htmlも追加しています。

長くなるので、ファイルはGitHubレポジトリを確認してください!

マイグレーションファイルとデータファイルを一掃したので、作り直します。

master directoryから以下を実行します。

$ python manage.py makemigrations

$ python manage.py migrate

では、ちゃんとログインできるか確かめてみましょう!

一人Personを使ってログインしてみます。

$ python manage.py shell

# from manager.models import *
# import datetime
# person = Person(identifier="gragragrao", name="gragragrao", email="example@gmail.com", sex=0, birthday=datetime.date.today(), address_from=21, current_address=21)
# person.set_password("grao_pass")
# person.save()

これでログイン用のpersonができました!

user名はgragragrao、パスワードはgrao_passですね。

ではログインできるか確かめてみましょう!(WorkerListViewも少し変更しているので注意してください。)

ログアウト用のエンドポイントを作成してこの章を終わりにしたいと思います。

(登録用のページはformが絡むのであとで作成しようと思います。)


ログアウト


base.html

<ul class="nav" id="side-menu">

<li><a href="/worker_list/"><i class="fa fa-bar-chart" aria-hidden="true"></i> Worker一覧</a></li>
<li><a href="/logout/"><i class="fa fa-bar-chart" aria-hidden="true"></i> ログアウト</a></li>
</ul>


views.py

from django.contrib.auth import logout

def logout_view(request):
logout(request)
return redirect('/login/')



urls.py

from django.contrib.auth.decorators import login_required  # そのページに飛ぶ時、ログインを要求される


urlpatterns = [
url(r'^logout/', manager_view.logout_view),
url(r'^worker_list/', login_required(manager_view.WorkerListView.as_view())),
]

これで2章は終わりです。

複雑だったので、うまくいかないなどあればコメントをください。


第3章 Hijack機能の実装

この章では、Hijack機能を実装していきます。

自分が管理者なら、登録しているユーザーとしてログインしたい場面があると思います。

そんなとき、登録ユーザーのパスワードを知らなくてもログインできるのがHijack機能です。

簡単なのでさっと実装しましょう!

ドキュメントに従います。


settings.py

INSTALLED_APPS = [

'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'debug_toolbar',
'manager',
'hijack',
'compat',
]

# hijack
HIJACK_LOGIN_REDIRECT_URL = '/worker_list/'
HIJACK_LOGOUT_REDIRECT_URL = '/worker_list/'
HIJACK_ALLOW_GET_REQUESTS = True

HIJACK_USE_BOOTSTRAP = True



INSTALLED_APPShijackと、依存性のあるcompatを追加します。

django-adminのconfigページに色々設定が書いてあリます。

設定
説明
Default

HIJACK_DISPLAY_WARNING
hijackしていることを示す黄色いバーを表示するかどうか
True

HIJACK_USE_BOOTSTRAP
Bootstrapに最適化するかどうか
False

HIJACK_URL_ALLOWED_ATTRIBUTES
userがどの属性を通してHijackされることができるか
下記参照

HIJACK_AUTHORIZE_STAFF

is_staff=True のuserが他の is_staff=False のuserに対してhijackできるか
False

HIJACK_AUTHORIZE_STAFF_TO_HIJACK_STAFF

is_staff=True のuserが他の is_staff=True のuserに対してhijackできるか
False

HIJACK_LOGIN_REDIRECT_URL
hijackした時にどのURLにリダイレクトされるか
settings.LOGIN_REDIRECT_URL

HIJACK_LOGOUT_REDIRECT_URL
hijackをreleaseした時にどのURLにリダイレクトされるか
settings.LOGIN_REDIRECT_URL

HIJACK_AUTHORIZATION_CHECK
hijackできる権限を決める関数
'hijack.helpers.is_authorized_default' (下記参照)

HIJACK_ALLOW_GET_REQUESTS
hijackがGETできるかどうか
False


HIJACK_URL_ALLOWED_ATTRIBUTES について

Default : ('user_id', 'email', 'username')

Djangoが捌けるURLは以下のようになっています。

^hijack/ ^email/(?P<email>[^@]+@[^@]+\.[^@]+)/$ [name='login_with_email']

^hijack/ ^username/(?P<username>.*)/$ [name='login_with_username']
^hijack/ ^(?P<user_id>[\w-]+)/$ [name='login_with_id']

つまり、emailのフィールドが example@example.comのpersonに対してなら、/hijack/email/example.com/でHijackできるんですね。

Django-hijackの中のコードを見ましたが、このDefault以外のカラムは使えないです。email だけでしかHijackできないようにしたい!とかなら、 ('email') に設定すればできます(カラムを減らすことはできても増やすことはできない感じですね)。


HIJACK_AUTHORIZATION_CHECK について

Default: 'hijack.helpers.is_authorized_default'

Defaultの関数はこんな感じです↓

def is_authorized_default(hijacker, hijacked):

if hijacker.is_superuser:
return True

if hijacked.is_superuser:
return False

if hijacker.is_staff and hijack_settings.HIJACK_AUTHORIZE_STAFF:
if hijacked.is_staff and not hijack_settings.HIJACK_AUTHORIZE_STAFF_TO_HIJACK_STAFF:
return False
return True

return False


Personにhijackで使われる以下の属性を追加していきます。


models.py

class Person(AbstractBaseUser):

# hijack機能の実装に必要
is_admin = models.BooleanField(default=False)
is_staff = models.BooleanField(default=True)
is_superuser = models.BooleanField(default=False)

urlに以下を追記します。


urls.py

url(r'^hijack/', include('hijack.urls')),


base.htmlheader{% load hijack_tags %} と <link rel="stylesheet" type="text/css" href="{% static 'hijack/hijack-styles.css' %}" /> , body の直下に {% hijack_notification %} を追加します。

このあたりの実装がわかりにくい人は、僕のGitHubコードを参照してください。

実装はこれで以上になります。

ちゃんと動くか確かめて見ましょう。

$ python manage.py makemigrations

$ python manage.py migrate

でモデルの変更をDBに適応して、以下のように is_supersuser=True のPersonを作っていきます(これが管理者という想定です)。

$ python manage.py shell

# from manager.models import *
# import datetime
# person = Person(identifier='grao_super', name='grao_super', email='grao@example.com', birthday=datetime.datetime(1990, 11, 3), sex=1, address_from=21, current_address=21, is_superuser=True)
# person.set_password('grao_pass')
# person.save()

これで完成です。

is_superuser=False のPersonがいない場合は適宜作ってください。

これからhijackするPersonは、 id=1, email='example@example.com'(uniqueな値) とします。

この場合、is_superuser=True のPerson(上で作った grao_super )でログインしたあと、hijackのURLを打てばhijackできます。

# python manage.py runserver 8080

ログインして、

http://localhost:8080/hijack/1/

http://localhost:8080/hijack/email/example@example.com/

にリクエストを送ると、以下のようにhijackできます。

release gragragrao をクリックすると、hijackを解除できます。

以上です。