Announcing Chipmunk Backup

I put up on Launchpad a backup utility I wrote called Chipmunk Backup. It's not extremely configurable nor does it have a huge set of features. It's a simple tool for maintaining a number of GnuPG encrypted full backups of a directory in a remote, rsync-accessible location.

There's no ready to download archive, but checking out lp:chipmunk-backup with bzr should give you a working version.

It's written in PLT Scheme and is known to work with version 4.1.4.

Emacs tips: Navigate CamelCase words

Emacs tip #0: Always search EmacsWiki when you think you might need something.

Emacs tip #1: To navigate studlyCapped words, M-x c-subword-mode, as found on the CamelCase page. I had to add the following lines to my .emacs to get it work with C-left/C-right, M-b/M-f worked right out of the box:

(define-key global-map [(control right)] 'forward-word)
(define-key global-map [(control left)] 'backward-word)

Before that, they were bound to the -nomark variants.

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.

© Juri Pakaste 2021