Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Django: Show reverse ForeignKey lookup on admin page, as read-only list?

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?

like image 665
Richard Avatar asked Apr 14 '14 21:04

Richard


1 Answers

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))
like image 60
johnboiles Avatar answered Nov 07 '22 12:11

johnboiles