En una Django forma, ¿cómo puedo hacer que un campo de sólo lectura (o movilidad), de modo que no se puede editar?
Pregunta
En una Django forma, ¿cómo puedo hacer que un campo de sólo lectura (o movilidad)?
Cuando el formulario se utiliza para crear una nueva entrada, todos los campos deben estar activada pero cuando el expediente está en el modo de actualización de algunos campos tienen que ser de sólo lectura.
Por ejemplo, al crear una nueva Item
el modelo, todos los campos deben ser editables, pero al actualizar el registro, hay una manera de desactivar el sku
campo, de modo que es visible, pero no se puede editar?
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
Puede la clase ItemForm
ser reutilizados?¿Qué cambios serían necesarios en el ItemForm
o Item
modelo de clase?Tengo que escribir otra clase, "ItemUpdateForm
", para actualizar el elemento?
def update_item_view(request):
if request.method == 'POST':
form = ItemUpdateForm(request.POST)
# Validate and save
else:
form = ItemUpdateForm()
Solución
Como se señaló en esta respuesta , Django 1.9 agregó el Field.disabled atributo:
El argumento booleano deshabilitado, cuando se establece en Verdadero, deshabilita un campo de formulario usando el atributo HTML deshabilitado para que no sea & # 8217; t sea editable por los usuarios. Incluso si un usuario manipula el valor del campo & # 8217; s enviado al servidor, se ignorará a favor del valor del formulario & # 8217; s data inicial.
Con Django 1.8 y versiones anteriores, para deshabilitar la entrada en el widget y evitar hacks POST maliciosos, debe eliminar la entrada además de configurar el atributo readonly
en el campo del formulario:
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']
O reemplace if instance and instance.pk
con otra condición que indique que está editando. También puede establecer el atributo disabled
en el campo de entrada, en lugar de clean_sku
.
La función POST
asegurará que el valor ModelForm
no sea anulado por un <=>.
De lo contrario, no hay un campo de formulario Django incorporado que represente un valor mientras rechaza los datos de entrada vinculados. Si esto es lo que desea, debe crear un <=> separado que excluya los campos no editables e imprimirlos dentro de su plantilla.
Otros consejos
Django 1.9 agregó el atributo Field.disabled: https: / /docs.djangoproject.com/en/stable/ref/forms/fields/#disabled
El argumento booleano deshabilitado, cuando se establece en Verdadero, deshabilita un campo de formulario usando el atributo HTML deshabilitado para que no sea & # 8217; t sea editable por los usuarios. Incluso si un usuario manipula el valor del campo & # 8217; s enviado al servidor, se ignorará a favor del valor del formulario & # 8217; s data inicial.
Configurar READONLY en el widget solo hace que la entrada en el navegador sea de solo lectura. Agregar un clean_sku que devuelve instancia.sku asegura que el valor del campo no cambiará en el nivel del formulario.
def clean_sku(self):
if self.instance:
return self.instance.sku
else:
return self.fields['sku']
De esta forma, puede usar el modelo (guardar sin modificar) y obtener el error de campo requerido.
la respuesta de awalker me ayudó mucho
He cambiado su ejemplo para trabajar con Django 1.3, usando get_readonly_fields .
Por lo general, debe declarar algo como esto en app/admin.py
:
class ItemAdmin(admin.ModelAdmin):
...
readonly_fields = ('url',)
Me he adaptado de esta manera:
# In the admin.py file
class ItemAdmin(admin.ModelAdmin):
...
def get_readonly_fields(self, request, obj=None):
if obj:
return ['url']
else:
return []
Y funciona bien. Ahora, si agrega un elemento, el campo url
es de lectura-escritura, pero al cambiarlo se convierte en solo lectura.
Para que esto funcione para un campo ForeignKey
, es necesario realizar algunos cambios. En primer lugar, la etiqueta SELECT HTML
no tiene el atributo de solo lectura. Necesitamos usar disabled="disabled"
en su lugar. Sin embargo, el navegador no envía ningún dato de formulario para ese campo. Por lo tanto, debemos establecer que ese campo no sea obligatorio para que el campo se valide correctamente. Luego, necesitamos restablecer el valor a lo que solía ser para que no esté en blanco.
Entonces, para claves foráneas deberá hacer 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)
De esta forma, el navegador no permitirá que el usuario cambie el campo y siempre POST
como se dejó en blanco. Luego anulamos el método clean
para establecer que el valor del campo sea el original de la instancia.
Para Django 1.2+, puede anular el campo así:
sku = forms.CharField(widget = forms.TextInput(attrs={'readonly':'readonly'}))
Hice una clase MixIn que puede heredar para poder agregar un campo iterable read_only que deshabilitará y asegurará los campos en la primera edición:
(Basado en las respuestas de Daniel y 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')
Acabo de crear el widget más simple posible para un campo de solo lectura. Realmente no veo por qué los formularios ya no tienen esto:
class ReadOnlyWidget(widgets.Widget):
"""Some of these values are read only - just a bit of text..."""
def render(self, _, value, attrs=None):
return value
En la forma:
my_read_only = CharField(widget=ReadOnlyWidget())
Muy simple, y solo me da salida. Práctico en un conjunto de formularios con un montón de valores de solo lectura. Por supuesto, también podrías ser un poco más inteligente y darle un div con los atributos para que puedas agregarle clases.
Me encontré con un problema similar. Parece que pude resolverlo definiendo un & Quot; get_readonly_fields & Quot; método en mi clase ModelAdmin.
Algo como esto:
# In the admin.py file
class ItemAdmin(admin.ModelAdmin):
def get_readonly_display(self, request, obj=None):
if obj:
return ['sku']
else:
return []
Lo bueno es que obj
será Ninguno cuando esté agregando un nuevo Elemento, o será el objeto que se está editando cuando esté cambiando un Elemento existente.
get_readonly_display se documenta aquí: http://docs.djangoproject.com/en/1.2/ ref / contrib / admin / # modeladmin-method
Una opción simple es escribir form.instance.fieldName
en la plantilla en lugar de form.fieldName
.
Como una adición útil a publicación de Humphrey , tuve algunos problemas con django-reversión, porque todavía registraba campos deshabilitados como 'cambiados'. El siguiente código soluciona el 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 todavía no puedo comentar ( solución de muhuk ), responderé como una respuesta separada. Este es un ejemplo de código completo, que funcionó para mí:
def clean_sku(self):
if self.instance and self.instance.pk:
return self.instance.sku
else:
return self.cleaned_data['sku']
Estaba teniendo el mismo problema, así que creé un Mixin que parece funcionar para mis 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, solo defina cuáles deben ser de solo lectura:
class MyFormWithReadOnlyFields(ReadOnlyFieldsMixin, MyForm):
readonly_fields = ('field1', 'field2', 'fieldx')
Una vez más, voy a ofrecer una solución más :) Estaba usando Código de Humphrey , por lo que esto se basa en eso.
Sin embargo, me encontré con problemas con el campo como ModelChoiceField. Todo funcionaría en la primera solicitud. Sin embargo, si el conjunto de formularios intentó agregar un nuevo elemento y falló la validación, algo iba mal con & Quot; existente & Quot; formularios en los que la opción SELECCIONADA se restablecía al valor predeterminado " --------- " ;.
De todos modos, no pude encontrar la manera de arreglar eso. Entonces, en cambio, (y creo que esto es realmente más limpio en el formulario), hice los campos HiddenInputField (). Esto solo significa que debe trabajar un poco más en la plantilla.
Entonces, la solución para mí fue simplificar el Formulario:
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()
Y luego, en la plantilla, deberá hacer algunas bucle manual del formulario .
Entonces, en este caso, haría algo como esto en la plantilla:
<div>
{{ form.instance.sku }} <!-- This prints the value -->
{{ form }} <!-- Prints form normally, and makes the hidden input -->
</div>
Esto funcionó un poco mejor para mí y con menos manipulación de formas.
Cómo lo hago con 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
Dos enfoques más (similares) con un ejemplo generalizado:
1) primer enfoque: eliminar el campo en el método save (), p. (no probado;)):
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) segundo enfoque: restablecer el campo al valor inicial en el método de limpieza:
def clean_<fieldname>(self):
return self.initial[<fieldname>] # or getattr(self.instance, fieldname)
Basado en el segundo enfoque, lo generalicé así:
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)
si necesita múltiples campos de solo lectura. puede usar cualquiera de los métodos que se proporcionan a continuación
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 de herencia
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 la versión Admin, creo que esta es una forma más compacta si tiene más de un 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
Basado en Respuesta de Yamikep , encontré una solución mejor y muy simple que también maneja ModelMultipleChoiceField
campos.
Eliminar el campo de form.cleaned_data
evita que los campos se guarden:
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')
Aquí es un poco más involucrados versión, basada en christophe31 la respuesta.No se basa en el atributo "readonly".Esto hace que sus problemas, como los cuadros de selección todavía ser variable y datapickers sigue apareciendo, desaparece.
En su lugar, envuelve a los campos de formulario widget en un readonly widget, con lo que la forma de confirmar.El contenido de la original widget se muestra dentro de <span class="hidden"></span>
las etiquetas.Si el widget tiene un render_readonly()
el método que se usa como texto visible, de lo contrario, se analiza el código HTML del widget original y trata de adivinar la mejor representación.
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)
¿Es esta la forma más simple?
Justo en un código de vista algo como esto:
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)
¡Funciona bien!
Para django 1.9+
Puede usar el argumento Campos deshabilitados para deshabilitar el campo.
p.ej. En el siguiente fragmento de código del archivo forms.py, he desactivado el campo employee_code
class EmployeeForm(forms.ModelForm):
employee_code = forms.CharField(disabled=True)
class Meta:
model = Employee
fields = ('employee_code', 'designation', 'salary')
Referencia https://docs.djangoproject.com/en/2.0/ref / formularios / campos / # deshabilitado
Si está trabajando con Django ver < 1.9
(el 1.9
ha agregado el atributo Field.disabled
) podría intentar agregar el siguiente decorador a su método de formulario __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):
...
La idea principal es que si el campo es readonly
no necesita ningún otro valor excepto initial
.
P.S: no olvide configurar yuor_form_field.widget.attrs['readonly'] = True
Si está utilizando el administrador de Django, esta es la solución más simple.
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',)
Creo que su mejor opción sería incluir el atributo de solo lectura en su plantilla representada en un <span>
o <p>
en lugar de incluirlo en el formulario si es de solo lectura.
Los formularios son para recopilar datos, no para mostrarlos. Dicho esto, las opciones para mostrar en un readonly
widget y borrar datos POST son buenas soluciones.
Resolví este problema así:
class UploadFileForm(forms.ModelForm):
class Meta:
model = FileStorage
fields = '__all__'
widgets = {'patient': forms.HiddenInput()}
en vistas:
form = UploadFileForm(request.POST, request.FILES, instance=patient, initial={'patient': patient})
Es todo.