Peter · of · the · Norse · Thinks · He’s · Smarter · than · You


He Might be Right

* * *

Yesterday was my birthday (I lied to Facebook), and that’s caused me to think about what the last year has entailed. I’d like you to humor me as I look back at what the past year of my life.


A typical day

7:00
Wake up, Breakfast, shower, etc.
8:00
Drive to work
8:30
Work
1:00
Lunch
2:00
More work
5:30
Drive home
6:00
Fix/eat dinner
7:00
Clean up the house
7:15
Struck with the sudden feeling that by throwing away and packing up my mother’s personal possessions, I’m intentionally forgetting her.
7:16
Eat junk food and watch television
1:00
Go to bed

I’m not sure what the cause is, but I’m suddenly nostalgic. Perhaps it’s a side effect of being in the mid-30s. I was seriously considering buying Now That’s What I Call Music of the 90s. If it weren't for Ace of Base being on it, I might have.

Hell, my company is moving to a new office a few blocks from my old high school, and when I passed it last weekend I wasn’t filled with rage at my time there. First time in 20 years.

Tags:
* * *
I just heard one of my favorite bands in a commercial. Should I be happy that they’re getting money, or annoyed that the song was so butchered? It could be worse. Fans of Phillip Phillips must be pulling their hair out.
Tags:
* * *

I was really looking forward to Tour de Fat this year. I even bought a glittering green cape for it. Ran into Aaron during the parade.

When I came back in the afternoon, I saw Aaron, but avoided him. I knew he was going to ask what happened to the cape. Why would you buy a glittering green cape and not wear it? I also knew the answer was “Things have changed since this morning”.

This is a problem. When things get tough like this, I retreat. I know I’m going to retreat even further in the coming days. Don’t let me.

Tags:
* * *
I was passed by someone on a fixie.
Tags:
* * *

My major Django project involves mixing Django urls with static. That is, I want /blog/ and /djpro/ to be handled by the same Python dæmon, but / to be the static file /html/index.html.

Turns out that’s not something you can do out of the box. mod_wsgi splits the url when you use WSGIScriptAlias and Django treats it as if you did an include in your URL. You can merge them together in your wsgi.py

from django.core.wsgi import get_wsgi_application
_application = get_wsgi_application()

# I found a thing at http://code.google.com/p/modwsgi/wiki/IntegrationWithDjango
# that merges the PATH_INFO & SCRIPT_NAME.
def application(environ, start_response):
    environ['PATH_INFO'] = environ['SCRIPT_NAME'] + environ['PATH_INFO']
    environ['SCRIPT_NAME'] = '/'
    return _application(environ, start_response)

Then, all you need is to do is use multiple WSGIScriptAliases on the same WSGIDaemonProcess.

Tags:
* * *

The last time I talked about tags in Django, there was only one game in town: Django-tagging. Now there is Django-taggit. It does (almost) everything right: A relation manager is used, which shows up in forms and the admin site. A slug is included. And it prefers commas to spaces as a delimiter. However, it wants to be user compatible with Django-tagging, so spaces can be used. Fortunately, that’s a simple problem to fix. I replaced the parse_tags and edit_string_for_tags functions, and it works great. Also, it’s case sensitive. I replaced the appropriate code so that it matches on the slug instead of the name. This has the added benefit of knowing that ‘hip hop’ and ‘hip-hop’ are the same thing.

You might not want to go that far, but if you’re using tags in Django, you should take a look at Django-taggit. It’s tagging done right.

Tags:
* * *

Sometime something happens that we can’t see because the effected part is scrolled off screen. Most of the time it doesn’t happen because the button to trigger a DOM change is part of the DOM element. But sometimes something in another window or frame will change the main view. I first noticed the problem when I was testing adding a new line at the end of the playlist. The new div was added below the bottom of the window, so I couldn’t see it. The answer is to scroll the window to show the element. There are auto-scroll jQuery plugins out there, but they are designed to replace anchor links. I would make my own plugin, but my requirements are orthogonal to a typical use case.

The first step is to find the where the top and bottom of the line we want to see are.

jQuery.fn.showLine = function() {
    var line_top = this.offset().top;
    var line_bottom = line_top + this.outerHeight() + 18;

The 18px added to the bottom is for the height of the insert link. It overlaps the next line a little bit. I also add for the height of the postition: fixed header.

    var win_top = $(window).scrollTop() + 45;
    var win_bottom = win_top + $(window).height();

    if (line_top < win_top) {
        var new_top = line_top - 54;
    } else if (line_bottom > win_bottom) {
        var new_top = line_bottom - $(window).height() + 9;
    } else {
        return;
    }

If any part of the line is scrolled off the top or bottom, then we find the nearest scroll amount that will show the whole thing, plus a little bit for padding. Finally, we animate the scroll.

    $('body, html').animate({scrollTop: new_top}, 400);
};

I use $(window) to find the value, because it always works. But it doesn’t animate. So I have to use both body and html because different browsers work differently. One will respond, and the other will simply ignore scrollTop.

