Em um formulário do Django, como faço para tornar um campo somente leitura (ou desativado) para que não possa ser editado?


431

Em um formulário do Django, como faço para um campo somente leitura (ou desativado)?

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

Por exemplo, ao criar um novo Itemmodelo, todos os campos devem ser editáveis, mas durante a atualização do registro, existe uma maneira de desativar o skucampo para que fique visível, mas não possa 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

A classe pode ItemFormser reutilizada? Quais alterações seriam necessárias na classe ItemFormou Itemmodelo? Preciso escrever outra classe " ItemUpdateForm", para atualizar o item?

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

Veja também a pergunta SO: Por que os campos de formulário somente leitura no Django são uma má idéia? @ stackoverflow.com/questions/2902024 , Resposta aceita (por Daniel Naab) cuida de hackers POST maliciosos.
X10

Respostas:


422

Como apontado nesta resposta , o Django 1.9 adicionou o atributo Field.disabled :

O argumento booleano desabilitado, quando definido como True, desabilita um campo de formulário usando o atributo HTML desabilitado para que não seja editável pelos usuários. Mesmo que um usuário altere o valor do campo enviado ao servidor, ele será ignorado em favor do valor dos dados iniciais do formulário.

Com o Django 1.8 e versões anteriores, para desativar a entrada no widget e impedir hackers POST maliciosos, você deve limpar a entrada, além de definir o readonlyatributo no campo do 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.pkpor outra condição indicando que você está editando. Você também pode definir o atributo disabledno campo de entrada, em vez de readonly.

A clean_skufunção garantirá que o readonlyvalor não seja substituído por a POST.

Caso contrário, não há nenhum campo de formulário interno do Django que renderize um valor ao rejeitar dados de entrada vinculados. Se é isso que você deseja, crie um separado ModelFormque exclua os campos não editáveis ​​e imprima-os dentro do seu modelo.


3
Daniel, Obrigado por postar uma resposta. Não está claro para mim como usar esse código? esse código não funcionaria da mesma forma para o novo modo de atualização? Você pode editar sua resposta para dar exemplos de como usá-la em formulários novos e atualizados? Obrigado.
X10

8
A chave do exemplo de Daniel é testar o campo .id. Os objetos criados recentemente terão o ID == Nenhum. A propósito, um dos mais antigos ingressos abertos do Django é sobre esse problema. Consulte code.djangoproject.com/ticket/342 .
22426 Peter Rowell #

2
@moadeep adiciona um clean_descriptionmétodo à classe do formulário.
Daniel Naab

3
no linux (ubuntu 15) / chrome v45, somente o ponteiro é alterado para uma "mão desativada", mas o campo é clicável. com desativado ele funciona como esperado
simone cittadini

7
Esta resposta precisa ser atualizada. Um novo argumento de campo disabledé adicionado no Django 1.9. Se Field.disabledestiver definido como True, o valor POST para isso Fieldé ignorado. Portanto, se você estiver usando o 1.9, não há necessidade de substituir clean, basta definir disabled = True. Verifique esta resposta.
Narendra-choudhary

174

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

O argumento booleano desabilitado, quando definido como True, desabilita um campo de formulário usando o atributo HTML desabilitado para que não seja editável pelos usuários. Mesmo que um usuário altere o valor do campo enviado ao servidor, ele será ignorado em favor do valor dos dados iniciais do formulário.


Nada por 1,8 LTS?
dnit13

9
alguma idéia de como podemos usar isso em um UpdateView? Como ele gera os campos do modelo ...
bcsanches

6
Resposta correta. Minha classe de solução MyChangeForm (forms.ModelForm): def __init __ (self, * args, ** kwargs): super (MyChangeForm, self) .__ init __ (* args, ** kwargs) self.fields ['my_field']. Disabled = Verdadeiro
Vijay Katam

8
Essa é uma resposta problemática - a configuração disabled=Truefará com que o modelo seja cuspido de volta ao usuário com erros de validação.
Ben

1
Seria incrível se você pudesse incluir um exemplo
geoidesic

95

