it-swarm.com.de

Wie kann ich in einem Django-Formular ein Feld schreibgeschützt machen (oder deaktivieren), damit es nicht bearbeitet werden kann?

Wie kann ich in einem Django-Formular ein Feld schreibgeschützt machen (oder deaktivieren)?

Wenn das Formular zum Erstellen eines neuen Eintrags verwendet wird, sollten alle Felder aktiviert sein. Wenn sich der Datensatz jedoch im Aktualisierungsmodus befindet, müssen einige Felder schreibgeschützt sein.

Beim Erstellen eines neuen Item-Modells müssen beispielsweise alle Felder bearbeitet werden können. Gibt es eine Möglichkeit, das sku-Feld beim Aktualisieren des Datensatzes zu deaktivieren, sodass er sichtbar ist, aber nicht bearbeitet werden kann?

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

Kann die Klasse ItemForm wiederverwendet werden? Welche Änderungen wären in der Modellklasse ItemForm oder Item erforderlich? Muss ich eine andere Klasse "ItemUpdateForm" schreiben, um das Element zu aktualisieren?

def update_item_view(request):
    if request.method == 'POST':
        form = ItemUpdateForm(request.POST)
        # Validate and save
    else:
        form = ItemUpdateForm()
372
e70

Wie in dieser Antwort hervorgehoben, fügte Django 1.9 das Attribut Field.disabled hinzu:

Wenn das deaktivierte boolesche Argument auf "True" gesetzt ist, wird ein Formularfeld mit dem deaktivierten HTML-Attribut deaktiviert, sodass es von Benutzern nicht bearbeitet werden kann. Selbst wenn ein Benutzer mit dem an den Server übergebenen Feldwert arbeitet, wird er zugunsten des Werts aus den ursprünglichen Daten des Formulars ignoriert.

Um mit Django 1.8 und früheren Versionen den Eintrag im Widget zu deaktivieren und schädliche POST - Hacks zu verhindern, müssen Sie die Eingabe bereinigen und zusätzlich das Attribut readonly im Formularfeld festlegen:

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

Oder ersetzen Sie if instance and instance.pk durch eine andere Bedingung, die angibt, dass Sie bearbeiten. Sie können anstelle von disabled auch das Attribut readonly im Eingabefeld festlegen.

Die clean_sku-Funktion stellt sicher, dass der readonly-Wert nicht von einer POST überschrieben wird.

Andernfalls gibt es kein integriertes Django-Formularfeld, das einen Wert rendert, während gebundene Eingabedaten abgelehnt werden. Wenn Sie dies wünschen, sollten Sie stattdessen eine separate ModelForm erstellen, die die nicht editierbaren Felder ausschließt, und sie einfach in Ihre Vorlage drucken.

379
Daniel Naab

Django 1.9 hat das Attribut Field.disabled hinzugefügt: https://docs.djangoproject.com/de/stable/ref/forms/fields/#disabled

Wenn das deaktivierte boolesche Argument auf True festgelegt ist, wird ein Formularfeld mit dem deaktivierten HTML-Attribut deaktiviert, sodass es von Benutzern nicht bearbeitet werden kann. Selbst wenn ein Benutzer mit dem an den Server übergebenen Feldwert arbeitet, wird er zugunsten des Werts aus den ursprünglichen Daten des Formulars ignoriert.

157
Mike Mahmud

Wenn Sie READONLY für das Widget festlegen, wird die Eingabe nur im Browser schreibgeschützt. Durch das Hinzufügen eines clean_sku, das instance.sku zurückgibt, wird sichergestellt, dass sich der Feldwert auf Formularebene nicht ändert.

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

Auf diese Weise können Sie die Modi (unmodified save) des Modells verwenden, um den erforderlichen Feldfehler zu erhalten.

92
muhuk

awalker's answer hat mir sehr geholfen!

Ich habe sein Beispiel geändert, um mit Django 1.3 zu arbeiten, und zwar mit get_readonly_fields .

Normalerweise sollten Sie so etwas in app/admin.py deklarieren:

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

Ich habe mich so angepasst:

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

