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

# 進捗

2017/11/3 現在
第1章　完成
第2章　完成
第３章　完成

# 今回やること

前回の記事で作った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自体の機能とは関係ないですが、これがあるのとないのでは開発の効率が全然違ってくるので導入することをオススメします。

[＊公式ドキュメント](https://django-debug-toolbar.readthedocs.io/en/stable/installation.html)

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

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

```
$ pip install django-debug-toolbar
```

以下をurls.pyに追加してください（前回分とのマージ結果は[Githubのレポジトリ](https://github.com/gragragrao/manager_project)を参考にしてください！）

```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.py`で`DEBUG=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の設定が完了しました。
実際に表示されるか見てみましょう。


![スクリーンショット 2017-08-05 18.41.30.png](https://qiita-image-store.s3.amazonaws.com/0/165023/157394cd-e7c9-d077-f476-3dafdb92e077.png)

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

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

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

SQLのところをクリックして、実際どんなクエリが投げられているのか見てみましょう。
（多く投げられていそうなところの＋をクリックすると中身が見れます）

![スクリーンショット 2017-08-05 18.44.24.png](https://qiita-image-store.s3.amazonaws.com/0/165023/99467c43-93c3-8f5d-26a1-c407bec5afe7.png)

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

`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章 ログイン機能の実装

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

[＊公式ドキュメント](https://docs.djangoproject.com/en/1.11/topics/auth/)

`models.py`を根本的に結構いじるので、エラーが出るのを防ぐため、前回作成したマイグレーションファイルとデータベースを消去しておきましょう。
（今回はPersonをログインできるようにして、色々変更しているため、特別です。でも、データベースを一掃したいときなど、たまにあるかもしれないので、やり方を紹介します。）

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

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

これだけで完了です！



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

### 概要図

<img width="1036" alt="スクリーンショット 2017-08-12 14.15.31.png" src="https://qiita-image-store.s3.amazonaws.com/0/165023/c23eaff7-a191-c345-b0ad-84c95e78201f.png">


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

### ログイン機能の実装

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

コードは重要なところをピックアップして書いているので、全体が見たい方は僕のGitHubのレポジトリを見てください！
[＊ GitHubのコードはこちら](https://github.com/gragragrao/manager_project)


```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.py`に`authenticate`関数が定義されています。

```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
```

このコード、テクってますよね！笑
`if`と`else`の間に`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レポジトリ](https://github.com/gragragrao/manager_project)を確認してください！


マイグレーションファイルとデータファイルを一掃したので、作り直します。
`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`も少し変更しているので注意してください。）


[![https://gyazo.com/f34a3c30c2f92a36d33ad87cdc9b5336](https://i.gyazo.com/f34a3c30c2f92a36d33ad87cdc9b5336.gif)](https://gyazo.com/f34a3c30c2f92a36d33ad87cdc9b5336)



ログアウト用のエンドポイントを作成してこの章を終わりにしたいと思います。
（登録用のページは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章は終わりです。

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


# 第３章　Hijack機能の実装

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

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

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

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

[ドキュメント](https://django-hijack.readthedocs.io/en/stable/)に従います。

```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_APPS` に`hijack`と、依存性のある`compat`を追加します。

[django-adminのconfigページ](https://django-hijack.readthedocs.io/en/stable/configuration/)に色々設定が書いてあリます。

|                               設定 |                                      説明 |               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.html` の `header` に `{% load hijack_tags %}` と　`<link rel="stylesheet" type="text/css" href="{% static 'hijack/hijack-styles.css' %}" />` , `body`  の直下に `{% hijack_notification %}` を追加します。



このあたりの実装がわかりにくい人は、僕の[GitHubコード](https://github.com/gragragrao/manager_project)を参照してください。


実装はこれで以上になります。
ちゃんと動くか確かめて見ましょう。

```
$ 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できます。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/165023/39181e91-035b-ef7e-0bbb-20c77a5c66ef.png)


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

以上です。