How to display a custom field only in change view (admin) - django

I'm totally new in Python and Django :) and i need some help.
What i want to do:
I have a model Page and i need to add a custom field "message" when someone try to update one object.
Why? Because i'm building a revision system. This field, it's just an explanation about the change. So this field is not linked to the Page (but to another model PageRevision)
After some research i managed to add this field to my form in the admin.py file, like this:
class PageAdminForm(forms.ModelForm):
# custom field not backed by database
message = forms.CharField(required=False)
class Meta:
model = Page
it's working, my field is now displayed...But i don't want this field everywhere. Just when someone try to update a Page object.
i have found this answer different-fields-for-add-and-change-pages-in-admin but it's not working for me because it's a custom field (i think).
The rest of my code in admin.py:
class PageAdmin(admin.ModelAdmin):
form = PageAdminForm
fields = ["title", "weight", "description", "message"]
list_display = ["title", "weight", "description"]
list_filter = ["updated_at"]
def get_form(self, request, obj=None, **kwargs):
if obj is None:
# not working ?
kwargs['exclude'] = ['message']
# else:
# kwargs['exclude'] = ['message']
return super(PageAdmin, self).get_form(request, obj, **kwargs)
def save_model(self, request, obj, form, change):
if not obj.id:
obj.author = request.user
obj.modified_by = request.user
wiki_page = obj.save()
# save page in revision table
revision = PageRevision(change=change, obj=wiki_page,
request=request)
# retrieve value in the custom field
revision.message = form.cleaned_data['message']
revision.save()
def get_form doesn't exclude my custom message field because i think it doesn't know is existence. If i put another field like title, it's works.
So how to exclude the custom field from add view ?
Thanks :)

You're right, it won't work this way, because 'message' is not a field found on the Page model and the ModelAdmin class will ignore the exclusion. You can achieve this in many ways, but I think the best way to do it is this:
class PageAdmin(admin.ModelAmin):
change_form = PageAdminForm
...
def get_form(self, request, obj=None, **kwargs):
if obj is not None:
kwargs['form'] = self.change_form
return super(UserAdmin, self).get_form(request, obj, **defaults)
Basicaly here django will use an auto-generated ModelForm when adding a Page and your custom form when editing the Page. Django itself uses a similar technique to display different forms when adding and changing a User:
https://github.com/django/django/blob/stable/1.6.x/django/contrib/auth/admin.py (the interesting part is in line 68)

I just stumbled upon this same question and, since it comes in the first search results I would like to add this other solution for people that do not want to use a custom form.
You can override the get_fields method of your admin class to remove a custom field from the Add page.
def get_fields(self, request, obj=None):
fields = list(super().get_fields(request, obj=obj))
if obj is None:
fields.remove("message")
return fields

Related

I can't figure out how to remove a form field in django admin

I want to achieve the following:
If user is admin, allow them to choose the blog author from a dropdown of users. If not admin, remove the author field from admin completely. The code I have works in that it removes the field from admin, but it doesn't allow a non admin user to save the form because presumably the author field exists and needs to be set to some value ie they get an error for the author field which isn't actually on the page.
I've tried multiple variations on this, including having 2 forms, one without the author field and one with it (this seems to break the published date for some reason) and I'm running out of ideas as to how to do this. There's plenty of examples on SO and elsewhere but for either they don't fit my use case or don't appear to work (using django 1.6).
Any ideas?
#forms.py
class BlogForm(forms.ModelForm):
author = forms.ModelChoiceField( queryset=User.objects.filter( Q(is_staff=True) ))
class Meta:
model = Blog
#admin.py
class BlogAdmin(AdminPlus):
form = BlogForm
default_fieldset = [( 'Publishing', {'fields': ['pub_date','is_published']} )]
admin_fieldset = [( 'Publishing', {'fields': ['pub_date', 'author','is_published']} )]
def get_form(self, request, obj=None, **kwargs):
current_user = request.user
if not request.user.is_superuser:
self.exclude = ('author',)
form = super(BlogAdmin, self).get_form(request, obj, **kwargs)
form.current_user = current_user
return form
def get_fieldsets(self, request, obj=None):
if not request.user.is_superuser:
return self.default_fieldset
return self.admin_fieldset
admin.site.register(Blog, BlogAdmin)
In your class BlogForm(forms.ModelForm) , add following which does nothing :
def clean_author(self):
author = self.cleaned_data['author']
## remove below 'if else' if u don't want to perform any validation.
if self.current_user.is_superuser:
## perform superuser validation if you want otherwise simply pass
pass
else:
## perform non-superuser validation if you want otherwise simply pass
pass
return author
Not Tested . but I hope its helpful for you . :)

