Mimsy Were the Borogoves

Hacks: Articles about programming in Python, Perl, Swift, BASIC, and whatever else I happen to feel like hacking at.

Django and Taskpaper

Jerry Stratton, November 2, 2007

I’ve been trying out Taskpaper for the last week, and it’s pretty cool. It’s very simple and easy to work with. It adds a bit of structure to the way I’ve been managing my tasks with Smultron: just a couple of headings and numbered tasks. The nice thing about Taskpaper is that it takes that style of project management and runs with it. So I’m still just typing out headlines and marking tasks beneath them, but now I get to hit a little radio button to draw a line through the task when I’ve completed it. I can tag the tasks and, when I feel like doing a little programming, pull down all of the programming tasks throughout my project list.

It sounds silly, but I think I’ve gotten more done this last week than the month before that.

So I think I’ll be buying Taskpaper. I have three main concerns, but judging from what the author (Jesse Grosjean) has said on the Taskpaper forums, they’re going to be addressed. I’d like the system to keep track of when a task is completed, I’d like it to be scriptable, and notes need to be attached to their task. Jesse already has a means of keeping track of completion dates planned out. Tags can be followed by parenthetical information. Archived tasks, for example, are “@project(project-name)”. It looks like done items will be “@done(date-completed)”. Tags can have parenthetical information… remember that, there’s going to be a test later.

One of the things I finally got done this last week was moving another of my FileMaker 6 databases to SQLite via Django. This database is a database of recurring tasks, probably my most-used database. One of the things that keeps me from getting those recurring tasks done is that I have to remember to pull up the database. Moving it to SQLite makes it easier to write a reminder script, but I still had no idea what the reminder script was going to actually do. E-mail was the obvious choice, but I hate it when reminder e-mails build up in my inbox.

So I thought, why not combine it with Taskpaper? Taskpaper can remember multiple windows and open them automatically. And it has a simple text format that would be easy to use with Django’s template language. So, under database conversion, I added:

  • autogenerate recurring task file by day for TaskPaper
  • check for completed tasks before rewriting task file

Autogenerate recurring task file by day for TaskPaper

This part was simple. If you’ve used Django before, you probably already know how simple. I already had the model, so I made the template. A Taskpaper file is just a collection of projects and tasks. A project is any line that ends with a colon; a task is any line that begins with a dash and a space. Tags are handled by putting a space and an @ symbol in front of a word.

The template

  • {% for task in tasks %}{% ifchanged %}{{ task.dueDate|date:"l, F jS" }}:
  • {% endifchanged %} - {{ task.title }}{% if task.category %} @{{ task.category.key|slugify }}{% endif %}
  • {% endfor %}

Because a Taskpaper file is basically a straight text file, white space does matter for display purposes, which makes the template a little uglier than its HTML counterpart would be. But the first line creates the headline, one for each day (“ifchanged” means, only output this part of the template if it has changed since the last iteration).

The second line creates the task, and adds a category tag to the end. (The third line just ends the “for” loop.)

I saved this in my Django templates folder, as “personal/taskpaper.html”.

The script

The script is also fairly simple. If you’ve used Python you should be able to follow it pretty easily.

  • #!/usr/bin/env python
  • import os, sys, datetime
  • #set up the environment
  • DOCS = os.path.join(os.environ['HOME'], 'Documents')
  • sys.path.append(os.path.join(DOCS, 'CMS'))
  • os.environ['DJANGO_SETTINGS_MODULE'] = 'CMS.settings'
  • #import necessary Django libraries
  • from django.template import Template, Context
  • from django.template.loader import get_template
  • #import the recurrers model; each Item is a recurring task
  • from CMS.recurrers.models import Item
  • #rewrite the task file from currently due tasks
  • taskfile = os.path.join(DOCS, 'Tasks', 'Recurrers.taskpaper')
  • #all items due on or before today
  • tasks = Item.objects.filter(dueDate__lte=datetime.date.today()).order_by('dueDate', 'rank')
  • #render the template
  • rcontext = Context()
  • rcontext['tasks'] = tasks
  • rtemplate = get_template('personal/taskpaper.html')
  • #and write it out to the task file
  • newtasks = open(taskfile, 'w')
  • newtasks.write(rtemplate.render(rcontext))
  • newtasks.close()

That’s really it. Half of the script is setting up the connection to Django. Then there’s one line to get the tasks, three lines to prepare the template, and four lines to write it out to the task file.

check for completed tasks before rewriting task file

This makes it easier for me to remember to check my recurring tasks. But I’d still have to go into the database to mark them as having been completed so that they stop appearing in my task file and get marked for their next recurrence. It would be nice, since Taskpaper has a means of marking an item as done, if the script could pay attention to that for me.

In order to do that, I need to match a task back to its record. Each of the records has a unique ID. Tags can hold information. So if I can add an ID tag to each task I can put the actual ID in it. Something like “@ID(97)”.

Add “ @ID()” right after “”. Don’t leave the space out in front of the @ symbol. The template should now be these three lines:

  • {% for task in tasks %}{% ifchanged %}{{ task.dueDate|date:"l, F jS" }}:
  • {% endifchanged %}- {{ task.title }} @ID({{ task.id }}){% if task.category %} @{{ task.category.key|slugify }}{% endif %}
  • {% endfor %}

In order to grab the ID out of the task, I’m going to need to use a regular expression, so I add “re” to the “import” line:

  • import os, sys, datetime, re

And, before writing out the tasks, I check to see if a task file already exists. After the “taskfile =” line, and before the “tasks =” line, I add:

[toggle code]

  • #if the task file is already there, check it for done items
  • if os.path.exists(taskfile):
    • #eventually use @done(date) rather than when the task file was last saved
    • taskdate = datetime.date.fromtimestamp(os.stat(taskfile).st_mtime)
    • oldtasks = open(taskfile)
    • for task in oldtasks:
      • #tasks begin with a dash and a space
      • #done tasks contain @done text
      • if task.startswith(' - ') and task.find(' @done') > 0:
        • #get the task ID
        • match = re.search('@ID\(([1-9][0-9]*)\)', task)
        • if match:
          • taskID = match.group(1)
          • doneTask = Item.objects.get(pk=taskID)
          • #don't mark it as done if it has already been rescheduled
          • if doneTask.dueDate <= taskdate:
            • doneTask.reschedule(completionDate=taskdate)
    • oldtasks.close()

The only odd thing here is that I’m using the task file’s modification date as the date that the task was completed. Hopefully, the @done tag will contain the completion date automatically in a later version of Taskpaper.

So this section of the script:

  1. goes through each line
  2. recognizes a task as a line starting with a dash and a space
  3. recognizes a completed task as a line containing “ @done”
  4. grabs the task’s SQLite ID number from the @ID tag in the line, via a regular expression
  5. picks the item by ID from the SQLite database via a Django call
  6. checks that the item hasn’t been updated already from somewhere else
  7. and then calls the item’s reschedule method to reschedule it for its next recurrence

Which is pretty cool. The final step is to add this script to my crontab file and have it run a few times every day so that when I open Taskpaper, any recurring tasks are waiting for me.

March 10 2010: Updated the Django template file to reflect the new Taskpaper format. It meant adding a tab in front of the dash for each day’s tasks.

  1. <- Selected iTunes Playlist
  2. ELinks text browser ->