forms - Dans un formulaire Django, comment créer un champ en lecture seule(ou désactivé)pour qu'il ne puisse pas être modifié?




field readonly (21)

Django 1.9 a ajouté l'attribut Field.disabled: Field.disabled

L'argument booléen désactivé, lorsqu'il est défini sur True, désactive un champ de formulaire à l'aide de l'attribut HTML désactivé afin qu'il ne soit pas modifiable 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.

Dans un formulaire Django, comment faire 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 - mais 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 d' 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 pour qu'il soit visible?

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

Peut-on réutiliser la classe ItemForm ? Quels changements seraient nécessaires dans la 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()

Est-ce la manière la plus simple?

Droit 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!


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

Deux autres approches (similaires) avec un exemple généralisé:

1) première approche - suppression du champ dans la méthode save (), par exemple (non 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 propre:

def clean_<fieldname>(self):
    return self.initial[<fieldname>] # or getattr(self.instance, fieldname)

Basé sur la deuxième 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 la réponse de christophe31 . Il ne repose pas sur l'attribut "readonly". Cela rend ses problèmes, comme les boîtes de sélection sont encore modifiables et les datapickers surgissent encore, disparaître.

Au lieu de cela, il enveloppe le widget des champs de formulaire dans un widget en lecture seule, rendant ainsi le formulaire toujours valide. Le contenu du widget d'origine est affiché à l'intérieur des 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 original 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)

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.


J'allais dans le même problème donc 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 en lecture seule:

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

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

Pour Django 1.2+, vous pouvez remplacer le champ comme ceci:

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 read_only itérable qui désactivera et sécurisera les champs de la première édition:

(Basé sur les réponses de Daniel et 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')

Si vous avez besoin de plusieurs champs en lecture seule, vous pouvez utiliser l'une des méthodes indiquées 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',)

Comme ajout utile à la publication de Humphrey , j'ai eu quelques problèmes avec django-reversion, car il enregistrait toujours des champs désactivés comme 'modifiés'. Le code suivant résout 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)

Une simple option consiste simplement à taper form.instance.fieldName dans le modèle au lieu de form.fieldName .


Pour que cela fonctionne pour un champ ForeignKey, quelques changements doivent être faits. Premièrement, la balise SELECT HTML n'a pas l'attribut readonly. Nous devons utiliser disabled = "disabled" à la place. Toutefois, 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 nécessaire afin que le champ soit validé correctement. Nous devons ensuite réinitialiser la valeur à ce qu'elle était auparavant afin 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 sera toujours POST comme il a été laissé vide. Nous remplaçons ensuite la méthode clean pour définir la valeur du champ comme étant à l'origine dans l'instance.


J'ai rencontré un problème similaire. Il semble que j'ai été 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 None lorsque vous ajoutez un nouvel élément, ou il sera l'objet en cours de modification lorsque vous modifiez un élément existant.

get_readonly_display est documenté ici: http://docs.djangoproject.com/en/1.2/ref/contrib/admin/#modeladmin-methods


La définition de READONLY sur le widget ne fait que l'entrée dans le navigateur en lecture seule. L'ajout d'un fichier 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 (save non modifié) et aviod obtenir le champ erreur requise.


Je pense que votre meilleure option serait d'inclure l'attribut readonly dans votre template rendu dans un <span> ou un <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 à afficher dans un widget en readonly et scrub les données POST sont de bonnes solutions.


Basé sur la réponse de Yamikep , j'ai trouvé une solution meilleure et très simple qui gère également les champs ModelMultipleChoiceField .

La suppression du champ de form.cleaned_data empêche l' form.cleaned_data champs:

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

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

La réponse d'Awalker m'a beaucoup aidé!

J'ai changé son exemple pour travailler avec Django 1.3, en utilisant get_readonly_fields .

Habituellement, vous devriez déclarer quelque chose comme ça 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 fonctionne bien. Maintenant, si vous ajoutez un élément, le champ url est en lecture-écriture, mais en cas de modification, il devient en lecture seule.


Il y a une manière très simple de faire le django.

"Mémorisez" les valeurs dans model init comme ceci:

def __init__(self, *args, **kwargs):
    super(MyClass, self).__init__(*args, **kwargs)
    self.initial_parametername = self.parametername
    ---
    self.initial_parameternameX = self.parameternameX

Exemple concret:

En classe:

def __init__(self, *args, **kwargs):
    super(MyClass, self).__init__(*args, **kwargs)
    self.__important_fields = ['target_type', 'target_id', 'target_object', 'number', 'chain', 'expiration_date']
    for field in self.__important_fields:
        setattr(self, '__original_%s' % field, getattr(self, field))

def has_changed(self):
    for field in self.__important_fields:
        orig = '__original_%s' % field
        if getattr(self, orig) != getattr(self, field):
            return True
    return False

Et puis dans la méthode de sauvegarde de modelform:

def save(self, force_insert=False, force_update=False, commit=True):
    # Prep the data
    obj = super(MyClassForm, self).save(commit=False)

    if obj.has_changed():

        # If we're down with commitment, save this shit
        if commit:
            obj.save(force_insert=True)

    return obj




django forms field readonly