パーソナルツール
現在位置: ホーム 技術ノート Django Formのフィールドを読み取り専用にする

Formのフィールドを読み取り専用にする

背景

 ユーザーの権限が不足している場合などの状況に応じて、フォーム中の特定のフィールドについて変更を許さず、読み取り専用にしたい、というケースがあります。

 このような場合、そのFieldオブジェクトのWidgetの属性として、disabledまたはreadonlyを設定すれば、その属性付きでHTML要素が出力されることになり、とりあえず要求は満たされます。

userform.fields['email'].widget.attrs['disabled'] = 'disabled'

 しかしこれは、ユーザエージェント上で変更できないだけであり、フォームを受け取るDjango Form側ではなにも処理していません。つまり、このフィールドに値を送信をしようと思えばできてしまうし、逆に送信されない場合も考慮していないということです(disabled属性の場合は送信されません)。

 そのため、この方法を使ったとしても、セキュリティを考えるのであれば、読み取り専用のフィールドのデータひとつひとつについて、その点をきちんと取り扱わねばなりません。

 仮にそうしたとしても、必須のフィールドに対して送信がないとバリデーションエラーになってしまう、読み取り専用にしたいフィールドを可変にするには面倒、同じようなコードが随所に現れてしまう可能性がある、といった問題があります。

 そこで、任意のフィールドを読み取り専用にするDjango Formを考えます。

解決方法

 次のような仕様のFormクラスとします。

  • 読み取り専用にするフィールドを簡単にFormに対し設定できる
  • 読み取り専用のフィールドについては、送信値に関わらず初期データを返すようにする

 これを実現したものが以下のReadonlyEnabledFormです。Django 1.1で動作の確認をしています。

 1: class ReadonlyEnabledForm(forms.Form):
 2:     def set_readonly(self, fieldnames=None):
 3:         self.readonly = fieldnames
 4:         self.data = self.data.copy()
 5:         for name, field in self.fields.items():
 6:             if self.readonly is None or name in self.readonly:
 7:                 field.required = False
 8:                 field.widget.attrs['disabled'] = 'disabled'
 9:                 if self.is_bound and name in self.initial:
10:                     self.data[self.add_prefix(name)] = self.initial[name]
11:
12:     def clean(self):
13:         if self._errors:
14:             return self.cleaned_data
15:         if not hasattr(self, 'readonly'):
16:             return self.cleaned_data
17:         for name in self.fields.keys():
18:             if self.readonly is None or name in self.readonly:
19:                 if name in self.initial:
20:                     self.cleaned_data[name] = self.initial[name]
21:         return self.cleaned_data

 このFormクラスを使ってFormを定義します。読み取り専用にしたいフィールドは、Formオブジェクトのset_readonlyメソッドで指定します。これは実際のところ、初期化メソッド(__init__)で渡すようにしてもよいのですが、今後のDjangoで引数名が衝突しても面倒なのでこのようにしました。is_validを呼び出してバリデーションを行う前までに、set_readonlyメソッドを呼び出せばよいことになります。

 ポイントは、bound済みのFormのデータはimmutableであり書き換えができないため、copy()メソッドでコピーしている(4行)点でしょうか。データのキーについてはプレフィックスを考慮しています(10行)が、このあたりのDjangoの仕様はドキュメントとして明示されていないため、今後のDjangoのリリースでは変わってしまう可能性はありますので、要注意です。

 ModelFormについてもほぼ同様の内容で対応可能です。

Tips

 読み取り専用にしたい入力フィールドの要素をdisable属性にするか、readonly属性にするか、どちらでも変更不可という点では変わりませんが、細かい点で異なります。上記ではHTML出力時にdisable属性になるようにしています。

 それぞれの属性の違いがどうレンダリングされるかは、ユーザーエージェントまかせのようですが、実際にはreadonlyの場合は見た目がほかと変わりません。disableであればグレーアウトします。

 そのほかにも、readonlyはINPUT,TEXTAREA要素のみという違いがありますので、disableを使うのがよさそうです。ただし、前述のようにdisableなフィールドは値が送信されません。

ドキュメントアクション