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


431

В форме 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()

См. Также вопрос SO: Почему поля формы, доступные только для чтения, в Django - плохая идея? @ stackoverflow.com/questions/2902024 , Принятый ответ (Дэниел Нааб) заботится о злонамеренных взломах POST.
X10

Ответы:


422

Как указано в этом ответе , Django 1.9 добавил атрибут Field.disabled :

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

В Django 1.8 и более ранних версиях, чтобы отключить запись в виджете и предотвратить злонамеренные POST-хаки, вы должны очистить ввод в дополнение к настройке readonlyатрибута в поле формы:

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

Или замените if instance and instance.pkдругое условие, указывающее, что вы редактируете. Вы также можете установить атрибут disabledв поле ввода вместо readonly.

clean_skuФункция гарантирует , что readonlyзначение не будет переопределен POST.

В противном случае, нет встроенного поля формы Django, которое будет отображать значение при отклонении связанных входных данных. Если это то, что вы хотите, вы должны вместо этого создать отдельное ModelForm, исключающее не редактируемые поля, и просто распечатать их внутри вашего шаблона.


3
Даниэль, спасибо за публикацию ответа. Мне не понятно, как использовать этот код? этот код не будет работать так же, как для нового, так и для режима обновления? Можете ли вы отредактировать свой ответ, чтобы привести примеры его использования для новых и обновленных форм? Спасибо.
X10

8
Ключ к примеру Даниэля - тестирование поля .id. Вновь созданные объекты будут иметь идентификатор == Нет. Кстати, об этой проблеме говорит один из самых старых открытых билетов Django. См. Code.djangoproject.com/ticket/342 .
Питер Роуэлл

2
@moadeep добавляет clean_descriptionметод в класс формы.
Даниэль Нааб

3
в Linux (Ubuntu 15) / Chrome v45, readonly меняет указатель на «отключенную руку», но поле затем кликабельно. с отключенным работает как положено
simone cittadini

7
Этот ответ необходимо обновить. Новый аргумент поля disabledдобавлен в Django 1.9. Если Field.disabledустановлено значение True, то значение POST для этого Fieldигнорируется. Так что, если вы используете 1.9, нет необходимости переопределять clean, просто установите disabled = True. Проверьте этот ответ.
Нарендра-Чоудхары

174

Django 1.9 добавил атрибут Field.disabled: https://docs.djangoproject.com/en/stable/ref/forms/fields/#disabled

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


Ничего для 1.8 LTS?
dnit13

9
Любая идея, как мы можем использовать это на UpdateView? Как он генерирует поля из модели ...
bcsanches

6
Правильный ответ. Мой класс решения MyChangeForm (forms.ModelForm): def __init __ (self, * args, ** kwargs): super (MyChangeForm, self) .__ init __ (* args, ** kwargs) self.fields ['my_field']. Disabled = Правда
Виджей Катам

8
Это проблемный ответ - настройка disabled=Trueприведет к тому, что модель будет возвращена пользователю с ошибками проверки.
Бен

1
Было бы здорово, если бы вы могли привести пример
геоидезический

95

Настройка readonlyдля виджета делает ввод только в браузере только для чтения. Добавление clean_skuвозвращающего instance.skuгарантирует, что значение поля не изменится на уровне формы.

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

Таким образом, вы можете использовать модель (неизмененное сохранение) и избежать появления ошибки в поле.


15
+1 Это отличный способ избежать более сложных переопределений save (). Однако вы хотите выполнить проверку экземпляра перед возвратом (в режиме комментария без новой строки): «if self.instance: вернуть self.instance.sku; еще: вернуть self.fields ['sku']»
Даниэль Нааб

2
Для последней строки будет return self.cleaned_data['sku']так же хорошо или лучше? В документах , кажется, предлагают использовать cleaned_data: «Возвращаемое значение этого метода заменяет существующее значение в cleaned_data, поэтому оно должно быть значением поля из cleaned_data(даже если этот метод не изменит его) или новое значение очищены.»
pianoJames

68

Ответ awalker мне очень помог!

Я изменил его пример для работы с Django 1.3, используя get_readonly_fields .

Обычно вы должны объявить что-то вроде этого в app/admin.py:

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

Я адаптировался таким образом:

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

И это работает отлично. Теперь, если вы добавляете Item, urlполе доступно для чтения и записи, но при изменении оно становится доступным только для чтения.


56

Чтобы сделать эту работу для ForeignKeyполя, необходимо внести несколько изменений. Во-первых, SELECT HTMLтег не имеет атрибута readonly. Нам нужно использовать disabled="disabled"вместо этого. Однако браузер не отправляет данные формы для этого поля. Поэтому мы должны установить это поле как необязательное, чтобы поле корректно проверялось. Затем нам нужно сбросить значение обратно к тому, что было раньше, чтобы оно не было пустым.

