web-dev-qa-db-fra.com

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

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()
372
e70

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.

379
Daniel Naab

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.

157
Mike Mahmud

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.

92
muhuk

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.

60
chirale

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.

49
Humphrey

Pour Django 1.2+, vous pouvez remplacer le champ de la manière suivante:

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

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

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.

10
Danny Staple

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

9
awalker

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']
5
Madis

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)
5
Evan Brumley

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.

4
JamesD

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

4
alzclarke

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
4
Lucas B

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

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',)
3
Sarath Ak

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)
3
Robert Lujo

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)
2
Rune Kaagaard

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

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
2
Hassek

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!

1
fly_frog

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.

1
Pavel

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

1
Ajinkya Bhosale

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

0
Yaroslav Varkhol

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.

0
austinheiman

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