I know there is a change/update view in Django admin but is there any detail view that just lists out the record's attributes? Kind of like the DetailView in the Django app?
Or does anyone know any 3rd party packages I can install to provide the same functionality?
I too was investigating this recently.
One approach that works is to create a custom ModelAdmin with a detail_view method that forwards the call to ModelAdmin's changeform_view() method. Then this view is added to the urls list via overriding ModelAdmin.get_urls().
Then, in this method set a class attribute, say __detail_view to True. Then override has_change_permission() method, which returns False, if __detail_view is detected and set to True. This will cause AdminSite to render the fields in readonly mode (using the AdminReadonlyField wrapper fields) instead of the standard AdminField objects.
You can also change the change_form_template to a custom template for detail_view to accommodate custom rendering for detail views.
class CustomModelAdmin(admin.ModelAdmin):
def has_change_permission(self, request, obj=None):
if getattr(self, '__detail_view', None):
return False
return super().has_change_permission(request, obj)
def detail_view(self, request, object_id, form_url='', extra_context=None):
setattr(self, '__detail_view', True)
# Custom template for detail view
org_change_form_template = self.change_form_template
self.change_form_template = self.detail_view_template or self.change_form_template
ret = self.changeform_view(request, object_id, form_url, extra_context)
self.change_form_template = org_change_form_template
delattr(self, '__detail_view')
return ret
def get_urls(self):
urls = super().get_urls()
# add detail-view for the object
from django.urls import path
def wrap(view):
def wrapper(*args, **kwargs):
return self.admin_site.admin_view(view)(*args, **kwargs)
wrapper.model_admin = self
return update_wrapper(wrapper, view)
info = self.model._meta.app_label, self.model._meta.model_name
# Replace the backwards compatibility (Django<1.9) change view
# for the detail view.
urls[len(urls)-1] = path('<path:object_id>/', wrap(self.detail_view), name='%s_%s_detail' % info)
return urls
I haven't tried the custom template approach, but using the __detail_view object attribute to force readonly rending seems to work.
The default change_form_template still shows the delete button at the bottom, which is okay I guess. But it needs another button to actually take you to the real change page where the object can be changed. Again template customization is the way to go here. Hint: look at {% submit_row %} in admin templates and model a custom inclusion template tag that displays the Change button, if the user has change permission. Take note to call the has_change_permission() here to get the real permission before setting the __detail_view attribute.
Not sure if there are other implications for doing it this way, but it ought to work.
HTH
I am using a Django form wizard to enter data into a form page, and then display it in a confirmation page. However, when I try to call self.get_cleaned_data_for_step(step_name), I get a "'MyForm' object has no attribute 'cleaned_data'." I know this can happen if the form fails to validate, so I overrode the is_valid method in my form class to always return True, just as a test, but I still get this error. My relevant code is below:
forms.py
...
class MealForm(forms.Form):
modifications = forms.CharField()
def __init__(self, *args, **kwargs):
menu_items = kwargs.pop('menu_items')
super(MealForm, self).__init__(*args, **kwargs)
for item in menu_items:
self.fields[str(item.name)] = forms.IntegerField(widget=forms.NumberInput(attrs={'value': 0}))
def is_valid(self):
return True
urls.py
...
url(r'^(?P<url>[-\w]+)/meal/$',
login_required(views.MealFormWizard.as_view(views.MealFormWizard.FORMS)), name="meal"),
views.py
...
class MealFormWizard(SessionWizardView):
FORMS = [('meal_form', MealForm),
('meal_form_confirmation', MealFormConfirmation)]
TEMPLATES = {'meal_form': 'restaurant/createMeal.html',
'meal_form_confirmation': 'restaurant/confirmation.html'}
def get_form_kwargs(self, step=None):
kwargs = {}
url = self.kwargs['url']
restaurant = Restaurant.objects.get(url=url)
menu_items = MenuItem.objects.filter(restaurant=restaurant)
if step == 'meal_form':
kwargs['menu_items'] = menu_items
return kwargs
def get_context_data(self, form, **kwargs):
context = super(MealFormWizard, self).get_context_data(form=form, **kwargs)
if self.steps.current == 'meal_form':
context.update({...objects/vars...})
if self.steps.current == 'meal_form_confirmation':
cd = self.get_cleaned_data_for_step('meal_form') **This is where my error occurs**
createMeal.html
...
<form action="" method="post">
{% csrf_token %}
{{ wizard.management_form }}
{{ wizard.form }}
<button name="wizard_goto_step" type="submit" value="{{ wizard.steps.next }}">Submit Meal</button>
</form>
Upon submitting the form in createMeal.html I should be able to access the cleaned data for the previous step in the get_context_data method of my MealFormWizard class in views.py via a call to self.get_cleaned_data_for_step('meal_form'). However, this is not the case, but I am not sure where I went wrong.
Overriding is_valid like that won't work - if you follow the code you will see that the form's cleaned_data attribute is set by the normal is_valid method.
The docs say that if the form is invalid, then get_cleaned_data_for_step will return None, you need to write your code so it can handle this.
In case this is helpful to anyone. My problem was that in my createMeal.html, my button was simply taking the wizard to the next step, bypassing any validation. The proper solution is to make a simple submit button to submit the form, at which point the wizard will then validate the form, and if it is valid, it will move on to the next step.
I have a Django Model which I wish to be only readonly. No adds and edits allowed.
I have marked all fields readonly and overridden has_add_permission in ModelAdmin as:
class SomeModelAdmin(admin.ModelAdmin):
def has_add_permission(self, request):
return False
Is there a similar has_edit_permission? Which can be disabled to remove "Save" and "Save and continue" buttons? And replace by a simple "Close and Return" button.
Django Documentation Only mentions only about read only fields not about overriding edit permissions.
For Django 1.11:
def has_add_permission(self, request, obj=None):
return False
def changeform_view(self, request, object_id=None, form_url='', extra_context=None):
extra_context = extra_context or {}
extra_context['show_save_and_continue'] = False
extra_context['show_save'] = False
return super(YourModelAdmin, self).changeform_view(request, object_id, extra_context=extra_context)
The easiest method would be disabling respective permissions in ModelAdmin class. For example, I have a Cart model that I want an admin to only view (list and details) and all I did was to add the following functions to CartAdmin class to disable delete, change and add
class CartAdmin(admin.ModelAdmin):
list_display = ['listing']
def has_add_permission(self, request, obj=None):
return False
def has_change_permission(self, request, obj=None):
return False
def has_delete_permission(self, request, obj=None):
return False
The three methods has_add_permission, has_change_permission and has_delete_permission are the ones that disable add button, add form, edit form and delete buttons in the admin
Here is a sample output when viewing a record details in the admin that has the above permissions disabled
As you can see the diagram above, you only have close button and the details are not displayed in a form
I had same problem. I fixed it in admin.py
from django.contrib.admin.templatetags.admin_modify import register, submit_row as original_submit_row
#register.inclusion_tag('admin/submit_line.html', takes_context=True)
def submit_row(context):
''' submit buttons context change '''
ctx = original_submit_row(context)
ctx.update({
'show_save_and_add_another': context.get('show_save_and_add_another',
ctx['show_save_and_add_another']),
'show_save_and_continue': context.get('show_save_and_continue',
ctx['show_save_and_continue']),
'show_save': context.get('show_save',
ctx['show_save']),
'show_delete_link': context.get('show_delete_link', ctx['show_delete_link'])
})
return ctx
In MyModelAdmin class, add following function
#classmethod
def has_add_permission(cls, request):
''' remove add and save and add another button '''
return False
def change_view(self, request, object_id, extra_context=None):
''' customize add/edit form '''
extra_context = extra_context or {}
extra_context['show_save_and_continue'] = False
extra_context['show_save'] = False
return super(MyModelAdmin, self).change_view(request, object_id, extra_context=extra_context)
Override the templates/admin/submit_line.html template and make the buttons whatever you want. You can do this for only the specific model by putting it in templates/admin/[app_label]/[model]/submit_line.html.
To conditionally show the default submit line or your custom submit line, override ModelAdmin.change_view, and add a boolean to extra_context:
class MyModelAdmin(admin.ModelAdmin):
...
def change_view(self, request, object_id, extra_context=None):
if not request.user.is_superuser:
extra_context = extra_context or {}
extra_context['readonly'] = True
return super(MyModelAdmin, self).change_view(request, object_id, extra_context=extra_context)
Updated answer using Django 1.8 (Python 3 syntax).
There are three things to do:
1) extend the admin change form template, adding an if to conditionally suppress the submit buttons
2) override admin.ModelAdmin.change_view() and set a context var for the template if to read
3) prohibit unwanted POST requests (from DOM hacking, curl/Postman)
MyProject/my_app/templates/admin/my_app/change_form.html
{% extends "admin/change_form.html" %}
{% load admin_modify %}
{% block submit_buttons_top %}{% if my_editable %}{% submit_row %}{% endif %}{% endblock %}
{% block submit_buttons_bottom %}{% if my_editable %}{% submit_row %}{% endif %}{% endblock %}
MyProject/my_app/admin.py (MyModelAdmin)
def change_view(self, request, object_id, form_url='', extra_context=None):
obj = MyModel.objects.get(pk=object_id)
editable = obj.get_status() == 'Active'
if not editable and request.method == 'POST':
return HttpResponseForbidden("Cannot change an inactive MyModel")
more_context = {
# set a context var telling our customized template to suppress the Save button group
'my_editable': editable,
}
more_context.update(extra_context or {})
return super().change_view(request, object_id, form_url, more_context)
I had the same problem - the easiest way to do this, is to include some custom JS.
In you admin.py file include
class Media:
js = ('/static/js/admin.js',)
Then in your admin.js file, include the following JS.
(function($) {
$(document).ready(function($) {
$(".submit-row").hide()
});
})(django.jQuery);
The row is gone - it should work in all versions of Django too.
This has been possible for a while. The names are has_add_permission, has_change_permission and has_delete_permission. See the django admin documentation for reference. Here is also an example:
#admin.register(Object)
class Admin(admin.ModelAdmin):
def has_add_permission(self, request):
return False
def has_change_permission(self, request, obj=None):
return False
def has_delete_permission(self, request, obj=None):
return False
Aug, 2022 Update:
You can remove "SAVE" button, "Save and continue editing" button, "Save and add another" button and "Delete" button from a specific admin.
For example, this is "Person" model in "store" app below:
# "store/models.py"
from django.db import models
class Person(models.Model):
first_name = models.CharField(max_length=30)
last_name = models.CharField(max_length=30)
def __str__(self):
return self.first_name + " " + self.last_name
class Meta:
verbose_name = "Person"
verbose_name_plural = "Person"
Then, this is "Person" admin in "store" app below:
# "store/admin.py"
from django.contrib import admin
from .models import Person
#admin.register(Person)
class PersonAdmin(admin.ModelAdmin):
pass
Then, this is how "Add person" page looks like:
Then, this is how "Change person" page looks like:
Then, this is how "Select person to change" page looks like:
Then, this is how "Person" admin on "Store administration" page looks like:
First, to remove "SAVE" button, set "False" to "extra_context['show_save']" in "changeform_view()" as shown below:
# "store/admin.py"
from django.contrib import admin
from .models import Person
#admin.register(Person)
class PersonAdmin(admin.ModelAdmin):
def changeform_view(self, request, object_id=None, form_url='', extra_context=None):
extra_context = extra_context or {}
extra_context['show_save'] = False # Here
return super().changeform_view(request, object_id, form_url, extra_context)
Then, "SAVE" button is removed from "Add person" page and "Change person" page. *Actually, "SAVE" button is replaced with "Close" buttom as shown below:
Next, to remove "Save and continue editing" button, set "False" to "extra_context['show_save_and_continue']" in "changeform_view()" as shown below:
# "store/admin.py"
from django.contrib import admin
from .models import Person
#admin.register(Person)
class PersonAdmin(admin.ModelAdmin):
def changeform_view(self, request, object_id=None, form_url='', extra_context=None):
extra_context = extra_context or {}
extra_context['show_save'] = False
extra_context['show_save_and_continue'] = False # Here
return super().changeform_view(request, object_id, form_url, extra_context)
Then, "Save and continue editing" button is removed from "Add person" page and "Change person" page as shown below:
Next, to remove "Save and add another" button, return "False" in "has_add_permission()" as shown below. *After this, "Add person" page can no longer be accessed:
# "store/admin.py"
from django.contrib import admin
from .models import Person
#admin.register(Person)
class PersonAdmin(admin.ModelAdmin):
def changeform_view(self, request, object_id=None, form_url='', extra_context=None):
extra_context = extra_context or {}
extra_context['show_save'] = False
extra_context['show_save_and_continue'] = False
return super().changeform_view(request, object_id, form_url, extra_context)
def has_add_permission(self, request, obj=None): # Here
return False
Then, "Save and add another" button is removed from "Change person" page as shown below:
Then, "ADD PERSON" button is also removed from "Select person to change" page as shown below:
Then, "➕ADD" button is also removed from "Person" admin on "Store administration" page as shown below:
Next, to remove "Delete" button, set "False" to "extra_context['show_delete']" in "changeform_view()" as shown below:
# "store/admin.py"
from django.contrib import admin
from .models import Person
#admin.register(Person)
class PersonAdmin(admin.ModelAdmin):
def changeform_view(self, request, object_id=None, form_url='', extra_context=None):
extra_context = extra_context or {}
extra_context['show_save'] = False
extra_context['show_save_and_continue'] = False
extra_context['show_delete'] = False # Here
return super().changeform_view(request, object_id, form_url, extra_context)
def has_add_permission(self, request, obj=None):
return False
Then, "Delete" button is removed from "Change person" page as shown below:
Actually, you can also remove "Delete" button by returning "False" in "has_delete_permission()" as shown below:
# "store/admin.py"
from django.contrib import admin
from .models import Person
#admin.register(Person)
class PersonAdmin(admin.ModelAdmin):
def changeform_view(self, request, object_id=None, form_url='', extra_context=None):
extra_context = extra_context or {}
extra_context['show_save'] = False
extra_context['show_save_and_continue'] = False
# extra_context['show_delete'] = False
return super().changeform_view(request, object_id, form_url, extra_context)
def has_add_permission(self, request, obj=None):
return False
def has_delete_permission(self, request, obj=None): # Here
return False
Then, "Delete" button is removed from "Change person" page as shown below:
Then, "Action" select dropdown box is also removed from "Select person to change" page as shown below:
In addition, you can make the fields on "Change person" page unchangeable by returning "False" in "has_change_permission()" as shown below:
# "store/admin.py"
from django.contrib import admin
from .models import Person
#admin.register(Person)
class PersonAdmin(admin.ModelAdmin):
def changeform_view(self, request, object_id=None, form_url='', extra_context=None):
extra_context = extra_context or {}
extra_context['show_save'] = False
extra_context['show_save_and_continue'] = False
# extra_context['show_delete'] = False
return super().changeform_view(request, object_id, form_url, extra_context)
def has_add_permission(self, request, obj=None):
return False
def has_delete_permission(self, request, obj=None):
return False
def has_change_permission(self, request, obj=None): # Here
return False
Then, the fields on "Change person" page are made unchangeable as shown below:
Then, "✏️Change" button is replaced with "👁️View" for "Person" admin on "Store administration" page as shown below:
In addition, you can remove "Person" admin from "Store administration" page by returning "False" in "has_view_permission()" as shown below:
# "store/admin.py"
from django.contrib import admin
from .models import Person
#admin.register(Person)
class PersonAdmin(admin.ModelAdmin):
def changeform_view(self, request, object_id=None, form_url='', extra_context=None):
extra_context = extra_context or {}
extra_context['show_save'] = False
extra_context['show_save_and_continue'] = False
# extra_context['show_delete'] = False
return super().changeform_view(request, object_id, form_url, extra_context)
def has_add_permission(self, request, obj=None):
return False
def has_delete_permission(self, request, obj=None):
return False
def has_change_permission(self, request, obj=None):
return False
def has_view_permission(self, request, obj=None): # Here
return False
Then, "Person" admin is removed from "Store administration" page as shown below:
Finally, you can replace "changeform_view()" with "render_change_form()" which can also remove "SAVE" button, "Save and continue editing" button and "Delete" button with "context.update()" as shown below:
# "store/admin.py"
from django.contrib import admin
from .models import Person
#admin.register(Person)
class PersonAdmin(admin.ModelAdmin):
# Here
def render_change_form(self, request, context, add=False, change=False, form_url='', obj=None):
context.update({
'show_save': False, # Here
'show_save_and_continue': False, # Here
# 'show_delete': False, # Here
})
return super().render_change_form(request, context, add, change, form_url, obj)
def has_add_permission(self, request, obj=None):
return False
def has_delete_permission(self, request, obj=None):
return False
def has_change_permission(self, request, obj=None):
return False
def has_view_permission(self, request, obj=None):
return False
You could try this package Django Admin View Permission. This package adds a view permission for the specified models and handles the other stuff automatically.
Based on the excellent answer from #mat_gessel, here's my solution:
The main differences are UX'y:
use messages and redirect (with reversing-admin-urls), rather than HttpResponseForbidden to prevent a save
define get_readonly_fields conditionally to prevent un-saveable inputs being displayed
Also:
override change_form.html app-wide, because read_only is such a useful, non-invasive enhancement
define has_delete_permission (may not be required by the OP)
test request.method != 'GET' to prevent PATCH and friends (not altogether sure if this is required, tbh)
my_app/admin.py
from django.core.urlresolvers import reverse
from django.shortcuts import redirect
from django.contrib import admin
from django.contrib import messages
class MyModelAdmin(admin.ModelAdmin):
# let's assume two fields...
fields = (field1, field2)
def changeform_view(self, request, object_id=None, form_url='', extra_context=None):
if object_id:
extra_context = extra_context or {}
extra_context['read_only'] = True
if request.method != 'GET':
messages.error(request, "Cannot edit a MyModel object")
return redirect(
reverse('admin:myapp_mymodel_change', args=[object_id])
)
return super(MyModelAdmin, self).changeform_view(request, object_id, extra_context=extra_context)
def has_delete_permission(self, request, obj=None):
return False
def get_readonly_fields(self, request, obj=None):
if obj:
# display all fields as text, rather than inputs
return (field1, field2)
else:
return []
admin/change_form.html
{% extends "admin/change_form.html" %}
{% load admin_modify %}
{# remove the save buttons if read_only is truthy #}
{% block submit_buttons_top %}{% if not read_only %}{% submit_row %}{% endif %}{% endblock %}
{% block submit_buttons_bottom %}{% if not read_only %}{% submit_row %}{% endif %}{% endblock %}
(Tested on Django 1.9: heads up: some imports have moved since then, eg reverse)
I'm creating some custom views for the Django admin interface that use the standard change-list as an interim stage. This works fine, apart from the fact the change-list page H1 is 'Select object to change'. 'Change' is not the right verb for the action the user will be undertaking in my custom views.
I have found the django.contrib.admin templates that control the layout of the change-list pages (change_list.html and change_list_results.html) but I cannot find where the title is supplied from. I'm guessing it is a variable passed in by a view someplace?
How can I override this text with something less misleading e.g. 'Select object' instead of 'Select object to change'? I'm OK with changing it across all the change-list views, not just the particular ones I'm trying to customise; but I'd prefer a solution that is an override as opposed to a modification of the django.contrib.admin code if possible.
Update: I have found the view responsible for the change list, it's main.py in django\contrib\admin\views. The variable is self.title on line 69 (Django 1.0). I have acheived the result I am looking for by editing this line
self.title = (self.is_popup and ugettext('Select %s') % force_unicode(self.opts.verbose_name) or ugettext('Select %s to change') % force_unicode(self.opts.verbose_name))
to read
self.title = (self.is_popup and ugettext('Select %s') % force_unicode(self.opts.verbose_name) or ugettext('Select %s') % force_unicode(self.opts.verbose_name))
I'd still be really interested to hear a better way of achieving the same result that doesn't involve hacking the django.contrib.admin code - it looks like there already is an option to have the title the way I'd like it, but I'm not sure how to trigger that?
Not sure if still relevant, but another way to do this would be passing the extra_content for the changelist_view method. For ex:
from django.contrib import admin
class MyCustomAdmin(admin.ModelAdmin):
def changelist_view(self, request, extra_context=None):
extra_context = {'title': 'Change this for a custom title.'}
return super(MyCustomAdmin, self).changelist_view(request, extra_context=extra_context)
For current versions of Django:
class CustomChangeList(django.contrib.admin.views.main.ChangeList):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.title = 'My Cool Title'
class MyAdmin(ModelAdmin):
def get_changelist(self, request, **kwargs):
return CustomChangeList
There is already ticket for ChangeList customization: http://code.djangoproject.com/ticket/9749. This will give the ability to change many additional aspects of admin application. Unfortunately there is no clean way to achieve your goals.
You can change the title on "Select ... to change" page with "extra_context" in "changelist_view()" as shown below:
# "admin.py"
from django.contrib import admin
from .models import Person
#admin.register(Person)
class PersonAdmin(admin.ModelAdmin):
def changelist_view(self, request, extra_context=None):
extra_context = {'title': 'This is a custom title.'} # Here
return super().changelist_view(request, extra_context=extra_context)
This is how the title on "Select Person to change" page looks like as shown below:
You can override the method and pass it the title in extra_content, see:
def change_view(self, request, object_id, form_url='', extra_context=None):
extra_context = {'title': 'Hello Title'}
return super(BlogAdmin, self).change_view(request, object_id,
form_url, extra_context=extra_context)
As of Django 3.1.7
I think the OP is asking about the changelist "content" title (the one shown on the page below breadcrumbs, not in the browser tab title). Django sets it from the model's verbose_name_plural (set in model class' class Meta). If it is not explicitly set, Django uses the model class name with 's' suffixed. Here is the code from Django admin change_list.html:
<!-- CONTENT-TITLE -->
{% block content_title %}
<h1>{{ cl.opts.verbose_name_plural|capfirst }}</h1>
{% endblock %}
So if just setting the verbose_name_plural does not suffice/work for you, consider overriding the change_list.html template and do your thing in the {% block content_title %}. If it is too complicated to do in the template, you can pass your own context data to the admin template as given in the SO answer here:
Django how to pass custom variables to context to use in custom admin template?