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

StackOverflow https://stackoverflow.com/questions/324477

  •  11-07-2019
  •  | 
  •  

Question

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 qu'il 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? Quelles modifications seraient nécessaires dans la classe de modèle ItemUpdateForm ou <=>? Aurais-je besoin d'écrire une autre classe, & Quot; <=> & Quot ;, 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()
Était-ce utile?

La solution

Comme indiqué dans cette réponse , Django 1.9 a ajouté le attribut 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 & # 8217; pas modifiable par les utilisateurs. Même si un utilisateur altère la valeur du champ & # 8217; envoyé au serveur, il sera ignoré au profit de la valeur des données initiales du formulaire & # 8217;

.

Avec Django 1.8 et versions antérieures, pour désactiver la saisie sur le widget et empêcher les piratages POST malveillants, 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 clean_sku.

La fonction POST garantit que la valeur ModelForm ne sera pas remplacée par un <=>.

Sinon, il n'y a pas de champ de formulaire Django intégré qui rendra une valeur lors du rejet des données d'entrée liées. Si vous le souhaitez, vous devez créer un <=> document séparé excluant les champs non modifiables et les imprimer dans votre modèle.

Autres conseils

Django 1.9 a ajouté l'attribut Field.disabled: https: / /docs.djangoproject.com/fr/stable/ref/forms/fields/#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 & # 8217; pas modifiable par les utilisateurs. Même si un utilisateur altère la valeur du champ & # 8217; envoyé au serveur, il sera ignoré au profit de la valeur des données initiales du formulaire & # 8217;

.

La définition de READONLY sur un widget permet uniquement la saisie en lecture seule dans le navigateur. L'ajout d'un clean_sku renvoyant 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 de recevoir l'erreur du champ requis.

La réponse de awalker m'a beaucoup aidé!

J'ai modifié son exemple pour qu'il fonctionne avec Django 1.3, en utilisant get_readonly_fields .

Habituellement, vous devez déclarer quelque chose comme ceci dans app/admin.py:

class ItemAdmin(admin.ModelAdmin):
    ...
    readonly_fields = ('url',)

Je me suis adapté de cette manière:

# 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. Désormais, si vous ajoutez un élément, le champ url est en lecture-écriture, mais lorsqu’il est modifié, il devient en lecture seule.

Pour que cela fonctionne pour un champ ForeignKey, quelques modifications doivent être apportées. Tout d'abord, la balise SELECT HTML ne possède 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, mais toujours POST comme 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 édition:

(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 ne l'ont pas déjà:

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. 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 un & Quot; get_readonly_fields & Quot; méthode 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 []

Ce qui est bien, c’est que obj sera Aucun lors de l’ajout d’un nouvel élément ou que l’objet sera modifié lors de la modification d’un élément existant.

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

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

Complément utile à Le message de Humphrey , quelques problèmes avec django-reversion, car il enregistrait toujours les champs désactivés avec la mention" modifié ". 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)

Comme je ne peux pas encore commenter ( La solution de muhuk ), je vous répondrai séparément. Voici 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']

J'entrais dans le même problème, j'ai donc 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 uniquement celles qui doivent être en lecture seule:

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

Encore une fois, je vais proposer une solution supplémentaire :) J'utilisais Le code de Humphrey , donc ceci est basé sur celui-ci.

Cependant, j'ai rencontré des problèmes avec le champ en tant que ModelChoiceField. Tout fonctionnerait à la première demande. Cependant, si le formset tentait d'ajouter un nouvel élément et échouait à la validation, quelque chose n'allait pas avec & "Existant &"; formulaires 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 effectuer 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()

Ensuite, dans le modèle, vous devrez créer des mise 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.

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

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

1) première approche - suppression du champ dans la méthode save (), par ex. (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 de nettoyage:

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

Sur la deuxième approche, je l’ai généralisé comme suit:

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

Pour la version Admin, je pense que c'est un moyen plus compact si vous avez plusieurs champs:

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

Basé sur La réponse de Yamikep , j'ai trouvé une solution plus simple et plus efficace, qui gère également ModelMultipleChoiceField les champs.

La suppression du champ de form.cleaned_data empêche l'enregistrement des 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()

Utilisation:

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

Voici une version légèrement plus compliquée, basée sur La réponse de christophe31 . Il ne repose pas sur le & Quot; readonly & Quot; attribut. 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 enveloppe 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é à l'intérieur de <span class="hidden"></span> balises. Si le widget utilise 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)

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)

Cela fonctionne bien!

Pour Django 1.9+
Vous pouvez utiliser l'argument Champs désactivés pour désactiver le champ. par exemple. 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 (l'attribut 1.9 a ajouté 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 le champ est readonly, vous n'avez besoin d'aucune valeur autre que initial.

P.S: N'oubliez pas de définir yuor_form_field.widget.attrs['readonly'] = True

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

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 dit, les options d'affichage dans un readonly widget et de nettoyage des données POST sont de bonnes solutions.

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.

Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top