Custom validation in admin for list_editable fields - django

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

Related

Django - display an error message when form is invalid with "CreateView"

Here is my simplified "ProjectCreate" ClassBasedView :
class ProjectForm(forms.ModelForm):
class Meta:
model = Project
fields = ['name', 'creation_date', 'price']
class ProjectCreate(LoginRequiredMixin, SuccessMessageMixin, CreateView):
model = Project
form_class = ProjectForm
success_message = "Project successfully created"
success_url = "project-list"
def get_form(self, form_class=None):
form = super(ProjectCreate, self).get_form(form_class)
form.fields.pop('creation_date')
return form
def form_valid(self, form):
if form.instance.name == "not_valid_name":
return super().form_invalid(form)
form.instance.last_editor = self.request.user
form.instance.last_modification_date = datetime.datetime.now()
return super().form_valid(form)
I want to create the project only if the name isn't "not_valid_name"
If the name is "not_valid_name", i want to display an error message (saying that the name isn't valid), and bring back the user to the 'project create' page
If you need any additional informations to understand my problem, don't hesitate to ask me.
Thanks :)
You can achieve this at different levels:
at the Form level: here is the link to the corresponding page in the documentation. In short, use the clean method of the field name, the following code should be easy to understand:
from django import forms
from ??? import Project
class ProjectForm(forms.ModelForm):
# I assume you already have some code here
def clean_name(self):
name = self.cleaned_data.get("name")
if name in ["invalid_name_1", "invalid_name_2"]: # etc.
raise ValidationError("Forbidden value for this field.")
return name
class Meta(forms.ModelForm.Meta):
model = Project
With this code (and a few lines more in the template), this is what the client will see:
at the Model level: you could use a custom validator. Check this page for further information, it's quite well written.
at the View level, as you were trying to do, there should be a way, but I think it's not the best solution because it's cleaner to keep the validation logic in the form & model fields. A reason for that, for instance, is that you might be willing to keep the same constraint in the admin app when editing your Project instances. Here's a hint if you prefer that alternative: this page lists all available & useful methods in a CreateView.

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 . :)

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

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

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

Django admin return custom error message during model saving

I would like to return some custom error messages in save_model function of Django admin page.
class EmployerAdmin(admin.ModelAdmin):
exclude = ('update_user','updatedate','activatedate','activate_user')
def save_model(self, request, obj, form, change):
if obj.department != None and obj.isDepartmentSuggested:
obj.isDepartmentSuggested =False
else:
return "You don't set a valid department. Do you want to continue ?"
obj.update_user = request.user
obj.updatedate = datetime.datetime.now()
obj.save()
Of course, Else part isn't correct but I want to illustrate what I want.
I am glad to suggest me a way or document to do that.
Thanks
You need to use a form to do your validation in your EmployerAdmin:
#forms.py
from your_app.models import Employer
class EmployerAdminForm(forms.ModelForm):
class Meta:
model = Employer
def clean(self):
cleaned_data = self.cleaned_data
department = cleaned_data.get('department')
isDepartmentSuggested = cleaned_data.get('isDepartmentSuggested')
if department == None and not isDepartmentSuggested:
raise forms.ValidationError(u"You haven't set a valid department. Do you want to continue?")
return cleaned_data
#admin.py
from django.contrib import admin
from your_app.forms import EmployerAdminForm
from your_app.models import Employer
class EmployerAdmin(admin.ModelAdmin):
exclude = ('update_user','updatedate','activatedate','activate_user')
form = EmployerAdminForm
admin.site.register(Employer, EmployerAdmin)
Hope that helps you out.
I'm using Django 1.6.3, and I'd like to add to Brandon's answer.
Add admin.site.register(Employer, EmployerAdmin) as a separate line below the EmployerAdmin class; that is, below form = EmployerAdminForm, unindented.
It took me some time to figure out why Brandon's answer wasn't working for me and the validations weren't running, apparently, you just need to register it on admin first.
Cheers.