hiding save buttons in admin if all fields are read only - django

I use the following get_readonly_fields method to not allow editing of objects in django's admin interface:
def get_readonly_fields(self, request, obj=None):
if obj == None or request.user.is_superuser:
return self.readonly_fields
# marks all fields as readonly otherwise
fields = [f.name for f in self.model._meta.fields]
return fields
This works perfectly, but the save and save and continue editing still show up. They won't do anything since all fields are read-only.
Hence my question: Is there a way to hide these save buttons dependent on whether all fields are read-only or not? How could I implement this?
EDIT1:
I'm aware on how to override the admin/submit_line.html template, but what I would like to do instead, is to set the show_save, show_save_as_new to False if I only have read-only fields. How can I change these variable values?

In django/contrib/admin there is a file called submit_line.html which renders the buttons. To override them, in your templates directory, create a folder called admin, and in admin/submit_line.html you would modify it the way you want (based on certain rules). Please note that modifying it this way would affect every admin object-view page.

Related

Hiding extra field Django Admin

I want to display an extra field based on a boolean check associated with the model.
if obj.boolean:
exclude(self.extra_field)
But the issue with this is that the extra field is not associated with the model so it is throwing error stating model does not contain this extra field.
The output that i am looking for is that, when this boolean is true the extra field should not get displayed in the model admin as well as model inline. But when it is false it should get displayed.
How can i achieve this?
If that's only for display, you need to define a #property on your model which will return something depending on your boolean. Or you may define a method on an admin class like this:
def my_method(self, obj):
# return some value depending on obj.boolean
return ...
my_method.short_description = 'A label for my_method'
Than you may use it in admin's list_display list. I don't think you may completely remove field from list display for some entries and leave it for others (as it is table), but you may render it empty depending on your boolean.
For inlines you need to add this field into both fields and readonly_fields list to avoid Unknown field(s) error.
To display the field in detailed view, you need to add it to admin's readonly_fields.
In both ModelAdmin and InlineAdmin you may override get_readonly_fields method to return different fields-lists for different objects depending on your boolean.
Also, admin classes have get_fields method which is also overridable, but since your field is readonly you probably don't need it.
See ModelAdmin's options and methods for more details.

How to check for duplicate records in a Django admin inline?

How do you do perform validation checks involving multi-inline forms in Django admin inlines?
For example, I have a simple Parent/Child model, with an admin interface showing the children in an inline table on the parent's admin change page.
Each child has a "name" field which must be unique.
On child model, I've implemented a clean() method to enforce this rule, raising a forms.ValidationError so the error is displayed in a user-friendly fashion in the admin UI. This method is called from the model's full_clean() method, which is called by Django admin during the validation step for each inline form. So, individually, if the user attempts to create a child record, that check caches the error.
However, since Django runs the validation for each inline table separately before saving the records, it doesn't cache duplicates in the new data. So if the user creates two new inline rows and enters duplicate names in each of those rows, they pass the validation check, but then when Django goes to actually save the records, it encounters the exception, which is now handled like a very user-unfriendly 500 error.
Is there an easy way to fix this? Looking through Django's code, I'm not seeing anything obvious in the _changeform_view() that houses most of the admin form validation logic.
Presumably, I'd override something on the inline's ModelForm, but even the clean method on that only validates the fields for a single record, not across multiple records.
I had a similar problem myself and spent quite a while on a solution.
I'm not sure if you found an answer (since it has been 5 months since you have asked) but either way I think that sharing my solution might be beneficial so here you go:
I tried to override the clean() method of various classes also but to no avail. Then I found this page (Customize Save In Django Admin Inline Form) which suggested overriding the save_new_objects and save_existing_objects methods on a CustomInLineFormSet class.
So in admin.py I added the following method to a CustomInLineFormSet class (or in your case this would be intended for the Child model):
class ChildInLineFormSet(BaseInLineFormSet):
def save_new_objects(self, commit=True):
saved_instances = super(ChildInLineFormSet, self).save_new_objects(commit)
if commit:
for instance in saved_instances:
instance.delete()
try:
ChildModel.objects.get(name=instance.name)
except ChildModel.DoesNotExist:
instance.save()
else:
saved_instances.remove(instance)
return saved_instances
Also, wherever you have declared your InLine class you must also add the definition for the formset field:
class ChildInLine(admin.StackedInline):
formset = ChildInLineFormSet #add this to whatever you already have
I hope this helps!
EDIT: I did a bit more digging on this: Using a custom formset is NOT necessary after all.
You can override the save_formset() method in the admin class and and should obtain the same result without having to save the models to the database:
class ParentAdmin(admin.ModelAdmin):
def save_formset(self, request, form, formset, change):
instances = formset.save(commit=False)
unique_names = []
for obj in formset.deleted_objects:
obj.delete()
for instance in instances:
if (instance.name) in unique_names:
instance.delete()
continue
unique_names.append(instance.name)
instance.save()
formset.save_m2m()

