進捗
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のレポジトリを参考にしてください!)
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.pyでDEBUG=Trueとなっている時だけDebug Toolbarが現れるようになります。
settings.pyでDjangoにインストールしたアプリを教えてあげましょう。
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が現れてくれなかった。
その説明を書きたい(でもなぜかわからないので、分かる方、ご教授ください)。
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に以下を追加します。
# 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でクエリを作成するところを以下のように変更してください。
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のコードはこちら
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
- ログイン用に、AbstractBaseUserを継承します
 - Person.objects.create()の時にもちゃんと作れるように定義します(PersonManagerは下で書きます)
 - アカウント名のカラムを追加します
 - usernameというカラムがないので、代わりに
identifierを使ってね、ということをDjangoに伝えます 
PersonManagerを書きます。
manager/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でログイン用のページの処理を書いていきます。
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
- ここが一番のキモです! userを認証しています。
このauthenticate関数が何をしているか少し説明します。 
Djangoのコードを見て見ましょう。
django/contrib/auth/backends.pyにauthenticate関数が定義されています。
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
このコード、テクってますよね!笑
ifとelseの間にtry-exceptを挟んでますが、どう処理されるかわかりづらいですよね。
簡単に言えば、ifにひかかってもelseの処理は受けるっていうのがここでのテクニックなのですが、自分で簡単な関数を作ったりして確かめてください!
- 
usernameのカラムがないときはUSERNAME_FIELDに定義されてるカラムを取りに行く - 
usernameをkeyにしてオブジェクトを取得している - 
パスワードが正しいかcheckしている
 
これらを有効にするために、setting.pyに以下を追加します!
僕がちゃんと目を通せてないだけかもしれませんが、なんかドキュメントとかstack overflowとかに書かれていなくて、結構困った記憶があります。
そういうときは、djangoの元コードをみて、どういう処理が走っているかを確認すると解決しますよ!
# 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が絡むのであとで作成しようと思います。)
ログアウト
<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>
from django.contrib.auth import logout
def logout_view(request):
    logout(request)
    return redirect('/login/')
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機能です。
簡単なのでさっと実装しましょう!
ドキュメントに従います。
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_APPS にhijackと、依存性のある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で使われる以下の属性を追加していきます。
class Person(AbstractBaseUser):
    # hijack機能の実装に必要
    is_admin = models.BooleanField(default=False)
    is_staff = models.BooleanField(default=True)
    is_superuser = models.BooleanField(default=False)
urlに以下を追記します。
url(r'^hijack/', include('hijack.urls')),
base.html の header に {% 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を解除できます。
以上です。