Custom validation in admin for list_editable fields

I have a admin form with custom validation. Some of the form fields are displayed in the list view via list_editable. When I modify these fields via the list view the custom validation does not kick in. It does work when I use the regular change form, though. So the question is how do I validate changes done via the "change_list" page.
The code might make it clearer
class ProjectForm(ModelForm):
class Meta:
model = Project
def clean(self):
print "validating!"
data = self.cleaned_data
if data.get('on_frontpage') and not data.get('frontpage_image'):
raise ValidationError('To put a project on the frontpage you must \
specify a "Frontpage image" first.')
return data
class ProjectAdmin(AdminImageMixin, DisplayableAdmin, SortableAdmin):
form = ProjectForm
...
list_editable = ("status", "on_frontpage",)
list_display = ("title", "status", "on_frontpage")
Thanks!
Found it. One can specify the form used on the "change_list" page by overriding "get_changelist_formset" method in ModelAdmin:
https://code.djangoproject.com/browser/django/trunk/django/contrib/admin/options.py#L524
Override the ModelAdmin.get_changelist_formset(request, **kwargs) method:
from django.forms import BaseModelFormSet
class MyAdminFormSet(BaseModelFormSet):
pass
class MyModelAdmin(admin.ModelAdmin):
def get_changelist_formset(self, request, **kwargs):
kwargs['formset'] = MyAdminFormSet
return super().get_changelist_formset(request, **kwargs)
For more details please check the Django admin site documentation.
I think #Jorge Barata's is the correct answer, thank you very much.
Please allow me to attach a success example here.
class MyAdminFormSet(BaseModelFormSet):
def clean(self):
form_set = self.cleaned_data
for form_data in form_set:
if form_data['field1'] != form_data['field2']:
raise forms.ValidationError(f'Item: {form_data["id"]} is not valid')
return form_set
Tested on Django 2.2

Dynamic fields in Django Admin

