在Django表单中,如何将字段设置为只读(或禁用)以便无法对其进行编辑?


430

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

另请参阅以下问题:为什么Django中的只读表单字段是个坏主意?@ stackoverflow.com/questions/2902024,被接受的答案(由Daniel Naab提供)负责恶意POST黑客。
X10

Answers:


421

该答案所指出,Django 1.9添加了Field.disabled属性:

禁用的布尔参数设置为True时,将使用禁用的HTML属性禁用表单字段,以便用户无法对其进行编辑。即使用户篡改了提交给服务器的字段的值,也将忽略该字段,而使用表单的初始数据中的值。

使用Django 1.8及更早版本,要禁用小部件上的输入并防止恶意POST黑客入侵,除了readonly在form字段上设置属性外,还必须清除输入内容:

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的字段,该字段将不可编辑的字段排除在外,然后将其打印在模板中。


3
丹尼尔,感谢您发布答案。我不清楚如何使用此代码?此代码不适用于新的以及更新模式吗?您可以编辑答案以提供有关如何将其用于新表格和更新表格的示例吗?谢谢。
X10

8
Daniel的示例的关键是测试.id字段。新创建的对象将具有id == None。顺便说一句,最古老的Django开放票证之一就是关于此问题的。参见code.djangoproject.com/ticket/342
彼得·罗威尔,

2
@moadeep clean_description向表单类添加方法。
Daniel Naab 2015年

3
在linux(ubuntu 15)/ chrome v45上,只读将指针更改为“已禁用的手”,但是可单击该字段。与残疾人士一样,按预期方式工作
simone cittadini

7
该答案需要更新。disabled在Django 1.9中添加了新的field参数。如果Field.disabled设置为True,则将Field忽略该值的POST值。因此,如果您使用的是1.9,则无需重写clean,只需设置即可disabled = True。检查答案。
narendra-choudhary

174

Django 1.9添加了Field.disabled属性: https

禁用的布尔参数设置为True时,将使用禁用的HTML属性禁用表单字段,以便用户无法对其进行编辑。即使用户篡改了提交给服务器的字段的值,也将忽略该字段,而使用表单的初始数据中的值。


没有1.8 LTS吗?
dnit13 '16

9
知道我们如何在UpdateView上使用它吗?当它从模型生成字段时…
bcsanches '16

6
正确答案。我的解决方案类MyChangeForm(forms.ModelForm):def __init __(self,* args,** kwargs):super(MyChangeForm,self).__ init __(* args,** kwargs)self.fields ['my_field']。disabled = True
Vijay Katam '01

8
这是一个有问题的答案-设置disabled=True将导致模型因验证错误而退还给用户。

1
如果您能提供一个例子,那就太棒了
geoidesic '18

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()覆盖的好方法。但是,您希望在返回之前进行实例检查(在无换行的注释模式下):“如果self.instance:返回self.instance.sku;否则:return self.fields ['sku']”
Daniel Naab 09年

2
对于最后一行,效果会return self.cleaned_data['sku']更好还是更好?该文档似乎使用建议cleaned_data:“这种方法的返回值将替换现有的值cleaned_data,因此它必须从该字段的值cleaned_data(即使这个方法并没有改变它)或新的清洁值。”
pianoJames

67

漫步者的答案对我帮助很大!

我已使用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字段是可读写的,但更改后该字段将变为只读。


55

为了使此功能适用于某个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
dhill

一个很棒的(更新)解决方案!不幸的是,当所有“禁用”的值都被清空时,当出现格式错误时,此问题和其余问题都会出现。
迈克尔·汤普森

28

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

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

6
这也不允许在添加时编辑字段,这就是原始问题所在。
Matt S.

这是我正在寻找的答案。Field disabled不执行我想要的操作,因为它禁用了该字段,但是也删除了标签/使其不可见。
sivabudh's

18

我做了一个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')

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

非常简单-并让我输出。在带有一堆只读值的表单集中很方便。当然-您也可以更聪明一些,并给它一个attrs的div,以便您可以向其添加类。


2
看起来很性感,但是如何处理外键?
andilabs 2015年

换句话说吧unicode(value)。假设Unicode笨拙是明智的,那么您就会明白这一点。
丹尼·史泰普

对于外键,您需要添加“模型”属性并使用“ get(值)”。检查我的要点
shadi

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当您添加新项目时将为“无”,或者在更改现有项目时将其为正在编辑的对象。

此处记录了get_readonly_display:http://docs.djangoproject.com/en/1.2/ref/contrib/admin/#modeladmin-methods


6

一种简单的选择是只输入form.instance.fieldName模板而不是form.fieldName


那场的verbos_namelabel呢?如何在Django模板中显示`label?@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

这只会从正面阻挡。任何人都可以绕过。如果您对敏感数据进行处理,这将造成安全问题
Sarath Ak


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

再一次,我将提供另一个解决方案:)我正在使用汉弗莱的代码,因此基于此。

但是,我遇到了一个问题,该领域是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')

我想它比我在这里建议的自己的mixin更具可读性。甚至可能更有效,因为这些清理不会引起验证错误……
christophe31

我收到一个错误:'collections.OrderedDict' object has no attribute 'iteritems'
geodesic

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)第一种方法-删除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)

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

根据christophe31的回答,这是一个涉及程度稍高的版本。它不依赖于“只读”属性。这样就产生了问题,例如选择框仍然可以更改,数据选择器仍然弹出。

而是将表单字段小部件包装在只读小部件中,从而使表单仍然有效。原始窗口小部件的内容显示在<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

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

表单用于收集数据,而不显示数据。话虽如此,在readonly小部件中显示和清除POST数据的选项是很好的解决方案。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.