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

在Django表单中,如何使字段为只读(或禁用)?

当表单被用来创build一个新条目时,所有的字段都应该被启用 – 但是当logging处于更新模式时,某些字段需要是只读的。

例如,当创build一个新的Item模型时,所有的字段都必须是可编辑的,但是在更新logging的时候,是否有办法禁用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 1.9添加了Field.disabled属性:

禁用的布尔参数设置为True时,将禁用使用禁用的HTML属性的表单字段,以便用户不可编辑。 即使用户篡改提交给服务器的字段值,也会忽略表单的初始数据中的值。

使用Django 1.8及更早版本,要禁用对小部件的input并防止恶意POST攻击,除了在表单字段中设置readonly属性之外,还必须擦除input:

 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.pkreplace为另一个表示正在编辑的条件。 您还可以在input字段上设置disabled的属性,而不是readonly

clean_sku函数将确保readonly值不会被POST覆盖。

否则,没有内置的Django表单字段,它会在拒绝绑定input数据的同时呈现值。 如果这是你的愿望,你应该创build一个单独的ModelForm ,排除不可编辑的字段,并打印出你的模板。

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

禁用的布尔参数设置为True时,将禁用使用禁用的HTML属性的表单字段,以便用户不可编辑。 即使用户篡改提交给服务器的字段值,也会忽略表单的初始数据中的值。

在小部件上设置READONLY只会使浏览器中的input成为只读。 添加一个返回instance.sku的clean_sku,确保字段值在表单级别上不会改变。

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

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

awalker的回答帮了我很多!

我已经改变了他的例子与Django 1.3,使用get_readonly_fields 。

通常你应该在app/admin.py声明这样的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字段是可读写的,但是在更改时它将变为只读。

为了使这项工作为ForeignKey领域,需要做一些改变。 首先,SELECT HTML标签没有readonly属性。 我们需要使用disabled =“disabled”来代替。 但是,浏览器不会为该字段发送任何表单数据。 所以我们需要设置该字段不被要求,以便字段validation正确。 然后,我们需要将该值重新设置为原来的值,所以不会设置为空白。

所以对于外键你需要做一些事情:

 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状态。 然后我们重写干净的方法来设置字段的值是最初在实例中的值。

对于Django 1.2+,你可以像这样重写字段:

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

我创build了一个MixIn类,您可以inheritance它,以便能够添加一个read_only iterable字段,该字段将禁用和保护非首次编辑的字段:

(根据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') 

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

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

forms如下:

 my_read_only = CharField(widget=ReadOnlyWidget()) 

很简单 – 让我输出。 方便在一堆只读值的formset。 当然 – 你也可以更聪明一些,并且可以给attrs一个div,这样你就可以将类添加到它。

我遇到了类似的问题。 看起来我可以通过在我的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_displaylogging在这里: http ://docs.djangoproject.com/en/1.2/ref/contrib/admin/#modeladmin-methods

作为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) 

由于我还不能评论( 穆胡克的解决scheme ),我会作为一个单独的答案作出回应。 这是一个完整的代码示例,为我工作:

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

我遇到了同样的问题,所以我创build了一个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') 

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

但是,我遇到了一个ModelChoiceField字段的问题。 一切都会在第一个请求上工作。 但是,如果formset尝试添加一个新项目并且validation失败,那么SELECTED选项被重置为默认“———”的“现有”表单会出现问题。

无论如何,我无法弄清楚如何解决这个问题。 所以相反,(我认为这实际上是更干净的forms),我做了字段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() 

然后在模板中,你需要做一些手动循环的formset 。

所以,在这种情况下,你可以在模板中做这样的事情:

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

这对我来说工作稍微好一点,而且对表单的操作也less一些。

另外两个(类似的)方法和一个广义的例子:

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

 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) 

一个简单的select是在模板中inputform.instance.fieldName而不是form.fieldName

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

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

inheritance方法

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

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

根据christophe31的回答 ,这是一个稍微更复杂的版本。 它不依赖于“只读”属性。 这使得它的问题,如select框仍然是可变的,数据引擎仍然popup,走开。

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

 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) 

这是最简单的方法吗?

在视图中的代码是这样的:

 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) 

它工作正常!

基于Yamikep的回答 ,我find了一个更好,更简单的解决scheme,它也处理ModelMultipleChoiceField字段。

form.cleaned_data删除字段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') 

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

如果你使用Django的pipe理员,这是最简单的解决scheme。

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

我认为你最好的select只是将你的模板中的readonly属性包含在<span><p>而不是将它包含在表单中。

表单用于收集数据,而不是显示数据。 这就是说,在readonly窗口小部件中显示的选项和擦除POST数据是很好的解决scheme。

在models.py中为'sku'CharField设置blank = True。 文件

models.py

 class Item(models.Model): sku = models.CharField(max_length=50, default='sku', blank=True) description = models.CharField(max_length=200) added_by = models.ForeignKey(User) 

forms.py

 class ItemForm(ModelForm): class Meta: model = Item fields = ('sku', 'description') 

views.py

 def new_item_view(request): if request.method == 'POST': form = ItemForm(request.POST) if form.is_valid(): itemf = form.save(commit=False) itemf.added_by = request.user itemf.save() return redirect('somewhere_url_name') else: form = ItemForm() return render(request, 'template.html', {'form': form}) def update_item_view(request, pk): item = get_object_or_404(Item, pk=pk) form = ItemForm(request.POST, instance=item) if request.method == 'POST': if form.is_valid(): itemf = form.save(commit=False) itemf.save() return redirect('somewhere_url_name') else: form = ItemForm(instance=item) item_edit_flag = True return render(request, 'template.html', {'form': form, 'item': item, 'item_ef': item_edit_flag}) 

在模板中

 ... <form method="POST"> ... <div class="form-sku"> {% if item_ef %} #edit item {{ item.sku }} {% else %} #new item {{ form.sku }} {% endif %} </div> ... </form> ...