Djangoのモデル定義をreSTやMarkdownで出力したい

## 概要

- https://github.com/tell-k/django-modelsdoc
- Djangoのモデル定義一覧を reStrucutedText や Markdownで出力できるライブラリを作って公開しています。
- 社内で使ってくれてる人が1名だけ確認できたので、つらつら書いてみようと思います。

## サンプル

例えば下記URLのようなモデルあるとします

```python
# refs https://github.com/tell-k/django-modelsdoc/blob/master/tests/models.py

from django.db import models
from django.contrib.auth.models import User

class Poll(models.Model):
    """ Poll

    * Poll has question and description fields
    """

    question = models.CharField('Question Name', max_length=255)
    description = models.TextField('Description', blank=True)
    """ Description field allows Blank """

    null_field = models.CharField('Null Test', null=True, max_length=255)
    blank_field = models.CharField('Blank Test', blank=True, max_length=255)
    both_field = models.CharField('Both Test',
                                  null=True, blank=True, max_length=255)
    index_field = models.CharField('Index Test', db_index=True, max_length=255)

    class Meta:
        verbose_name = 'Poll'
```

`listing_models` のコマンドを叩くと 下記のようなモデル定義がreSTやMarkdownのテキストとして手に入ります。

![](https://dl.dropboxusercontent.com/spa/ghyn87yb4ejn5yy/8603722b2b484cdcb355d013615ef1c2.png)

全体的にどうなるかは下記のサンプルファイルをご覧ください。

- https://github.com/tell-k/django-modelsdoc/blob/master/tests/sample_models.rst
- https://github.com/tell-k/django-modelsdoc/blob/master/tests/sample_models.md

## 動機

作った動機は以下のような感じです。

- Djangoプロジェクトの引き継ぎ時にモデルの説明する時に一覧があると良い
- `schema2rst` という素晴らしいライブラリを使っていた。
  - `schema2rst`はDBから直接テーブル定義をreSTで抽出してくれる
  - http://tk0miya.hatenablog.com/entry/2013/12/12/012054

- But, Djangoのマイグレーション機能では**カラム/テーブルコメントとかをDBには反映できない**
- つまりカラムに対する注意書きや、カラムにどんな値が入るかとかを出力できない。

もう面倒なのでDBからではなくDjangoのモデル定義を取ってきていい感じに出力しようと思った次第

## 工夫したところ

### Fieldのdocstringを抽出できるようにした

- 上記にも説明した通り **DBのテーブル/カラムコメント** に対応する データが DjangoのFieldには存在しない。
- `help_text` というものがあるが、これは編集画面等で提示する文章なので、開発者向けの説明をいれる場所ではない。
- なので **Field定義のdocstring** を拾って出力できるようにしました。

``` python
description = models.TextField('Description', blank=True)
""" Description field allows Blank """  # <- この文書をコメント列として出力する
```

- 上記のような docstring は Sphinxの中にある `sphinx.pycode.ModuleAnalyzer` というクラスを利用すると抽出することが可能なので利用しました。 (ミニマムに再実装したかったけど結構なヘビーな感じですぐにはできなそうだったので断念)

### ChoicesやForeignKeyの情報も表示できるようにした

- ChociesやForeignKeyがFieldに設定されていればそれをよしなに表示できるようにしました。

![](https://dl.dropboxusercontent.com/spa/ghyn87yb4ejn5yy/2fa17ed56fe04b64b7ac2b3adfcdc7a3.png)




## 使い方

基本的な使い方は[README](https://github.com/tell-k/django-modelsdoc/blob/master/README.rst)をみてもらうとして、settings.py でカスタマイズできる内容を紹介します。

### 出力対象のアプリを絞りたい

- 例えば、特定のアプリのモデルだけを表示したいとかいう場合には MODELSDOC_APPS という設定を設定します。
- 出力されるアプリの順番もこれで変えることが可能です。
- 下記のように書くと、pollsアプリのモデルしか出力されません。

```
MODELSDOC_APPS = (polls,)
```

### 出力される列の順番/項目を変えたい

- MODELSDOC_DISPLAY_FIELDS という設定を settings.py 書いてください。
- デフォルトは以下のようになっています。
- 順番と項目を定義できます。上に書いたものが左から列として出力されます。

```python
# settings.py

MODELSDOC_DISPLAY_FIELDS = (
    ('Fullname', 'verbose_name'),
    ('Name', 'name'),
    ('Type', 'db_type'),
    ('PK', 'primary_key'),
    ('Unique', 'unique'),
    ('Index', 'db_index'),
    ('Null/Blank', 'null_blank'),
    ('Comment', 'comment'),
)
```

- Django の `Field` オブジェクトが持っている属性値であれば、大概なんでもいけると思います。
- 中身は getattr で該当の属性をひっぱているだけです。

```python
getattr(field_obj, 'versbose_name')
```

### モデルの制約やオプションの表示内容の変更

- MODELSDOC_MODEL_OPTIONSを設定してください
- ModelのMetaに設定しているような情報を引っ張って出力してくれます。
- 例えば以下のような項目があります。

```
MODELSDOC_MODEL_OPTIONS = (
    'unique_together',
    'index_together',
    'ordering',
    'permissions',
    'get_latest_by',
    'order_with_respect_to',
    'db_tablespace',
    'abstract',
    'swappable',
    'select_on_save',
    'default_permissions',
    'default_related_name'
)
```

### Djangoが自動で作るモデル群は除外したい

- ManyToManyField で自動的に作られる中間テーブル用のモデル
- Djangoが最初から用意してるモデル群
- これらのモデル群は、自分で定義したわけではないので出力してほしくない場合もあります
- Djangoは内部的にこれらのモデル群を「自動で作られるモデル」として判別しています。
- それを利用して自動で作られたモデル群を除外しています。
- 下記のように設定を False にしてもらえれば除外されて出力されます。

```python
MODELSDOC_INCLUDE_AUTO_CREATED = False
```

## ModelやFieldに追加で属性を加えたい

- ModelやFieldのオブジェクトはそのまま使っているわけでなく、それぞれをラップするクラスを用意しています。

```python
MODELSDOC_MODEL_WRAPPER = 'modelsdoc.wrappers.ModelWrapper'
MODELSDOC_FIELD_WRAPPER = 'modelsdoc.wrappers.FieldWrapper'
```

- デフォルトでは上記のようになってますが、好きなクラスに差し替えることができます。
- 例えば下記のようにカスタムクラスを作って設定すると、MODELSDOC_DISPLAY_FIELDSの項目として使えるようになります


```python
from modelsdoc.wrappers import FieldWrapper

class MyFieldWrapper(FieldWrapper):

  @property
  def unique_ja(self):
      return '唯一無二項目' if self.unique else ''
```

```python
MODELSDOC_FIELD_WRAPPER = 'path.to.MyFieldWrapper'

MODELSDOC_DISPLAY_FIELDS = (
    ('Fullname', 'verbose_name'),
    ('Name', 'name'),
    ('Type', 'db_type'),
    ('PK', 'primary_key'),
    ('Unique', 'unique'),
    ('ユニーク(日本語)', 'unique_ja'),  <- 追加
    ('Index', 'db_index'),
    ('Null/Blank', 'null_blank'),
    ('Comment', 'comment'),
)
```

## テンプレートをカスタマイズしたい

- Djangoのテンプレートをオーバライドする機能が使えます。
- https://docs.djangoproject.com/en/1.11/howto/overriding-templates/
- テンプレートのパスが通ってるディレクトリに以下のようなテンプレートを作るだけです。

```
modelsdoc/models.md
modelsdoc/models.rst
```

- 内容はgithubの元テンプレートをみながらカスタマイズしてください。

- https://github.com/tell-k/django-modelsdoc/blob/master/modelsdoc/templates/modelsdoc/models.rst
- https://github.com/tell-k/django-modelsdoc/blob/master/modelsdoc/templates/modelsdoc/models.md

- カスタマイズする時は以下のような点に注意してください
 - reST や Markdown は **空行** が入るとうまく認識してくれません
 - 一方でDjangoのテンプレートタグを使うと不要に改行が入ったりする。
 - なので空行をよしなに削除する `emptylineless ` というテンプレートタグを用意しています。

```
{% load modelsdoc_tags %}

{% emptylineless %}
{% if hoge %} <- ここの空行が消える
- hogehoge
{% endif %} <- ここの空行が消える
{% endemptylineless %}
```

## 今後の展望

- ていう話をちゃんとドキュメント化したい
- Django2.0 対応 -> 昨日終わった
- コメントをDBに一括で反映するコマンドを用意したい
- [plantUML](http://plantuml.com/)でER図的なものを出力できるようにしたい。

以上おわり