Trying to build a custom Django file upload form field - django

I am trying to get a file upload form field working in Django and the part I am having problems with is dynamically changing the form field required attribute. I have tried using "self.fields['field_name'].required=True' in the init method of the form but that isn't working for me.
I have looked at Django dynamically changing the required property on forms but I don't want to build several custom models and a custom render function for one form as surely it must be easier than that.
The reason I am trying to do this is because when a django form validates and has errors it doesn't pass any uploaded files back to the browser form for reediting. It will pass text areas and text inputs that didn't validate back to the form for reediting but not file uploads. I thought if I made the file upload fields mandatory for the first time the record is created mandatory and for subsequent times make them optional. That is basically what I am trying to do.
So here is what I have been trying so far:
In forms.py
from django.forms import fields
from .widgets import PDFUploadWidget, PlainTextWidget
class WQPDFField(fields.Field):
widget = PDFUploadWidget
def widget_attrs(self, widget):
attrs = super().widget_attrs(widget)
attrs['label'] = self.label
return attrs
def clean(self, *args, **kwargs):
return super().clean(*args, **kwargs)
In widgets.py
from django.forms import widgets
from django.utils.safestring import mark_safe
# We subclass from HiddenInput because we want to suppress the printing
# of the label and prefer to print it ourselves.
class PDFUploadWidget(widgets.HiddenInput):
template_name = 'webquest_widgets/widgets/pdf_upload.html'
input_type = 'file'
def __init__(self, *args, **kwargs):
style = 'visibility:hidden'
attrs = kwargs.pop('attrs', None)
if attrs:
attrs['style'] = style
else:
attrs = {'style':style}
attrs['accept'] = '.pdf'
print (attrs)
super().__init__(attrs)
def get_context(self, name, value, attrs):
context = super().get_context(name, value, attrs)
return context
#property
def is_hidden(self):
return True
class Media:
css = { 'all': ( 'css/pdfupload.css', ) }
js = ('js/pdfupload.js', )
and finally in forms.py
class WorkWantedForm(forms.Form):
category = forms.ChoiceField(choices=CHOICES)
about = forms.CharField(label="About Yourself", widget=forms.Textarea())
static1 = WQStaticField(text="Enter either phone or email")
phone = forms.CharField(required=False)
email = forms.EmailField(required=False)
cv = WQPDFField(label="Upload CV")
supporting_document = WQPDFField(label="Supporting Document (optional)", required=False)
I am not sure how to pass the "required" attribute to the custom field class after the initialisation of the form but before rendering the form as HTML.

Related

Django: send dictionary/list data to form

GOAL: Send a dictionary of data to a form, to be used in a dropdown boxself.
Views.py
form = FormsdbForm(initial={'user': default_state})
# (to set the default value of the 'user' field)
Forms.py
class FormsdbForm(forms.ModelForm):
ROOMLIST = (('roomID_abcdefghi','Room ABC'),
('roomID_jklmnopqr','Room JKL'))
roomid = forms.ChoiceField(required=False, choices=ROOMLIST)
class Meta:
model = Formsdb
fields = ('user', 'uniqueid', 'roomid')
The above setup displays a form where the field 'roomid' is a dropdown box showing to entries:
Room ABC
Room JKL
After saving, the database is populated with the matching 'RoomID_xxxxxxxxx'
Perfect so far!
In my Views.py I have a dictionary (that I can easily convert into a list-of-lists) with the data that is now statically configured in Forms.py (ROOMLIST).
QUESTION: How can I pass this dictionary (or list) to the form so it displays a dropdown box with choices?
This would replace the current "ROOMLIST" variable and it could easily contain 400-600 entries.
The view:
from django.views.generic import FormView
class FormsdbView(FormView):
# ...
def get_form_kwargs(self):
kwargs = super(FormsdbView, self).get_form_kwargs()
ROOMLIST = (('roomID_abcdefghi','Room ABC'),
('roomID_jklmnopqr','Room JKL'))
kwargs['roomlist'] = ROOMLIST
return kwargs
If you're not using FormView, you might also do form = FormsdbForm(initial={'user': default_state}, roomlist=ROOMLIST)
The form:
from django import forms
class FormsdbForm(forms.ModelForm):
roomid = forms.ChoiceField(required=False)
# ...
def __init__(self, *args, **kwargs):
self.roomlist = kwargs.pop('roomlist', None)
super(FormsdbForm, self).__init__(*args, **kwargs)
self.fields['roomid'].choices = self.roomlist

