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


431

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

SOの質問も参照してください。なぜDjangoの読み取り専用フォームフィールドが悪い考えなのですか?@ stackoverflow.com/questions/2902024、(ダニエルナーブによる)承認された回答が悪意のあるPOSTハックを処理します。
X10 2011

回答:


422

この回答で指摘したように、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値がAで上書きされることはありませんPOST

それ以外の場合、バインドされた入力データを拒否しながら値をレンダリングする組み込みのDjangoフォームフィールドはありません。これが必要な場合は、代わりにModelForm編集不可能なフィールドを除外した別のフィールドを作成し、テンプレート内に印刷する必要があります。


3
ダニエル、回答を投稿していただきありがとうございます。このコードの使い方がわかりません。このコードは、新規および更新モードでも同じように機能しませんか?回答を編集して、新しいフォームや更新フォームで使用する方法の例を挙げられますか?ありがとう。
X10

8
Danielの例の鍵は、.idフィールドをテストすることです。新しく作成されたオブジェクトにはid == Noneがあります。ちなみに、最も古いオープンなDjangoチケットの1つは、この問題に関するものです。code.djangoproject.com/ticket/342を参照してください。
Peter Rowell、

2
@moadeep clean_descriptionは、フォームクラスにメソッドを追加します。
Daniel Naab 2015

3
Linux(ubuntu 15)/ chrome v45では、readonlyはポインターを「無効な手」に変更しますが、フィールドはクリック可能です。無効にすると、期待どおりに機能します
simone cittadini 2015年

7
この回答は更新する必要があります。disabledDjango 1.9に新しいフィールド引数が追加されました。Field.disabledがに設定されている場合True、そのPOST値Fieldは無視されます。したがって、1.9を使用している場合はclean、オーバーライドする必要はありませんdisabled = True。設定するだけです。この答えを確認してください。
narendra-choudhary 2016年

174

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

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


1.8 LTSには何もありませんか?
dnit13

9
UpdateViewでこれを使用する方法はありますか?モデルからフィールドを生成するので...
bcsanches

6
正解。私のソリューションクラスMyChangeForm(forms.ModelForm):def __init __(self、* args、** kwargs):super(MyChangeForm、self).__ init __(* args、** kwargs)self.fields ['my_field']。disabled =真
ビジェイKatam

8
これは問題のある答えです。設定disabled=Trueすると、検証エラーが発生してモデルがユーザーに戻されます。
Ben

1
例を含めることができればすばらしいでしょう
ジオイデシック

95

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

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

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


15
+1これは、より複雑なsave()オーバーライドを回避する優れた方法です。ただし、戻る前に(改行なしのコメントモードで)インスタンスチェックを実行する必要があります。「if self.instance:return self.instance.sku; else:return self.fields ['sku']」
Danielナーブ

2
最後の行については、それとreturn self.cleaned_data['sku']同じかそれ以上か。ドキュメントが使用することをお勧めているようだcleaned_data:「このメソッドの戻り値は、既存の値を置き換えcleaned_data、それはフィールドのからの値でなければなりませんので、cleaned_data(この方法は、それを変更しなかった場合でも)、または新しい掃除値。」
pianoJames

67

awalkerの答えは私を大いに助けてくれました!

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 []

そしてそれはうまくいきます。アイテムを追加すると、urlフィールドは読み取り/書き込みになりますが、変更すると読み取り専用になります。


56

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


でフォームとして使用しようとTabularInlineしましたattrsが、widgetインスタンス間で共有されていたため、失敗しました。新しく追加された最初の行を除くすべてが、読み取り専用でレンダリングされました。
16

素晴らしい(更新)ソリューション!残念ながら、これに加えて、残りのすべての「無効」な値が空になるため、フォームエラーが発生すると問題が発生します。
Michael Thompson

28

Django 1.2以降の場合、次のようにフィールドをオーバーライドできます。

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

6
これにより、追加時にフィールドを編集することもできません。これは、元の質問に関するものです。
マットS.