A configuração readonlyem um widget apenas torna a entrada no navegador somente leitura. A adição de um clean_skuque retorna instance.skugarante que o valor do campo não seja alterado no nível do formulário.

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

Dessa forma, você pode usar o modelo (salvamento não modificado) e evitar o erro de campo necessário.


15
+1 Esta é uma ótima maneira de evitar substituições mais complicadas de save (). No entanto, você deseja fazer uma verificação de instância antes do retorno (no modo de comentário sem linha de nova linha): "if self.instance: return self.instance.sku; else: return self.fields ['sku']"
Daniel Naab

2
Para a última linha, seria return self.cleaned_data['sku']tão bom ou melhor? Os documentos parecem sugerir o uso cleaned_data: "O valor de retorno desse método substitui o valor existente cleaned_data, portanto deve ser o valor do campo cleaned_data(mesmo que esse método não o tenha alterado) ou um novo valor limpo".
pianoJames

67

A resposta do awalker me ajudou muito!

Mudei seu exemplo para trabalhar com o Django 1.3, usando get_readonly_fields .

Normalmente você deve declarar algo assim em app/admin.py:

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

Eu me adaptei desta maneira:

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

E isso funciona bem. Agora, se você adicionar um item, o urlcampo será de leitura e gravação, mas, quando alterado, ele será somente leitura.


56

Para fazer isso funcionar em um ForeignKeycampo, é necessário fazer algumas alterações. Em primeiro lugar, a SELECT HTMLtag não possui o atributo readonly. Precisamos usar em seu disabled="disabled"lugar. No entanto, o navegador não envia dados de formulário para esse campo. Portanto, precisamos definir esse campo para não ser necessário, para que o campo seja validado corretamente. Em seguida, precisamos redefinir o valor para o que costumava ser, para que não fique em branco.

Portanto, para chaves estrangeiras, você 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)

Dessa forma, o navegador não permitirá que o usuário altere o campo e sempre será POSTdeixado em branco. Em seguida, substituímos o cleanmétodo para definir o valor do campo como o que estava originalmente na instância.


Tentei usá-lo como formulário TabularInline, mas falhei porque attrsforam compartilhados entre widgetinstâncias e todos, exceto a primeira linha, incluindo os recém-adicionados, renderizados somente leitura.
dhill

Uma ótima solução (atualização)! Infelizmente, isso e o restante têm problemas quando há erros de formulário, pois todos os valores "desativados" são esvaziados.
Michael Thompson

28

Para o Django 1.2+, você pode substituir o campo da seguinte maneira:

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

6
Isso também não permite que o campo seja editado no momento da adição, que é a questão original.
Matt S.

Esta é a resposta que estou procurando. Field disablednão faz o que eu quero porque desativa o campo, mas também remove o rótulo / o torna invisível.
sivabudh

18

Eu criei uma classe MixIn que você pode herdar para poder adicionar um campo iterável read_only que desabilitará e protegerá os campos na não primeira edição:

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

11

Acabei de criar o widget mais simples possível para um campo somente leitura - eu realmente não vejo por que os formulários ainda não têm isso:

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 dá apenas saída. Útil em um formset com vários valores somente leitura. É claro - você também pode ser um pouco mais inteligente e dividir com os attrs para poder acrescentar classes a ele.


2
Parece sexy, mas como lidar com chave estrangeira?
andilabs

Faça isso unicode(value)no retorno, talvez. Supondo que o dunder unicode é sensato, você entenderia.
Danny Staple

Para chaves estrangeiras, você precisará adicionar um atributo "model" e usar "get (value)". Check my gist
shadi

10

Encontrei um problema semelhante. Parece que eu consegui resolvê-lo definindo um método "get_readonly_fields" na minha classe ModelAdmin.

Algo assim:

# 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ê estiver adicionando um novo Item ou será o objeto que está sendo editado quando você estiver alterando um Item existente.

get_readonly_display está documentado aqui: http://docs.djangoproject.com/en/1.2/ref/contrib/admin/#modeladmin-methods


6

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


