Search code examples
pythondjangodjango-mpttfeincms

Django custom admin action for FeinCMS actions column


I'm making an admin panel for a Django-Mptt tree structure using the FeinCMS TreeEditor interface. This interface provides an 'actions column' per-node for things like adding or moving nodes quickly without using the typical Django admin action select box.

Example inteface from Django/TreeEditor/MPTT app

What I am trying to do is add a custom admin action to this collection which passes the pk of the node to a celery task which will then add a collection of nodes as children. Existing functions are simply href links to the URL for that task(add/delete/move), so thus far I have simply mimicked this.

My solution currently involves:

  1. Define the action as a function on the model
  2. Create a view which uses this function and redirects back to the changelist
  3. Add this view to the admin URLs
  4. Super the TreeEditor actions column into the ModelAdmin class
  5. Add an action to the collection which calls this URL

Surely there must be a better method than this? It works, but it feels massively convoluted and un-DRY, and I'm sure it'll break in odd ways.

Unfortunately I'm only a month or two into working with Django so there's probably some obvious functions I could be using. I suspect that I might be able to do something with get_urls() and defining the function directly in the ModelAdmin, or use a codeblock within the injected HTML to call the function directly, though I'm not sure how and whether it's considered a better option.

Code: I've renamed everything to a simpler library <> books example to remove the unrelated functionality from the above example image.

models.py

class Library(models.Model):
    def get_books(self):
        # Celery task; file omitted for brevity
        get_books_in_library.delay(self.pk)

views.py

def get_books_in_library(request, library_id):
    this_library = Library.objects.get(pk=library_id)
    this_library.get_books_in_library()
    messages.add_message(request, messages.SUCCESS, 'Library "{0}" books requested.'.format(this_library.name))
    redirect_url = urlresolvers.reverse('admin:myapp_library_changelist')
    return HttpResponseRedirect(redirect_url)

urls.py

urlpatterns = [
    url(r'^admin/myapp/library/(?P<library_id>[0-9]+)/get_books/$', get_books_in_library, name='get books in library'),
    url(r'^admin/', include(admin.site.urls)),
]

admin.py

class LibraryAdmin(TreeEditor):
    model = Library
    def _actions_column(self, obj):
        actions = super(LibraryAdmin, self)._actions_column(obj)
        actions.insert(
            0, u'<a title="{0}" href="{1}/get_books"><img src="{2}admin/img/icon_addlink.gif" alt="{0}" /></a>'.format(
                _('Get Books'),
                obj.pk,
                settings.STATIC_URL
            )
        )
        return actions

Note that I may have broken something in renaming things and removing the extraneous cruft if you try to execute this code, I think it should adequately illustrate what I'm trying to do here however.


Solution

  • After digging around today and simply trying various other solutions, I've put together one that uses get_urls and a view defined directly into the admin interface which feels tidier though it's effectively just moving the code from multiple django files into the admin interface - though it does make use of the admin wrapper to stop unauthenticated users, which is an improvement.

    I'll leave a copy of the working code here for anyone who finds this in future, as I've seen very few examples of TreeEditor et al. being used in newer versions of Django.

    class NodeAdmin(TreeEditor):
        model = Node
        # < ... > Other details removed for brevity
        def get_urls(self):
            urls = super(NodeAdmin, self).get_urls()
            my_urls = [
                url(r'^(?P<node_id>[0-9]+)/get_suggestions/$', self.admin_site.admin_view(self.get_suggestions)),
            ]
            return my_urls + urls
    
        def get_suggestions(self, request, node_id):
            this_node = Node.objects.get(pk=node_id)
            get_suggestions(this_node.pk)
            messages.add_message(request, messages.SUCCESS, 'Requested suggestions for {0}'.format(this_node.term))
            redirect_url = urlresolvers.reverse('admin:trinket_node_changelist')
            return HttpResponseRedirect(redirect_url)
    
    
        def _actions_column(self, obj):
            actions = super(NodeAdmin, self)._actions_column(obj)
            # Adds an 'get suggestions' action to the Node editor using a search icon
            actions.insert(
                0, u'<a title="{0}" href="{1}/get_suggestions"><img src="{2}admin/img/selector-search.gif" alt="{0}" /></a>'.format(
                    _('Get Suggestions'),
                    obj.pk,
                    settings.STATIC_URL,
                )
            )
            # Adds an 'add child' action to the Node editor using a plus icon
            actions.insert(
                0, u'<a title="{0}" href="add/?{1}={2}"><img src="{3}admin/img/icon_addlink.gif" alt="{0}" /></a>'.format(
                    _('Add child'),
                    getattr(self.model._meta,'parent_attr', 'parent'),
                    obj.pk,
                    settings.STATIC_URL
                )
            )
            return actions