Watch for my next post where I rave about django-taggit.

* * *

Most calendar programs today accept URLs to iCalendar files to keep up to date on something. That’s how I get Radio 1190’s concerts. There are two ways to build such a thing in Django. You can either use a template like normal, and just set the content type to text/calendar. Or you can use vObject. I’m going with the first, because I’m just publishing a list of concerts. If there was any kind of feedback or incoming data, I would need vObject. In either case, make the URL end in .ics.

There are two good validators. http://arnout.engelen.eu/icalendar-validator/ is standards compliant. If there is a single thing off, it will let you know and point to the part of the spec you’re violating. http://icalvalid.cloudapp.net/ only cares about whether it’s understood. Since user agents don’t follow the the spec precisely, you need to find those problems too. Use both. While they both find major mistakes, there are errors that only show up in one or the other.

I’m going to go though the template and tech you what each line does and how it is understood.

{% autoescape off %} Because this isn’t HTML
BEGIN:VCALENDAR Required
VERSION:2.0
METHOD:PUBLISH
PRODID:Peter of the Norse wrote this though Django
You need a PRODID. Don’t use someone else’s.
X-WR-CALNAME:Radio 1190 Concerts A suggested name for the new calendar.
CALSCALE:GREGORIAN
X-WR-TIMEZONE:America/Denver
Since I only use one time zone, this acts as a hint for some user agents to speed things up.
BEGIN:VTIMEZONE This is a copy/paste of the time zone information. While required, it’s not actually used.
TZID:America/Denver You need to use a recognized time zone name. Some user agents don’t parse time zones and will fail if they don’t recognize it. Others don’t parse time zones when they know the name. Others ignore all time zone information when they see X-WR-TIMEZONE. And a small number actually use the VTIMEZONE.
BEGIN:DAYLIGHT
TZOFFSETFROM:-0700
RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU
DTSTART:20070311T020000
TZNAME:MDT
TZOFFSETTO:-0600
END:DAYLIGHT
BEGIN:STANDARD
TZOFFSETFROM:-0600
RRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU
DTSTART:20071104T020000
TZNAME:MST
TZOFFSETTO:-0700
END:STANDARD
END:VTIMEZONE
{% for event in list %}BEGIN:VEVENT
Now we get into some actual code.
UID:Radio1190-concert-{{event.id}} Each event needs a unique id. Life’s too short to develop a UUID.
ORGANIZER:http://www.Radio1190.org/
{% if event.time %}DTSTART;TZID=America/Denver:{{event.date|date:"Ymd"}}T{{event.time|time:"Hi"}}00
Some events have times; others don’t.
DURATION:PT2H We only know the start time. Two hours shorter than most concerts, but if we did four or five, every concert would overlap. I might even change it to one hour.
{% else %}DTSTART;VALUE=DATE:{{event.date|date:"Ymd"}}
DURATION:P1D
{% endif %}DTSTAMP:{{event.date|date:"Ymd"}}T000000Z
This should be the last modified date, but we don’t keep that kind of information.
LOCATION:{{event.venue|escape}} I wrote a filter to escape newlines, semicolons, and commas. They have special meaning to ics files.
SUMMARY:{{ event.performer_set.all|join:"—"|escape }}
DESCRIPTION:{% if event.we_present %}Radio 1190 presents!\n{% endif %}{% if event.minimum_age %}{{event.minimum_age|escape}}\n{% endif %}{{event.info|escape}}
{% if event.site %}URL;VALUE=URI:{{event.site|escape}}
{% else %}{% if event.venue.homepage %}URL;VALUE=URI:{{event.venue.homepage|escape}}
{% endif %}{% endif %}END:VEVENT
{% endfor %}END:VCALENDAR{% endautoescape %}

All the rest is more of the same. Since blank lines break some user agents, there are a lot of run-on code lines. It makes the code a mess. Run your results against the validators again and again. You will make mistakes more than once, even when you’re working off of someone else’s example.

Tags:
* * *

At Radio 1190, one of the things we do is print an insert into each of the CD cases that has the information we use. In addition to a brief review, there’s also extra info about the songs, like the length and tempo. This insert has to be 12cm by 12cm to fit in the case. While CSS has a cm measurement, no browser respects it.

First, I should point out that the standard unit of measurement for fonts on the computer, the point, is a real world thing. There are 72 points in an inch, just like there are 12 inches in a foot. So according to that 72pt = 1in = 2.54cm = 25.4mm. But look at what happens when you try to put that in a browser:

MMMM

Depending on the browser you’re using, the Ms might be different sizes. For even more fun, try printing them. I would set things up to be perfect on one browser, and they’d be slightly off on another. It was really annoying when it would be lined up perfect on the screen, and have gaps on paper. And don’t forget that some browsers fill to fit the page.

There’s only one thing for it: I’ll have to use PDF.