E como sobre a verbos_nameou labelde campo? Como posso mostrar `label no template django? @alzclarke
Whale 52Hz

6

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

isso bloqueará apenas da frente. qualquer um pode ignorar. isso criará um problema de segurança se você estiver usando dados confidenciais
Sarath Ak

É seguro; também bloqueia o back-end desde o Django> = 1.10 docs.djangoproject.com/en/1.10/ref/forms/fields/…
timdiels

5

Como uma adição útil ao post de Humphrey , tive alguns problemas com o django-reversion, porque ele ainda registrava os campos desativados como 'alterados'. O código a seguir corrige 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)

5

Como ainda não posso comentar ( a solução de muhuk ), responderei como uma resposta separada. 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']

5

Mais uma vez, vou oferecer mais uma solução :) Eu estava usando o código de Humphrey , então isso se baseia nisso.

No entanto, tive problemas com o campo sendo a ModelChoiceField. Tudo funcionaria no primeiro pedido. No entanto, se o conjunto de formulários tentasse adicionar um novo item e falhar na validação, algo estava errado com os formulários "existentes" em que a SELECTEDopção estava sendo redefinida para o padrão ---------.

Enfim, eu não conseguia descobrir como consertar isso. Então, em vez disso (e acho que isso é realmente mais limpo no formulário), criei os campos HiddenInputField(). Isso significa apenas que você precisa trabalhar um pouco mais no modelo.

Portanto, a correção para mim foi 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 então, no modelo, você precisará fazer um loop manual do formset .

Portanto, nesse caso, você faria 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 de formulários.


4

Eu estava enfrentando o mesmo problema, então criei um Mixin que parece funcionar nos 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 defina quais devem ser somente leitura:

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

Suponho que seja um pouco mais legível do que minha própria mixin que sugeri aqui. Mesmo provavelmente mais eficientes, como aqueles limpa não levantam erros validações ...
christophe31

Eu recebo um erro:'collections.OrderedDict' object has no attribute 'iteritems'
geoidesic 23/08

4

se você precisar de vários campos somente leitura. você pode usar qualquer um dos métodos 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 de 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',)

3

Mais duas abordagens (semelhantes) com um exemplo generalizado:

1) primeira abordagem - remoção de campo no método 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 - redefinir o campo 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, generalizei 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)

3

Para a versão Admin, acho que é uma maneira mais compacta se você tiver 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

3

Com base na resposta de Yamikep , encontrei uma solução melhor e muito simples que também lida comModelMultipleChoiceField campos.

A remoção do campo form.cleaned_dataimpede que os campos sejam salvos:

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

2

Aqui está uma versão um pouco mais envolvente, baseada na resposta de christophe31 . Ele não depende do atributo "somente leitura". Isso causa problemas, como caixas de seleção ainda sendo alteráveis ​​e datapickers ainda aparecendo, desaparecem.

Em vez disso, agrupa o widget de campos do formulário em um widget somente leitura, tornando o formulário ainda válido. O conteúdo do widget original é exibido dentro das <span class="hidden"></span>tags. Se o widget tiver um render_readonly()método, ele será usado como texto visível; caso contrário, ele analisa o 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)

1

Essa é a maneira mais simples?

Em um código de exibição, algo como isto:

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)

Funciona bem!


1

Para django 1.9+
Você pode usar o argumento Fields disabled para desativar o campo. Por exemplo, no seguinte trecho de código do arquivo forms.py, desabilitei o campo employee_code

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/forms/fields/#disabled


1

Se você estiver trabalhando Django ver < 1.9(o atributo 1.9adicionou Field.disabled), tente adicionar o seguinte decorador ao seu __init__método de formulário :

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 for, readonlyvocê não precisará de nenhum outro valor, exceto initial.

PS: Não se esqueça de definir yuor_form_field.widget.attrs['readonly'] = True


0

Se você estiver usando o administrador do Django, aqui está 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',)

0

Acho que sua melhor opção seria incluir o atributo readonly no seu modelo renderizado em um <span>ou<p> vez de incluí-lo no formulário, se for somente leitura.

Os formulários são para coletar dados, não exibi-los. Dito isto, as opções para exibir em um readonlywidget e limpar os dados do POST são boas soluções.

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.