Django, adding a readonly field (from instance method) to a modelform.

In Django's admin, you can add instance method calls to an edit page via the readonly option.
Can I do something similar with a ModelForm, and display the results of an instance method call? Preferably making it part of the forms visible_fields list.
My templates are quite generic so they are looping through the forms visible fields list and I would prefer not to alter these.
Oke, my solution will be quite hacky, but you could maybe do something like this:
class YourModelForm(forms.ModelForm):
class Meta:
model = YourModel
fields = []
def __init__(self, *args, *kwargs):
super(YourModelForm, self).__init__(*args, **kwargs)
# You can also user insert, to add on a certain position
self.visible_fields.append(self.instance.method())
Now a problem could be that you append a value, because I don't know how you render your fields. But you could fix this by append a Field-like object, which returns escaped (and saved) html on the necessary methods you call.
Other hacky option, add an additional field, set with an widget its attributes to disabled=disabled, and since a disabled input value isn't submitted with the form, set it required=False.

How to add custom section to admin/auth/user/(x) page?

I'm not talking about just custom fields to the form or static data, I'm talking about adding a section which actually has it's own code. Kind of a new entry in the fieldset but which introduces not a new field but some small reports on the users's activity.
Actually this questions stands for any model's change page. The Django docs show you how to overwrite the template for this page but what good is that without adding some python code also?
You can overrride default templates and default views.
Django has two different views and templates for admin record displaying. One for creating a new one and one for displaying an existing one and editing it. Related methods are:
Add Form Template and Add View for adding a new record
Change Form Template and Change View for displaying and changing an existing record
This is an example of how to prepare related override views (taken from Add/Change View link)
class MyModelAdmin(admin.ModelAdmin):
# A template for a very customized change view:
change_form_template = 'admin/myapp/extras/openstreetmap_change_form.html'
def get_osm_info(self):
# ...
pass
def change_view(self, request, object_id, form_url='', extra_context=None):
extra_context = extra_context or {}
extra_context['osm_data'] = self.get_osm_info()
return super(MyModelAdmin, self).change_view(request, object_id,
form_url, extra_context=extra_context)
You must check default django add and change templates from django source code (and maybe copying it as your new template and editing afterwards) to see how you can prepare your custom templates.
A final note, Never edit django template or vieew codes directly from source, since they are used by many different applications and and update to django source code might override your edit or may cause problems.
The Django admin is very extensible beyond overriding the templates.
Make sure you look at the ModelAdmin methods section in the documentation.
You can modify pretty much any behavior of the ModelAdmin.
You should also look at the custom form validation and ModelForms documentation, as a custom form for your model attached to its ModelAdmin gives you another (deeper, in most ways) level of customization.

Django: Read only field

How do I allow fields to be populated by the user at the time of object creation ("add" page) and then made read-only when accessed at "change" page?
The simplest solution I found was to override the get_readonly_fields function of ModelAdmin:
class TestAdmin(admin.ModelAdmin):
def get_readonly_fields(self, request, obj=None):
'''
Override to make certain fields readonly if this is a change request
'''
if obj is not None:
return self.readonly_fields + ('title',)
return self.readonly_fields
admin.site.register(TestModel, TestAdmin)
Object will be none for the add page, and an instance of your model for the change page.
Edit: Please note this was tested on Django==1.2
There's two thing to address in your question.
1. Read-only form fields
Doesn't exist as is in Django, but you can implement it yourself, and this blog post can help.
2. Different form for add/change
I guess you're looking for a solution in the admin site context (otherwise, just use 2 different forms in your views).
You could eventually override add_view or change_view in your ModelAdmin and use a different form in one of the view, but I'm afraid you will end up with an awful load of duplicated code.
Another solution I can think of, is a form that will modify its fields upon instantiation, when passed an instance parameter (ie: an edit case). Assuming you have a ReadOnlyField class, that would give you something like:
class MyModelAdminForm(forms.ModelForm):
class Meta:
model = Stuff
def __init__(self, *args, **kwargs):
super(MyModelAdminForm, self).__init__(*args, **kwargs)
if kwargs.get('instance') is not None:
self.fields['title'] = ReadOnlyField()
In here, the field title in the model Stuff will be read-only on the change page of the admin site, but editable on the creation form.
Hope that helps.
You can edit that model's save method to handle such a requirement. For example, you can check if the field already contains some value, if it does, ignore the new value.
One option is to override or replace the change_form template for that specific model.