I want to have additional fields regarding value of one field. Therefor I build a custom admin form to add some new fields.
Related to the blogpost of jacobian 1 this is what I came up with:
class ProductAdminForm(forms.ModelForm):
class Meta:
model = Product
def __init__(self, *args, **kwargs):
super(ProductAdminForm, self).__init__(*args, **kwargs)
self.fields['foo'] = forms.IntegerField(label="foo")
class ProductAdmin(admin.ModelAdmin):
form = ProductAdminForm
admin.site.register(Product, ProductAdmin)
But the additional field 'foo' does not show up in the admin. If I add the field like this, all works fine but is not as dynamic as required, to add the fields regarding the value of another field of the model
class ProductAdminForm(forms.ModelForm):
foo = forms.IntegerField(label="foo")
class Meta:
model = Product
class ProductAdmin(admin.ModelAdmin):
form = ProductAdminForm
admin.site.register(Product, ProductAdmin)
So is there any initialize method that i have to trigger again to make the new field working? Or is there any other attempt?
Here is a solution to the problem. Thanks to koniiiik i tried to solve this by extending the *get_fieldsets* method
class ProductAdmin(admin.ModelAdmin):
def get_fieldsets(self, request, obj=None):
fieldsets = super(ProductAdmin, self).get_fieldsets(request, obj)
fieldsets[0][1]['fields'] += ['foo']
return fieldsets
If you use multiple fieldsets be sure to add the to the right fieldset by using the appropriate index.
The accepted answer above worked in older versions of django, and that's how I was doing it. This has now broken in later django versions (I am on 1.68 at the moment, but even that is old now).
The reason it is now broken is because any fields within fieldsets you return from ModelAdmin.get_fieldsets() are ultimately passed as the fields=parameter to modelform_factory(), which will give you an error because the fields on your list do not exist (and will not exist until your form is instantiated and its __init__ is called).
In order to fix this, we must override ModelAdmin.get_form() and supply a list of fields that does not include any extra fields that will be added later. The default behavior of get_form is to call get_fieldsets() for this information, and we must prevent that from happening:
# CHOOSE ONE
# newer versions of django use this
from django.contrib.admin.utils import flatten_fieldsets
# if above does not work, use this
from django.contrib.admin.util import flatten_fieldsets
class MyModelForm(ModelForm):
def __init__(self, *args, **kwargs):
super(MyModelForm, self).__init__(*args, **kwargs)
# add your dynamic fields here..
for fieldname in ('foo', 'bar', 'baz',):
self.fields[fieldname] = form.CharField()
class MyAdmin(ModelAdmin):
form = MyModelForm
fieldsets = [
# here you put the list of fieldsets you want displayed.. only
# including the ones that are not dynamic
]
def get_form(self, request, obj=None, **kwargs):
# By passing 'fields', we prevent ModelAdmin.get_form from
# looking up the fields itself by calling self.get_fieldsets()
# If you do not do this you will get an error from
# modelform_factory complaining about non-existent fields.
# use this line only for django before 1.9 (but after 1.5??)
kwargs['fields'] = flatten_fieldsets(self.declared_fieldsets)
# use this line only for django 1.9 and later
kwargs['fields'] = flatten_fieldsets(self.fieldsets)
return super(MyAdmin, self).get_form(request, obj, **kwargs)
def get_fieldsets(self, request, obj=None):
fieldsets = super(MyAdmin, self).get_fieldsets(request, obj)
newfieldsets = list(fieldsets)
fields = ['foo', 'bar', 'baz']
newfieldsets.append(['Dynamic Fields', { 'fields': fields }])
return newfieldsets
This works for adding dynamic fields in Django 1.9.3, using just a ModelAdmin class (no ModelForm) and by overriding get_fields. I don't know yet how robust it is:
class MyModelAdmin(admin.ModelAdmin):
fields = [('title','status', ), 'description', 'contact_person',]
exclude = ['material']
def get_fields(self, request, obj=None):
gf = super(MyModelAdmin, self).get_fields(request, obj)
new_dynamic_fields = [
('test1', forms.CharField()),
('test2', forms.ModelMultipleChoiceField(MyModel.objects.all(), widget=forms.CheckboxSelectMultiple)),
]
#without updating get_fields, the admin form will display w/o any new fields
#without updating base_fields or declared_fields, django will throw an error: django.core.exceptions.FieldError: Unknown field(s) (test) specified for MyModel. Check fields/fieldsets/exclude attributes of class MyModelAdmin.
for f in new_dynamic_fields:
#`gf.append(f[0])` results in multiple instances of the new fields
gf = gf + [f[0]]
#updating base_fields seems to have the same effect
self.form.declared_fields.update({f[0]:f[1]})
return gf
Maybe I am a bit late... However, I am using Django 3.0 and also wanted to dynamically ad some custom fields to the form, depending on the request.
I end up with a solution similar to the one described by #tehfink combined with #little_birdie.
However, just updating self.form.declared_fields as suggested didn't help. The result of this procedure is, that the list of custom fields defined in self.form.declared_fields always grows from request to request.
I solved this by initialising this dictionary first:
class ModelAdminGetCustomFieldsMixin(object):
def get_fields(self, request, obj=None):
fields = super().get_fields(request, obj=None)
self.form.declared_fields = {}
if obj:
for custom_attribute in custom_attribute_list:
self.form.declared_fields.update({custom_attribute.name: custom_attribute.field})
return fields
where custom_attribute.field is a form field instance.
Additionally, it was required to define a ModelForm, wherein during initialisation the custom fields have been added dynamically as well:
class SomeModelForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
for custom_attribute in custom_attribute_list:
self.fields[custom_attribute.name] = custom_attribute.field
and use this ModelForm in the ModelAdmin.
Afterwards, the newly defined attributes can be used in, e.g., a fieldset.
While Jacob's post might work all right for regular ModelForms (even though it's more than a year and a half old), the admin is a somewhat different matter.
All the declarative way of defining models, forms ModelAdmins and whatnot makes heavy use of metaclasses and class introspection. Same with the admin – when you tell a ModelAdmin to use a specific form istead of creating a default one, it introspects the class. It gets the list of fields and other stuff from the class itself without instantiating it.
Your custom class, however, does not define the extra form field at class level, instead it dynamically adds one after it has been instantiated – that's too late for the ModelAdmin to recognize this change.
One way to go about your problem might be to subclass ModelAdmin and override its get_fieldsets method to actually instantiate the ModelForm class and get the list of fields from the instance instead of the class. You'll have to keep in mind, though, that this might be somewhat slower than the default implementation.
You can create dynamic fields and fieldset using the form meta class. Sample code is given below. Add the loop logic as per you requirements.
class CustomAdminFormMetaClass(ModelFormMetaclass):
"""
Metaclass for custom admin form with dynamic field
"""
def __new__(cls, name, bases, attrs):
for field in get_dynamic_fields: #add logic to get the fields
attrs[field] = forms.CharField(max_length=30) #add logic to the form field
return super(CustomAdminFormMetaClass, cls).__new__(cls, name, bases, attrs)
class CustomAdminForm(six.with_metaclass(CustomAdminFormMetaClass, forms.ModelForm)):
"""
Custom admin form
"""
class Meta:
model = ModelName
fields = "__all__"
class CustomAdmin(admin.ModelAdmin):
"""
Custom admin
"""
fieldsets = None
form = CustomAdminForm
def get_fieldsets(self, request, obj=None):
"""
Different fieldset for the admin form
"""
self.fieldsets = self.dynamic_fieldset(). #add logic to add the dynamic fieldset with fields
return super(CustomAdmin, self).get_fieldsets(request, obj)
def dynamic_fieldset(self):
"""
get the dynamic field sets
"""
fieldsets = []
for group in get_field_set_groups: #logic to get the field set group
fields = []
for field in get_group_fields: #logic to get the group fields
fields.append(field)
fieldset_values = {"fields": tuple(fields), "classes": ['collapse']}
fieldsets.append((group, fieldset_values))
fieldsets = tuple(fieldsets)
return fieldsets
Stephan's answer is elegant, but when I used in in dj1.6 it required the field to be a tuple.
The complete solution looked like this:
class ProductForm(ModelForm):
foo = CharField(label='foo')
class ProductAdmin(admin.ModelAdmin):
form = ProductForm
def get_fieldsets(self, request, obj=None):
fieldsets = super(ProductAdmin, self).get_fieldsets(request, obj)
fieldsets[0][1]['fields'] += ('foo', )
return fieldsets
not sure why that's not working, but could a possible workaround be to define the field statically (on the form) and then override it in the __init__?
I for a long time could not solve a problem with dynamic addition of fields.
The solution "little_birdie" really works. Thank you Birdie))
The only nuance is:
"Self.declared_fieldsets" should be replaced with "self.fieldsets".
#kwargs['fields'] = flatten_fieldsets(self.declared_fieldsets)
kwargs['fields'] = flatten_fieldsets(self.fieldsets)
I used version 1.10. Perhaps something has changed.
If someone finds an even simpler and elegant solution, show here.
Thanks to all )))

