Dans un formulaire Django, comment créer un champ en lecture seule (ou désactivé)?
Lorsque le formulaire est utilisé pour créer une nouvelle entrée, tous les champs doivent être activés. Toutefois, lorsque l'enregistrement est en mode de mise à jour, certains champs doivent être en lecture seule.
Par exemple, lors de la création d'un nouveau modèle Item
, tous les champs doivent être modifiables, mais lors de la mise à jour de l'enregistrement, existe-t-il un moyen de désactiver le champ sku
afin qu'il soit visible, mais ne puisse pas être modifié?
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
La classe ItemForm
peut-elle être réutilisée? Quels changements seraient nécessaires dans la classe de modèle ItemForm
ou Item
? Aurais-je besoin d'écrire une autre classe, "ItemUpdateForm
", pour mettre à jour l'élément?
def update_item_view(request):
if request.method == 'POST':
form = ItemUpdateForm(request.POST)
# Validate and save
else:
form = ItemUpdateForm()
Comme indiqué dans cette réponse , Django 1.9 a ajouté l'attribut Field.disabled :
Lorsqu'il est défini sur True, l’argument booléen désactivé désactive un champ de formulaire utilisant l’attribut HTML désactivé afin qu’il ne puisse pas être modifié par les utilisateurs. Même si un utilisateur altère la valeur du champ soumis au serveur, il sera ignoré en faveur de la valeur des données initiales du formulaire.
Avec Django 1.8 et les versions antérieures, pour désactiver la saisie sur le widget et empêcher les piratages malveillants POST, vous devez nettoyer l'entrée en plus de définir l'attribut readonly
sur le champ de formulaire:
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, remplacez if instance and instance.pk
par une autre condition indiquant que vous modifiez. Vous pouvez également définir l'attribut disabled
sur le champ de saisie au lieu de readonly
.
La fonction clean_sku
s'assurera que la valeur readonly
ne sera pas remplacée par une POST
.
Sinon, aucun champ de formulaire Django intégré ne rendra une valeur lors du rejet des données d'entrée liées. Si vous le souhaitez, vous devez plutôt créer une variable ModelForm
qui exclut les champs non modifiables et les imprimer dans votre modèle.
Django 1.9 a ajouté l'attribut Field.disabled: https://docs.djangoproject.com/fr/stable/ref/forms/fields/#disabled
Lorsqu'il est défini sur True, l’argument booléen désactivé désactive un champ de formulaire utilisant l’attribut HTML désactivé afin qu’il ne puisse pas être modifié par les utilisateurs. Même si un utilisateur altère la valeur du champ soumise au serveur, il sera ignoré en faveur de la valeur des données initiales du formulaire.
Si vous définissez READONLY sur un widget, l'entrée dans le navigateur est en lecture seule. L'ajout d'un clean_sku qui renvoie instance.sku garantit que la valeur du champ ne changera pas au niveau du formulaire.
def clean_sku(self):
if self.instance:
return self.instance.sku
else:
return self.fields['sku']
De cette façon, vous pouvez utiliser le modèle (sauvegarde non modifiée) et éviter d'obtenir l'erreur du champ requis.
La réponse de awalker m'a beaucoup aidé!
J'ai changé son exemple pour utiliser Django 1.3, en utilisant get_readonly_fields .
Habituellement, vous devriez déclarer quelque chose comme ceci dans app/admin.py
:
class ItemAdmin(admin.ModelAdmin):
...
readonly_fields = ('url',)
Je me suis adapté de cette façon:
# In the admin.py file
class ItemAdmin(admin.ModelAdmin):
...
def get_readonly_fields(self, request, obj=None):
if obj:
return ['url']
else:
return []
Et ça marche bien. Maintenant, si vous ajoutez un élément, le champ url
est en lecture-écriture, mais il devient en lecture seule lors d'une modification.
Pour que cela fonctionne pour un champ ForeignKey, quelques modifications doivent être apportées. Premièrement, la balise SELECT HTML n'a pas l'attribut readonly. Nous devons utiliser disabled = "disabled" à la place. Cependant, le navigateur n'envoie aucune donnée de formulaire pour ce champ. Nous devons donc définir ce champ pour qu'il ne soit pas requis afin qu'il soit correctement validé. Nous devons ensuite réinitialiser la valeur à ce qu'elle était pour qu'elle ne soit pas vide.
Donc, pour les clés étrangères, vous devrez faire quelque chose comme:
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 cette façon, le navigateur ne laissera pas l’utilisateur changer le champ, et toujours POST tel qu’il était laissé vide. Nous substituons ensuite la méthode clean pour définir la valeur du champ comme étant à l'origine dans l'instance.
Pour Django 1.2+, vous pouvez remplacer le champ de la manière suivante:
sku = forms.CharField(widget = forms.TextInput(attrs={'readonly':'readonly'}))
J'ai créé une classe MixIn dont vous pouvez hériter pour pouvoir ajouter un champ itératif read_only qui désactivera et sécurisera les champs lors de la première modification:
(Basé sur les réponses de Daniel et 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')
Je viens de créer le widget le plus simple possible pour un champ en lecture seule - je ne vois pas vraiment pourquoi les formulaires n'ont pas déjà cela:
class ReadOnlyWidget(widgets.Widget):
"""Some of these values are read only - just a bit of text..."""
def render(self, _, value, attrs=None):
return value
Sous la forme:
my_read_only = CharField(widget=ReadOnlyWidget())
Très simple - et me donne juste la sortie. Bien pratique dans un formset avec un tas de valeurs en lecture seule ... Bien sûr - vous pourriez aussi être un peu plus intelligent et lui donner une div avec les attributs afin que vous puissiez y ajouter des classes.
J'ai rencontré un problème similaire. Il semble que je sois capable de le résoudre en définissant une méthode "get_readonly_fields" dans ma classe ModelAdmin.
Quelque chose comme ça:
# In the admin.py file
class ItemAdmin(admin.ModelAdmin):
def get_readonly_display(self, request, obj=None):
if obj:
return ['sku']
else:
return []
La bonne chose est que obj
sera Non lorsque vous ajoutez un nouvel élément, ou ce sera l'objet en cours de modification lorsque vous modifiez un élément existant.
get_readonly_display est documenté ici: http://docs.djangoproject.com/fr/1.2/ref/contrib/admin/#modeladmin-methods
Comme je ne peux pas encore commenter ( la solution de muhuk ), je vous répondrai séparément. Ceci est un exemple de code complet, qui a fonctionné pour moi:
def clean_sku(self):
if self.instance and self.instance.pk:
return self.instance.sku
else:
return self.cleaned_data['sku']
En tant que complément utile à le message de Humphrey , je rencontrais quelques problèmes avec Django-reversion, car il enregistrait toujours les champs désactivés comme "modifiés". Le code suivant corrige le problème.
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)
Encore une fois, je vais proposer une solution supplémentaire :) J'utilisais le code de Humphrey , donc c'est basé sur cela.
Cependant, j'ai rencontré des problèmes avec le champ étant un ModelChoiceField. Tout fonctionnerait à la première demande. Toutefois, si le formset tentait d'ajouter un nouvel élément et échouait à la validation, il y avait un problème avec les formulaires "existants" où l'option SELECTED était réinitialisée sur la valeur par défaut "---------".
Quoi qu'il en soit, je ne savais pas comment résoudre ce problème. Alors au lieu de cela (et je pense que cela est en fait plus propre dans le formulaire), j’ai créé les champs HiddenInputField (). Cela signifie simplement que vous devez faire un peu plus de travail dans le modèle.
La solution pour moi était donc de simplifier le formulaire:
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()
Et ensuite, dans le modèle, vous devrez faire une boucle en boucle manuelle du formset .
Donc, dans ce cas, vous feriez quelque chose comme ceci dans le modèle:
<div>
{{ form.instance.sku }} <!-- This prints the value -->
{{ form }} <!-- Prints form normally, and makes the hidden input -->
</div>
Cela a fonctionné un peu mieux pour moi et avec moins de manipulation de formulaire.
Une option simple consiste simplement à taper form.instance.fieldName
dans le modèle au lieu de form.fieldName
.
Comment je le fais avec 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
J'entrais dans le même problème alors j'ai créé un Mixin qui semble fonctionner pour mes cas d'utilisation.
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
Utilisation, définissez simplement ceux qui doivent être lus uniquement:
class MyFormWithReadOnlyFields(ReadOnlyFieldsMixin, MyForm):
readonly_fields = ('field1', 'field2', 'fieldx')
si vous avez besoin de plusieurs champs en lecture seule, vous pouvez utiliser l’une des méthodes décrites ci-dessous.
méthode 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éthode 2
méthode d'héritage
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',)
Deux autres approches (similaires) avec un exemple généralisé:
1) première approche - suppression du champ dans la méthode save (), par ex. (pas testé ;) ):
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) deuxième approche - réinitialiser le champ à la valeur initiale dans la méthode de nettoyage:
def clean_<fieldname>(self):
return self.initial[<fieldname>] # or getattr(self.instance, fieldname)
Basé sur la seconde approche, je l’ai généralisé comme ceci:
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)
Voici une version légèrement plus compliquée, basée sur réponse de christophe31 . Il ne repose pas sur l'attribut "readonly". Cela fait en sorte que ses problèmes, comme si les cases de sélection étaient toujours changeantes et les pics de données qui surgissaient toujours, disparaissaient.
Au lieu de cela, il encapsule le widget de champs de formulaire dans un widget en lecture seule, rendant ainsi le formulaire toujours valide. Le contenu du widget d'origine est affiché dans les balises <span class="hidden"></span>
. Si le widget a une méthode render_readonly()
, il l'utilise comme texte visible, sinon, il analyse le code HTML du widget d'origine et essaie de deviner la meilleure représentation.
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)
Sur la base de la réponse de Yamikep , j'ai trouvé une solution plus simple et plus efficace, qui gère également les champs ModelMultipleChoiceField
.
Supprimer le champ de form.cleaned_data
empêche les champs d'être sauvegardés:
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()
Usage:
class MyFormWithReadOnlyFields(ReadOnlyFieldsMixin, MyForm):
readonly_fields = ('field1', 'field2', 'fieldx')
Pour la version Admin, je pense que c'est un moyen plus compact si vous avez plus d'un champ:
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
Est-ce le moyen le plus simple?
Dans un code de vue, quelque chose comme ceci:
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)
Ça fonctionne bien!
J'ai résolu ce problème comme ceci:
class UploadFileForm(forms.ModelForm):
class Meta:
model = FileStorage
fields = '__all__'
widgets = {'patient': forms.HiddenInput()}
dans les vues:
form = UploadFileForm(request.POST, request.FILES, instance=patient, initial={'patient': patient})
C'est tout.
Pour Django 1.9+
Vous pouvez utiliser l’argument Fields disabled pour rendre le champ désactivé . Dans l'extrait de code suivant du fichier forms.py, j'ai désactivé le champ employee_code.
class EmployeeForm(forms.ModelForm):
employee_code = forms.CharField(disabled=True)
class Meta:
model = Employee
fields = ('employee_code', 'designation', 'salary')
Référence https://docs.djangoproject.com/fr/2.0/ref/forms/fields/#disabled
Si vous travaillez avec Django ver < 1.9
(le 1.9
a ajouté l'attribut Field.disabled
), vous pouvez essayer d'ajouter le décorateur suivant à votre méthode de formulaire __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):
...
L'idée principale est que si field est readonly
, vous n'avez besoin d'aucune autre valeur que initial
.
P.S: N'oubliez pas de définir yuor_form_field.widget.attrs['readonly'] = True
Je pense que votre meilleure option serait simplement d'inclure l'attribut readonly dans votre modèle, rendu dans un <span>
ou <p>
plutôt que de l'inclure dans le formulaire s'il est en lecture seule.
Les formulaires servent à collecter des données et non à les afficher. Cela étant dit, les options d'affichage dans un widget readonly
et de données de nettoyage POST sont de bonnes solutions.
Si vous utilisez Django admin, voici la solution la plus 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',)