これが私が探している答えです。Field disabledそれはフィールドを無効にするので私が望むことをしませんが、ラベルを削除します/それを非表示にします。
sivabudh

18

最初の編集以外のフィールドを無効にして保護するread_only反復可能フィールドを追加できるように、継承できるMixInクラスを作成しました。

(ダニエルとムフクの回答に基づく)

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

11

私は読み取り専用フィールドの最も簡単なウィジェットを作成しました-フォームにこれがまだない理由は本当にわかりません:

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

非常に単純です-そして私に出力するだけです。読み取り専用の値がたくさんあるフォームセットで便利です。もちろん-もう少し賢くして、クラスにattrを追加できるように、divに属性を追加することもできます。


2
セクシーに見えますが、外部キーの処理方法は?
andilabs 2015

そのunicode(value)代わりに、おそらくそれを代わりにしてください。ユニコードダンダーが賢明であると仮定すると、それを取得できます。
ダニーステープル2015

外部キーの場合は、「モデル」属性を追加し、「get(value)」を使用する必要があります。要点を
shadi 2017

10

私は同様の問題に遭遇しました。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 []

いいことは obj、新しいアイテムを追加するときはNoneになるか、既存のアイテムを変更するときは編集されるオブジェクトになることです。

get_readonly_displayはここに文書化されています:http ://docs.djangoproject.com/en/1.2/ref/contrib/admin/#modeladmin-methods


6

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


そして、どの程度verbos_nameか、labelフィールドの?djangoテンプレートで `ラベルを表示するにはどうすればよいですか?@alzclarke
クジラ52Hz

6

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

これは正面からのみブロックします。誰でもバイパスできます。機密データを
扱う

安全です; それはまた、バックエンドでのブロックは、ジャンゴ> = 1.10以来docs.djangoproject.com/en/1.10/ref/forms/fields/...
timdiels

5

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

まだコメントできないので(muhukの解決策)、別の回答として回答します。これは私のために働いた完全なコード例です:

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

5

さらに、私はもう1つの解決策を提供します:)私はハンフリーのコードを使用していましたので、これはそれに基づいています。

しかし、私はフィールドがであるという問題に遭遇しました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

私も同じ問題を抱えていたので、自分のユースケースで機能するように見える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')

私がここで提案した自分のミックスインよりも少し読みやすいと思います。これらのクリーンアップは検証エラーを発生させないため、おそらくより効率的です
christophe31

エラーが表示されます:'collections.OrderedDict' object has no attribute 'iteritems'
ジオイデシック2018

4

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

方法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

1つの一般化された例でさらに2つの(類似の)アプローチ:

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

管理バージョンでは、複数のフィールドがある場合、これはよりコンパクトな方法だと思います。

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

3

Yamikepの答えに基づいて、私はまた処理するより優れた非常にシンプルなソリューションを見つけましたModelMultipleChoiceFieldフィールド。

からフィールドを削除form.cleaned_dataすると、フィールドが保存されなくなります。

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

これは、クリストフ31の回答に基づく、もう少し複雑なバージョンです。「読み取り専用」属性には依存しません。これにより、選択ボックスがまだ変更可能であり、データピッカーがまだポップアップしているなどの問題がなくなります。

代わりに、フォームフィールドウィジェットを読み取り専用ウィジェットでラップし、フォームを引き続き検証します。元のウィジェットのコンテンツは<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)

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

django 1.9以降の
場合フィールド無効化引数を使用して、フィールドを無効化できます。たとえば、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

あなたが作業している場合Django ver < 1.91.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):
        ...

主な考え方は、フィールドがのreadonly場合、以外の値は必要ないということですinitial

PS:設定することを忘れないでください yuor_form_field.widget.attrs['readonly'] = True


0

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

あなたの最良の選択肢は、<span>またはでレンダリングされたテンプレートに読み取り専用属性を含めることです<p>ではなく、それが読み取り専用かどう形でそれを含めます。

フォームはデータを収集するためのものであり、表示するためのものではありません。そうは言っても、readonlyウィジェットに表示してPOSTデータをスクラブするオプションは、優れたソリューションです。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.