Djangoの備忘録


1年くらい前にDjangoで少しアプリ書いたのですが忘れてしまったので備忘録的に残しながら思い出していきます。日本語訳のチュートリアルが1.4とかなり古いリリースのものしか無いため公式ドキュメントの最新のチュートリアルを見比べてましたが、[こちら](http://qiita.com/kaki_k/items/511611cadac1d0c69c54)の記事が大変参考になったと記憶しています。

すでに1.8が出てますが、前回1.7でやったので1.7でやってきます。

## この記事での環境

```bash
[sayamada@~]$cat /etc/lsb-release 
DISTRIB_ID=Ubuntu
DISTRIB_RELEASE=12.04
DISTRIB_CODENAME=precise
DISTRIB_DESCRIPTION="Ubuntu 12.04.5 LTS"
[sayamada@~]$uname -a
Linux XXXXXX 3.13.0-65-generic #106~precise1-Ubuntu SMP Fri Oct 2 22:07:14 UTC 2015 i686 i686 i386 GNU/Linux
[sayamada@~]$python -V
Python 2.7.9 :: Anaconda 2.0.1 (32-bit)
[sayamada@~]$pip list |grep -i django
Django (1.7.1)
django-bootstrap-form (3.1)
```

社用PCではrootがとれないので、Anacondaでお茶を濁してます。普通の方は

```bash
$ pip install django==1.7.1
```

でいいんじゃないでしょうか。

### プロジェクトつくる

WEBサイト1個が1プロジェクトに相当するんですかね。とりあえずまずプロジェクト作りました。

```bash
[sayamada@git]$django-admin.py startproject demoPrj
[sayamada@git]$tree demoPrj/
demoPrj/
├── demoPrj
│   ├── __init__.py
│   ├── settings.py --設定とか書く奴
│   ├── urls.py  -- 一番上のルーティング書く奴
│   └── wsgi.py
└── manage.py -- 管理コマンド用の実行スクリプト
```

### バックエンドDBの変更
デフォルトでsqliteで動くみたいですが、職業柄PostgreSQLが好きなので変更します。**pyscopg2**いれないといけなかった気がします。PostgreSQLが好きじゃない人はそのままsqliteでいいと思います。

```python:settings.py
DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.postgresql_psycopg2',
        'NAME': 'DB名',
        'USER': 'ユーザー名',
        'PASSWORD': '',
        'HOST': 'localhost',
        'PORT': '5432'
    }
}
```

### ロケールの変更
日本人なので変えておきます。

```python:settings.py
# LANGUAGE_CODE = 'en-us'
LANGUAGE_CODE = 'ja'

# TIME_ZONE = 'UTC'
TIME_ZONE = 'Asia/Tokyo'
```

### アプリケーションの作成

サイト(プロジェクト)は複数の**アプリケーション**で構成される、みたいです。機能ごとにアプリをつくって追加する感じ？なんですかね。

```bash
[sayamada@git]$cd demoPrj/
[sayamada@demoPrj]$python manage.py startapp demoApp
[sayamada@demoPrj]$tree 
.
├── demoApp --このディレクトリが増えた
│   ├── __init__.py
│   ├── admin.py
│   ├── migrations
│   │   └── __init__.py
│   ├── models.py -- モデル定義するやつ
│   ├── tests.py 
│   └── views.py -- VIEWというかロジックのエントリポイント書く奴？
├── demoPrj
│   ├── __init__.py
│   ├── __init__.pyc
│   ├── settings.py
│   ├── settings.pyc
│   ├── urls.py
│   └── wsgi.py
└── manage.py

3 directories, 13 files
```

ちなみに、デフォルトでは作られませんが大きなルーティングをdemoPrj/urls.pyに定義してアプリケーションごとのルーティングは個別に設定するものらしいので、**demoApp/urls.py**を手動で作っておきます。


### ユーザ認証
djangoは事前にアカウント管理のためのモデル(**User**)を定義しています。また認証用のミドルウェアも用意されています。今回は認証処理をまとめて**Auth**というアプリケーションでつくておきましたので、INSTALLED_APPSにも追加します。

```python:settings.py
INSTALLED_APPS = (
    'django.contrib.admin',
    'django.contrib.auth', ★これと
    'django.contrib.contenttypes', ★これが必要
:
    'Authctl', ★これはstartappで作った
:
LOGIN_URL = '/auth/login'
LOGIN_REDIRECT_URL = '/auth/home'
:
AUTHENTICATION_BACKENDS = (
    'Authctl.AuthLogic.AuthBackEnd',
    'django.contrib.auth.backends.ModelBackend',
)
```

プロジェクト作った時点で組み込まれています。あとはAUTHENTICATION_BACKENDSに認証用のロジックを指定します。上は自分で作った認証ロジックです。なんで2個いるのかは忘れました。認証が必要とするページのview側は以下にします。今回はAuth配下AuthLogic.pyを作り、その中でAuthBackendというファンクションを定義することにしました。



```python:demoApp/views.py
# coding:utf-8
from django.http import HttpResponse
from django.contrib.auth.decorators import login_required

@login_required
def index(request):
    return HttpResponse(u'インデックス')
```

login_requiredデコレータをつけたメソッドは事前にログイン済みかのチェックが行われます。 ログインしていない場合は**settings.pyのLOGIN_URL**にリダイレクトされます。今回は/authctl/loginとしているので、それを解決できるようにルーティングを設定する必要があります。




### forms.ModelFormでexcludeしたフィールドがある場合

form.save時に大体そこがNOT NULL制約でエラーになる。formのコンストラクタに**instance=**でexcludeした分の属性をもたせたModelのインスタンスを渡せばいける

```python: views.py
any_model_inst = AnyModel()
any_model_inst.excluded_any_ked = u"hoge"
form = AnyForm(request.POST, instance=any_model_inst)
form.save()
```


### デコレータでリクエストオブジェクトを見る

requestオブジェクトどこだっけ、てなやんだ。

```python
# アクセス許可があるAPIアクセスかのチェック用のデコレータ
def check_access_permission(func):
    import functools
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        # 第一引数はrequestオブジェクト
        request = args[0]
        print request.user
        if check_any_func():
            return HttpResponseForbidden()
        # OK
        return func(*args, **kwargs)
    return wrapper
```

### Authorizationヘッダーを見たかった

こういうのを投げた時のヘッダを見たかった

```bash
curl localhost:8000/api/ \
-H "Authorization: demo token" \
-X GET 
```

こうだった

```python
def check_access_permission(func):
    import functools
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        # 第一引数はrequestオブジェクト
        request = args[0]
        # これでとれる
        print request.META.get('HTTP_AUTHORIZATION')
        if False:
            return HttpResponseForbidden()

        return func(*args, **kwargs)
    return wrapper
```

### FormでENUMみたいなことしたかった

RESTで指定される値を絞りたい時にValidationのためだけにFormつくってChoiceFileldが丁度良かった

```python:forms.py
    contact_choice = (
        ("TEL", "TEL"),
        ("Email", "Email"),
        ("FAX", "FAX"),
        ("Web", "Web"),
    )
    contact_way = forms.ChoiceField(label=u"問合せ方法", choices=contact_choice,required=False, initial=None)
```
これで、contact_wayにhogeとか入ってたらis_valid()で死ぬのでいい

### Formでなんでvalid失敗したのか知りたい

[マニュアル](https://docs.djangoproject.com/en/1.8/ref/forms/api/)にちゃんと書いてありました。1.7以降はいろいろ表示させ方もあるみたいです。


```python
if form.is_valid():

    something doing..
else:
        # FromClass.errorsにエラーが入っている
        # as_json()でJSON形式で取り出せる
        print  form.errors.as_json()
```


### Formのinitialがcleaned_dataでは取得されない

```python
sender_name = forms.CharField(label=u'送信者名', required=False, initial=u"default sender")
```

で、こうすると

```python
sender_name = form.cleaned_data["sender_name"] # -> I expect "default sender" but None
```

になってしまいました。

[ここ](http://stackoverflow.com/questions/9069705/form-cleaned-data-is-ignoring-initial-values)を見る感じ、initialは表示上の初期データであり、cleaned_dataでは取得できない模様。form.cleanをオーバーライドする必要があるみたいです。ちょっと動かない箇所があったので最終的には以下のように代えました。

```python
class DisplaySharerForm(forms.Form):
...
    def clean(self):
        cleaned_data = super(DisplaySharerForm, self).clean()
        for key, value in cleaned_data.items():
            if not value:
                cleaned_data[key] = self.fields[key].initial

        return cleaned_data
```