read only fields disable django forms field readonly

only - En un formulario Django, ¿cómo hago un campo de solo lectura(o deshabilitado) para que no pueda editarse?



django forms fields (26)

En un formulario Django, ¿cómo hago un campo de solo lectura (o deshabilitado)?

Cuando se usa el formulario para crear una nueva entrada, todos los campos deben estar habilitados, pero cuando el registro está en modo de actualización, algunos campos deben ser de solo lectura.

Por ejemplo, al crear un nuevo modelo de Item , todos los campos deben ser editables, pero al actualizar el registro, ¿hay una manera de deshabilitar el campo sku para que esté visible, pero no se pueda 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

¿ ItemForm puede reutilizar la clase ItemForm ? ¿Qué cambios serían necesarios en la clase de modelo ItemForm o Item ? ¿Necesitaría escribir otra clase, " ItemUpdateForm ", para actualizar el artículo?

def update_item_view(request): if request.method == ''POST'': form = ItemUpdateForm(request.POST) # Validate and save else: form = ItemUpdateForm()


¿Es esta la forma más sencilla?

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!


Acabo de crear el widget más simple posible para un campo de solo lectura. Realmente no veo por qué los formularios aún 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 me acaba de dar salida. Práctico en un conjunto de formas con un montón de valores de solo lectura. Por supuesto, también podría ser un poco más inteligente y darle un div con los atributos para poder agregarle clases.


Aquí hay una versión un poco más complicada, basada en la respuesta de christophe31 . No se basa en el atributo "readonly". Esto hace que sus problemas, como los cuadros de selección que aún se pueden cambiar y los datos que aún aparecen, desaparezcan.

En su lugar, envuelve el widget de campos de formulario en un widget de solo lectura, lo que hace que el formulario aún se valide. El contenido del widget original se muestra dentro de las etiquetas <span class="hidden"></span> . Si el widget tiene un método render_readonly() , lo utiliza como texto visible, de lo contrario, analiza el HTML del widget original e intenta 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)


Basándome en la respuesta de Yamikep , encontré una solución mejor y muy simple que también maneja los campos ModelMultipleChoiceField .

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


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


Como se señaló en esta respuesta , Django 1.9 agregó el atributo Field.disabled :

El argumento booleano deshabilitado, cuando se establece en Verdadero, deshabilita un campo de formulario usando el atributo HTML deshabilitado para que los usuarios no puedan editarlo. Incluso si un usuario manipula el valor del campo enviado al servidor, se ignorará a favor del valor de los datos iniciales del formulario.

Con Django 1.8 y versiones anteriores, para deshabilitar la entrada en el widget y evitar los hacks POST maliciosos, debe limpiar la entrada además de configurar el atributo de readonly en el campo de 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 readonly .

La función clean_sku asegurará que el valor de readonly no será anulado por un POST .

De lo contrario, no hay un campo de formulario Django incorporado que genere un valor mientras rechaza los datos de entrada enlazados. Si esto es lo que desea, debe crear un ModelForm separado que excluya los campos no editables e imprimirlos dentro de su plantilla.


Como todavía no puedo comentar ( la 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'']


Como una adición útil a la publicación de Humphrey , tuve algunos problemas con django-reversion, porque todavía registra 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)


Creo que su mejor opción sería incluir el atributo de solo lectura en su plantilla representada en un <span> o <p> lugar de incluirlo en el formulario si es de solo lectura.

Los formularios son para recopilar datos, no mostrarlos. Dicho esto, las opciones para mostrar en un widget de readonly y fregar datos POST son buenas soluciones.


Django 1.9 agregó el atributo Field.disabled: Field.disabled

El argumento booleano deshabilitado, cuando se establece en Verdadero, deshabilita un campo de formulario usando el atributo HTML deshabilitado para que los usuarios no puedan editarlo. Incluso si un usuario manipula el valor del campo enviado al servidor, se ignorará a favor del valor de los datos iniciales del formulario.


Dos enfoques más (similares) con un ejemplo generalizado:

1) primer acercamiento: eliminación del campo en el método save (), por ejemplo (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: reajuste 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)


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 define cuales deben ser de solo lectura:

class MyFormWithReadOnlyFields(ReadOnlyFieldsMixin, MyForm): readonly_fields = (''field1'', ''field2'', ''fieldx'')


Hice una clase MixIn que puede heredar para poder agregar un campo iterable read_only que deshabilitará y protegerá 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'')


La configuración de READONLY en el widget solo hace que la entrada del navegador sea de solo lectura. Al agregar un clean_sku que devuelve instance.sku, se garantiza 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 manera, puede usar el modelo (guardar sin modificar) y aviod para obtener el error de campo requerido.


Me encontré con un problema similar. Parece que pude resolverlo definiendo un método "get_readonly_fields" 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 Artículo, o será el objeto que se está editando cuando está cambiando un Artículo existente.

get_readonly_display se documenta aquí: http://docs.djangoproject.com/en/1.2/ref/contrib/admin/#modeladmin-methods


Para Django 1.2+, puedes anular el campo así:

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


Para Django 1.9+
Puede usar el argumento Campos deshabilitados para deshabilitar el campo. por ejemplo, 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'')

Consulte https://docs.djangoproject.com/en/2.0/ref/forms/fields/#disabled


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


Para que esto funcione en 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, entonces el navegador no envía ningún dato de formulario para ese campo. Por lo tanto, debemos configurar ese campo para que no sea necesario para que el campo se valide correctamente. Entonces necesitamos restablecer el valor a lo que solía ser para que no esté en blanco.

Así que para las claves foráneas necesitarás 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 manera, el navegador no permitirá que el usuario cambie el campo, y siempre POSTARÁ, ya que se dejó en blanco. Luego, anulamos el método de limpieza para establecer que el valor del campo sea lo que originalmente estaba en la instancia.


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


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 __init__ formulario:

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 de readonly , no necesita ningún otro valor excepto el initial .

PD: No olvides configurar yuor_form_field.widget.attrs[''readonly''] = True


Si está utilizando el administrador de Django, esta es la solución más sencilla.

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


Si necesita varios campos de solo lectura. Puede utilizar cualquiera de los métodos que se indican 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'',)


Una opción simple es simplemente escribir form.instance.fieldName en la plantilla en lugar de form.fieldName .


Una vez más, voy a ofrecer una solución más :) Estaba usando el código de Humphrey , así que esto se basa en eso.

Sin embargo, me encontré con problemas con el campo siendo un ModelChoiceField. Todo funcionaría en la primera petición. Sin embargo, si el formset intentó agregar un nuevo elemento y falló la validación, algo salió mal con los formularios "existentes" en los que la opción SELECTED se estaba restableciendo al valor predeterminado "---------".

De todos modos, no pude averiguar cómo arreglar eso. Entonces, en lugar de eso, (y creo que esto está realmente más limpio en el formulario), hice los campos HiddenInputField (). Esto solo significa que tienes que hacer un poco más de trabajo en la plantilla.

Así que 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, necesitarás hacer algunos bucles manuales de formset .

Entonces, en este caso, harías 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.


¡La respuesta de Awalker me ayudó mucho!

He cambiado su ejemplo para trabajar con Django 1.3, usando get_readonly_fields .

Por lo general, deberías 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 y escritura, pero al cambiar se convierte en de solo lectura.