在Django表单中,如何将字段只读(或禁用)以使其无法编辑?

In a Django form, how do I make a field readonly (or disabled) so that it cannot be edited?

在django表单中,如何将字段设置为只读(或禁用)?

当窗体用于创建新条目时,应启用所有字段-但当记录处于更新模式时,某些字段需要为只读。

例如,创建新的Item模型时,所有字段都必须是可编辑的,但在更新记录时,是否有方法禁用sku字段,使其可见,但不能编辑?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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是否可以重用?在ItemFormItem模型类中需要进行哪些更改?我需要写另一个类"ItemUpdateForm"来更新项目吗?

1
2
3
4
5
6
def update_item_view(request):
    if request.method == 'POST':
        form = ItemUpdateForm(request.POST)
        # Validate and save
    else:
        form = ItemUpdateForm()


如本答案所指出的,Django 1.9添加了field.disabled属性:

The disabled boolean argument, when set to True, disables a form field using the disabled HTML attribute so that it won’t be editable by users. Even if a user tampers with the field’s value submitted to the server, it will be ignored in favor of the value from the form’s initial data.

对于django 1.8及更早版本,要禁用小部件上的条目并防止恶意的后黑客攻击,除了在表单字段上设置readonly属性外,还必须清除输入:

1
2
3
4
5
6
7
8
9
10
11
12
13
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

The disabled boolean argument, when set to True, disables a form field using the disabled HTML attribute so that it won’t be editable by users. Even if a user tampers with the field’s value submitted to the server, it will be ignored in favor of the value from the form’s initial data.


在widget上设置read only只会使浏览器中的输入为只读。添加一个干净的返回instance.sku的sku可以确保字段值不会在表单级别发生更改。

1
2
3
4
5
def clean_sku(self):
    if self.instance:
        return self.instance.sku
    else:
        return self.fields['sku']

通过这种方式,您可以使用模型的(未修改的保存)和avoid获取字段所需的错误。


阿瓦克的回答帮助了我很多!

我已经将他的示例改为使用get_readonly_字段与Django 1.3一起使用。

通常您应该在app/admin.py中声明类似的内容:

1
2
3
class ItemAdmin(admin.ModelAdmin):
    ...
    readonly_fields = ('url',)

我是这样适应的:

1
2
3
4
5
6
7
8
# In the admin.py file
class ItemAdmin(admin.ModelAdmin):
    ...
    def get_readonly_fields(self, request, obj=None):
        if obj:
            return ['url']
        else:
            return []

而且效果很好。现在,如果添加一个项,url字段是读写的,但是一旦更改,它就变成只读的。


要使此项对ForeignKey字段有效,需要进行一些更改。首先,SELECT HTML标记没有readonly属性。我们需要用disabled="disabled"来代替。但是,浏览器不会为该字段发送任何表单数据。因此,我们需要将该字段设置为不需要,以便字段正确验证。然后我们需要将该值重置回原来的值,这样它就不会被设置为空白。

因此,对于外键,您需要执行以下操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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+,您可以这样覆盖字段:

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


我创建了一个mixin类,您可以继承它来添加一个只读的iterable字段,该字段将在非第一次编辑时禁用并保护字段:

(基于丹尼尔和穆胡克的答案)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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')

我刚刚为只读字段创建了一个可能最简单的小部件-我真的不明白表单为什么还没有这样的小部件:

1
2
3
4
class ReadOnlyWidget(widgets.Widget):
   """Some of these values are read only - just a bit of text..."""
    def render(self, _, value, attrs=None):
        return value

形式:

1
my_read_only = CharField(widget=ReadOnlyWidget())

非常简单-我只需要输出。使用一组只读值的表单集很方便。当然-你也可以更聪明一点,给它一个带有属性的DIV,这样你就可以向它附加类了。


我遇到了一个类似的问题。看起来我可以通过在modeladmin类中定义一个"get-readonly-fields"方法来解决这个问题。

像这样:

1
2
3
4
5
6
7
8
9
# In the admin.py file

class ItemAdmin(admin.ModelAdmin):

    def get_readonly_display(self, request, obj=None):
        if obj:
            return ['sku']
        else:
            return []

好的是,添加新项目时,obj将为"无",或者在更改现有项目时,它将是正在编辑的对象。

get_readonly_显示记录如下:http://docs.djangoproject.com/en/1.2/ref/contrib/admin/modeladmin方法


一个简单的选择是只在模板中键入form.instance.fieldName,而不是form.fieldName