Django custom field widget and widget behaviour for a custom "ListField"

I'm creating the following custom field based off How to create list field in django
import re
from django.db import models
from django.forms.widgets import TextInput
class ListField(models.TextField):
__metaclass__ = models.SubfieldBase
description = "Stores a python list"
widget = TextInput
def __init__(self, *args, **kwargs):
super(ListField, self).__init__(*args, **kwargs)
def to_python(self, value):
if not value:
return []
return filter(None, re.split(r'\,|\s*', value))
def get_prep_value(self, value):
if value is None:
return value
return ', '.join(value)
def value_to_string(self, obj):
value = self._get_val_from_obj(obj)
return self.get_db_prep_value(value)
from south.modelsinspector import add_introspection_rules
add_introspection_rules([], ["^cflowportal\.utils\.modelutils\.ListField"])
Basically, what I want to achieve is a field where you write something like "1, asd, asdf fdgd", it stores it as such in the database but when retrieved it should return that string as an array and when given an array it should convert it back to a comma-seperated string.
I'm still not sure if what I've written so far works, but I'm having trouble displaying it as an input field and not a textarea even if I've set widget=TextInput.
So, how do I show it in the admin with the same input used by the standard CharField?
How can I customize it so that it displays a comma-separated string when showed on such input, but is given back as a Python List when accessed elsewhere?
Thanks
The following is a method to realize what you want
from django.db import models
class Blog(models.Model):
title = models.CharField(max_length=256)
labels = models.TextField()
def get_labels(self):
return self.content.split('\n')
def set_labels(self,value):
if isinstance(value,list) or isinstance(value,tuple) or isinstance(value,set):
content = '\n'.join(value)
else:
content = value
self.content = content
You can regard labels as a ListField, set value use obj.set_labels(list) function, and get value use obj.get_labels()
It act as a List Field, and admin site will run as a normal TextField.
This is what I did, but a better solution is excepted.
and a better way to do this is using save_model in admin.py:
class BlogAdmin(admin.ModelAdmin):
def save_model(self, request, obj, form, change):
# extra data handling, prevent data convert
obj.save()

How to map model fields with form field in Django

We have one application containing models.py which contains n no. of classes that inherits base class.We want to create form which dynamically takes value from user n saves in db but problem is that we want to use django form fields instead of django model forms.
As we know there are some fields missing in django forms such as PositiveIntegerField, CommaSeparetedIntegerFields etc. How can we achieve this using django form fields?
If we write follwing code in shell.
from djnago.db import models
mvar = models.PositiveIntegerFields()
from django import forms
fvar = forms.PositiveIntegerFields()
AttributeError: 'module' object has no attribute 'PositiveIntegerField'
forms.py
from django import forms
class ContextForm(forms.Form):
def __init__(self, rdict, *args, **kwargs):
super(ContextForm, self).__init__(*args, **kwargs)
for key in rdict.keys():
self.fields['%s' % str(key)] = getattr(forms,rdict.get(key))()
rdict = {'address': 'CharField','phone': 'CharField', 'Salary': 'PositiveIntegerField','first name': 'CharField','last name':'CharField'}
Looking at the source, all the field does is call the default form field with a keyword argument: min_value.
class PositiveIntegerField(IntegerField):
description = _("Positive integer")
def get_internal_type(self):
return "PositiveIntegerField"
def formfield(self, **kwargs):
defaults = {'min_value': 0}
defaults.update(kwargs)
return super(PositiveIntegerField, self).formfield(**defaults)
Therefore what you are looking for is merely
from django import forms
fvar = forms.IntegerField(min_value=0)
fvar.clean(-1)
# ValidationError: [u'Ensure this value is greater than or equal to 0.']
As for CommaSeparatedIntegerField, it looks like a CharField with some django.core.validators.validate_comma_separated_integer_list passed in.
f = forms.CharField(validators=[django.core.validators.validate_comma_separated_integer_list])
f.clean('1,2,3')
All this does is make sure the passed in string is '^[\d,]+$'. The field doesn't even do any python conversions... it doesn't really seem to save much time if just validates form input. Indeed, there's a comment that says "maybe move to contrib". Agreed..
Decided to look into this for fun. Here's a ModelForm generator that overrides model fields with new fields... It doesn't yet handle kwargs. It was just the first method I could think of to do this.. without looking into modelform generation itself. It constructs a regular ModelForm that modifies the form /after/ initialization.
MODEL_FIELD_MAP = {
models.IntegerField: forms.CharField,
# change all IntegerField to forms.CharField
}
def modelform_generator(mymodel):
class MyModelForm(forms.ModelForm):
class Meta:
model = mymodel
def __init__(self, *args, **kwargs):
super(MyModelForm, self).__init__(*args, **kwargs)
for name, form_field in self.fields.items():
try:
model_field = self._meta.model._meta.get_field_by_name(name)[0]
# is this a model field?
field_override = MODEL_FIELD_MAP.get(model_field.__class__)
# do we have this model field mapped to a form field?
if field_override:
self.fields[name] = field_override()
# set the form field to the target field class
except models.FieldDoesNotExist:
pass
return MyModelForm

