在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()
答案 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.9
(1.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