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

304

В форме 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()
  • 0
    См. Также ТАК вопрос: почему поля формы только для чтения в Django плохая идея? @ stackoverflow.com/questions/2902024 , Принятый ответ (Дэниел Нааб) заботится о вредоносных взломах POST.
Теги:
forms
field
readonly

24 ответа

312
Лучший ответ

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

Отключенный логический аргумент, когда установлен в значение "Истина", отключает поле формы, используя атрибут отключенного 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, который исключает неотредактируемые поля и просто напечатает их внутри вашего шаблона.

  • 2
    Даниэль, спасибо за публикацию ответа. Мне не понятно, как использовать этот код? не будет ли этот код работать так же, как для нового, так и для режима обновления? Можете ли вы отредактировать свой ответ, чтобы привести примеры использования его для новых и обновленных форм? Благодарю.
  • 6
    Ключ к примеру Даниэля - тестирование поля .id. Вновь созданные объекты будут иметь идентификатор == Нет. Кстати, об этой проблеме говорит один из самых старых открытых билетов на Django. См. Code.djangoproject.com/ticket/342 .
Показать ещё 12 комментариев
110

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

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

  • 0
    Ничего для 1.8 LTS?
  • 7
    Любая идея, как мы можем использовать это на UpdateView? Как он генерирует поля из модели ...
Показать ещё 3 комментария
88

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

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

Таким образом вы можете использовать модель (немодифицированное сохранение) и aviod, чтобы получить требуемую поле.

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

ответ от 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 []

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

49

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

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

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

  • 0
    Я попытался использовать его как форму в TabularInline , но не получилось, потому что attrs были общими для экземпляров widget и для всех, кроме первой строки, включая только что добавленную, отображаемую только для чтения.
  • 0
    Отличное (обновленное) решение! К сожалению, у этого и остальных возникают проблемы, когда возникают ошибки формы, поскольку все «отключенные» значения очищаются.
20

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

sku = forms.CharField(widget = forms.TextInput(attrs={'readonly':'readonly'}))
  • 5
    Это также не позволяет редактировать поле во время добавления, что и является первоначальным вопросом.
  • 0
    Это ответ, который я ищу. Field disabled не делает то, что я хочу, потому что оно отключает поле, но также удаляет метку / делает его невидимым.
16

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

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

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

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

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, чтобы вы могли добавлять к нему классы.

  • 1
    Выглядит сексуально, но как обращаться с внешним ключом?
  • 0
    Вместо этого, возможно, сделайте этот unicode(value) в возвращении. Если предположить, что Unicode имеет смысл, то вы получите это.
Показать ещё 1 комментарий
9

Я столкнулся с аналогичной проблемой. Похоже, я смог его решить, указав метод 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

5

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

def clean_sku(self):
  if self.instance and self.instance.pk:
    return self.instance.sku
  else:
    return self.cleaned_data['sku']
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)
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')
  • 0
    Я предполагаю, что это немного более читабельно, чем мой собственный миксин, который я предложил здесь. Даже, вероятно, более эффективный, так как эти очистки не вызывают ошибок проверки ...
  • 0
    Я получаю сообщение об ошибке: 'collections.OrderedDict' object has no attribute 'iteritems'
4

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

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

Во всяком случае, я не мог понять, как это исправить. Поэтому вместо этого (и я думаю, что это на самом деле чище в форме), я создал поля 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

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

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

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

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

2

На основе ответа Yamikep я нашел лучшее и очень простое решение, которое также обрабатывает поля 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 answer. Он не полагается на атрибут "только для чтения". Это приводит к его проблемам, например, по выбору ящиков, которые по-прежнему меняются, а datapickers все еще появляются, уходят.

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

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

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

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

Он отлично работает!

  • 0
    Попробуйте отформатировать код с помощью предоставленных инструментов.
0

Установите blank = True для "sku" CharField в models.py. Doc

models.py

class Item(models.Model):
    sku = models.CharField(max_length=50, default='sku', blank=True)
    description = models.CharField(max_length=200)
    added_by = models.ForeignKey(User)

forms.py

class ItemForm(ModelForm):
    class Meta:
        model = Item
        fields = ('sku', 'description')

views.py

def new_item_view(request):
    if request.method == 'POST':
        form = ItemForm(request.POST)
        if form.is_valid():
            itemf = form.save(commit=False)
            itemf.added_by = request.user
            itemf.save()

            return redirect('somewhere_url_name')
    else:
        form = ItemForm()

    return render(request, 'template.html', {'form': form})

def update_item_view(request, pk):
    item = get_object_or_404(Item, pk=pk)
    form = ItemForm(request.POST, instance=item)
    if request.method == 'POST':
       if form.is_valid():
            itemf = form.save(commit=False)
            itemf.save()

            return redirect('somewhere_url_name')
    else:
        form = ItemForm(instance=item)

    item_edit_flag = True
    return render(request, 'template.html', {'form': form, 'item': item, 'item_ef': item_edit_flag})

в шаблоне

...
<form method="POST">
    ...
    <div class="form-sku">
        {% if item_ef %}                    #edit item
            {{ item.sku }}
        {% else %}                          #new item
            {{ form.sku }}                  
        {% endif %}
    </div>
    ...
</form>
...
0

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

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

0

Если вы используете Django admin, вот простейшее решение.

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
    Обратите внимание, что list_editable игнорирует readonly_fields .

Ещё вопросы

Сообщество Overcoder
Наверх
Меню