In una forma Django, come posso rendere un campo di sola lettura (o disabilitato) in modo che non possa essere modificato?

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

  •  11-07-2019
  •  | 
  •  

Domanda

In un modulo Django, come posso rendere un campo di sola lettura (o disabilitato)?

Quando il modulo viene utilizzato per creare una nuova voce, tutti i campi devono essere abilitati, ma quando il record è in modalità di aggiornamento alcuni campi devono essere di sola lettura.

Ad esempio, quando si crea un nuovo modello Item, tutti i campi devono essere modificabili, ma durante l'aggiornamento del record esiste un modo per disabilitare il campo sku in modo che sia visibile, ma non può essere modificato?

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

È possibile riutilizzare la classe ItemForm? Quali modifiche sarebbero necessarie nella classe del modello ItemUpdateForm o <=>? Dovrei scrivere un'altra classe, & Quot; <=> & Quot ;, per aggiornare l'elemento?

def update_item_view(request):
    if request.method == 'POST':
        form = ItemUpdateForm(request.POST)
        # Validate and save
    else:
        form = ItemUpdateForm()
È stato utile?

Soluzione

Come sottolineato in questa risposta , Django 1.9 ha aggiunto attributo Field.disabled :

  

L'argomento booleano disabilitato, se impostato su True, disabilita un campo modulo utilizzando l'attributo HTML disabilitato in modo che vinca & # 8217; t sia modificabile dagli utenti. Anche se un utente manomette il valore del campo & # 8217; inviato al server, verrà ignorato a favore del valore dai dati iniziali del modulo & # 8217;

Con Django 1.8 e precedenti, per disabilitare l'ingresso sul widget e prevenire gli hack POST dannosi è necessario cancellare l'input oltre a impostare l'attributo readonly nel campo del modulo:

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

In alternativa, sostituisci if instance and instance.pk con un'altra condizione che indica che stai modificando. È inoltre possibile impostare l'attributo disabled nel campo di input, anziché clean_sku.

La funzione POST assicurerà che il valore ModelForm non venga sovrascritto da un <=>.

Altrimenti, non esiste un campo modulo Django incorporato che visualizzerà un valore mentre rifiuta i dati di input associati. Se questo è ciò che desideri, devi invece creare un <=> separato che esclude i campi non modificabili e stamparli semplicemente all'interno del tuo modello.

Altri suggerimenti

Django 1.9 ha aggiunto l'attributo Field.disabled: https: / /docs.djangoproject.com/en/stable/ref/forms/fields/#disabled

  

L'argomento booleano disabilitato, se impostato su True, disabilita un campo modulo utilizzando l'attributo HTML disabilitato in modo che vinca & # 8217; t sia modificabile dagli utenti. Anche se un utente manomette il valore del campo & # 8217; inviato al server, verrà ignorato a favore del valore dai dati iniziali del modulo & # 8217;

L'impostazione READONLY sul widget rende l'input nel browser di sola lettura. L'aggiunta di un clean_sku che restituisce instance.sku garantisce che il valore del campo non cambierà a livello di modulo.

def clean_sku(self):
    if self.instance: 
        return self.instance.sku
    else: 
        return self.fields['sku']

In questo modo puoi usare il modello (salvataggio non modificato) e aviod ottenendo l'errore richiesto dal campo.

risposta di Awalker mi ha aiutato molto!

Ho cambiato il suo esempio per lavorare con Django 1.3, usando get_readonly_fields .

Di solito dovresti dichiarare qualcosa del genere in app/admin.py:

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

Mi sono adattato in questo modo:

# In the admin.py file
class ItemAdmin(admin.ModelAdmin):
    ...
    def get_readonly_fields(self, request, obj=None):
        if obj:
            return ['url']
        else:
            return []

E funziona benissimo. Ora se aggiungi un elemento, il campo url è di lettura-scrittura, ma al momento della modifica diventa di sola lettura.