Так что для внешних ключей вам нужно сделать что-то вроде:

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)

Таким образом, браузер не позволит пользователю изменить поле и всегда будет оставаться POSTпустым. Затем мы переопределяем cleanметод, чтобы установить значение поля равным тому, что было изначально в экземпляре.


Я попытался использовать его как форму в TabularInline, но не получилось, потому что attrsбыли разделены между widgetэкземплярами и всеми, кроме первой строки, включая только что добавленную, отображаемую только для чтения.
dhill

Отличное (обновление) решение! К сожалению, это и остальные проблемы возникают, когда возникают ошибки формы, поскольку все «отключенные» значения очищаются.
Майкл Томпсон

28

Для Django 1.2+ вы можете переопределить поле следующим образом:

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

6
Это также не позволяет редактировать поле во время добавления, что и является первоначальным вопросом.
Мэтт С.

Это ответ, который я ищу. Field disabledне делает то, что я хочу, потому что это отключает поле, но также удаляет метку / делает его невидимым.
Сивабуд

18

Я создал класс MixIn, который вы можете унаследовать, чтобы иметь возможность добавлять итерируемое поле read_only, которое отключит и защитит поля при не первом редактировании:

(На основе ответов Даниила и Мухука)

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

11

Я только что создал простейший виджет для поля, доступного только для чтения - я не понимаю, почему в формах его уже нет:

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

В виде:

my_read_only = CharField(widget=ReadOnlyWidget())

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


2
Выглядит сексуально, но как обращаться с внешним ключом?
Andilabs

unicode(value)Возможно, сделайте это взамен. Если предположить, что Unicode Dunder имеет смысл, вы получите это.
Дэнни Стейпл

Для внешних ключей вам нужно добавить атрибут «модель» и использовать «получить (значение)». Проверьте мою суть
Шади

10

Я столкнулся с подобной проблемой. Похоже, я смог решить эту проблему, определив метод get_readonly_fields в своем классе ModelAdmin.

Что-то вроде этого:

# In the admin.py file

class ItemAdmin(admin.ModelAdmin):

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

Приятно, что objэто будет None, когда вы добавляете новый Предмет, или это будет объект, который редактируется, когда вы меняете существующий Предмет.

get_readonly_display задокументировано здесь: http://docs.djangoproject.com/en/1.2/ref/contrib/admin/#modeladmin-methods


6

Один простой вариант - просто набрать form.instance.fieldNameшаблон вместо form.fieldName.


А как насчет verbos_nameили labelполя? Как мне показать `label в шаблоне django? @alzclarke
Кит 52 Гц

6

Как я делаю это с 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

это будет только блокировать с фронта. любой может обойти. это создаст проблему безопасности, если вы будете работать с конфиденциальными данными
Sarath Ak

Это безопасно; он также блокирует внутренний интерфейс, поскольку Django> = 1.10 docs.djangoproject.com/en/1.10/ref/forms/fields/…
timdiels

5

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

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

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

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

5

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

Тем не менее, я столкнулся с проблемами в этой области 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>

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


4

Я столкнулся с той же проблемой, поэтому я создал 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')

Я предполагаю, что это немного более читабельно, чем мой собственный миксин, который я предложил здесь. Даже, вероятно, более эффективный, поскольку эти очистки не вызывают ошибок валидации ...
christophe31

Я получаю сообщение об ошибке:'collections.OrderedDict' object has no attribute 'iteritems'
Geoidesic

4

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

способ 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

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

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)

3

Для версии 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

3

Основываясь на ответе Ямикепа , я нашел лучшее и очень простое решение, которое также обрабатывает 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

Вот немного более сложная версия, основанная на ответе 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)

1

Это самый простой способ?

Прямо в коде представления что-то вроде этого:

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)

Работает отлично!


1

Для django 1.9+
Вы можете использовать аргумент «Поля отключен», чтобы отключить поле. Например, в следующем фрагменте кода из файла forms.py я отключил поле employee_code

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

Ссылка https://docs.djangoproject.com/en/2.0/ref/forms/fields/#disabled


1

Если вы работаете с Django ver < 1.9( атрибут 1.9has Field.disabledAdded), вы можете попробовать добавить следующий декоратор в ваш __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):
        ...

Основная идея заключается в том, что если поле - readonlyвам не нужно ничего другого, кроме initial.

PS: не забудьте установить yuor_form_field.widget.attrs['readonly'] = True


0

Если вы используете администратор Django, вот самое простое решение.

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

Я думаю, что вашим лучшим вариантом будет просто включить атрибут readonly в шаблон, отображаемый в <span>или <p>вместо того, чтобы включать его в форму, если он доступен только для чтения.

Формы предназначены для сбора данных, а не их отображения. При этом параметры для отображения в readonlyвиджете и очистки данных POST являются прекрасным решением.

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.