В форме Django, как сделать поле 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
? Какие изменения потребуются в классе модели ItemForm
или Item
? Нужно ли мне писать другой класс "ItemUpdateForm
" для обновления элемента?
def update_item_view(request):
if request.method == 'POST':
form = ItemUpdateForm(request.POST)
# Validate and save
else:
form = ItemUpdateForm()
Ответы
Ответ 1
Как указано в этом ответе, Django 1.9 добавил атрибут Field.disabled:
Отключенный логический аргумент, если задано значение 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
readonly
значения не будет переопределена POST
.
В противном случае, нет встроенного поля формы Django, которое будет отображать значение при отклонении связанных входных данных. Если это то, что вы хотите, вместо этого вы должны создать отдельную ModelForm
, исключающую не редактируемые поля, и просто распечатать их внутри шаблона.
Ответ 2
Django 1.9 добавил атрибут Field.disabled: https://docs.djangoproject.com/en/stable/ref/forms/fields/#disabled
Отключенный логический аргумент, если задано значение True, отключает поле формы с помощью отключенного атрибута HTML, чтобы пользователи не могли его редактировать. Даже если пользователь изменяет значение поля, отправленное на сервер, оно будет игнорироваться в пользу значения из исходных данных форм.
Ответ 3
Настройка READONLY на виджетах делает вход только в браузере только для чтения. Добавление clean_sku, которое возвращает instance.sku, гарантирует, что значение поля не изменится на уровне формы.
def clean_sku(self):
if self.instance:
return self.instance.sku
else:
return self.fields['sku']
Таким образом вы можете использовать модель (немодифицированное сохранение) и aviod, чтобы получить требуемую поле.
Ответ 4
Ответ awalker мне очень помог!
Я изменил его пример для работы с Django 1.3, используя get_readonly_fields.
Обычно вы должны объявить что-то вроде этого в 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
чтения и записи, но при изменении оно становится доступным только для чтения.
Ответ 5
Чтобы это работало для поля ForeignKey
, необходимо внести несколько изменений. Во-первых, SELECT HTML
тег 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
чтобы установить значение поля, которое было изначально в экземпляре.
Ответ 6
Для Django 1.2+ вы можете переопределить поле следующим образом:
sku = forms.CharField(widget = forms.TextInput(attrs={'readonly':'readonly'}))
Ответ 7
Я сделал класс MixIn, который вы можете наследовать, чтобы добавить поле read_only iterable, которое отключит и защитит поля в не-первом редактировании:
(На основании ответов Даниила и Мухука)
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')
Ответ 8
Я только что создал простейший возможный виджет для поля 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())
Очень просто - и меня просто выводит. Удобен в наборе форм с кучей значений только для чтения.
Конечно, вы также можете быть немного умнее и дать ему div с attrs, чтобы вы могли добавлять к нему классы.
Ответ 9
Я столкнулся с аналогичной проблемой.
Похоже, я смог его решить, указав метод get_readonly_fields в моем классе ModelAdmin.
Что-то вроде этого:
# 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
Ответ 10
Один простой вариант - просто набрать form.instance.fieldName
в шаблоне вместо form.fieldName
.
Ответ 11
В качестве полезного дополнения к сообщению Хамфри у меня возникли проблемы с 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)
Ответ 12
Как я еще не могу прокомментировать (решение muhuk), я отвечу как отдельный ответ. Это полный пример кода, который работал у меня:
def clean_sku(self):
if self.instance and self.instance.pk:
return self.instance.sku
else:
return self.cleaned_data['sku']
Ответ 13
Опять же, я собираюсь предложить еще одно решение:) Я использовал код Хамфри, поэтому это основано на этом.
Однако я столкнулся с проблемами, когда поле было ModelChoiceField. Все будет работать по первому запросу. Однако, если формат попытался добавить новый элемент и не прошел проверку, что-то не соответствовало "существующим" формам, где параметр SELECTED был reset по умолчанию "---------".
Во всяком случае, я не мог понять, как это исправить. Поэтому вместо этого (и я думаю, что это на самом деле чище в форме), я создал поля 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>
Это немного улучшилось для меня и с меньшими формами.
Ответ 14
Я входил в ту же проблему, поэтому создал 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')
Ответ 15
Как я это делаю с 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
Ответ 16
Еще два (похожих) подхода с одним обобщенным примером:
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) второй подход - reset поле к исходному значению в чистом методе:
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)
Ответ 17
если вам нужно несколько полей только для чтения. Вы можете использовать любой из приведенных ниже методов
метод 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',)
Ответ 18
Для версии 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
Ответ 19
Вот немного более сложная версия, основанная на ответе 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)
Ответ 20
На основе ответа 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')
Ответ 21
Это самый простой способ?
Прямо в коде вида что-то вроде этого:
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)
Он отлично работает!
Ответ 22
Для Джанго 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
Ответ 23
Если вы работаете с 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
для readonly
вам не нужно никакого другого значения, кроме initial
.
PS: не забудьте установить yuor_form_field.widget.attrs['readonly'] = True
Ответ 24
Если вы используете 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',)
Ответ 25
Я думаю, что ваш лучший вариант состоял бы в том, чтобы включить атрибут readonly в ваш шаблон, отображаемый в <span>
или <p>
, а не включать его в форму, если он читается только.
Формы предназначены для сбора данных, а не для отображения. При этом опции для отображения в виджетах readonly
и скраб POST-данных являются точными решениями.
Ответ 26
Я решил эту проблему так:
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})
Все это.
Ответ 27
Установите blank = True для "sku" CharField в models.py. Doc
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>
...