Em uma forma Django, como eu faço um campo somente leitura (ou desativado) para que ele não pode ser editado?
Pergunta
Em uma forma Django, como eu faço um campo só de leitura (ou desativado)?
Quando o formulário está sendo usado para criar uma nova entrada, todos os campos devem ser habilitado -. Mas quando o registro está em modo de atualização alguns campos precisam ser somente leitura
Por exemplo, ao criar um novo modelo Item
, todos os campos devem ser editáveis, mas ao atualizar o registro, há uma maneira para desativar o campo sku
de modo que é visível, mas não pode ser editado?
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
Can classe ItemForm
ser reutilizado? Que mudanças seriam necessárias na classe ItemForm
ou modelo Item
? Será que eu preciso para escrever outra classe, "ItemUpdateForm
", para atualizar o produto?
def update_item_view(request):
if request.method == 'POST':
form = ItemUpdateForm(request.POST)
# Validate and save
else:
form = ItemUpdateForm()
Solução
Como apontado em esta resposta , Django 1.9 adicionou a Field.disabled atributo:
O boolean argumento desativado, quando definida como Verdadeiro, desativa um campo de formulário usando o atributo HTML desativado para que ele não será editável pelos usuários. Mesmo se um usuário mexe com o valor do campo enviada para o servidor, ele será ignorado em favor do valor dos dados iniciais do formulário.
Com Django 1.8 e anteriores, para desativar a entrada no widget e evitar hacks POST maliciosos você deve esfregar a entrada além de definir o atributo readonly
no campo de formulário:
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']
Ou, substitua if instance and instance.pk
com outra condição que indica que você está editando. Você também pode definir o disabled
atributo no campo de entrada, em vez de readonly
.
A função clean_sku
irá garantir que o valor readonly
não será substituído por um POST
.
Caso contrário, não há built-in Django campo de formulário que irá processar um valor enquanto rejeita dados de entrada encadernados. Se é isso que você deseja, você deveria criar um ModelForm
separado que exclui o campo não editável (s), e apenas imprimi-los dentro do seu modelo.
Outras dicas
Django 1.9 adicionou o atributo Field.disabled: https: / /docs.djangoproject.com/en/stable/ref/forms/fields/#disabled
O boolean argumento desativado, quando definida como Verdadeiro, desativa um campo de formulário usando o atributo HTML desativado para que ele não será editável pelos usuários. Mesmo se um usuário mexe com o valor do campo enviada para o servidor, ele será ignorado em favor do valor dos dados iniciais do formulário.
Configuração READONLY no Widget só faz a entrada no navegador somente leitura. Adicionando um clean_sku que retorna instance.sku garante o valor do campo não vai mudar no nível do formulário.
def clean_sku(self):
if self.instance:
return self.instance.sku
else:
return self.fields['sku']
Desta forma, você pode usar de (não modificada save) modelo e Aviod recebendo o campo de erro necessário.
a resposta de awalker me ajudou muito!
Eu mudei o seu exemplo para trabalhar com Django 1.3, utilizando get_readonly_fields .
Normalmente, você deve declarar algo como isto em app/admin.py
:
class ItemAdmin(admin.ModelAdmin):
...
readonly_fields = ('url',)
Eu adaptei desta forma:
# In the admin.py file
class ItemAdmin(admin.ModelAdmin):
...
def get_readonly_fields(self, request, obj=None):
if obj:
return ['url']
else:
return []
E ele funciona muito bem. Agora, se você adicionar um item, o campo url
é de leitura e escrita, mas sobre a mudança torna-se somente leitura.
Para fazer este trabalho para um campo ForeignKey
, algumas mudanças precisam ser feitas. Em primeiro lugar, a tag SELECT HTML
não tem o atributo somente leitura. Precisamos usar disabled="disabled"
vez. No entanto, o navegador não envia quaisquer dados de volta formulário para esse campo. Então precisamos definir esse campo para não ser necessária para que o campo valida corretamente. Depois, temos que redefinir o valor de volta para o que costumava ser, então não é definido como em branco.
Assim, para as chaves estrangeiras que você vai precisar fazer algo como:
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)
Desta forma, o navegador não vai deixar a mudança de usuário do campo, e será sempre POST
como ele era em branco esquerdo. Em seguida, substituir o método clean
para definir o valor do campo a ser o que era originalmente na instância.
Para Django 1.2+, você pode substituir o campo assim:
sku = forms.CharField(widget = forms.TextInput(attrs={'readonly':'readonly'}))
Eu fiz uma classe MixIn que você pode herdar de ser capaz de adicionar um campo read_only iterable que irá desativar e campos seguras sobre a não-primeira edição:
(Baseado em Daniel e respostas de 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')
Acabei de criar o mais simples Widget possível para um campo somente leitura - Eu realmente não vejo por que formas não tem isso já:
class ReadOnlyWidget(widgets.Widget):
"""Some of these values are read only - just a bit of text..."""
def render(self, _, value, attrs=None):
return value
Na forma:
my_read_only = CharField(widget=ReadOnlyWidget())
Muito simples - e me deixa apenas de saída. Handy em um formset com um monte de valores somente leitura. Claro -. Você também pode ser um pouco mais inteligente e dar-lhe um div com as attrs assim você pode acrescentar aulas a ele
deparei com um problema semelhante. Parece que eu era capaz de resolvê-lo através da definição de um método "get_readonly_fields" na minha classe ModelAdmin.
Algo parecido com isto:
# In the admin.py file
class ItemAdmin(admin.ModelAdmin):
def get_readonly_display(self, request, obj=None):
if obj:
return ['sku']
else:
return []
O bom é que obj
será Nenhum quando você está adicionando um novo item, ou ele vai ser o objeto que está sendo editado quando você está mudando um item existente.
get_readonly_display está documentado aqui: http://docs.djangoproject.com/en/1.2/ ref / contrib / admin / # ModelAdmin-métodos
Uma opção simples é o tipo de apenas form.instance.fieldName
no modelo em vez de form.fieldName
.
Como uma adição útil para pós de Humphrey, eu tive alguns problemas com django-reversão, porque ainda registrado campos com deficiência como 'mudou'. As seguintes correções de código o problema.
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)
Como eu ainda não pode comentar ( solução de muhuk), eu vou resposta como uma resposta em separado. Este é um exemplo de código completo, que funcionou para mim:
def clean_sku(self):
if self.instance and self.instance.pk:
return self.instance.sku
else:
return self.cleaned_data['sku']
Eu estava indo para o mesmo problema, então eu criei um Mixin que parece funcionar para os meus casos de uso.
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
Uso, apenas definir quais os que devem ser lidos apenas:
class MyFormWithReadOnlyFields(ReadOnlyFieldsMixin, MyForm):
readonly_fields = ('field1', 'field2', 'fieldx')
Mais uma vez, eu estou indo para oferecer mais uma solução :) eu estava usando Humphrey código , de modo que este é baseado fora disso.
No entanto, eu corri para problemas com o campo sendo um ModelChoiceField. Tudo iria trabalhar na primeira solicitação. No entanto, se o formset tentou adicionar um novo item e validação falhou, algo estava acontecendo de errado com as formas "existentes", onde a opção selecionada foi sendo redefinido para o padrão "---------".
De qualquer forma, eu não conseguia descobrir como consertar isso. Então, ao invés, (e eu acho que isso é realmente mais limpo na forma), eu fiz a campos HiddenInputField (). Isto apenas significa que você tem que fazer um pouco mais de trabalho no modelo.
Assim, a correção para mim era simplificar o formulário:
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()
E, em seguida, no modelo, você precisa fazer alguma looping manual da formset .
Assim, neste caso, você poderia fazer algo assim no modelo:
<div>
{{ form.instance.sku }} <!-- This prints the value -->
{{ form }} <!-- Prints form normally, and makes the hidden input -->
</div>
Isso funcionou um pouco melhor para mim e com menos manipulação formulário.
Como eu faço isso com 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
Dois mais (semelhante) aproxima-se com um exemplo generalizada:
1) primeira abordagem - Método de remoção de campo em save (), por exemplo (Não testado;)):
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) segunda abordagem - campo redefinida para o valor inicial no método limpo:
def clean_<fieldname>(self):
return self.initial[<fieldname>] # or getattr(self.instance, fieldname)
Com base na segunda abordagem I generalizada assim:
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)
Se a sua necessidade múltipla read-only fields.you pode usar qualquer um dos métodos indicados abaixo
método 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
Método 2
método herança
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',)
Para a versão de administração, penso que esta é uma forma mais compacta, se você tem mais de um campo:
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
Com base em o Yamikep resposta , eu encontrei uma solução melhor e muito simples que também alças ModelMultipleChoiceField
campos.
A remoção campo dos campos evita form.cleaned_data
de ser salvo:
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()
Uso:
class MyFormWithReadOnlyFields(ReadOnlyFieldsMixin, MyForm):
readonly_fields = ('field1', 'field2', 'fieldx')
Aqui está uma versão um pouco mais envolvidos, com base em das christophe31 resposta . Não contar com o atributo "somente leitura". Isso faz com que os seus problemas, como caixas de seleção ainda está sendo mutável e datapickers ainda aparecendo, vá embora.
Em vez disso, ele envolve os campos do formulário Widget em um readonly widget, tornando assim o formulário ainda validar. O conteúdo do widget original é exibida dentro de tags <span class="hidden"></span>
. Se o widget tem um método render_readonly()
ele usa isso como o texto visível, caso contrário, ele analisa o código HTML do widget original e tenta adivinhar a melhor representação.
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)
É esta a maneira mais simples?
Direito em uma coisa de código vista como esta:
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)
Ele funciona muito bem!
Para Django 1.9+
Você pode usar campos argumento desativado para fazer disable campo.
por exemplo. No seguinte trecho de código a partir do arquivo forms.py, eu fiz campo employee_code desativado
class EmployeeForm(forms.ModelForm):
employee_code = forms.CharField(disabled=True)
class Meta:
model = Employee
fields = ('employee_code', 'designation', 'salary')
Referência https://docs.djangoproject.com/en/2.0/ref / formas / campos / # desativada
Se você está trabalhando com Django ver < 1.9
(a 1.9
adicionou atributo Field.disabled
) você poderia tentar adicionar seguinte decorador para o seu método de forma __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):
...
A idéia principal é que se o campo é readonly
você não precisa de qualquer outro valor, exceto initial
.
P.S: Não se esqueça de conjunto yuor_form_field.widget.attrs['readonly'] = True
Se você estiver usando Django admin, aqui é a solução mais simples.
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',)
Eu acho que a melhor opção seria apenas para incluir o atributo somente leitura em seu modelo processado em uma <span>
ou <p>
ao invés de incluí-lo na forma se é somente leitura.
Os formulários são para a recolha de dados, e não exibi-lo. Dito isto, as opções para exibição em um widget readonly
e POST matagal dados são soluções finas.
Eu resolvi esse problema como este:
class UploadFileForm(forms.ModelForm):
class Meta:
model = FileStorage
fields = '__all__'
widgets = {'patient': forms.HiddenInput()}
em vista:
form = UploadFileForm(request.POST, request.FILES, instance=patient, initial={'patient': patient})
É tudo.