Und es funktioniert gut. Wenn Sie nun ein Element hinzufügen, ist das Feld url schreibgeschützt, wird jedoch bei Änderung schreibgeschützt.

60
chirale

Damit dies für ein ForeignKey-Feld funktioniert, müssen einige Änderungen vorgenommen werden. Erstens verfügt das SELECT HTML-Tag nicht über das Readonly-Attribut. Wir müssen stattdessen disabled = "disabled" verwenden. Der Browser sendet jedoch keine Formulardaten für dieses Feld zurück. Daher müssen wir dieses Feld so einstellen, dass es nicht erforderlich ist, damit das Feld korrekt überprüft wird. Dann müssen wir den Wert auf den alten Wert zurücksetzen, damit er nicht leer bleibt. 

Für Fremdschlüssel müssen Sie also Folgendes tun:

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)

Auf diese Weise lässt der Browser den Benutzer das Feld nicht ändern und wird immer POST bleiben, da es leer gelassen wurde. Wir überschreiben dann die clean-Methode, um den Feldwert auf den ursprünglichen Wert in der Instanz festzulegen.

49
Humphrey

Bei Django 1.2+ können Sie das Feld wie folgt überschreiben:

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

Ich habe eine MixIn-Klasse erstellt, die Sie erben können, um ein read_only-iterierbares Feld hinzufügen zu können, das Felder in der nicht-ersten Bearbeitung deaktiviert und sichert:

(Basierend auf den Antworten von Daniel und 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

Ich habe gerade ein möglichst einfaches Widget für ein Readonly-Feld erstellt. Ich verstehe nicht, warum Formulare dies nicht bereits haben:

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

In der Form:

my_read_only = CharField(widget=ReadOnlyWidget())

Sehr einfach - und bringt mir nur Output. Praktisch in einem Formset mit einer Menge schreibgeschützter Werte. Natürlich können Sie auch etwas klüger sein und es mit den Attrs ein Div geben, sodass Sie Klassen anfügen können.

10
Danny Staple

Ich bin auf ein ähnliches Problem gestoßen. Es scheint, dass ich es lösen konnte, indem ich in meiner ModelAdmin-Klasse eine "get_readonly_fields" -Methode definierte.

Etwas wie das:

# In the admin.py file

class ItemAdmin(admin.ModelAdmin):

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

Das Schöne daran ist, dass obj None ist, wenn Sie ein neues Element hinzufügen, oder dass es das Objekt ist, das bearbeitet wird, wenn Sie ein vorhandenes Element ändern.

get_readonly_display ist hier dokumentiert: http://docs.djangoproject.com/de/1.2/ref/contrib/admin/#modeladmin-methods

9
awalker

Da ich noch nicht kommentieren kann ( muhuks Lösung ), antworte ich als separate Antwort. Dies ist ein vollständiges Codebeispiel, das für mich funktioniert hat:

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

Als nützliche Ergänzung zu Humphreys Beitrag hatte ich einige Probleme mit Django-Reversion, da deaktivierte Felder immer noch als 'geändert' registriert wurden. Der folgende Code behebt das Problem.

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

Nochmals, ich werde eine weitere Lösung anbieten :) Ich habe Humphreys Code verwendet, also basiert dies auf diesem.

Ich bin jedoch auf Probleme mit dem Feld als ModelChoiceField gestoßen. Bei der ersten Anfrage würde alles funktionieren. Wenn das Formset jedoch versucht hat, ein neues Element hinzuzufügen und die Validierung fehlgeschlagen ist, ist bei den "vorhandenen" Formularen, bei denen die SELECTED-Option auf den Standardwert "---------" zurückgesetzt wurde, ein Fehler aufgetreten.

Wie auch immer, ich konnte nicht herausfinden, wie ich das beheben kann. Stattdessen (und ich denke, das ist eigentlich sauberer in der Form), habe ich die Felder HiddenInputField () gemacht. Dies bedeutet nur, dass Sie in der Vorlage etwas mehr Arbeit erledigen müssen.

Die Lösung für mich bestand also darin, das Formular zu vereinfachen:

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

