form - django multiple choice field




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

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

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

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)

Comme souligné dans cette réponse , Django 1.9 a ajouté l'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 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.

Avec Django 1.8 et versions antérieures, pour désactiver l'entrée sur le widget et empêcher les hacks 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 readonly .

La fonction clean_sku garantit que la valeur readonly ne sera pas remplacée par un POST .

Sinon, il n'y a pas de champ de formulaire Django intégré qui affichera une valeur tout en rejetant les données d'entrée liées. Si c'est ce que vous désirez, vous devriez plutôt créer un ModelForm distinct qui exclut les champs non modifiables, et les imprimer simplement dans votre template.


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

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.


Encore une fois, je vais offrir une solution de plus :) J'utilisais le code de Humphrey , donc c'est basé sur ça.

Cependant, j'ai rencontré des problèmes avec le champ étant un ModelChoiceField. Tout fonctionnerait à la première demande. Cependant, si le formset essayait d'ajouter un nouvel élément et d'échouer à la validation, quelque chose n'allait pas avec les formes "existantes" où l'option SELECTED était réinitialisée à la valeur par défaut "---------".

De toute façon, je ne pouvais pas comprendre comment résoudre ce problème. Donc à la place, (et je pense que c'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.

Donc la solution pour moi était 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 puis dans le modèle, vous devrez faire une boucle manuelle du formset .

Donc, dans ce cas, vous feriez quelque chose comme ça dans le template:

<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 forme.


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

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

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.


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.


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

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

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

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.


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

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


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.