オリジナルのcontext_processorを追加する

この記事は[Django2016アドベントカレンダー](http://qiita.com/advent-calendar/2016/django)の25日目の記事です。

最終日に基本的なことで恐縮ですが、自分で追加したことがなかったのでcontext_processorsについて勉強＆実装したことを書きます。

※この記事はDjango1.10, Python3.5.2で書いているので、古いバージョンとは一部異なると思います。（`TEMPLATE_LOADERS`や`TEMPLATE_CONTEXT_PROCESSORS`はDjango1.10では廃止されています。）

そういや、Python3.6がでましたね！

**この記事に書いていること**: context_processorsの追加方法、実装例
**この記事に書いていること**: context_processorsの内部での動き

# きっかけ
今開発しているサイトでは現時点では完全にレスポンシブでtemplateを書いているのですが、モバイルとPCでテンプレートの一部を出し分ける必要が出てきました。

そこで、[django-mobile](https://github.com/gregmuellegger/django-mobile)を使おうとしたのですが、まだDjango1.10に未対応でした。自分でプルリクをだして対応させるという方法もあったのですが、あまり時間がないのと、そんなに難しいことはしなくてよかったのでサクッと自分で実装することにしました。ただcontext_processorsを使ったことなかったのでdocumentを読むところからはじめました。めちゃ簡単でした。

django-mobileはmiddlewareもありますが、今回は必要なかったのでcontext_processorsだけ勉強しました。

# context_processorsとは
viewから直接自分で渡さなくてもテンプレート上で変数を使えるようにするものです。

## 実装
デフォルトだとsettings.pyのTEMPLATESは（たぶん）下記のようになっていると思います。



```py3
TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [],
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
            ],
        },
    },
]
```

以下、django/templates/context_processors.pyの中身です。長いので途中は省略してます。

```py3:django/templates/context_processors.py

"""
A set of request processors that return dictionaries to be merged into a
template context. Each function takes the request object as its only parameter
and returns a dictionary to add to the context.

These are referenced from the 'context_processors' option of the configuration
of a DjangoTemplates backend and used by RequestContext.
"""

from __future__ import unicode_literals

import itertools

from django.conf import settings
from django.middleware.csrf import get_token
from django.utils.encoding import smart_text
from django.utils.functional import SimpleLazyObject, lazy


def csrf(request):
    """
    Context processor that provides a CSRF token, or the string 'NOTPROVIDED' if
    it has not been provided by either a view decorator or the middleware
    """
    def _get_val():
        token = get_token(request)
        if token is None:
            # In order to be able to provide debugging info in the
            # case of misconfiguration, we use a sentinel value
            # instead of returning an empty dict.
            return 'NOTPROVIDED'
        else:
            return smart_text(token)

    return {'csrf_token': SimpleLazyObject(_get_val)}

"""
途中省略
"""

def request(request):
    return {'request': request}

```


上記のTEMPLATESの`context_processors`に関数のパスを指定してあげるとその関数をtemplate内で呼び出し、その戻り値をtemplate上で使えるようになります。viewからtemplateに明示的に渡す必要はありません。

ここで注意が必要なのは、`context_processors`で`hoge`という関数を作ってもhogeをtemplateで呼び出せるわけではないことです。
viewからtemplateに変数を渡すときと同様、dictで欲しい名前の変数と中身を指定して返すようになっており、そのdictのキーをtemplateで参照できる形になっています。したがって、下記の関数hogeをcontext_processorsに含む場合、template上ではfugaを参照し、文字列fugafugaがテンプレート上で使えることになります。

定義

```py3
def hoge(request):
    return {"fuga": "fugafuga"}
```

参照

```
{{ fuga }}
```

したがって、デフォルトで用意されているcsrfについてはcsrfではなく`django/templates/context_processors.py`にあるようにcsrf_tokenを呼び出すことになります

```
{% csrf_token %}
```

### 注意
上記のsettings.pyでは`'django.template.context_processors.csrf',`を読み込んでいませんが、csrfについては特別扱いとなっており、下記の場所でデフォルトで読み込まれるように設定されています。

```py3:django/template/context.py

# Hard-coded processor for easier use of CSRF protection.
_builtin_context_processors = ('django.template.context_processors.csrf',)

```

```py3:django/template/engine.py
class Engine(object):
    """
    省略
    """

    @cached_property
    def template_context_processors(self):
        context_processors = _builtin_context_processors
        context_processors += tuple(self.context_processors)
        return tuple(import_string(path) for path in context_processors)
    """
    省略
    """

```


## 自分で実装してみる

冒頭に書いたように、django-mobileと同じようなことをしたいので、request内に含まれるユーザーエージェントの情報からスマホかどうか判断し、True or Falseを返すcontext_processorを追加します。

作ったものは下記のとおりです。
メチャ簡単ですw

```py3:app/context_processors.py

import re
MOBILE_REGEXP = re.compile(r"Android|webOS|iPhone|iPad|iPod|BlackBerry|Windows Phone|IEMobile|Opera Mini|WILLCOM")

def is_mobile(request):
    is_mobile = False
    if MOBILE_REGEXP.search(request.META['HTTP_USER_AGENT']) is not None:
        is_mobile = True
    return {"is_mobile": is_mobile}

```

`app.context_processors.is_mobile`をTEMPLATESの`context_processors`に追加します。

```py3:settings.py
TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [],
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
                'app.context_processors.is_mobile',  # これ
            ],
        },
    },
]
```

そして下記のようなかんじで使います。

```html
{% if is_mobile == True %}
<p>モバイルからのアクセスです</p>
{% else %}
<p>PCからのアクセスです</p>
{% endif %}
```

## まとめ
context_processorの追加はとても簡単なことがわかりました。
実装のやり方しか調べなかったので、時間があれば内部の動作もちゃんと勉強したいです。


# 最後に
Djangoのアドベントカレンダーがここまで埋まると思っておらず、とても驚きました。複数日担当していただいた方も多く、本当にありがとうございました！

僕自身はまだ初心者レベルで基本的な投稿が多かったので、来年の今頃はもう少しハイレベルなことを書けるといいなぁと思います！（会社的には僕がバリバリ開発してるのは良いことではないですが...！）

来年、今年よりもDjangoが日本でも盛り上がるように祈りつつ、アドベントカレンダー最終日をしめたいと思います。それでは皆様、クリスマスをお楽しみください！！（吐血）＆良いお年を！！！