Und dann müssen Sie in der Vorlage einige manuelle Schleifen des Formset durchführen.

In diesem Fall würden Sie also in der Vorlage Folgendes tun:

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

Dies funktionierte für mich etwas besser und mit weniger Formmanipulationen.

4
JamesD

Eine einfache Möglichkeit ist, einfach form.instance.fieldName in die Vorlage einzugeben, anstatt form.fieldName.

4
alzclarke

Wie mache ich das mit 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

Ich ging auf das gleiche Problem ein, also habe ich ein Mixin erstellt, das für meine Anwendungsfälle zu funktionieren scheint.

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

Verwendung, definieren Sie einfach, welche nur gelesen werden dürfen:

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

wenn Sie mehrere schreibgeschützte Felder benötigen, können Sie eine der folgenden Methoden verwenden 

Methode 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

Methode 2  

vererbungsmethode

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

Zwei weitere (ähnliche) Ansätze mit einem verallgemeinerten Beispiel:

1) erster Ansatz - Entfernen des Felds in der save () - Methode, z. (nicht getestet ;) ):

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) zweiter Ansatz - Feld in der Clean-Methode auf den Anfangswert zurücksetzen:

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

Basierend auf dem zweiten Ansatz habe ich es so verallgemeinert:

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

Hier ist eine etwas aufwendigere Version, basierend auf christophe31s Antwort . Es ist nicht auf das Attribut "readonly" angewiesen. Dies führt zu Problemen, da Auswahlfelder immer noch änderbar sind und Datapicker immer noch auftauchen.

Stattdessen wird das Formularfeld-Widget in ein Readonly-Widget eingeschlossen, sodass das Formular weiterhin gültig ist. Der Inhalt des ursprünglichen Widget wird in <span class="hidden"></span>-Tags angezeigt. Wenn das Widget eine render_readonly()-Methode hat, verwendet es diese als sichtbaren Text. Andernfalls wird der HTML-Code des ursprünglichen Widgets analysiert und die beste Darstellung erraten.

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

Basierend auf Yamikeps Antwort habe ich eine bessere und sehr einfache Lösung gefunden, die auch ModelMultipleChoiceField-Felder behandelt.

Das Entfernen des Feldes aus form.cleaned_data verhindert, dass Felder gespeichert werden:

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

Verwendungszweck:

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

Für die Admin-Version halte ich dies für einen kompakteren Weg, wenn Sie mehrere Felder haben:

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

Ist das der einfachste Weg?

Richtig in einer Ansicht Code wie folgt: 

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)

Es funktioniert gut!

1
fly_frog

Ich habe dieses Problem so gelöst:

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

in Ansichten:

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

Es ist alles.

1
Pavel

Für Django 1.9+
Sie können das Feld disabled-Argument verwenden, um das Feld zu deaktivieren Z. Im folgenden Codeausschnitt aus der Datei forms.py habe ich das Feld employee_code deaktiviert 

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

Reference https://docs.djangoproject.com/de/2.0/ref/forms/fields/#disabled

1
Ajinkya Bhosale

Wenn Sie mit Django ver < 1.9 arbeiten (das 1.9 hat das Field.disabled-Attribut hinzugefügt), können Sie versuchen, der Formular-__init__-Methode den folgenden Dekorator hinzuzufügen:

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):
        ...

Die Hauptidee ist, dass, wenn das Feld readonly ist, Sie keinen anderen Wert außer initial benötigen.

P.S: Vergessen Sie nicht, yuor_form_field.widget.attrs['readonly'] = True einzustellen

0

Ich denke, Ihre beste Option wäre nur, das readonly-Attribut in Ihre Vorlage einzufügen, die in einem <span> oder <p> gerendert wird, anstatt es in das Formular aufzunehmen, wenn es readonly ist.

Formulare dienen dazu, Daten zu sammeln, nicht anzuzeigen. Allerdings sind die Optionen, die in einem readonly-Widget angezeigt werden sollen, und die Daten zum Scrub POST eine gute Lösung.

0
austinheiman

Wenn Sie den Django-Administrator verwenden, ist hier die einfachste Lösung.

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