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

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

  •  11-07-2019
  •  | 
  •  

Вопрос

Как в форме 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()
Это было полезно?

Решение

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

  

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

В 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 в поле ввода вместо clean_sku.

Функция POST гарантирует, что значение ModelForm не будет переопределено <=>.

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

Другие советы

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

  

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

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

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

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

ответ Авалкера мне очень помог!

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

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

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

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

Я создал класс 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')

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

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

Я столкнулся с подобной проблемой. Похоже, я смог решить ее, определив & Quot; get_readonly_fields & Quot; метод в моем классе 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/ реф / CONTRIB / админ / # modeladmin-метода

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

Как полезное дополнение к Пост Хамфри , у меня были некоторые проблемы с реверсией 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)

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

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

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

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

Однако у меня возникли проблемы с полем ModelChoiceField. Все будет работать по первому запросу. Однако, если набор форм попытался добавить новый элемент и не прошел проверку, что-то пошло не так с & Quot; существующий & Quot; формы, в которых параметр 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>

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

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

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

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

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

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

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

Вместо этого он оборачивает виджет полей формы в виджет только для чтения, тем самым делая форму по-прежнему валидной. Содержимое исходного виджета отображается внутри тегов <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)

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

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

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.9+
Вы можете использовать аргумент «Поля отключены», чтобы отключить поле.напримерВ следующем фрагменте кода из файла form.py я отключил поле сотрудника_кода.

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

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

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

Если вы используете администратор 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',)

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

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

Я решил эту проблему следующим образом:

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

в представлениях:

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

Это все.

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top