Djangoでスプレッドシートライクの管理画面を作成する

# はじめに
社内でDjangoを利用しているプロジェクト内で「とあるmodelをスプレッドシート形式で管理したい」という要件があり、当時DjangoのDも分かっているのか曖昧な状態の私が頑張って作成しました。Django Adminも触りだけしか分かっていない状態でしたが、なんとか周りのサポートをいただいて作成することができました。

同じようにDjango Adminでスプレッドシート形式の管理がしたいという方は一読していただけると幸いです。

本記事ではDjangoでスプレッドシートライクなUIを表示するところまで実施してみます。

# 実装にあたって
今回実装補する方法としては以下の3つが候補に上がりました
1. pypiからDjango用のライブラリを利用する
2. Template HTMLを利用して実装する
3. form.formViewの拡張クラスを作成してスプレッドシートをrenderするWidgetクラスを作成する

結論から言うと`2. Template HTMLを利用して実装する`で今回は実装しました。
1は調査した結果見つからず、3は2よりも難易度が高いということで消去法で2になりました。
※強いて言えば1はあったにはあったのですが、有料ライセンスだったため今回は不採用としました。

# 実装方針
.htmlで表示することを考えるとjsライブラリが必要になるということでスプレッドシートライクなUIを実装できるライブラリを探したところ、1件良さそうなライブラリがありました。
- jspreadsheet
  https://bossanova.uk/jspreadsheet/
  CDN経由で利用可能 + MITライセンス + カスタマイズ可能という観点からTemplate HTMLにjspreadsheetを組み込んで利用する方針で作成することにしました。

# 実装例
今回は以下のmodelをスプレッドシート上に表示します。

```
class Book(models.Model):
    """
    本を管理するモデル
    """
    title = models.CharField(
        max_length=255, verbose_name='タイトル', null=True, blank=True
        )
    author = models.CharField(
        max_length=255, verbose_name='著者', null=True, blank=True
    )
    count = models.IntegerField(default=0, verbose_name='著者')
```
1. Form/FormViewクラスの作成
Bookクラスに対応するFormクラスは以下になります。
```
class BookForm(forms.Form):
    model = Book

    @classmethod
    def sheet_header(cls) -> str:
        """
        スプレッドシートのヘッダー
        """
        return json.dumps(
            [
                {
                    'title': field.verbose_name,
                    'type': cls.get_type(field),
                    'key': field.name
                }
                for field in cls.model._meta.get_fields()
            ]
        )

    @classmethod
    def get_type(cls, field: Field) -> str:
        """
        スプレッドシートのヘッダータイプ
        """
        if isinstance(field, models.IntegerField):
            return 'numeric'
        else:
            return 'text'

    @classmethod
    def sheet_data(cls) -> str:
        """
        スプレッドシートのデータ
        """
        data = list(Book.objects.all().values())
        return json.dumps(data)

      def is_valid(self):
        """
        バリデーションはここで行う。
        return Trueでform_valid, Falseでform_invalidが実行される。
        """
        return super().is_valid()
```

```
class BookFormView(FormView):
    """
    本の管理クラスを表示するFormView
    """
    form_class = BookForm
    template_name = 'spreadsheet.html'

    def form_valid(self, form):
        """
        バリデーション成功時はここでmodel保存処理を実施
        """
        return super().form_valid(form)

    def form_invalid(self, form):
        """
        バリデーション失敗時はここで失敗時処理を実施
        """
        return super().form_invalid(form)

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context['sheet_data'] = self.get_form_class().sheet_data
        context['sheet_header'] = self.get_form_class().sheet_header

        return context
```

Formクラスでは主にスプレッドシートで表示するヘッダーとデータを整理します。
FormViewクラスはあくまで表示する内容をまとめる役割なので、ヘッダーや表示データの整理はFormクラスで実行するのがベターだと思っています。

保存時処理はform_validに記載してください。
あくまでFormViewを継承したクラスなのでDjangoで勝手に自動変換して保存することはありません。

ヘッダーのjsonやtypeなどについてはドキュメントを参照してください。
https://bossanova.uk/jspreadsheet/docs/getting-started

2. Template HTMLの作成
```
{% extends 'admin/app_index.html' %}

{% block content %}
<script src="https://bossanova.uk/jspreadsheet/v5/jspreadsheet.js"></script>
<script src="https://jsuites.net/v5/jsuites.js"></script>
<link rel="stylesheet" href="https://bossanova.uk/jspreadsheet/v5/jspreadsheet.css" type="text/css" />
<link rel="stylesheet" href="https://jsuites.net/v5/jsuites.css" type="text/css" />

<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Material+Icons" />

<div id='spreadsheet'></div>

<script>
const data = {{ sheet_data | safe }}
const header = {{ sheet_header | safe }}
console.log(data)
console.log(header)
// Create a new spreadsheet
jspreadsheet(document.getElementById('spreadsheet'), {
    worksheets: [{
        data: data,
        columns: header
    }]
});
</script>
{% endblock %}
```

FormViewクラスと連携するTemplate HTMLです。
data, columns(header)のデータはFormViewのget_context_data経由で取得します。
上記はあくまで表示するのみですので、保存ボタンは別途作成して必要に応じてform.submit経由でデータをDjango側に渡してください。

3. admin.pyにAdminクラスを作成
```

@admin.register(Book)
class BookAdmin(admin.ModelAdmin):
    list_display = ('title', 'author', 'count')

    def get_urls(self):
        urls = super().get_urls()

        extra_urls = [
            path(
                'spreadsheet/',
                self.admin_site.admin_view(BookFormView.as_view()),
                name='book_spreadheet'
            )
        ]

        return extra_urls + urls
```

get_urlsを利用して自身で作成したFormクラスを管理画面で表示できるようにします。
上記一通り実施することで`admin/アプリ名/book/spreadsheet/`で表示できるようになります。

実際に実行した画像がこちらになります。

![スクリーンショット 0007-03-16 20.46.39.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3848371/637272bf-69aa-4d33-a71b-d2181626ecee.png)


# 最後に
今回はDjangoの管理画面でスプレッドシートライクの画面を表示するところまで実施しました。
実際に編集して送信する場合については使用用途で色々変わってくるので、今回は記載しません。（いずれ書くかもしれません。）
他にも差分チェックやエラーチェックを実装するとなるとかなり（面倒）手間がかかるので、頑張ってください。