由于我还不能发表评论(Muhuk的解决方案),我将作为一个单独的答案回答。这是一个完整的代码示例,适用于我:

1
2
3
4
5
def clean_sku(self):
  if self.instance and self.instance.pk:
    return self.instance.sku
  else:
    return self.cleaned_data['sku']


作为Humphrey文章的一个有用补充,我对django的恢复有一些问题,因为它仍然将禁用字段注册为"changed"。以下代码修复了问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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)


我如何使用django 1.11:

1
2
3
4
5
6
7
8
9
10
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


再一次,我将提供另一个解决方案:)我使用的是汉弗莱的代码,所以这是基于这个。

然而,我遇到了一个问题,这个领域是一个模式选择领域。每件事都会按第一个请求进行。但是,如果表单集试图添加新项,但验证失败,则"现有"表单出现问题,其中所选选项被重置为默认的"--------"。

不管怎么说,我不知道怎么解决这个问题。因此,(我认为这实际上在形式上更清晰),我将字段设置为hiddeninputfield()。这意味着你需要在模板中做更多的工作。

所以对我来说,解决方法是简化表单:

1
2
3
4
5
6
7
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()

然后在模板中,您需要对表单集进行一些手动循环。

因此,在这种情况下,您可以在模板中执行如下操作:

1
2
    {{ form.instance.sku }} <!-- This prints the value -->
    {{ form }} <!-- Prints form normally, and makes the hidden input -->

这对我来说有点好,而且对身体的操控也更少。


我也遇到了同样的问题,所以我创建了一个mixin,它似乎适用于我的用例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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

用法,只需定义哪些必须是只读的:

1
2
class MyFormWithReadOnlyFields(ReadOnlyFieldsMixin, MyForm):
    readonly_fields = ('field1', 'field2', 'fieldx')


还有两种(类似的)方法,一个通用示例:

1)第一种方法-删除save()方法中的字段,例如(未测试;):

1
2
3
4
5
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)第二种方法-将字段重置为清洁方法中的初始值:

1
2
def clean_<fieldname>(self):
    return self.initial[<fieldname>] # or getattr(self.instance, fieldname)

基于第二种方法,我将其概括如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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)


如果需要多个只读字段,可以使用下面给出的任何方法。

方法1

1
2
3
4
5
6
7
8
9
10
11
12
13
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

继承方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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',)


下面是一个稍微复杂一点的版本,基于Christophe31的答案。它不依赖于"readonly"属性。这使得它的问题,如选择框仍然是可变的,数据选择器仍然出现,消失。

相反,它将表单字段小部件包装在只读小部件中,从而使表单仍然有效。原始小部件的内容显示在标记内。如果小部件有一个render_readonly()方法,它使用它作为可见文本,否则它会解析原始小部件的HTML并尝试猜测最佳表示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
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的回答,我找到了一个更好且非常简单的解决方案,它也可以处理ModelMultipleChoiceField字段。

form.cleaned_data中删除字段会阻止保存字段:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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()

用途:

1
2
class MyFormWithReadOnlyFields(ReadOnlyFieldsMixin, MyForm):
    readonly_fields = ('field1', 'field2', 'fieldx')


对于管理版本,如果您有多个字段,我认为这是一种更紧凑的方法:

1
2
3
4
5
6
7
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

这是最简单的方法吗?

就在这样的视图代码中:

1
2
3
4
5
6
7
8
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字段

1
2
3
4
5
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/已禁用


如果使用Django ver < 1.9(1.9添加了Field.disabled属性),可以尝试在表单__init__方法中添加以下修饰符:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
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以外的任何其他值。

旁白:别忘了设置yuor_form_field.widget.attrs['readonly'] = True


如果您使用的是django admin,这里是最简单的解决方案。

1
2
3
4
5
6
7
8
9
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',)


我认为您最好的选择是将readonly属性包含在呈现在

中的模板中,而不是将其包含在只读形式中。

表单用于收集数据,而不是显示数据。也就是说,在readonly小部件中显示的选项和清除日志数据是很好的解决方案。


我这样解决了这个问题:

1
2
3
4
5
    class UploadFileForm(forms.ModelForm):
     class Meta:
      model = FileStorage
      fields = '__all__'
      widgets = {'patient': forms.HiddenInput()}

在视图中:

1
form = UploadFileForm(request.POST, request.FILES, instance=patient, initial={'patient': patient})

就这些。