How to limit fields in django-admin depending on user?

I suppose similar problem would have been discussed here, but I couldn't find it.
Let's suppose I have an Editor and a Supervisor. I want the Editor to be able to add new content (eg. a news post) but before publication it has to be acknowledged by Supervisor.
When Editor lists all items, I want to set some fields on the models (like an 'ack' field) as read-only (so he could know what had been ack'ed and what's still waiting approval) but the Supervisor should be able to change everything (list_editable would be perfect)
What are the possible solutions to this problem?
I think there is a more easy way to do that:
Guest we have the same problem of Blog-Post
blog/models.py:
Class Blog(models.Model):
...
#fields like autor, title, stuff..
...
class Post(models.Model):
...
#fields like blog, title, stuff..
...
approved = models.BooleanField(default=False)
approved_by = models.ForeignKey(User)
class Meta:
permissions = (
("can_approve_post", "Can approve post"),
)
And the magic is in the admin:
blog/admin.py:
...
from django.views.decorators.csrf import csrf_protect
...
def has_approval_permission(request, obj=None):
if request.user.has_perm('blog.can_approve_post'):
return True
return False
Class PostAdmin(admin.ModelAdmin):
#csrf_protect
def changelist_view(self, request, extra_context=None):
if not has_approval_permission(request):
self.list_display = [...] # list of fields to show if user can't approve the post
self.editable = [...]
else:
self.list_display = [...] # list of fields to show if user can approve the post
return super(PostAdmin, self).changelist_view(request, extra_context)
def get_form(self, request, obj=None, **kwargs):
if not has_approval_permission(request, obj):
self.fields = [...] # same thing
else:
self.fields = ['approved']
return super(PostAdmin, self).get_form(request, obj, **kwargs)
In this way you can use the api of custom permission in django, and you can override the methods for save the model or get the queryset if you have to. In the methid has_approval_permission you can define the logic of when the user can or can't to do something.
Starting Django 1.7, you can now use the get_fields hook which makes it so much simpler to implement conditional fields.
class MyModelAdmin(admin.ModelAdmin):
...
def get_fields(self, request, obj=None):
fields = super(MyModelAdmin, self).get_fields(request, obj)
if request.user.is_superuser:
fields += ('approve',)
return fields
I have a system kind of like this on a project that I'm just finishing up. There will be a lot of work to put this together, but here are some of the components that I had to make my system work:
You need a way to define an Editor and a Supervisor. The three ways this could be done are 1.) by having an M2M field that defines the Supervisor [and assuming that everyone else with permission to read/write is an Editor], 2.) make 2 new User models that inherit from User [probably more work than necessary] or 3.) use the django.auth ability to have a UserProfile class. Method #1 is probably the most reasonable.
Once you can identify what type the user is, you need a way to generically enforce the authorization you're looking for. I think the best route here is probably a generic admin model.
Lastly you'll need some type of "parent" model that will hold the permissions for whatever needs to be moderated. For example, if you had a Blog model and BlogPost model (assuming multiple blogs within the same site), then Blog is the parent model (it can hold the permissions of who approves what). However, if you have a single blog and there is no parent model for BlogPost, we'll need some place to store the permissions. I've found the ContentType works out well here.
Here's some ideas in code (untested and more conceptual than actual).
Make a new app called 'moderated' which will hold our generic stuff.
moderated.models.py
class ModeratedModelParent(models.Model):
"""Class to govern rules for a given model"""
content_type = models.OneToOneField(ContentType)
can_approve = models.ManyToManyField(User)
class ModeratedModel(models.Model):
"""Class to implement a model that is moderated by a supervisor"""
is_approved = models.BooleanField(default=False)
def get_parent_instance(self):
"""
If the model already has a parent, override to return the parent's type
For example, for a BlogPost model it could return self.parent_blog
"""
# Get self's ContentType then return ModeratedModelParent for that type
self_content_type = ContentType.objects.get_for_model(self)
try:
return ModeratedModelParent.objects.get(content_type=self_content_type)
except:
# Create it if it doesn't already exist...
return ModeratedModelParent.objects.create(content_type=self_content_type).save()
class Meta:
abstract = True
So now we should have a generic, re-usable bit of code that we can identify the permission for a given model (which we'll identify the model by it's Content Type).
Next, we can implement our policies in the admin, again through a generic model:
moderated.admin.py
class ModeratedModelAdmin(admin.ModelAdmin):
# Save our request object for later
def __call__(self, request, url):
self.request = request
return super(ModeratedModelAdmin, self).__call__(request, url)
# Adjust our 'is_approved' widget based on the parent permissions
def formfield_for_dbfield(self, db_field, **kwargs):
if db_field.name == 'is_approved':
if not self.request.user in self.get_parent_instance().can_approve.all():
kwargs['widget'] = forms.CheckboxInput(attrs={ 'disabled':'disabled' })
# Enforce our "unapproved" policy on saves
def save_model(self, *args, **kwargs):
if not self.request.user in self.get_parent_instance().can_approve.all():
self.is_approved = False
return super(ModeratedModelAdmin, self).save_model(*args, **kwargs)
Once these are setup and working, we can re-use them across many models as I've found once you add structured permissions for something like this, you easily want it for many other things.
Say for instance you have a news model, you would simply need to make it inherit off of the model we just made and you're good.
# in your app's models.py
class NewsItem(ModeratedModel):
title = models.CharField(max_length=200)
text = models.TextField()
# in your app's admin.py
class NewsItemAdmin(ModeratedModelAdmin):
pass
admin.site.register(NewsItem, NewsItemAdmin)
I'm sure I made some code errors and mistakes in there, but hopefully this can give you some ideas to act as a launching pad for whatever you decide to implement.
The last thing you have to do, which I'll leave up to you, is to implement filtering for the is_approved items. (ie. you don't want un-approved items being listed on the news section, right?)
The problem using the approach outlined by #diegueus9 is that the ModelAdmin acts liked a singleton and is not instanced for each request. This means that each request is modifying the same ModelAdmin object that is being accessed by other requests, which isn't ideal. Below is the proposed solutions by #diegueus9:
# For example, get_form() modifies the single PostAdmin's fields on each request
...
class PostAdmin(ModelAdmin):
def get_form(self, request, obj=None, **kwargs):
if not has_approval_permission(request, obj):
self.fields = [...] # list of fields to show if user can't approve the post
else:
self.fields = ['approved', ...] # add 'approved' to the list of fields if the user can approve the post
...
An alternative approach would be to pass fields as a keyword arg to the parent's get_form() method like so:
...
from django.contrib.admin.util import flatten_fieldsets
class PostAdmin(ModelAdmin):
def get_form(self, request, obj=None, **kwargs):
if has_approval_permission(request, obj):
fields = ['approved']
if self.declared_fieldsets:
fields += flatten_fieldsets(self.declared_fieldsets)
# Update the keyword args as needed to allow the parent to build
# and return the ModelForm instance you require for the user given their perms
kwargs.update({'fields': fields})
return super(PostAdmin, self).get_form(request, obj=None, **kwargs)
...
This way, you are not modifying the PostAdmin singleton on every request; you are simply passing the appropriate keyword args needed to build and return the ModelForm from the parent.
It is probably worth looking at the get_form() method on the base ModelAdmin for more info: https://code.djangoproject.com/browser/django/trunk/django/contrib/admin/options.py#L431

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.