Em uma forma Django, como eu faço um campo somente leitura (ou desativado) para que ele não pode ser editado?

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

  •  11-07-2019
  •  | 
  •  

Pergunta

Em uma forma Django, como eu faço um campo só de leitura (ou desativado)?

Quando o formulário está sendo usado para criar uma nova entrada, todos os campos devem ser habilitado -. Mas quando o registro está em modo de atualização alguns campos precisam ser somente leitura

Por exemplo, ao criar um novo modelo Item, todos os campos devem ser editáveis, mas ao atualizar o registro, há uma maneira para desativar o campo sku de modo que é visível, mas não pode ser editado?

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

Can classe ItemForm ser reutilizado? Que mudanças seriam necessárias na classe ItemForm ou modelo Item? Será que eu preciso para escrever outra classe, "ItemUpdateForm", para atualizar o produto?

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

Solução

Como apontado em esta resposta , Django 1.9 adicionou a Field.disabled atributo:

O boolean argumento desativado, quando definida como Verdadeiro, desativa um campo de formulário usando o atributo HTML desativado para que ele não será editável pelos usuários. Mesmo se um usuário mexe com o valor do campo enviada para o servidor, ele será ignorado em favor do valor dos dados iniciais do formulário.

Com Django 1.8 e anteriores, para desativar a entrada no widget e evitar hacks POST maliciosos você deve esfregar a entrada além de definir o atributo readonly no campo de formulário:

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

Ou, substitua if instance and instance.pk com outra condição que indica que você está editando. Você também pode definir o disabled atributo no campo de entrada, em vez de readonly.

A função clean_sku irá garantir que o valor readonly não será substituído por um POST.

Caso contrário, não há built-in Django campo de formulário que irá processar um valor enquanto rejeita dados de entrada encadernados. Se é isso que você deseja, você deveria criar um ModelForm separado que exclui o campo não editável (s), e apenas imprimi-los dentro do seu modelo.

Outras dicas

Django 1.9 adicionou o atributo Field.disabled: https: / /docs.djangoproject.com/en/stable/ref/forms/fields/#disabled

O boolean argumento desativado, quando definida como Verdadeiro, desativa um campo de formulário usando o atributo HTML desativado para que ele não será editável pelos usuários. Mesmo se um usuário mexe com o valor do campo enviada para o servidor, ele será ignorado em favor do valor dos dados iniciais do formulário.

Configuração READONLY no Widget só faz a entrada no navegador somente leitura. Adicionando um clean_sku que retorna instance.sku garante o valor do campo não vai mudar no nível do formulário.

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

Desta forma, você pode usar de (não modificada save) modelo e Aviod recebendo o campo de erro necessário.

a resposta de awalker me ajudou muito!

Eu mudei o seu exemplo para trabalhar com Django 1.3, utilizando get_readonly_fields .

Normalmente, você deve declarar algo como isto em app/admin.py:

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

Eu adaptei desta forma:

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

E ele funciona muito bem. Agora, se você adicionar um item, o campo url é de leitura e escrita, mas sobre a mudança torna-se somente leitura.

Para fazer este trabalho para um campo ForeignKey, algumas mudanças precisam ser feitas. Em primeiro lugar, a tag SELECT HTML não tem o atributo somente leitura. Precisamos usar disabled="disabled" vez. No entanto, o navegador não envia quaisquer dados de volta formulário para esse campo. Então precisamos definir esse campo para não ser necessária para que o campo valida corretamente. Depois, temos que redefinir o valor de volta para o que costumava ser, então não é definido como em branco.

Assim, para as chaves estrangeiras que você vai precisar fazer algo como:

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)

Desta forma, o navegador não vai deixar a mudança de usuário do campo, e será sempre POST como ele era em branco esquerdo. Em seguida, substituir o método clean para definir o valor do campo a ser o que era originalmente na instância.

Para Django 1.2+, você pode substituir o campo assim:

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

Eu fiz uma classe MixIn que você pode herdar de ser capaz de adicionar um campo read_only iterable que irá desativar e campos seguras sobre a não-primeira edição:

(Baseado em Daniel e respostas de 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')

Acabei de criar o mais simples Widget possível para um campo somente leitura - Eu realmente não vejo por que formas não tem isso já:

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

Na forma:

my_read_only = CharField(widget=ReadOnlyWidget())

Muito simples - e me deixa apenas de saída. Handy em um formset com um monte de valores somente leitura. Claro -. Você também pode ser um pouco mais inteligente e dar-lhe um div com as attrs assim você pode acrescentar aulas a ele

deparei com um problema semelhante. Parece que eu era capaz de resolvê-lo através da definição de um método "get_readonly_fields" na minha classe ModelAdmin.

Algo parecido com isto:

# In the admin.py file

class ItemAdmin(admin.ModelAdmin):

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

O bom é que obj será Nenhum quando você está adicionando um novo item, ou ele vai ser o objeto que está sendo editado quando você está mudando um item existente.

get_readonly_display está documentado aqui: http://docs.djangoproject.com/en/1.2/ ref / contrib / admin / # ModelAdmin-métodos

Uma opção simples é o tipo de apenas form.instance.fieldName no modelo em vez de form.fieldName.

Como uma adição útil para pós de Humphrey, eu tive alguns problemas com django-reversão, porque ainda registrado campos com deficiência como 'mudou'. As seguintes correções de código o problema.

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)

Como eu ainda não pode comentar ( solução de muhuk), eu vou resposta como uma resposta em separado. Este é um exemplo de código completo, que funcionou para mim:

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

Eu estava indo para o mesmo problema, então eu criei um Mixin que parece funcionar para os meus casos de uso.

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

Uso, apenas definir quais os que devem ser lidos apenas:

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

Mais uma vez, eu estou indo para oferecer mais uma solução :) eu estava usando Humphrey código , de modo que este é baseado fora disso.

No entanto, eu corri para problemas com o campo sendo um ModelChoiceField. Tudo iria trabalhar na primeira solicitação. No entanto, se o formset tentou adicionar um novo item e validação falhou, algo estava acontecendo de errado com as formas "existentes", onde a opção selecionada foi sendo redefinido para o padrão "---------".

De qualquer forma, eu não conseguia descobrir como consertar isso. Então, ao invés, (e eu acho que isso é realmente mais limpo na forma), eu fiz a campos HiddenInputField (). Isto apenas significa que você tem que fazer um pouco mais de trabalho no modelo.

Assim, a correção para mim era simplificar o formulário:

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

E, em seguida, no modelo, você precisa fazer alguma looping manual da formset .

Assim, neste caso, você poderia fazer algo assim no modelo:

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

Isso funcionou um pouco melhor para mim e com menos manipulação formulário.

Como eu faço isso com 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

Dois mais (semelhante) aproxima-se com um exemplo generalizada:

1) primeira abordagem - Método de remoção de campo em save (), por exemplo (Não testado;)):

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) segunda abordagem - campo redefinida para o valor inicial no método limpo:

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

Com base na segunda abordagem I generalizada assim:

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)

Se a sua necessidade múltipla read-only fields.you pode usar qualquer um dos métodos indicados abaixo

método 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

Método 2

método herança

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

Para a versão de administração, penso que esta é uma forma mais compacta, se você tem mais de um campo:

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

Com base em o Yamikep resposta , eu encontrei uma solução melhor e muito simples que também alças ModelMultipleChoiceField campos.

A remoção campo dos campos evita form.cleaned_data de ser salvo:

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

Uso:

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

Aqui está uma versão um pouco mais envolvidos, com base em das christophe31 resposta . Não contar com o atributo "somente leitura". Isso faz com que os seus problemas, como caixas de seleção ainda está sendo mutável e datapickers ainda aparecendo, vá embora.

Em vez disso, ele envolve os campos do formulário Widget em um readonly widget, tornando assim o formulário ainda validar. O conteúdo do widget original é exibida dentro de tags <span class="hidden"></span>. Se o widget tem um método render_readonly() ele usa isso como o texto visível, caso contrário, ele analisa o código HTML do widget original e tenta adivinhar a melhor representação.

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)

É esta a maneira mais simples?

Direito em uma coisa de código vista como esta:

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)

Ele funciona muito bem!

Para Django 1.9+
Você pode usar campos argumento desativado para fazer disable campo. por exemplo. No seguinte trecho de código a partir do arquivo forms.py, eu fiz campo employee_code desativado

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

Referência https://docs.djangoproject.com/en/2.0/ref / formas / campos / # desativada

Se você está trabalhando com Django ver < 1.9 (a 1.9 adicionou atributo Field.disabled) você poderia tentar adicionar seguinte decorador para o seu método de forma __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):
        ...

A idéia principal é que se o campo é readonly você não precisa de qualquer outro valor, exceto initial.

P.S: Não se esqueça de conjunto yuor_form_field.widget.attrs['readonly'] = True

Se você estiver usando Django admin, aqui é a solução mais simples.

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

Eu acho que a melhor opção seria apenas para incluir o atributo somente leitura em seu modelo processado em uma <span> ou <p> ao invés de incluí-lo na forma se é somente leitura.

Os formulários são para a recolha de dados, e não exibi-lo. Dito isto, as opções para exibição em um widget readonly e POST matagal dados são soluções finas.

Eu resolvi esse problema como este:

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

em vista:

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

É tudo.

Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top