Python Django チュートリアル(2)


勉強会用資料です．
django1.8のチュートリアルを辿りつつ説明していきます．
https://docs.djangoproject.com/en/1.8/intro/tutorial02/

日本語の公式ドキュメントはバージョン1.4が最新なので若干違いが有りますが大まかな流れは一緒なので一読するのもいいと思います．
http://django-docs-ja.readthedocs.org/en/latest/intro/tutorial02.html


[チュートリアル1](http://qiita.com/maisuto/items/d01e2cca63c8e5b2c18c)
→ [チュートリアルまとめ](http://qiita.com/maisuto/items/17653f344d1f64afa019)

## 概要

今回はdjangoの管理サイトについて説明していきます．
djangoには標準で管理サイトが用意されており，スタッフ権限を持つユーザはここからモデルを操作することができます．
djangoが標準で持っているユーザモデルを始め，自分が追加したモデル
（例えばチュートリアルで作成した投票用の2つのモデル）も半自動でそれっぽいページを作ってくれます．

データを登録する，一覧表示する，検索する，のようなデータを扱うだけのwebアプリなら
このページをカスタマイズしていくだけで簡単に作成できます．


## スーパーユーザの用意

ドキュメント→https://docs.djangoproject.com/en/1.8/intro/tutorial02/#creating-an-admin-user
ソース→`add_app_4_database_migration`タグ （変更なし）


管理サイトを触るにはスタッフ権限を持つユーザが必要です．
django用のスーパーユーザは`createsuperuser`コマンドを使って作成します．


```bash
$ ./manage.py createsuperuser
Username (leave blank to use 'shimomura'): admin
Email address:
Password:
Password (again):
Superuser created successfully.
```

createsuperuserコマンドを実行するとUsername, Email, Passwordを入力するよう求められます．
UsernameはdefaultだとPCのusernameがそのまま入ります．今回は`admin`にしています．
Emailは空でも大丈夫です．
Passwordは同じ文字列を2回入力してください．

最後に `Superuser created successfully.` と出ると完了です．


## 管理サイトにアクセス

ドキュメント→https://docs.djangoproject.com/en/1.8/intro/tutorial02/#start-the-development-server
ソース→`add_app_4_database_migration`タグ → `954ba86`

スーパーユーザを作ったらテスト用サーバを起動させて管理サイトにアクセスしてみましょう．
サーバの起動は`runserver`コマンドです．

```bash
$ ./manage.py runserver
Performing system checks...

System check identified no issues (0 silenced).
November 05, 2015 - 07:37:15
Django version 1.8.5, using settings 'tutorial.settings'
Starting development server at http://127.0.0.1:8000/
Quit the server with CONTROL-C.
```

この状態でブラウザで
http://localhost:8000/admin/ 
にアクセスすると以下のようなログイン画面にリダイレクトするはずです．

![Kobito.b0bz7R.png](https://qiita-image-store.s3.amazonaws.com/0/58048/ee3a2f8c-6a4d-99c3-2e1d-2781cde694a4.png "Kobito.b0bz7R.png")


無事ログインできると以下のような画面になります．
デフォルトでは認証用のモデル（GroupsとUsers)だけ表示されています．

![Kobito.WQw8Oi.png](https://qiita-image-store.s3.amazonaws.com/0/58048/34e32e24-552a-5d0b-a617-a6bb27ed2542.png "Kobito.WQw8Oi.png")

--

### 日本語化
無事サイトは表示できたと思いますが，ご覧のように管理サイトはデフォルトでは英語です．
英語は嫌なので日本語にしましょう．

djangoはもちろん多言語対応してます．
表示する言語はsettings.pyの103行目付近で定義されています．
デフォルトでは`en-us`になっているので，これを`ja`に変更しましょう．


```python:settings.py_103行目付近
# LANGUAGE_CODE = 'en-us'
LANGUAGE_CODE = 'ja'

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

直したら管理サイトにもう一度アクセスしてみてください．
http://localhost:8000/admin/

![Kobito.dVaOZw.png](https://qiita-image-store.s3.amazonaws.com/0/58048/93d7c52d-5e0e-6526-9573-e896aeb5223a.png "Kobito.dVaOZw.png")

無事日本語化されたようです．

## 投票モデル追加
ドキュメント→https://docs.djangoproject.com/en/1.8/intro/tutorial02/#make-the-poll-app-modifiable-in-the-admin
ソース→`954ba86`→ `35ec2c6`

無事管理サイトは見えたところで，今度は自前のモデルも表示させましょう．
管理サイトに表示するモデルの設定やカスタマイズは`app/admin.py`に記述します．

今回はチュートリアルにならってpollsアプリのQuestionモデルを追加します．

```python:polls/admin.py
from django.contrib import admin

from .models import Question


admin.site.register(Question)
```

これで管理サイトで表示されるようになったはずです．
      
![Kobito.Q3fesY.png](https://qiita-image-store.s3.amazonaws.com/0/58048/513e2171-8f21-e4f7-91fa-fda3ec651d3a.png "Kobito.Q3fesY.png")

## データの追加とカスタマイズ

### データの追加

データの追加も試してみましょう．
追加リンクを押すとデータ追加用のフォームが表示されます．
Questionモデルは`question_text`と`pub_date`の2つのフィールドを持っていたので，
フォームの表示にも2つの項目が表示されます．

![Kobito.24CSrL.png](https://qiita-image-store.s3.amazonaws.com/0/58048/17479e31-a242-6aa2-7d00-14a1c1832841.png "Kobito.24CSrL.png")


![Kobito.dMucDK.png](https://qiita-image-store.s3.amazonaws.com/0/58048/f789fdab-42c8-c67a-0a68-d170e3b72ac3.png "Kobito.dMucDK.png")

登録すると`Question object`というデータが1つできているはずです．
もう一回登録すると`Question object`が2つ表示されます．

![Kobito.eBQJpR.png](https://qiita-image-store.s3.amazonaws.com/0/58048/5fbc6c27-848b-f10d-6fa6-dc89905b3cd6.png "Kobito.eBQJpR.png")

### オブジェクト名のカスタマイズ

このままだとなんのデータかわからないので，objectを表す文字列を変更しましょう．
変更の仕方は，変更したいモデルの`__str__`メソッド（python2系の場合は`__unicode__`)をオーバーライドするだけです．
今回は質問内容を表示させるように修正します．

```python:polls/models.py
class Question(models.Model):
    question_text = models.CharField(max_length=200)
    pub_date = models.DateTimeField('date published')

    def __str__(self):
        return self.question_text
```

![Kobito.ilTlCf.png](https://qiita-image-store.s3.amazonaws.com/0/58048/fd13b183-d481-298f-51c0-3a84b10b1d32.png "Kobito.ilTlCf.png")


### モデル名のカスタマイズ

モデル自体を表す文字列（この場合はQuestion）を変えたい場合は<b>Meta</b>クラスで設定します．
`verbose_name`でそのモデルを表す名前，
`verbose_name_plural`でそのモデルの複数形を表す名前を設定できます．

```python:polls/models.py
class Question(models.Model):
    class Meta:
        verbose_name = '質問'
        verbose_name_plural = '質問の複数形'

    question_text = models.CharField(max_length=200)
    pub_date = models.DateTimeField('date published')

    def __str__(self):
        return self.question_text
```

![Kobito.r2Xvyz.png](https://qiita-image-store.s3.amazonaws.com/0/58048/0845113d-fa06-36d5-061f-c56a87cceb06.png "Kobito.r2Xvyz.png")

`Question`, `Questions` が `質問`, `質問の複数形` に変わってるのを確認してください．

### 並び順のカスタマイズ

デフォルトでは登録の降順（新しいものが上に来る）ようになっていますがこれを公開日(pub_date)の新しいものから並べるように変更してみましょう．

変更はモデルのMetaクラスの`ordering`プロパティで設定します．
設定値はフィールド名，降順にしたい場合は頭に`-`をつけます．

```
class Question(models.Model):
    class Meta:
        verbose_name = '質問'
        verbose_name_plural = '質問の複数形'
        ordering = ['-pub_date']

    question_text = models.CharField(max_length=200)
    pub_date = models.DateTimeField('date published')

    def __str__(self):
        return self.question_text
```

まず質問内容順で並べ，同じ場合は公開日の降順で並べる場合は
`ordering = ['question_text', '-pubdate']`
のように書きます．
実際に並び変わることを確認してみてください．


--

>追加時のフォーム自体をカスタマイズすることも可能です．（2つ下の節で説明します）
本家のチュートリアルではこの後Questionオブジェクトを追加する際の
Formのデザインの変え方を説明していますが，今回は説明を割愛します．
興味のある方は本家のチュートリアルを見てください．
https://docs.djangoproject.com/en/1.8/intro/tutorial02/#customize-the-admin-form

## relationのあるオブジェクト

ドキュメント→https://docs.djangoproject.com/en/1.8/intro/tutorial02/#adding-related-objects
（途中から`Inline項目の追加`で説明）
ソース→ `35ec2c6` → `84f1086`

pollsアプリが持っているもう一つのモデルである`Choice`を追加していきます．
このモデルはForeignKey（Questionモデルへのリレーション）を持っていますが，どのように表示されるでしょうか？

まずはadminに表示させるために`polls/admin.py`を編集してChoiceを追加します．

```python:polls/admin.py
from django.contrib import admin

from .models import Question
from .models import Choice  # ここを追加


admin.site.register(Question)
admin.site.register(Choice)  # ここを追加
```


![Kobito.yD8Cfw.png](https://qiita-image-store.s3.amazonaws.com/0/58048/277e91c6-f36a-f6a3-4193-a8cef5fa038b.png "Kobito.yD8Cfw.png")

無事追加されたようです．
ChoiceモデルはMeta設定などをしてないのでそのまま出てますね．
続いて追加画面を見ていきます．

![Kobito.dfblb5.png](https://qiita-image-store.s3.amazonaws.com/0/58048/cc8b3f63-a763-c408-4165-18775338d90d.png "Kobito.dfblb5.png")

ForeignKeyフィールドの場合，Formではこのようにセレクトリストで表示されます．
右側にある緑色のプラスボタンを押すことでQuestionオブジェクトを新しく作ることもできます．

>![Kobito.DcXGUI.png](https://qiita-image-store.s3.amazonaws.com/0/58048/e9af6165-8e03-a17e-db42-c179ad5bcefb.png "Kobito.DcXGUI.png")

>内容はこんな感じで先ほど作成したQuestionモデルのオブジェクトが入っています．
`__str__`を設定していない場合はここで3つとも`Question object`と表示されて悲しいことになります．
もちろんこの選択肢も何を出すかも自由にカスタマイズできます．
並び順はモデルのMetaクラスに設定されている`ordering`で並び替えられます．


# Adminサイト用モデルカスタマイズ

このように`admin.site.register`関数でモデルを登録するだけで自動的に色々やってくれますが，
少し手を加えるだけでFormの内容変更やリストの表示，検索項目の追加など色々カスタマイズすることができます．

カスタマイズするときは`admin.site.reigster`関数の第2引数にAdminサイトでの振る舞いを記述したクラスを渡します．
このクラスは`admin.ModelAdmin`クラスを継承し，必要な箇所だけ自分でオーバーライドして作ります．
文章だけだとわかりにくいので実際にそれぞれのカスタマイズ例を示していきます．

## Inline項目の追加
ドキュメント→https://docs.djangoproject.com/en/1.8/intro/tutorial02/#adding-related-objects
ソース→ `84f1086`→`e4bacf7`


ここまでの例ではQuestionとChoiceを追加したい場合，まず元になるQuestionを作成し，
それをChoiceの追加ページで選択する必要がありました．
ChoiceでいちいちQuestionを選択するのはめんどうですし，選択間違いをする可能性もあります．

そこで便利なのがInlineFormです．これを使えばリレーション元のオブジェクト（この場合はQuestion）と
リレーション先のオブジェクト（この場合はChoice)を同時に作成することができます．

adminサイトの場合，InlineAdminというInline表示用のクラスが用意されています．
今回は`StackedInline`クラスを使用します．

修正したソースコードは以下のようになります．

```python:polls/admin.py
from django.contrib import admin

from .models import Question
from .models import Choice


class ChoiceInline(admin.StackedInline):
    model = Choice
    extra = 3


class QuestionAdmin(admin.ModelAdmin):
    inlines = [ChoiceInline]

admin.site.register(Question, QuestionAdmin)
admin.site.register(Choice)
```

`ChoiceInline`, `QuestionAdmin`の定義を追加し，Questionを登録する際の第2引数に
`QuestionAdmin`を渡すように修正しています．

`admin.StackedInline`はStacked形式のInlineを作成するためのベースクラスで，
対象となるモデル（この場合はChoice）と表示数（extra=3）を設定します．
他にも最大数，削除可能かどうかなどを設定できます．

`admin.ModelAdmin`は管理サイトでのモデルの振る舞いをカスタマイズするためのクラスです．
今回はinlinesを設定し，オブジェクトの追加，編集時の振る舞いを変更しています．

ここまで変更して，再度管理サイトのQuestionの追加ページ（もしくは編集ページ）にいくと
Choicesを同時に編集できるようになっているのが確認できると思います．

![Kobito.3GQrd4.png](https://qiita-image-store.s3.amazonaws.com/0/58048/e359b738-50bd-1259-0670-f76d18c91d12.png "Kobito.3GQrd4.png")


## オブジェクト表示のカスタマイズ

ドキュメント→https://docs.djangoproject.com/en/1.8/intro/tutorial02/#customize-the-admin-change-list
ソース→`e4bacf7`→`7f5128a`

登録や編集はいい感じになってきたので，今度は一覧表示をカスタマイズしましょう．
表示を変えるには`admin.ModelAdmin`を継承して作ったクラス(`QuestionAdmin`)にlist_displayを設定します．

```python:polls/admin.py
...
class QuestionAdmin(admin.ModelAdmin):
    inlines = [ChoiceInline]
    list_display = ('question_text', 'pub_date')
...
```

![Kobito.2sV4uA.png](https://qiita-image-store.s3.amazonaws.com/0/58048/d1c64902-1def-78b3-eaf6-0c1da6f2772e.png "Kobito.2sV4uA.png")

殺風景だったリストが少し賑やかになりました．
全てのモデルはdjangoが自動的に主キーを付けてくれます．
ついでなのでそれもつけてみましょう．

```python:polls/admin.py
...
class QuestionAdmin(admin.ModelAdmin):
    inlines = [ChoiceInline]
    list_display = ('pk', 'question_text', 'pub_date')
...
```

![Kobito.MR1ddC.png](https://qiita-image-store.s3.amazonaws.com/0/58048/4906edbb-b6f2-b252-e636-1df559efbdb6.png "Kobito.MR1ddC.png")


いい感じですね．

### もっとカスタマイズ
本家チュートリアルにならって少し特殊なこともしてみましょう．
list_displayにはフィールドの他にも引数なしのメソッドを設定することができます．

Questionモデルに質問が1日以内に公開されたものか確認する`was_published_recently`メソッドを追加します．

```python:polls/models.py
import datetime

from django.db import models
from django.utils import timezone


class Question(models.Model):
...

    def was_published_recently(self):
        return self.pub_date >= timezone.now() - datetime.timedelta(days=1)
...
```

次にlist_displayにこのメソッドを追加します．

```python:polls/admin.py
...
class QuestionAdmin(admin.ModelAdmin):
    inlines = [ChoiceInline]
    list_display = ('pk', 'question_text', 'pub_date', 'was_published_recently')
...
```

![Kobito.gpHA62.png](https://qiita-image-store.s3.amazonaws.com/0/58048/18913e1a-98c1-c493-ab22-f8e9ede63346.png "Kobito.gpHA62.png")

ちゃんと見えてますね．
Questionに紐付いているChoiceの数を返すメソッドなどを作ってみるとおもしろいかもしれません．

### さらにカスタマイズ

list_displayの対象がメソッドの場合，メソッドに属性を追加することで表示の変更やオーダ条件の設定が可能です．
本家チュートリアルに習って`admin_order_field`, `boolean`, `short_description`を設定してみましょう．

```python:polls/models.py
...
    def was_published_recently(self):
        return self.pub_date >= timezone.now() - datetime.timedelta(days=1)
    was_published_recently.admin_order_field = 'pub_date'
    was_published_recently.boolean = True
    was_published_recently.short_description = 'Published recently?'
...
```

![Kobito.qOD9IC.png](https://qiita-image-store.s3.amazonaws.com/0/58048/7e930461-f3c0-8659-f4ed-15cff7f351ea.png "Kobito.qOD9IC.png")

とても良い感じになりました．
`admin_order_field`は並び替えをする時にどのフィールドを見るかを設定しています．
この例では`pub_date`フィールドで並び替えています．
この属性を設定することでテーブルのヘッダをクリックできるようになり，並び替えができるようになります．

### フィルター，検索追加

最後にフィルターと検索を追加してみます．
例のごとくQuestionAdminにプロパティを設定するだけです．

```python:polls/admin.py
...
class QuestionAdmin(admin.ModelAdmin):
    inlines = [ChoiceInline]
    list_display = ('pk', 'question_text', 'pub_date', 'was_published_recently')
    list_filter = ['pub_date']
    search_fields = ['question_text']
...
```

![Kobito.SrObQZ.png](https://qiita-image-store.s3.amazonaws.com/0/58048/feb4e0bd-cf27-ad93-4f24-0ba3a992b7d5.png "Kobito.SrObQZ.png")

こんな感じで簡単にフィルター，検索を追加することができます．

--
[次のチュートリアルへ] (http://qiita.com/maisuto/items/eece9d880d94fd241a0d)

→ [チュートリアルまとめ](http://qiita.com/maisuto/items/17653f344d1f64afa019)