web-dev-qa-db-ja.com

Djangoのフォームで、編集できないようにフィールドを読み取り専用(または無効)にするにはどうすればいいですか?

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()
389
e70

この答え で指摘されているように、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を作成し、そしてあなたのテンプレートの中にそれらを単に印刷するべきです。

387
Daniel Naab

Django 1.9はField.disabled属性を追加しました: https://docs.djangoproject.com/en/stable/ref/forms/fields/#disabled

Disabledブール引数をTrueに設定すると、disabled HTML属性を使用してフォームフィールドが無効になり、ユーザーが編集できなくなります。ユーザーがサーバーに送信されたフィールドの値を改ざんしたとしても、フォームの初期データの値を優先して無視されます。

159
Mike Mahmud

ウィジェットにREADONLYを設定すると、ブラウザの入力のみが読み取り専用になります。 instance.skuを返すclean_skuを追加すると、フィールド値がフォームレベルで変更されないようになります。

def clean_sku(self):
    if self.instance: 
        return self.instance.sku
    else: 
        return self.fields['sku']

このようにして、モデルの(変更されていない保存)およびaviodを使用してフィールドに必須のエラーを取得できます。

92
muhuk

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フィールドは読み書き可能になりますが、変更すると読み取り専用になります。

61
chirale

これを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メソッドをオーバーライドして、フィールドの値を元々インスタンス内にあったものに設定します。

52
Humphrey

Django 1.2以降では、このようにフィールドを上書きすることができます。

sku = forms.CharField(widget = forms.TextInput(attrs={'readonly':'readonly'}))
24
StefanNch

最初の非編集時に無効にしてフィールドを保護する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')
16
christophe31

読み取り専用フィールドのための最も単純なウィジェットを作成したところです - なぜフォームにすでにこれがないのか、私にはよくわかりません。

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を与えることができるので、あなたはそれにクラスを追加することができます。

10
Danny Staple

私は同じような問題に出くわした。 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

9
awalker

簡単な方法の1つは、テンプレート内でform.instance.fieldNameの代わりにform.fieldNameを入力することです。

6
alzclarke

私はまだコメントできないので( muhukの解決策 )、私は別の答えとして返事をするつもりです。これは私にとってうまくいった完全なコード例です。

def clean_sku(self):
  if self.instance and self.instance.pk:
    return self.instance.sku
  else:
    return self.cleaned_data['sku']
5
Madis

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)
5
Evan Brumley

もう一度、私はもう一つの解決策を提供するつもりです:)私が使用していた 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>

これは私にとっては少し良くなり、フォーム操作も少なくなりました。

4
JamesD

私は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
4
Lucas B

私は同じ問題に遭遇していたので、私は私のユースケースのために働くと思われる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')
4
Michael

複数の読み取り専用フィールドが必要な場合は、以下の方法のいずれかを使用できます。

方法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',)
3
Sarath Ak

一般化した例を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)
3
Robert Lujo

これは、 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)
2
Rune Kaagaard

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')
2
darklow

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
2
Hassek

Django ver < 1.91.9Field.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を設定することを忘れないでください

1

これは最も簡単な方法ですか?

ビューの中では、次のようにコーディングしてください。

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)

それはうまくいきます!

1
fly_frog

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

1
Ajinkya Bhosale

読み取り専用の場合はフォームに含めるのではなく、<span>または<p>でレンダリングされたテンプレートに読み取り専用の属性を含めることをお勧めします。

フォームはデータを収集するためのもので、表示するためのものではありません。そうは言っても、readonlyウィジェットとscrub POSTデータに表示するオプションは良い解決策です。

0
austinheiman

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',)
0
utapyngo

私はこの問題をこのように解決しました:

    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})

それはすべてです。

0
Pavel