Reputation: 65560
I'd like to show a reverse ForeignKey lookup on a Django admin page, and make it read-only, as a lightweight list of strings.
The standard way to do this seems to be with an inline admin field, but I don't want it to be editable, so I wonder if there's a lighter-weight way to do it.
These are my fields:
class Book:
title = models.TextField(blank=True)
author = models.ForeignKey(Author, on_delete=models.PROTECT)
class Author:
name = models.TextField(blank=True)
On the admin change/delete page for an author, I'd like to show a read-only list of their books.
I can do this with an admin.StackedInline
, but it's quite cumbersome to make it read-only:
class BooksInline(admin.StackedInline):
model = Book
fields = ('title',)
readonly_fields = ('title',)
def has_add_permission(request, obj):
return False
def has_delete_permission(request, obj, self):
return False
And the resulting list takes up a lot of space on the page, because the design expects it all to be editable.
Is there a simpler way to make a read-only list?
Upvotes: 9
Views: 3883
Reputation: 3524
I didn't like the idea of having admin-only logic in the model and I didn't like the idea of mucking with the templates if I didn't need to, so I instead implemented this in the admin.
from django.contrib import admin
class AuthorAdmin(admin.ModelAdmin):
readonly_fields = ('books_list')
fields = ('name', 'books_list')
def books_list(self, obj):
books = Book.objects.filter(author=obj)
if books.count() == 0:
return '(None)'
output = ', '.join([unicode(book) for book in books])
return output
books_list.short_description = 'Book(s)'
For bonus points, we can make each book link to the change page for that book.
from django.contrib import admin
from django.core import urlresolvers
from django.utils.html import format_html
class AuthorAdmin(admin.ModelAdmin):
readonly_fields = ('books_list')
fields = ('name', 'books_list')
def books_list(self, obj):
books = Book.objects.filter(author=obj)
if books.count() == 0:
return '(None)'
book_links = []
for book in books:
change_url = urlresolvers.reverse('admin:myapp_book_change', args=(book.id,))
book_links.append('<a href="%s">%s</a>' % (change_url, unicode(book))
return format_html(', '.join(book_links))
books_list.allow_tags = True
books_list.short_description = 'Book(s)'
Double bonus, you can encapsulate this into a function so that you don't have to rewrite this logic everytime you want to show a list of reverse foreignkey objects in Admin:
from django.contrib import admin
from django.core import urlresolvers
from django.utils.html import format_html
def reverse_foreignkey_change_links(model, get_instances, description=None, get_link_html=None, empty_text='(None)'):
if not description:
description = model.__name__ + '(s)'
def model_change_link_function(_, obj):
instances = get_instances(obj)
if instances.count() == 0:
return empty_text
output = ''
links = []
for instance in instances:
change_url = urlresolvers.reverse('admin:%s_change' % model._meta.db_table, args=(instance.id,))
links.append('<a href="%s">%s</a>' % (change_url, unicode(instance))
return format_html(', '.join(links))
model_change_link_function.short_description = description
model_change_link_function.allow_tags = True
return model_change_link_function
class AuthorAdmin(admin.ModelAdmin):
readonly_fields = ('books_list')
fields = ('name', 'books_list')
def books_list = reverse_foreignkey_change_links(Book, lambda obj: Book.objects.filter(author=obj))
Upvotes: 11
Reputation: 2215
I would normally do this by overriding the admin template. That's because this is mainly about presentation logic (but you may want to manipulate the queryset in the ModelAdmin to use prefetch_related()
). Create a file myapp/templates/admin/myapp/mymodel/change_form.html
. In the file, put:
{% extends "admin/change_form.html" %}
{% load i18n admin_materials %}
{% block content %}
{% if change %}
<div class="author-list">
<h1>{% trans "Authors" %}</h1>
<ul>
{% for author in original.author_set.all %}
<li>{{ author.name }}</li>
{% endfor %}
</ul>
</div>
{% endif %}
{{ block.super }}
{% endblock %}
This is compatible with standard django.contrib.admin and django-suit... I can't say if more exotic admin skins like django-grappelli modifies the admin template block names.
Upvotes: 0
Reputation: 11
I would use a class method as mentioned in the django docs and use the _set related reverse lookup described in the docs here.
The list result is read only, so there is no need to do checking in the Admin.
// models.py
class Book:
title = models.TextField(blank=True)
author = models.ForeignKey(Author, on_delete=models.PROTECT)
class Author:
name = models.TextField(blank=True)
def get_author_books(self):
cur_books = self.book_set.all()
books = [b.title for b in cur_books]
return books
get_author_books.short_description = "Author's Books"
// admin.py
class BooksInline(admin.StackedInline):
model = Book
fields = ('title','get_author_books')
Upvotes: 1