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

时间:2008-11-27 18:54:07

标签: django forms field readonly

在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吗? ItemFormItem模型类需要进行哪些更改?我是否需要编写另一个类“ItemUpdateForm”来更新项目?

def update_item_view(request):
    if request.method == 'POST':
        form = ItemUpdateForm(request.POST)
        # Validate and save
    else:
        form = ItemUpdateForm()

27 个答案:

答案 0 :(得分:385)

正如this answer所指出的,Django 1.9添加了Field.disabled属性:

  

禁用的boolean参数设置为True时,使用禁用的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来排除不可编辑的字段,然后在模板中打印它们。

答案 1 :(得分:159)

Django 1.9添加了Field.disabled属性:https://docs.djangoproject.com/en/stable/ref/forms/fields/#disabled

  

禁用的boolean参数设置为True时,使用禁用的HTML属性禁用表单字段,以便用户无法编辑它。即使用户篡改了提交给服务器的字段值,也会忽略该格式以支持表单初始数据中的值。

答案 2 :(得分:92)

在窗口小部件上设置READONLY只会使浏览器中的输入为只读。添加返回instance.sku的clean_sku可确保字段值不会在表单级别上更改。

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

这样你可以使用模型(未经修改的保存)和aviod获取字段所需的错误。

答案 3 :(得分:61)

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字段是可读写的,但是在更改时它变为只读。

答案 4 :(得分:52)

要使此功能适用于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方法,将字段的值设置为最初在实例中的值。

答案 5 :(得分:24)

对于Django 1.2+,你可以覆盖这样的字段:

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

答案 6 :(得分:16)

我创建了一个MixIn类,您可以继承该类,以便能够添加一个read_only可迭代字段,该字段将禁用并保护非首次编辑的字段:

(基于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')

答案 7 :(得分:10)

我刚刚为readonly字段创建了最简单的小部件 - 我真的不明白为什么表单没有这个:

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

非常简单 - 让我只输出。在一个带有一堆只读值的formset中得心应手。 当然 - 你也可以更聪明一点,给它一个带有attrs的div,这样你就可以为它添加类。

答案 8 :(得分:9)

我遇到了类似的问题。 看起来我能够通过在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

答案 9 :(得分:5)

一个简单的选择就是在模板中输入form.instance.fieldName而不是form.fieldName

答案 10 :(得分:5)

作为Humphrey's post的有用补充,我在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)

答案 11 :(得分:5)

由于我还不能发表评论(muhuk's solution),我会作为一个单独的答案作出答复。这是一个完整的代码示例,对我有用:

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

答案 12 :(得分:4)

然而,我将再提供一个解决方案:)我正在使用Humphrey's code,所以这是基于此。

但是,我遇到的问题是该字段是ModelChoiceField。一切都会在第一次请求时起作用。但是,如果formset尝试添加新项目并且验证失败,则“现有”表单出现问题,其中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()

然后在模板中,您需要执行一些manual looping of the formset

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

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

这对我来说效果更好,并且操作更少。

答案 13 :(得分: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')

答案 14 :(得分:4)

我如何使用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

答案 15 :(得分:3)

另外两个(类似的)方法有一个通用的例子:

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)第二种方法 - 在清洁方法中将字段重置为初始值:

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

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

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)

答案 16 :(得分:3)

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

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

答案 17 :(得分:2)

对于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

答案 18 :(得分:2)

这是一个稍微复杂一点的版本,基于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)

答案 19 :(得分:2)

基于Yamikep's answer,我找到了一个更好,更简单的解决方案,它也可以处理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')

答案 20 :(得分:1)

您可以在小部件中优雅地添加只读:

class SurveyModaForm(forms.ModelForm):
    class Meta:
        model  = Survey
        fields = ['question_no']
        widgets = {
        'question_no':forms.NumberInput(attrs={'class':'form-control','readonly':True}),
        }

答案 21 :(得分: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

答案 22 :(得分: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)

一切正常!

答案 23 :(得分:0)

我认为您最好的选择就是在<span><p>中呈现的模板中包含readonly属性,而不是将其包含在表单中(如果只是只读)。

表单用于收集数据,而不是显示数据。话虽这么说,在readonly窗口小部件中显示和清理POST数据的选项都是很好的解决方案。

答案 24 :(得分:0)

我解决了这个问题:

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

一切都是。

答案 25 :(得分:0)

如果您使用的是Django管理员,这是最简单的解决方案。

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

答案 26 :(得分:0)

如果您正在使用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之外不需要任何其他值。

P.S:不要忘记设置yuor_form_field.widget.attrs['readonly'] = True