Per farlo funzionare per un campo ForeignKey, è necessario apportare alcune modifiche. Innanzitutto, il tag SELECT HTML non ha l'attributo readonly. Dobbiamo invece utilizzare disabled="disabled". Tuttavia, il browser non restituisce i dati del modulo per quel campo. Quindi dobbiamo impostare quel campo in modo che non sia obbligatorio in modo che il campo venga validato correttamente. Abbiamo quindi bisogno di ripristinare il valore su quello che era in precedenza, quindi non è impostato su vuoto.

Quindi per le chiavi esterne dovrai fare qualcosa del tipo:

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)

In questo modo il browser non consentirà all'utente di modificare il campo e POST sarà sempre lasciato vuoto. Quindi sovrascriviamo il metodo clean per impostare il valore del campo in modo che fosse quello originariamente nell'istanza.

Per Django 1.2+, puoi scavalcare il campo in questo modo:

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

Ho creato una classe MixIn che potresti ereditare per poter aggiungere un campo iterable read_only che disabiliterà e proteggerà i campi nella modifica non prima:

(Basato sulle risposte di Daniel e 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')

Ho appena creato il widget più semplice possibile per un campo di sola lettura - non vedo davvero perché i moduli non lo abbiano già:

class ReadOnlyWidget(widgets.Widget):
    """Some of these values are read only - just a bit of text..."""
    def render(self, _, value, attrs=None):
        return value

Nel modulo:

my_read_only = CharField(widget=ReadOnlyWidget())

Molto semplice - e mi dà solo l'output. Comodo in un formset con un sacco di valori di sola lettura. Certo, potresti anche essere un po 'più intelligente e fare un div con gli attr in modo da poter aggiungere delle classi ad esso.

Ho riscontrato un problema simile. Sembra che sono stato in grado di risolverlo definendo un & Quot; get_readonly_fields & Quot; metodo nella mia classe ModelAdmin.

Qualcosa del genere:

# In the admin.py file

class ItemAdmin(admin.ModelAdmin):

    def get_readonly_display(self, request, obj=None):
        if obj:
            return ['sku']
        else:
            return []

La cosa bella è che obj sarà Nessuno quando si aggiunge un nuovo Articolo, o sarà l'oggetto che si sta modificando quando si cambia un Articolo esistente.

get_readonly_display è documentato qui: http://docs.djangoproject.com/en/1.2/ ref / contrib / admin / # ModelAdmin-metodi

Una semplice opzione è semplicemente digitare form.instance.fieldName nel modello anziché form.fieldName.

Come utile aggiunta a Il post di Humphrey , ho avuto dei problemi con django-reversion, perché registrava ancora i campi disabilitati come" cambiati ". Il codice seguente risolve il problema.

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)

Non posso ancora commentare ( soluzione di muhuk ), risponderò come risposta separata. Questo è un esempio di codice completo, che ha funzionato per me:

def clean_sku(self):
  if self.instance and self.instance.pk:
    return self.instance.sku
  else:
    return self.cleaned_data['sku']

Stavo riscontrando lo stesso problema, quindi ho creato un Mixin che sembra funzionare per i miei casi d'uso.

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

Utilizzo, basta definire quali devono essere di sola lettura:

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

Ancora una volta, ho intenzione di offrire un'altra soluzione :) Stavo usando Codice di Humphrey , quindi questo si basa su quello.

Tuttavia, ho riscontrato problemi con il campo essendo ModelChoiceField. Tutto funzionerebbe alla prima richiesta. Tuttavia, se il formset ha provato ad aggiungere un nuovo elemento e non è riuscito a convalidare, qualcosa stava andando storto con il & Quot; esistente & Quot; moduli in cui l'opzione SELEZIONATA veniva reimpostata sul " predefinito; --------- " ;.