Since I’m using Python, I’ll have to use ReportLab. It is the most schizophrenic API for python I’ve ever seen. In fact the only thing I’ve seen that’s worse is PostScript. Which probably explains things. I suspect that ReportLab is tightly fitted around PDF. But these problems do make it difficult to work with.

I’m going to talk about only two parts of ReportLab: paragraphs and tables. Both are, in ReportLab’s terminology, “Platypus Flowables”. For the details of what that means, see the official documentation. Note that the official documentation starts with Canvas, which is the low level implementation. That’s how you program, but not how you learn. So I’m only going use the high level stuff.


Paragraphs are objects that know how to break lines. They also support a simple subset of XHTML so that you can do some styling. The signature is Paragraph(text, style, bulletText=None). Ignoring bulletText, it should be obvious how it works. The only gotcha is, style has to be a ParagraphStyle object. I don’t think there’s a difference between ParagraphStyle and a dict, so I’m forced to conclude that there’s some PDF equivalent that needs to be encoded. To add needless complexity, ParagraphStyle requires a name attribute that is only used by StyleSheet1s. That extra 1 is not a typo. It is an example of what’s wrong with this API. A StyleSheet1 is a dict that only accepts ParagraphStyle and uses its name as the key. I decided that normal dicts are good enough for me.

    styles = {'normal': ParagraphStyle(name='normal', 
                        fontName='Times-Roman', 
                        fontSize=0.5*cm, 
                        leading=0.4*cm),
              'small': ParagraphStyle(name='small', 
                       fontName='Times-Roman', 
                       fontSize=0.3*cm, 
                       leading=0.3*cm),
              'review': ParagraphStyle(name='review', 
                        fontName='Times-Roman', 
                        fontSize=10, 
                        leading=10.2),
              'song': ParagraphStyle(name='song', 
                      fontName='Times-Roman', 
                      fontSize=0.5*cm, 
                      alignment=1)}

Note that alignment is 1. That means center. There’s a constant somewhere, but I didn’t want to dig for it.

Assuming that obj is an Album model, creating the content of the table cells is simple.

    album = Paragraph(escape(obj.album), styles['normal'])
    artist = Paragraph(escape(obj.artist.artist), styles['normal'])

Now things get interesting. The artist and album need to fit in a table cell. We know the exact dimensions of that cell because we specified it. (It’s the whole reason we went to PDF in the first place.) We can find the dimensions by calling wrap with the available width. wrap is usually called by the surrounding frame, but we can do it ahead of time to see what happens.

    artist.wrap(5.8*cm, 1*cm)
    if artist.minWidth() > 5.8*cm or len(artist.getActualLineWidths0()) > 2:
        artist = Paragraph(artist.text, styles['small'])

minWidth is calculated from the longest word in this font. There are artists and albums that contain a run-on word. We also know that the cell height only allows for two lines. So if there’s more, that’s a problem. We switch to a smaller font. You can’t change the style after the wrap, and you can’t rewrap, so we need to create a new one.

The rest of the text areas are much the same, except for the review. Since it can have bold and italic and carriage returns, we can’t escape it. We’ll have to make sure that only the supported tags are used. It’s a little bit long and obvious so I won’t quote it.


Table and TableStyle work to make tables. While there have to be differences because it’s for tables and not paragraphs, it seems entirely unrelated. Written by an different group of people. Who should have their computers taken away. First, TableStyle is unnecessary. Any place you can use a TableStyle, you can use a list of tuples. Second, everything is done with lists of tuples. But not just any tuples. Specially formatted tuples. With sub-tuples.

Let’s look at the constructor. Table(data, colWidths=None, rowHeights=None, style=None, splitByRow=1, repeatRows=0, repeatCols=0) data is a list of equal length rows. Everything in a row has to be a Flowable (including Paragraphs or other Tables) or a string. There is a way to make data span cells, sub-tables are simpler. And strings don’t wrap, so unless it’s a single word ore phrase, you’ll probably want a paragraph. While colWidth and rowHeights determine the sizes of the cells. If you leave them blank, ReportLab will make educated guesses. The last three raise NotImplementedError when changed.

table styles are built up from “commands”. Commands are tuples of the form (property, (starting_column, starting_row), (ending_column, ending_row), value, ...).

Properties are always strings in capitals, and the value or values depend on the property. A negative column or row means from the end like slices. For example, ('ALIGN', (0,0), (-1,-1), 'CENTER') makes everything center aligned. Notice how the value is a string while in the paragraphs it’s an int. Also, every command has the start and end cells. Since most cells get multiple properties at once, it’s more than a little bit redundant.</p>

Actually creating the table is easier than with HTML. The cell sizes are absolute and borders and padding don’t effect them. It makes precise layouts much easier. Which is good for our purposes.


The whole thing sucks. I want a replacement, but don’t have the time or skills to make it myself. If you know of a free PDF generator, I’d like to see it.

* * *
I can’t wait for everything to move to Python 3.
Tags:
* * *