В форме Django как сделать поле доступным только для чтения (или отключенным), чтобы его нельзя было редактировать? - PullRequest
388 голосов
/ 27 ноября 2008

В форме Django как сделать поле доступным только для чтения (или отключенным)?

Когда форма используется для создания новой записи, все поля должны быть включены, но когда запись находится в режиме обновления, некоторые поля должны быть доступны только для чтения.

Например, при создании новой модели Item все поля должны быть редактируемыми, но при обновлении записи можно ли отключить поле sku, чтобы оно было видимым, но не могло быть отредактировано?

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

Может ли класс ItemForm быть повторно использован? Какие изменения потребуются в классе моделей ItemForm или Item? Нужно ли мне писать другой класс, "ItemUpdateForm", для обновления элемента?

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

Ответы [ 26 ]

5 голосов
/ 10 марта 2011

В качестве полезного дополнения к сообщению Хамфри у меня были некоторые проблемы с реверсией django, потому что она все еще регистрировала отключенные поля как «измененные». Следующий код решает проблему.

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 голосов
/ 13 мая 2011

Поскольку я пока не могу комментировать ( решение Мухука ), я отвечу отдельным ответом. Это полный пример кода, который работал для меня:

def clean_sku(self):
  if self.instance and self.instance.pk:
    return self.instance.sku
  else:
    return self.cleaned_data['sku']
4 голосов
/ 24 июля 2017

Как мне это сделать с 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 голосов
/ 09 февраля 2014

Я столкнулся с той же проблемой, поэтому создал Mixin, который, похоже, подходит для моих случаев использования.

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

Использование, просто укажите, какие из них должны быть только для чтения:

class MyFormWithReadOnlyFields(ReadOnlyFieldsMixin, MyForm):
    readonly_fields = ('field1', 'field2', 'fieldx')
4 голосов
/ 30 марта 2012

Еще раз, я собираюсь предложить еще одно решение :) Я использовал код Хамфри , так что это основано на этом.

Однако у меня возникли проблемы с полем ModelChoiceField. Все будет работать по первому запросу. Однако, если набор форм попытался добавить новый элемент и не прошел проверку, что-то пошло не так с «существующими» формами, где для параметра SELECTED было установлено значение по умолчанию «---------».

Во всяком случае, я не мог понять, как это исправить. Итак, вместо этого (и я думаю, что это на самом деле чище в форме), я сделал поля HiddenInputField (). Это просто означает, что вам нужно сделать немного больше работы с шаблоном.

Итак, для меня исправление было в том, чтобы упростить форму:

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

А затем в шаблоне вам потребуется выполнить ручное зацикливание набора форм .

Итак, в этом случае вы должны сделать что-то подобное в шаблоне:

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

Это сработало немного лучше для меня и с меньшими манипуляциями с формой.

3 голосов
/ 19 августа 2016

, если вам нужно несколько полей только для чтения. Вы можете использовать любой из методов, указанных ниже

метод 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

метод 2

метод наследования

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 голосов
/ 27 ноября 2013

Еще два (похожих) подхода с одним обобщенным примером:

1) первый подход - удаление поля в методе save (), например (не проверено;)):

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) второй подход - сброс поля к начальному значению чистым методом:

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

Основываясь на втором подходе, я обобщил это так:

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)
2 голосов
/ 20 апреля 2016

Основываясь на ответе Ямикепа , я нашел лучшее и очень простое решение, которое также обрабатывает ModelMultipleChoiceField поля.

Удаление поля из form.cleaned_data предотвращает сохранение полей:

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

Использование:

class MyFormWithReadOnlyFields(ReadOnlyFieldsMixin, MyForm):
    readonly_fields = ('field1', 'field2', 'fieldx')
2 голосов
/ 04 сентября 2012

Вот немного более сложная версия, основанная на ответе christophe31 . Он не зависит от атрибута «только для чтения». Это создает свои проблемы, такие как выбор полей, которые все еще могут быть изменены, и сборщики данных, все еще выскакивающие, исчезают.

Вместо этого он оборачивает виджет полей формы в виджет только для чтения, тем самым делая форму по-прежнему валидной. Содержимое исходного виджета отображается внутри тегов <span class="hidden"></span>. Если у виджета есть метод render_readonly(), он использует его в качестве видимого текста, в противном случае он анализирует HTML исходного виджета и пытается угадать наилучшее представление.

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 голосов
/ 16 июня 2012

Для версии Admin, я думаю, это более компактный способ, если у вас есть более одного поля:

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