Djangoのフォームで、どうやってフィールドを読み取り専用にする(あるいは無効にする)のですか?
フォームを使用して新しいエントリを作成するときは、すべてのフィールドを有効にする必要があります。ただし、レコードが更新モードの場合は、一部のフィールドを読み取り専用にする必要があります。
たとえば、新しいItem
モデルを作成するときは、すべてのフィールドを編集可能にする必要がありますが、レコードを更新している間、sku
フィールドを無効にして表示されるようにする方法はありますか
class Item(models.Model):
sku = models.CharField(max_length=50)
description = models.CharField(max_length=200)
added_by = models.ForeignKey(User)
class ItemForm(ModelForm):
class Meta:
model = Item
exclude = ('added_by')
def new_item_view(request):
if request.method == 'POST':
form = ItemForm(request.POST)
# Validate and save
else:
form = ItemForm()
# Render the view
クラスItemForm
は再利用できますか? ItemForm
またはItem
モデルクラスにはどのような変更が必要ですか?アイテムを更新するために別のクラス "ItemUpdateForm
"を書く必要がありますか?
def update_item_view(request):
if request.method == 'POST':
form = ItemUpdateForm(request.POST)
# Validate and save
else:
form = ItemUpdateForm()
この答え で指摘されているように、Django 1.9は Field.disabled 属性を追加しました:
Disabledブール引数をTrueに設定すると、disabled HTML属性を使用してフォームフィールドが無効になり、ユーザーが編集できなくなります。ユーザーがサーバーに送信されたフィールドの値を改ざんしたとしても、フォームの初期データの値を優先して無視されます。
Django 1.8以前では、ウィジェットへの入力を無効にし、悪意のあるPOSTハックを防ぐために、フォームフィールドにreadonly
属性を設定することに加えて入力をスクラブする必要があります。
class ItemForm(ModelForm):
def __init__(self, *args, **kwargs):
super(ItemForm, self).__init__(*args, **kwargs)
instance = getattr(self, 'instance', None)
if instance and instance.pk:
self.fields['sku'].widget.attrs['readonly'] = True
def clean_sku(self):
instance = getattr(self, 'instance', None)
if instance and instance.pk:
return instance.sku
else:
return self.cleaned_data['sku']
または、if instance and instance.pk
を編集中であることを示す別の条件に置き換えます。 disabled
の代わりに、入力フィールドに属性readonly
を設定することもできます。
clean_sku
関数はreadonly
の値がPOST
によって上書きされないようにします。
そうでなければ、バインドされた入力データを拒否しながら値をレンダリングする組み込みのDjangoフォームフィールドはありません。これがあなたが望むものであるならば、あなたは代わりに編集不可能なフィールドを除外する別のModelForm
を作成し、そしてあなたのテンプレートの中にそれらを単に印刷するべきです。
Django 1.9はField.disabled属性を追加しました: https://docs.djangoproject.com/en/stable/ref/forms/fields/#disabled
Disabledブール引数をTrueに設定すると、disabled HTML属性を使用してフォームフィールドが無効になり、ユーザーが編集できなくなります。ユーザーがサーバーに送信されたフィールドの値を改ざんしたとしても、フォームの初期データの値を優先して無視されます。
ウィジェットにREADONLYを設定すると、ブラウザの入力のみが読み取り専用になります。 instance.skuを返すclean_skuを追加すると、フィールド値がフォームレベルで変更されないようになります。
def clean_sku(self):
if self.instance:
return self.instance.sku
else:
return self.fields['sku']
このようにして、モデルの(変更されていない保存)およびaviodを使用してフィールドに必須のエラーを取得できます。
awalker's answer とても助かりました!
私は get_readonly_fields を使ってDjango 1.3で動作するように彼の例を変更しました。
通常はapp/admin.py
で次のように宣言します。
class ItemAdmin(admin.ModelAdmin):
...
readonly_fields = ('url',)
私はこのように調整しました:
# In the admin.py file
class ItemAdmin(admin.ModelAdmin):
...
def get_readonly_fields(self, request, obj=None):
if obj:
return ['url']
else:
return []
そしてそれはうまくいきます。これでItemを追加すると、url
フィールドは読み書き可能になりますが、変更すると読み取り専用になります。
これをForeignKey
フィールドに対して機能させるには、いくつかの変更を加える必要があります。まず、SELECT HTML
タグにreadonly属性がありません。代わりにdisabled="disabled"
を使う必要があります。ただし、ブラウザはそのフィールドにフォームデータを返信しません。そのため、フィールドが正しく検証されるように、そのフィールドを必須ではないように設定する必要があります。次に、値を空白に設定されないように、以前の値にリセットする必要があります。
したがって、外部キーの場合は、次のようにする必要があります。
class ItemForm(ModelForm):
def __init__(self, *args, **kwargs):
super(ItemForm, self).__init__(*args, **kwargs)
instance = getattr(self, 'instance', None)
if instance and instance.id:
self.fields['sku'].required = False
self.fields['sku'].widget.attrs['disabled'] = 'disabled'
def clean_sku(self):
# As shown in the above answer.
instance = getattr(self, 'instance', None)
if instance:
return instance.sku
else:
return self.cleaned_data.get('sku', None)
この方法ではブラウザはユーザにフィールドを変更させず、空白のままになっているので常にPOST
になります。その後、clean
メソッドをオーバーライドして、フィールドの値を元々インスタンス内にあったものに設定します。
Django 1.2以降では、このようにフィールドを上書きすることができます。
sku = forms.CharField(widget = forms.TextInput(attrs={'readonly':'readonly'}))
最初の非編集時に無効にしてフィールドを保護するread_only反復可能フィールドを追加できるように継承可能なMixInクラスを作成しました。
(DanielとMuhukの回答に基づく)
from Django import forms
from Django.db.models.manager import Manager
# I used this instead of lambda expression after scope problems
def _get_cleaner(form, field):
def clean_field():
value = getattr(form.instance, field, None)
if issubclass(type(value), Manager):
value = value.all()
return value
return clean_field
class ROFormMixin(forms.BaseForm):
def __init__(self, *args, **kwargs):
super(ROFormMixin, self).__init__(*args, **kwargs)
if hasattr(self, "read_only"):
if self.instance and self.instance.pk:
for field in self.read_only:
self.fields[field].widget.attrs['readonly'] = "readonly"
setattr(self, "clean_" + field, _get_cleaner(self, field))
# Basic usage
class TestForm(AModelForm, ROFormMixin):
read_only = ('sku', 'an_other_field')
読み取り専用フィールドのための最も単純なウィジェットを作成したところです - なぜフォームにすでにこれがないのか、私にはよくわかりません。
class ReadOnlyWidget(widgets.Widget):
"""Some of these values are read only - just a bit of text..."""
def render(self, _, value, attrs=None):
return value
形式では:
my_read_only = CharField(widget=ReadOnlyWidget())
非常に単純です - そして私にちょうど出力を取得します。読み取り専用の値がたくさん入っているフォームセットで使用すると便利です。もちろん - あなたはもう少し賢くてそれにattrsを持つdivを与えることができるので、あなたはそれにクラスを追加することができます。
私は同じような問題に出くわした。 ModelAdminクラスに "get_readonly_fields"メソッドを定義することで解決できたようです。
このようなもの:
# In the admin.py file
class ItemAdmin(admin.ModelAdmin):
def get_readonly_display(self, request, obj=None):
if obj:
return ['sku']
else:
return []
いいことに、新しいItemを追加しているときはobj
がNoneになるか、既存のItemを変更しているときは編集中のオブジェクトになります。
get_readonly_displayはここに文書化されています: http://docs.djangoproject.com/en/1.2/ref/contrib/admin/#modeladmin-methods
簡単な方法の1つは、テンプレート内でform.instance.fieldName
の代わりにform.fieldName
を入力することです。
私はまだコメントできないので( muhukの解決策 )、私は別の答えとして返事をするつもりです。これは私にとってうまくいった完全なコード例です。
def clean_sku(self):
if self.instance and self.instance.pk:
return self.instance.sku
else:
return self.cleaned_data['sku']
Humphreyの投稿 に便利な追加として、私はDjango-reversionに関していくつかの問題を抱えていました。次のコードは問題を解決します。
class ItemForm(ModelForm):
def __init__(self, *args, **kwargs):
super(ItemForm, self).__init__(*args, **kwargs)
instance = getattr(self, 'instance', None)
if instance and instance.id:
self.fields['sku'].required = False
self.fields['sku'].widget.attrs['disabled'] = 'disabled'
def clean_sku(self):
# As shown in the above answer.
instance = getattr(self, 'instance', None)
if instance:
try:
self.changed_data.remove('sku')
except ValueError, e:
pass
return instance.sku
else:
return self.cleaned_data.get('sku', None)
もう一度、私はもう一つの解決策を提供するつもりです:)私が使用していた Humphreyのコード 、これはそれに基づいています。
しかし、私はフィールドがModelChoiceFieldであるという問題に遭遇しました。すべてが最初のリクエストで機能します。しかし、フォームセットが新しい項目を追加しようとして検証に失敗した場合、SELECTEDオプションがデフォルトの「---------」にリセットされていた「既存の」フォームで問題が発生していました。
とにかく、私はそれを修正する方法を把握することができませんでした。その代わりに(そしてこれは実際にはフォームのほうがきれいだと思います)、フィールドをHiddenInputField()にしました。これはテンプレートでもう少し作業をしなければならないことを意味します。
だから私のための修正はフォームを簡素化することでした:
class ItemForm(ModelForm):
def __init__(self, *args, **kwargs):
super(ItemForm, self).__init__(*args, **kwargs)
instance = getattr(self, 'instance', None)
if instance and instance.id:
self.fields['sku'].widget=HiddenInput()
そしてテンプレートでは、 フォームセットの手動ループ を実行する必要があります。
したがって、この場合はテンプレートで次のようにします。
<div>
{{ form.instance.sku }} <!-- This prints the value -->
{{ form }} <!-- Prints form normally, and makes the hidden input -->
</div>
これは私にとっては少し良くなり、フォーム操作も少なくなりました。
私はDjango 1.11でそれをする方法:
class ItemForm(ModelForm):
disabled_fields = ('added_by',)
class Meta:
model = Item
fields = '__all__'
def __init__(self, *args, **kwargs):
super(ItemForm, self).__init__(*args, **kwargs)
for field in self.disabled_fields:
self.fields[field].disabled = True
私は同じ問題に遭遇していたので、私は私のユースケースのために働くと思われるMixinを作成しました。
class ReadOnlyFieldsMixin(object):
readonly_fields =()
def __init__(self, *args, **kwargs):
super(ReadOnlyFieldsMixin, self).__init__(*args, **kwargs)
for field in (field for name, field in self.fields.iteritems() if name in self.readonly_fields):
field.widget.attrs['disabled'] = 'true'
field.required = False
def clean(self):
cleaned_data = super(ReadOnlyFieldsMixin,self).clean()
for field in self.readonly_fields:
cleaned_data[field] = getattr(self.instance, field)
return cleaned_data
使い方は、読み取り専用にする必要があるものを定義するだけです。
class MyFormWithReadOnlyFields(ReadOnlyFieldsMixin, MyForm):
readonly_fields = ('field1', 'field2', 'fieldx')
複数の読み取り専用フィールドが必要な場合は、以下の方法のいずれかを使用できます。
方法1
class ItemForm(ModelForm):
readonly = ('sku',)
def __init__(self, *arg, **kwrg):
super(ItemForm, self).__init__(*arg, **kwrg)
for x in self.readonly:
self.fields[x].widget.attrs['disabled'] = 'disabled'
def clean(self):
data = super(ItemForm, self).clean()
for x in self.readonly:
data[x] = getattr(self.instance, x)
return data
方法2
継承方法
class AdvancedModelForm(ModelForm):
def __init__(self, *arg, **kwrg):
super(AdvancedModelForm, self).__init__(*arg, **kwrg)
if hasattr(self, 'readonly'):
for x in self.readonly:
self.fields[x].widget.attrs['disabled'] = 'disabled'
def clean(self):
data = super(AdvancedModelForm, self).clean()
if hasattr(self, 'readonly'):
for x in self.readonly:
data[x] = getattr(self.instance, x)
return data
class ItemForm(AdvancedModelForm):
readonly = ('sku',)
一般化した例を1つ使用した、さらに2つの(類似の)アプローチ
1)第1のアプローチ - save()メソッド内のフィールドを削除する。 (未検証 ;) ):
def save(self, *args, **kwargs):
for fname in self.readonly_fields:
if fname in self.cleaned_data:
del self.cleaned_data[fname]
return super(<form-name>, self).save(*args,**kwargs)
2)2番目のアプローチ - クリーンな方法でフィールドを初期値にリセットします。
def clean_<fieldname>(self):
return self.initial[<fieldname>] # or getattr(self.instance, fieldname)
2番目のアプローチに基づいて、私はこれを次のように一般化しました。
from functools import partial
class <Form-name>(...):
def __init__(self, ...):
...
super(<Form-name>, self).__init__(*args, **kwargs)
...
for i, (fname, field) in enumerate(self.fields.iteritems()):
if fname in self.readonly_fields:
field.widget.attrs['readonly'] = "readonly"
field.required = False
# set clean method to reset value back
clean_method_name = "clean_%s" % fname
assert clean_method_name not in dir(self)
setattr(self, clean_method_name, partial(self._clean_for_readonly_field, fname=fname))
def _clean_for_readonly_field(self, fname):
""" will reset value to initial - nothing will be changed
needs to be added dynamically - partial, see init_fields
"""
return self.initial[fname] # or getattr(self.instance, fieldname)
これは、 christophe31's answer に基づく、もう少し複雑なバージョンです。 「readonly」属性には依存しません。これにより、選択ボックスがまだ変更可能でデータピッカーがまだポップアップ表示されているなどの問題がなくなります。
代わりに、フォームフィールドウィジェットを読み取り専用ウィジェットにラップして、フォームを検証します。元のウィジェットの内容は<span class="hidden"></span>
タグ内に表示されます。ウィジェットがrender_readonly()
メソッドを持っていればそれを可視テキストとして使います。そうでなければ元のウィジェットのHTMLをパースして最良の表現を推測しようとします。
import Django.forms.widgets as f
import xml.etree.ElementTree as etree
from Django.utils.safestring import mark_safe
def make_readonly(form):
"""
Makes all fields on the form readonly and prevents it from POST hacks.
"""
def _get_cleaner(_form, field):
def clean_field():
return getattr(_form.instance, field, None)
return clean_field
for field_name in form.fields.keys():
form.fields[field_name].widget = ReadOnlyWidget(
initial_widget=form.fields[field_name].widget)
setattr(form, "clean_" + field_name,
_get_cleaner(form, field_name))
form.is_readonly = True
class ReadOnlyWidget(f.Select):
"""
Renders the content of the initial widget in a hidden <span>. If the
initial widget has a ``render_readonly()`` method it uses that as display
text, otherwise it tries to guess by parsing the html of the initial widget.
"""
def __init__(self, initial_widget, *args, **kwargs):
self.initial_widget = initial_widget
super(ReadOnlyWidget, self).__init__(*args, **kwargs)
def render(self, *args, **kwargs):
def guess_readonly_text(original_content):
root = etree.fromstring("<span>%s</span>" % original_content)
for element in root:
if element.tag == 'input':
return element.get('value')
if element.tag == 'select':
for option in element:
if option.get('selected'):
return option.text
if element.tag == 'textarea':
return element.text
return "N/A"
original_content = self.initial_widget.render(*args, **kwargs)
try:
readonly_text = self.initial_widget.render_readonly(*args, **kwargs)
except AttributeError:
readonly_text = guess_readonly_text(original_content)
return mark_safe("""<span class="hidden">%s</span>%s""" % (
original_content, readonly_text))
# Usage example 1.
self.fields['my_field'].widget = ReadOnlyWidget(self.fields['my_field'].widget)
# Usage example 2.
form = MyForm()
make_readonly(form)
Yamikep's answer に基づいて、私はModelMultipleChoiceField
フィールドも処理する、より良くて非常に単純な解決策を見つけました。
form.cleaned_data
からfieldを削除すると、フィールドが保存されなくなります。
class ReadOnlyFieldsMixin(object):
readonly_fields = ()
def __init__(self, *args, **kwargs):
super(ReadOnlyFieldsMixin, self).__init__(*args, **kwargs)
for field in (field for name, field in self.fields.iteritems() if
name in self.readonly_fields):
field.widget.attrs['disabled'] = 'true'
field.required = False
def clean(self):
for f in self.readonly_fields:
self.cleaned_data.pop(f, None)
return super(ReadOnlyFieldsMixin, self).clean()
使用法:
class MyFormWithReadOnlyFields(ReadOnlyFieldsMixin, MyForm):
readonly_fields = ('field1', 'field2', 'fieldx')
Adminバージョンでは、複数のフィールドがある場合はこれがよりコンパクトな方法だと思います。
def get_readonly_fields(self, request, obj=None):
skips = ('sku', 'other_field')
fields = super(ItemAdmin, self).get_readonly_fields(request, obj)
if not obj:
return [field for field in fields if not field in skips]
return fields
Django ver < 1.9
(1.9
にField.disabled
属性が追加されています)を使用している場合は、フォームの__init__
メソッドに次のデコレータを追加してみます。
def bound_data_readonly(_, initial):
return initial
def to_python_readonly(field):
native_to_python = field.to_python
def to_python_filed(_):
return native_to_python(field.initial)
return to_python_filed
def disable_read_only_fields(init_method):
def init_wrapper(*args, **kwargs):
self = args[0]
init_method(*args, **kwargs)
for field in self.fields.values():
if field.widget.attrs.get('readonly', None):
field.widget.attrs['disabled'] = True
setattr(field, 'bound_data', bound_data_readonly)
setattr(field, 'to_python', to_python_readonly(field))
return init_wrapper
class YourForm(forms.ModelForm):
@disable_read_only_fields
def __init__(self, *args, **kwargs):
...
主な考え方は、fieldがreadonly
であれば、initial
以外の値は必要ないということです。
P.S:yuor_form_field.widget.attrs['readonly'] = True
を設定することを忘れないでください
これは最も簡単な方法ですか?
ビューの中では、次のようにコーディングしてください。
def resume_edit(request, r_id):
.....
r = Resume.get.object(pk=r_id)
resume = ResumeModelForm(instance=r)
.....
resume.fields['email'].widget.attrs['readonly'] = True
.....
return render(request, 'resumes/resume.html', context)
それはうまくいきます!
Django 1.9以降の場合
フィールドを無効にするには、Fields disabled引数を使用できます。例えば次のforms.pyファイルのコードスニペットで、employee_codeフィールドを無効にしました
class EmployeeForm(forms.ModelForm):
employee_code = forms.CharField(disabled=True)
class Meta:
model = Employee
fields = ('employee_code', 'designation', 'salary')
参照先 https://docs.djangoproject.com/en/2.0/ref/forms/fields/#disabled
読み取り専用の場合はフォームに含めるのではなく、<span>
または<p>
でレンダリングされたテンプレートに読み取り専用の属性を含めることをお勧めします。
フォームはデータを収集するためのもので、表示するためのものではありません。そうは言っても、readonly
ウィジェットとscrub POSTデータに表示するオプションは良い解決策です。
Django adminを使っているなら、これが最も簡単な解決策です。
class ReadonlyFieldsMixin(object):
def get_readonly_fields(self, request, obj=None):
if obj:
return super(ReadonlyFieldsMixin, self).get_readonly_fields(request, obj)
else:
return Tuple()
class MyAdmin(ReadonlyFieldsMixin, ModelAdmin):
readonly_fields = ('sku',)
私はこの問題をこのように解決しました:
class UploadFileForm(forms.ModelForm):
class Meta:
model = FileStorage
fields = '__all__'
widgets = {'patient': forms.HiddenInput()}
ビューで:
form = UploadFileForm(request.POST, request.FILES, instance=patient, initial={'patient': patient})
それはすべてです。