Inline admin forms with admin site links in Django

I have a somewhat difficult relationship with Django's admin site. It's a very useful feature, but I haven't really done enough with it to know when I'm going to hit a wall, if that wall's in the code or in my understanding, and how hard it's going to be to climb over the wall.

This time I wanted to have inline admin forms, except that I didn't actually want to have the forms there, I just wanted to have links to the objects — and not their views on the actual site, but on the admin site. As far as I can tell, there's no built-in support for this.

According to the admin docs, there are two subclasses of InlineModelAdmin: TabularInline and StackedInline. Looking at django/contrib/admin/options.py confirms this. And as the docs say, the only difference is the template they use. The stacked version comes pretty close when we add all the fields to an InlineModelAdmin subclass's exclude array, but it doesn't have the link.

To solve this we first create a new subclass:

class LinkedInline(admin.options.InlineModelAdmin):
    template = "admin/edit_inline/linked.html"

When you want to create inline links to a model, you subclass this new LinkedInline class. So to use a slightly contrived example, if we have a Flight with Passengers:

class PassengerInline(LinkedInline):
    model = models.Passenger
    extra = 0
    exclude = [ "name", "sex" ] # etc

class FlightAdmin(admin.ModelAdmin):
    inlines = [ PassengerInline ]

And yes, we have to exclude all the fields explicitly: an empty fields tuple or list is ignored.

The new template is easiest to create by cutting down aggressively the stacked template. Like this:

{% load i18n %}
<div class="inline-group">
  <h2>{{ inline_admin_formset.opts.verbose_name_plural|title}}</h2>
{{ inline_admin_formset.formset.management_form }}
{{ inline_admin_formset.formset.non_form_errors }}

{% for inline_admin_form in inline_admin_formset %}
<div class="inline-related {% if forloop.last %}last-related{% endif %}">
  <h3><b>{{ inline_admin_formset.opts.verbose_name|title }}:</b>&nbsp;{% if inline_admin_form.original %}{{ inline_admin_form.original }}{% else %} #{{ forloop.counter }}{% endif %}
    {% if inline_admin_formset.formset.can_delete and inline_admin_form.original %}<span class="delete">{{ inline_admin_form.deletion_field.field }} {{ inline_admin_form.deletion_field.label_tag }}</span>{% endif %}
  </h3>
  {{ inline_admin_form.pk_field.field }}
  {{ inline_admin_form.fk_field.field }}
</div>
{% endfor %}
</div>

The primary/foreign key fields are necessary to keep Django happy.

The result looks about right, it just lacks the links. It seems that Django doesn't give the template all the information we need to make them work: there's root_path that gives us /admin/, app_label contains the application's name and inline_admin_form.original.id contains the id of the inline object. What is lacking is the path component that names the model. I don't think it's available by default (is there a clean way to ask Django what's available in a template's context?), so we need to add it. Amend LinkedInline to look like this:

class LinkedInline(admin.options.InlineModelAdmin):
    template = "admin/edit_inline/linked.html"
    admin_model_path = None

    def __init__(self, *args):
        super(LinkedInline, self).__init__(*args)
        if self.admin_model_path is None:
            self.admin_model_path = self.model.__name__.lower()

Now inline_admin_formset.opts.admin_model_path will be bound to the lowercase name of the inline object's model, which is what the admin site uses in its paths.

With this, we can now replace the inline-related div in the template with this:

<div class="inline-related {% if forloop.last %}last-related{%  endif %}">
  <h3><b>{{ inline_admin_formset.opts.verbose_name|title  }}:</b>&nbsp;<a href="{{ root_path }}{{ app_label }}/{{ inline_admin_formset.opts.admin_model_path }}/{{ inline_admin_form.original.id }}/">{% if inline_admin_form.original %}{{ inline_admin_form.original }}{% else %} #{{ forloop.counter }}{% endif %}</a>
    {% if inline_admin_formset.formset.can_delete and inline_admin_form.original %}<span class="delete">{{ inline_admin_form.deletion_field.field }} {{ inline_admin_form.deletion_field.label_tag }}</span>{% endif %}
  </h3>
  {{ inline_admin_form.pk_field.field }}
  {{ inline_admin_form.fk_field.field }}
</div>

That's it. Now Flights get links to Passengers without big forms cluttering up the page.

iPhone shuffle

These days I use an iPhone as my mobile music device. I have a bit over 1800 songs on it. I usually use shuffle and had it stuck in a weird state a couple of weeks ago — it was constantly playing me just a few tracks. I usually listen for just half an hour to an hour at a time, so I don't know if it would have started looping or what, but those were basically always there for a week's worth of commutes. I finally restarted the phone and that seemed to help, but what do you know, a couple of weeks, several restarts and one operating system upgrade later, it's again playing me exactly the same tracks.

As much as I love the The Roots, honestly, at this points Adrenaline!'s "Once a-again, once a-gain..." start makes me mostly think "once again indeed."

Crashes with NSURLConnection

Speaking of Cocoa (and iPhone) programming, for a change.

Having trouble with spurious EXC_BAD_ACCESS crashes when using NSURLConnection? NSZombie giving you not very clear messages about [Not A Type retain], pointing to an address that malloc_history says has been allocated somewhere with only framework code in the call stack? See Amro Mousa's blog entry about the subject.

In a nutshell, don't send the start message to a NSURLConnection object you've initialized with a +connectionWithRequest:delegate: or -initWithRequest:delegate:. It'll break stuff.

Apple, how about a warning about this in the docs? The docs for -start say "Causes the receiver to begin loading data, if it has not already.", not "Will break your program and make you waste uncounted hours debugging if receiver has already started." Or how about preventing this in the code? Is there some scenario where you'd want to call start after the object has already started?

© Juri Pakaste 2021