get_readonly_fields in a TabularInline class in Django?

I'm trying to use get_readonly_fields in a TabularInline class in Django:
class ItemInline(admin.TabularInline):
model = Item
extra = 5
def get_readonly_fields(self, request, obj=None):
if obj:
return ['name']
return self.readonly_fields
This code was taken from another StackOverflow question:
Django admin site: prevent fields from being edited?
However, when it's put in a TabularInline class, the new object forms don't render properly. The goal is to make certain fields read only while still allowing data to be entered in new objects. Any ideas for a workaround or different strategy?
Careful - "obj" is not the inline object, it's the parent. That's arguably a bug - see for example this Django ticket
As a workaround to this issue I have associated a form and a Widget to my Inline:
admin.py:
...
class MasterCouponFileInline(admin.TabularInline):
model = MasterCouponFile
form = MasterCouponFileForm
extra = 0
in Django 2.0:
forms.py
from django import forms
from . import models
from feedback.widgets import DisablePopulatedText
class FeedbackCommentForm(forms.ModelForm):
class Meta:
model = models.MasterCouponFile
fields = ('Comment', ....)
widgets = {
'Comment': DisablePopulatedText,
}
in widgets.py
from django import forms
class DisablePopulatedText(forms.TextInput):
def render(self, name, value, attrs=None, renderer=None):
"""Render the widget as an HTML string."""
if value is not None:
# Just return the value, as normal read_only fields do
# Add Hidden Input otherwise the old fields are still required
HiddenInput = forms.HiddenInput()
return format_html("{}\n"+HiddenInput.render(name, value), self.format_value(value))
else:
return super().render(name, value, attrs, renderer)
older Django Versions:
forms.py
....
class MasterCouponFileForm(forms.ModelForm):
class Meta:
model = MasterCouponFile
def __init__(self, *args, **kwargs):
super(MasterCouponFileForm, self).__init__(*args, **kwargs)
self.fields['range'].widget = DisablePopulatedText(self.instance)
self.fields['quantity'].widget = DisablePopulatedText(self.instance)
in widgets.py
...
from django import forms
from django.forms.util import flatatt
from django.utils.encoding import force_text
class DisablePopulatedText(forms.TextInput):
def __init__(self, obj, attrs=None):
self.object = obj
super(DisablePopulatedText, self).__init__(attrs)
def render(self, name, value, attrs=None):
if value is None:
value = ''
final_attrs = self.build_attrs(attrs, type=self.input_type, name=name)
if value != '':
# Only add the 'value' attribute if a value is non-empty.
final_attrs['value'] = force_text(self._format_value(value))
if "__prefix__" not in name and not value:
return format_html('<input{0} disabled />', flatatt(final_attrs))
else:
return format_html('<input{0} />', flatatt(final_attrs))
This is still currently not easily doable due to the fact that obj is the parent model instance not the instance displayed by the inline.
What I did in order to solve this, was to make all the fields, in the inline form, read only and provide a Add/Edit link to a ChangeForm for the inlined model.
Like this
class ChangeFormLinkMixin(object):
def change_form_link(self, instance):
url = reverse('admin:%s_%s_change' % (instance._meta.app_label,
instance._meta.module_name), args=(instance.id,))
# Id == None implies and empty inline object
url = url.replace('None', 'add')
command = _('Add') if url.find('add') > -1 else _('Edit')
return format_html(u'%s' % command, url)
And then in the inline I will have something like this
class ItemInline(ChangeFormLinkMixin, admin.StackedInline):
model = Item
extra = 5
readonly_fields = ['field1',...,'fieldN','change_form_link']
Then in the ChangeForm I'll be able to control the changes the way I want to (I have several states, each of them with a set of editable fields associated).
As others have added, this is a design flaw in django as seen in this Django ticket (thanks Danny W). get_readonly_fields returns the parent object, which is not what we want here.
Since we can't make it readonly, here is my solution to validate it can't be set by the form, using a formset and a clean method:
class ItemInline(admin.TabularInline):
model = Item
formset = ItemInlineFormset
class ItemInlineFormset(forms.models.BaseInlineFormSet):
def clean(self):
super(ItemInlineFormset, self).clean()
for form in self.forms:
if form.instance.some_condition:
form.add_error('some_condition', 'Nope')
You are on the right track. Update self.readonly_fields with a tuple of what fields you want to set as readonly.
class ItemInline(admin.TabularInline):
model = Item
extra = 5
def get_readonly_fields(self, request, obj=None):
# add a tuple of readonly fields
self.readonly_fields += ('field_a', 'field_b')
return self.readonly_fields