Comunque, non sono riuscito a capire come risolverlo. Quindi, invece, (e penso che questo sia effettivamente più pulito nel modulo), ho creato i campi HiddenInputField (). Questo significa solo che devi fare un po 'più di lavoro nel modello.

Quindi la soluzione per me era semplificare il modulo:

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

E poi nel modello, dovrai fare un po 'di loop manuale del formset .

Quindi, in questo caso, faresti qualcosa del genere nel modello:

<div>
    {{ form.instance.sku }} <!-- This prints the value -->
    {{ form }} <!-- Prints form normally, and makes the hidden input -->
</div>

Questo ha funzionato un po 'meglio per me e con meno manipolazione della forma.

Come lo faccio con 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

Altri due approcci (simili) con un esempio generalizzato:

1) primo approccio: rimozione del campo nel metodo save (), ad es. (non testato;)):

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) secondo approccio: reimpostare il campo sul valore iniziale in metodo pulito:

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

Basandomi sul secondo approccio l'ho generalizzato in questo modo:

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)

se hai bisogno di più campi di sola lettura. puoi utilizzare uno dei metodi indicati di seguito

metodo 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

metodo 2

metodo di ereditarietà

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

Per la versione Admin, penso che questo sia un modo più compatto se hai più di un campo:

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

Basato su La risposta di Yamikep , ho trovato una soluzione migliore e molto semplice che gestisce anche ModelMultipleChoiceField campi.

La rimozione del campo da form.cleaned_data impedisce il salvataggio dei campi:

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

Utilizzo:

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

Ecco una versione leggermente più coinvolta, basata su la risposta di christophe31 . Non si basa sul & Quot; solo & Quot; attributo. Questo fa sì che i suoi problemi, come le caselle di selezione ancora modificabili e i datapicker che spuntano ancora, scompaiano.

Al contrario, avvolge il widget dei campi modulo in un widget di sola lettura, rendendo così il modulo ancora valido. Il contenuto del widget originale viene visualizzato all'interno di <span class="hidden"></span> tag. Se il widget ha un metodo render_readonly() lo utilizza come testo visibile, altrimenti analizza l'HTML del widget originale e cerca di indovinare la rappresentazione migliore.

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)

È questo il modo più semplice?

Proprio in un codice vista qualcosa del genere:

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)

Funziona benissimo!

Per django 1.9+
È possibile utilizzare l'argomento Fields disabled per disabilitare il campo. per esempio. Nel seguente frammento di codice dal file forms.py, ho disabilitato il campo employee_code

class EmployeeForm(forms.ModelForm):
    employee_code = forms.CharField(disabled=True)
    class Meta:
        model = Employee
        fields = ('employee_code', 'designation', 'salary')

Riferimento https://docs.djangoproject.com/en/2.0/ref / forme / campi / # disabilitati

Se stai lavorando con Django ver < 1.9 (l'attributo 1.9 ha aggiunto Field.disabled) potresti provare ad aggiungere il seguente decoratore al tuo metodo __init__ modulo:

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'idea principale è che se il campo è readonly non è necessario alcun altro valore tranne initial.

P.S: non dimenticare di impostare yuor_form_field.widget.attrs['readonly'] = True

Se stai usando l'amministratore di Django, ecco la soluzione più semplice.

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

Penso che la tua migliore opzione sarebbe quella di includere l'attributo readonly nel tuo modello reso in <span> o <p> piuttosto che includerlo nel modulo se è di sola lettura.

I moduli servono per raccogliere dati, non visualizzarli. Detto questo, le opzioni da visualizzare in un readonly widget e cancellare i dati POST sono ottime soluzioni.

Ho risolto questo problema in questo modo:

    class UploadFileForm(forms.ModelForm):
     class Meta:
      model = FileStorage
      fields = '__all__'
      widgets = {'patient': forms.HiddenInput()}

nelle visualizzazioni:

form = UploadFileForm(request.POST, request.FILES, instance=patient, initial={'patient': patient})

È tutto.

Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top