django-fobiによるWeb Form Desginerの利用

# はじめに

単一のFormを作成するだけであれば、Googleフォームのような方法はもはや一般的で、自分でForm作成を必要とする場面は少なくなっていると思います。

通常は、要件に応じてFormを作成すれば良い場合がほとんどで、Formの変更はそれほどの手間ではないと思いますが、複数のFormを作成し、利用する頻度が上がってくると手間になってきます。

* 1サイトの中に複数のFormを設定したい
* 作成開始時点では形式が決まっていない
* セキュリティ上、外部サイトは使いたくない

いろいろ調べてみたのですが、今回は *django-fobi* を使ってみることにしたのでメモを残します。

# 参考情報

* [Pypl - django-fobi](https://pypi.org/project/django-fobi/)

# django-fobiの特徴

django-fobiを利用すると、通常のdjangoのORMフレームワークの中で、Webブラウザから複数のFormを設計することができるようになります。

![20220822_django-fobi概要図.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/78296/45c56ec4-8dc0-b976-2c3e-ce0ff3edd0fc.png)

一方でFormを作成することに特化しているため、起票されたFormの保存などの後処理はPluginに処理が任されています。このため、保存するデータはPluginのModelに変換され、フレームワークから切り離されますので、保存したデータの更新はdjango-fobiの守備範囲外になり、通常のORM機構を利用してデータを利用・変更することになります。

後からデータの更新が必要である要件もそれなりにあると思いますので、Webブラウザから操作できるForm Designerに相当なメリットを感じないのであれば、必要の都度、アプリケーションを追加し、Model、Form/View を作成する方が便利でしょう。

いわゆるワークフロー機能のない、Web Form Designerというのがdjango-fobiの特徴だと思います。

# django-fobiの利用

基本的にはガイドに従って設定を追加すれば利用できます。

ただurls.pyに追加する設定は、後述されているClass Based Viewに変更することをお勧めします。

```python:
# View URLs
url(r'^fobi/', include('fobi.urls.class_based.view')),

# Edit URLs
url(r'^fobi/', include('fobi.urls.class_based.edit')),

# DB Store plugin URLs
url(r'^fobi/plugins/form-handlers/db-store/',
    include('fobi.contrib.plugins.form_handlers.db_store.urls')),
```

大丈夫だと思いますが、公式ガイドの設定例にあるINSTALLED_APPSに追加するモジュールの最後にある*foo*は存在しないはずですので、追加しないよう気をつけてください。

## Formの作成について

「ガイドをどこまで読んでも使い方の説明が始まらない」というのが最初の感想でした。
とりあえずどういう風に使えるか知りたい場合には、デモサイトを利用しましょう。

設定が終ると、公式ガイドの文書はFormを構成するTextAreaなどの部品(element)をどのように作成するのか、記入されたFormの後処理の機構(handler)の作成方法、見栄えを変更するテーマの作成方法、などが紹介されています。

設定が完了した後の、django-fobiの基本的な操作は、/fobi/に進み、*Create Form*からNameを入力して保存し、入れ物を作ります。一旦保存してから、*Edit*に進み、Edit Form画面に進んでから、Textフィールドなどの要素(element)を加えて、使用したいFormを作成していきます。


<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/78296/3309d0b8-b6b2-a325-0d10-afbd81dbd3b9.png" alt="iamge.png"  width="25%" />


この時、Handlerに*db_store*などを指定しないと、入力した内容がどこにも保存されずに消えてしまいます。

# Formデータの活用について

作成したFormを特定・不特定の人達に公開して使ってもらう利用方法が多いと思いますが、ユーザーアクセスのコントロールについては、django-fobi固有の設定はないようです。通常のユーザー情報に付与されるis_staff属性などを利用することはできますが、このフォームはこのメンバーグループに結果を見せる、このフォームはこっちのグループに回答させる、といったコントールには向きません。

/fobi/以下に一般ユーザーをアクセスさせてしまうと、意図せずに他のFormにアクセスしてしまう可能性も発生すると思います。

そのため、作成したFormと閲覧可能なメンバーグループを関連付けるModelを作成することで、admin画面からコントロールできるようにしています。

## 自分のapp内で作成したformを表示する

Formを自分のApp内で表示するのであれば、fobi.views.class_based.ViewFormEntryView を継承すれば可能です。

その一方で、ViewFormEntryViewクラスは最終的に、SubmitされたFormデータをhandlerに渡した後のページ遷移が "fobi.form_entry_submitted" に固定されているため、これを変更するためには、postメソッド全体を上書きする必要があります。

この部分がまったく構造化されていないため、残念ながら長い処理全体をコピーして一部を書き換えるといった処理が必要になり、関連して参照関係を処理しなければいけないので、かなりの from 〜 import 文を追記することになります。

```python:views.py
from fobi.views.class_based import ViewFormEntryView
from django.contrib import messages
from django.utils.translation import gettext_lazy as _
from django.core.exceptions import ObjectDoesNotExist, PermissionDenied
from django.http import Http404
from django.shortcuts import get_object_or_404, redirect, render
from django.urls import reverse_lazy
from fobi.base import (  # get_registered_form_handler_plugins
    fire_form_callbacks,
    form_element_plugin_registry,
    form_handler_plugin_registry,
    form_wizard_handler_plugin_registry,
    get_theme,
    run_form_handlers,
    run_form_wizard_handlers,
    submit_plugin_form_data,
)
from fobi.constants import (
    CALLBACK_BEFORE_FORM_VALIDATION,
    CALLBACK_FORM_INVALID,
    CALLBACK_FORM_VALID,
    CALLBACK_FORM_VALID_AFTER_FORM_HANDLERS,
    CALLBACK_FORM_VALID_BEFORE_SUBMIT_PLUGIN_FORM_DATA,
)
class FormCreateView(ViewFormEntryView):
    def post(self, request, *args, **kwargs):
        ...
            return redirect(
                reverse_lazy(
                    ## "fobi.form_entry_submitted", args=[form_entry.slug]
                    "top"
                )
            )
        ...
```

このクラスへの参照をurlpatternsに加えます。

```python:urls.py
from .views import FormCreateView

urlpatterns = [
    ...
    url(r"^view/(?P<form_entry_slug>[\w_\-]+)/$", FormCreateView.as_view(), name='form_view'),
]
```

このページに適切なFormEntry.slug値と一緒に遷移すれば、Formの入力画面が表示され、適切に処理されます。
ただどんなslug値でも受け入れてしまうと問題が発生しますので、そのユーザーが表示して問題ないか、dispatch()等で検証する必要があります。

## 保存されたFormの内容を表示したい

あらかじめhandlerに*db_store*を指定していれば、Formに入力された情報はSavedFormDataEntryに登録されています。

これはdjango-fobiが管理しているものではなく、plugin側で管理されているため、このデータにアクセスするためにはpluginのModelを通して行ないます。

```python:views.py
from django.views.generic import TemplateView
from fobi.contrib.plugins.form_handlers.db_store.models import SavedFormDataEntry
class LandingView(TemplateView):
    template_name = 'landing.html'

    def get_context_data(self, **kwargs):
        if self.request.user.is_authenticated:
            saved_forms = SavedFormDataEntry.objects.filter(user=self.request.user)
            ...
```

templates/landing.html ファイルの中で参照することができるようになります。

## セキュリティについて

Formを作成した本人の情報はデータに含まれているので、その点は問題ありませんが、ワークフロー機能がないので、そのデータを誰に開示するのか、authorizationの機構はユースケースに応じて適切に検討しなければいけません。

# まとめ

django-fobiに対する情報は日本語ではあまりないようです。
djangoのフレームワークの一般的な機能だけを利用しているので、利用については全体の雰囲気がつかめれば問題になることが少ないからかもしれません。

これを使っていくのであれば、自分のユースケースに応じてhandlerをカスタマイズするのが次の作業になるかなといった感じです。

*db_store*に格納されている情報は、JSONを文字列に変換したものなので、実際にSavedFormDataEntryから取り出したデータにアクセスするためには、json.loads()が必要になります。

```python:概念コード
    for form in SavedFormDataEntry.objects.filter(user=self.request.user).all():
        headers = json.loads(form.form_data_headers)
        saved_data = json.loads(form.saved_data)
```

またアップロードしたファイルは、MEDIA_ROOT直下に全て上書きされないようにrenameされた上で配置されるので、障害時や長期間利用などの場面で問題になるでしょう。