In a Django form, how do I make a field readonly (or disabled) so that it cannot be edited?

In a Django form, how do I make a field read-only (or disabled)?
When the form is being used to create a new entry, all fields should be enabled - but when the record is in update mode some fields need to be read-only.
For example, when creating a new Item model, all fields must be editable, but while updating the record, is there a way to disable the sku field so that it is visible, but cannot be edited?
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 class ItemForm be reused? What changes would be required in the ItemForm or Item model class? Would I need to write another class, "ItemUpdateForm", for updating the item?
def update_item_view(request):
if request.method == 'POST':
form = ItemUpdateForm(request.POST)
# Validate and save
else:
form = ItemUpdateForm()
As pointed out in this answer, Django 1.9 added the Field.disabled attribute:
The disabled boolean argument, when set to True, disables a form field using the disabled HTML attribute so that it won’t be editable by users. Even if a user tampers with the field’s value submitted to the server, it will be ignored in favor of the value from the form’s initial data.
With Django 1.8 and earlier, to disable entry on the widget and prevent malicious POST hacks you must scrub the input in addition to setting the readonly attribute on the form field:
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']
Or, replace if instance and instance.pk with another condition indicating you're editing. You could also set the attribute disabled on the input field, instead of readonly.
The clean_sku function will ensure that the readonly value won't be overridden by a POST.
Otherwise, there is no built-in Django form field which will render a value while rejecting bound input data. If this is what you desire, you should instead create a separate ModelForm that excludes the uneditable field(s), and just print them inside your template.
Django 1.9 added the Field.disabled attribute: https://docs.djangoproject.com/en/stable/ref/forms/fields/#disabled
The disabled boolean argument, when set to True, disables a form field using the disabled HTML attribute so that it won’t be editable by users. Even if a user tampers with the field’s value submitted to the server, it will be ignored in favor of the value from the form’s initial data.
Setting readonly on a widget only makes the input in the browser read-only. Adding a clean_sku which returns instance.sku ensures the field value will not change on form level.
def clean_sku(self):
if self.instance:
return self.instance.sku
else:
return self.fields['sku']
This way you can use model's (unmodified save) and avoid getting the field required error.
awalker's answer helped me a lot!
I've changed his example to work with Django 1.3, using get_readonly_fields.
Usually you should declare something like this in app/admin.py:
class ItemAdmin(admin.ModelAdmin):
...
readonly_fields = ('url',)
I've adapted in this way:
# In the admin.py file
class ItemAdmin(admin.ModelAdmin):
...
def get_readonly_fields(self, request, obj=None):
if obj:
return ['url']
else:
return []
And it works fine. Now if you add an Item, the url field is read-write, but on change it becomes read-only.
To make this work for a ForeignKey field, a few changes need to be made. Firstly, the SELECT HTML tag does not have the readonly attribute. We need to use disabled="disabled" instead. However, then the browser doesn't send any form data back for that field. So we need to set that field to not be required so that the field validates correctly. We then need to reset the value back to what it used to be so it's not set to blank.
So for foreign keys you will need to do something like:
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)
This way the browser won't let the user change the field, and will always POST as it it was left blank. We then override the clean method to set the field's value to be what was originally in the instance.
For Django 1.2+, you can override the field like so:
sku = forms.CharField(widget = forms.TextInput(attrs={'readonly':'readonly'}))
I made a MixIn class which you may inherit to be able to add a read_only iterable field which will disable and secure fields on the non-first edit:
(Based on Daniel's and Muhuk's answers)
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')
I ran across a similar problem.
It looks like I was able to solve it by defining a get_readonly_fields method in my ModelAdmin class.
Something like this:
# In the admin.py file
class ItemAdmin(admin.ModelAdmin):
def get_readonly_display(self, request, obj=None):
if obj:
return ['sku']
else:
return []
The nice thing is that obj will be None when you are adding a new Item, or it will be the object being edited when you are changing an existing Item.
get_readonly_display is documented here.
I've just created the simplest possible widget for a readonly field - I don't really see why forms don't have this already:
class ReadOnlyWidget(widgets.Widget):
"""Some of these values are read only - just a bit of text..."""
def render(self, _, value, attrs=None):
return value
In the form:
my_read_only = CharField(widget=ReadOnlyWidget())
Very simple - and gets me just output. Handy in a formset with a bunch of read only values.
Of course - you could also be a bit more clever and give it a div with the attrs so you can append classes to it.
For django 1.9+
You can use Fields disabled argument to make field disable.
e.g. In following code snippet from forms.py file , I have made employee_code field disabled
class EmployeeForm(forms.ModelForm):
employee_code = forms.CharField(disabled=True)
class Meta:
model = Employee
fields = ('employee_code', 'designation', 'salary')
Reference
https://docs.djangoproject.com/en/dev/ref/forms/fields/#disabled
How I do it with 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
One simple option is to just type form.instance.fieldName in the template instead of form.fieldName.
You can elegantly add readonly in the widget:
class SurveyModaForm(forms.ModelForm):
class Meta:
model = Survey
fields = ['question_no']
widgets = {
'question_no':forms.NumberInput(attrs={'class':'form-control','readonly':True}),
}
Yet again, I am going to offer one more solution :) I was using Humphrey's code, so this is based off of that.
However, I ran into issues with the field being a ModelChoiceField. Everything would work on the first request. However, if the formset tried to add a new item and failed validation, something was going wrong with the "existing" forms where the SELECTED option was being reset to the default ---------.
Anyway, I couldn't figure out how to fix that. So instead, (and I think this is actually cleaner in the form), I made the fields HiddenInputField(). This just means you have to do a little more work in the template.
So the fix for me was to simplify the Form:
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()
And then in the template, you'll need to do some manual looping of the formset.
So, in this case you would do something like this in the template:
<div>
{{ form.instance.sku }} <!-- This prints the value -->
{{ form }} <!-- Prints form normally, and makes the hidden input -->
</div>
This worked a little better for me and with less form manipulation.
I was going into the same problem so I created a Mixin that seems to work for my use cases.
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
Usage, just define which ones must be read only:
class MyFormWithReadOnlyFields(ReadOnlyFieldsMixin, MyForm):
readonly_fields = ('field1', 'field2', 'fieldx')
As a useful addition to Humphrey's post, I had some issues with django-reversion, because it still registered disabled fields as 'changed'. The following code fixes the problem.
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)
As I can't yet comment (muhuk's solution), I'll response as a separate answer. This is a complete code example, that worked for me:
def clean_sku(self):
if self.instance and self.instance.pk:
return self.instance.sku
else:
return self.cleaned_data['sku']
Based on Yamikep's answer, I found a better and very simple solution which also handles ModelMultipleChoiceField fields.
Removing field from form.cleaned_data prevents fields from being saved:
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()
Usage:
class MyFormWithReadOnlyFields(ReadOnlyFieldsMixin, MyForm):
readonly_fields = ('field1', 'field2', 'fieldx')
if your need multiple read-only fields.you can use any of methods given below
method 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
method 2
inheritance method
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',)
Two more (similar) approaches with one generalized example:
1) first approach - removing field in save() method, e.g. (not tested ;) ):
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) second approach - reset field to initial value in clean method:
def clean_<fieldname>(self):
return self.initial[<fieldname>] # or getattr(self.instance, fieldname)
Based on second approach I generalized it like this:
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)
For the Admin version, I think this is a more compact way if you have more than one field:
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
Here is a slightly more involved version, based on christophe31's answer. It does not rely on the "readonly" attribute. This makes its problems, like select boxes still being changeable and datapickers still popping up, go away.
Instead, it wraps the form fields widget in a readonly widget, thus making the form still validate. The content of the original widget is displayed inside <span class="hidden"></span> tags. If the widget has a render_readonly() method it uses that as the visible text, otherwise it parses the HTML of the original widget and tries to guess the best representation.
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)
Today I encountered the exact same problem for a similar use case. However, I had to deal with a class-based views. Class-based views allow inheriting attributes and methods thus making it easier to reuse code in a neat manner.
I will answer your question by discussing the code needed for creating a profile page for users. On this page, they can update their personal information. However, I wanted to show an email field without allowing the user to change the information.
Yes, I could have just left out the email field but my OCD would not allow it.
In the example below I used a form class in combination with the disabled = True method. This code is tested on Django==2.2.7.
# form class in forms.py
# Alter import User if you have created your own User class with Django default as abstract class.
from .models import User
# from django.contrib.auth.models import User
# Same goes for these forms.
from django.contrib.auth.forms import UserCreationForm, UserChangeForm
class ProfileChangeForm(UserChangeForm):
class Meta(UserCreationForm)
model = User
fields = ['first_name', 'last_name', 'email',]
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['email'].disabled = True
As one can see, the needed user fields are specified. These are the fields that must be shown on the profile page. If other fields need to be added one has to specify them in the User class and add the attribute name to the fields list of the Meta class of this form.
After getting the required metadata the __init__ method is called initializing the form. However, within this method, the email field parameter 'disabled' is set to True. By doing so the behavior of the field in the front-end is altered resulting in a read-only field that one cannot edit even if one changes the HTML code. Reference Field.disabled
For completion, in the example below one can see the class-based views needed to use the form.
# view class in views.py
from django.contrib import messages
from django.contrib.messages.views import SuccessMessageMixin
from django.contrib.auth.mixins import LoginRequiredMixin
from django.views.generic import TemplateView, UpdateView
from django.utils.translation import gettext_lazy as _
class ProfileView(LoginRequiredMixin, TemplateView):
template_name = 'app_name/profile.html'
model = User
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context.update({'user': self.request.user, })
return context
class UserUpdateView(LoginRequiredMixin, SuccesMessageMixin, UpdateView):
template_name = 'app_name/update_profile.html'
model = User
form_class = ProfileChangeForm
success_message = _("Successfully updated your personal information")
def get_success_url(self):
# Please note, one has to specify a get_absolute_url() in the User class
# In my case I return: reverse("app_name:profile")
return self.request.user.get_absolute_url()
def get_object(self, **kwargs):
return self.request.user
def form_valid(self, form):
messages.add_message(self.request, messages.INFO, _("Successfully updated your profile"))
return super().form_valid(form)
The ProfileView class only shows an HTML page with some information about the user. Furthermore, it holds a button that if pressed leads to an HTML page configured by the UserUpdateView, namely 'app_name/update_profile.html'. As one can see, the UserUpdateView holds two extra attributes, namely 'form_class' and 'success_message'.
The view knows that every field on the page must be filled with data from the User model. However, by introducing the 'form_class' attribute the view does not get the default layout of the User fields. Instead, it is redirected to retrieve the fields through the form class. This has a huge advantage in the sense of flexibility.
By using form classes it is possible to show different fields with different restrictions for different users. If one sets the restrictions within the model itself every user would get the same treatment.
The template itself is not that spectacular but can be seen below.
# HTML template in 'templates/app_name/update_profile.html'
{% extends "base.html" %}
{% load static %}
{% load crispy_form_tags %}
{% block content %}
<h1>
Update your personal information
<h1/>
<div>
<form class="form-horizontal" method="post" action="{% url 'app_name:update' %}">
{% csrf_token %}
{{ form|crispy }}
<div class="btn-group">
<button type="submit" class="btn btn-primary">
Update
</button>
</div>
</div>
{% endblock %}
As can be seen, the form tag holds an action tag that holds the view URL routing.
After pressing the Update button the UserUpdateView gets activated and it validates if all conditions are met. If so, the form_valid method is triggered and adds a success message. After successfully updating the data the user is returned to the specified URL in the get_success_url method.
Below one can find the code allowing the URL routing for the views.
# URL routing for views in urls.py
from django.urls import path
from . import views
app_name = 'app_name'
urlpatterns = [
path('profile/', view=views.ProfileView.as_view(), name='profile'),
path('update/', view=views.UserUpdateView.as_view(), name='update'),
]
There you have it. A fully worked out implementation of class-based views using form so one can alter an email field to be read-only and disabled.
My apologies for the extremely detailed example. There might be more efficient ways to design the class-based views, but this should work. Of course, I might have been wrong about some things said. I'm still learning as well. If anyone has any comments or improvements let me know!
You can do it just like this:
Check if the request is update or save a new object.
If request is update then disable field sku.
If request is to add a new object then you must render the form with out disabling the field sku.
Here is an example of how to do like this.
class Item(models.Model):
sku = models.CharField(max_length=50)
description = models.CharField(max_length=200)
added_by = models.ForeignKey(User)
class ItemForm(ModelForm):
def disable_sku_field(self):
elf.fields['sku'].widget.attrs['readonly'] = True
class Meta:
model = Item
exclude = ('added_by')
def new_item_view(request):
if request.method == 'POST':
form = ItemForm(request.POST)
# Just create an object or instance of the form.
# Validate and save
else:
form = ItemForm()
# Render the view
def update_item_view(request):
if request.method == 'POST':
form = ItemForm(request.POST)
# Just create an object or instance of the form.
# Validate and save
else:
form = ItemForm()
form.disable_sku_field() # call the method that will disable field.
# Render the view with the form that will have the `sku` field disabled on it.
Is this the simplest way?
Right in a view code something like this:
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)
It works fine!
If you are working with Django ver < 1.9 (the 1.9 has added Field.disabled attribute) you could try to add following decorator to your form __init__ method:
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):
...
The main idea is that if field is readonly you don't need any other value except initial.
P.S: Don't forget to set yuor_form_field.widget.attrs['readonly'] = True
Start from disable fields mixin:
class ModelAllDisabledFormMixin(forms.ModelForm):
def __init__(self, *args, **kwargs):
'''
This mixin to ModelForm disables all fields. Useful to have detail view based on model
'''
super().__init__(*args, **kwargs)
form_fields = self.fields
for key in form_fields.keys():
form_fields[key].disabled = True
then:
class MyModelAllDisabledForm(ModelAllDisabledFormMixin, forms.ModelForm):
class Meta:
model = MyModel
fields = '__all__'
prepare view:
class MyModelDetailView(LoginRequiredMixin, UpdateView):
model = MyModel
template_name = 'my_model_detail.html'
form_class = MyModelAllDisabledForm
place this in my_model_detail.html template:
<div class="form">
<form method="POST" enctype="multipart/form-data">
{% csrf_token %}
{{ form | crispy }}
</form>
</div>
You will obtain same form as in update view but with all fields disabled.
Based on the answer from #paeduardo (which is overkill), you can disable a field in the form class initializer:
class RecordForm(ModelForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
var = self.fields['the_field']
var.disabled = True
If you are using Django admin, here is the simplest solution.
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',)
I think your best option would just be to include the readonly attribute in your template rendered in a <span> or <p> rather than include it in the form if it's readonly.
Forms are for collecting data, not displaying it. That being said, the options to display in a readonly widget and scrub POST data are fine solutions.