Lesetagebuch 5

Die neue Lesetagebuch-Version wurde letzte Nacht auf den Server geschubst.

Weise Programmierer sagen immer »Was auch immer du machst, mach auf keinen Fall einen kompletten Rewrite«, und darum bin ich Anfang des Jahres losgezogen um einen kompletten Rewrite zu machen. Wie ein echter Profi.

In den bisherigen Versionen hatte ich meine Datenbankanbindung selbst geschrieben. Das bewegte sich zwischen »Alle Objekte sind Dictionaries und enthalten nur, was die Datenbank enthält« und »Alle Objekte haben eigene Klassen, die auf verschiedene Weisen mit der Datenbank sprechen, bis auf die ›Später Lesen‹-Einträge, weil ich dafür zu faul bin«.

Natürlich war das kein dauerhaft haltbarer Zustand. Darum fing ich an, die Seite komplett neu zu schreiben, diesmal nicht nur auf Basis von Flask und Jinja2, sondern mit Hilfe von Peewee und WTForms. Peewee ist ein wunderbarer, leichter ORM, der subclassing sehr einfach macht, und WTForms geht mir manchmal auf die Nerven, erspart es mir aber, Formularvalidierung selber schreiben zu müssen.

»Und warum hat das dann so lange gedauert?«, fragt ihr euch jetzt vielleicht, »Du bekamst doch quasi alles geschenkt?«. Tja. John Lennon hat es ja schon gesagt: Das Leben ist, was passiert, während man versucht, einen Rewrite einer Bücherleseplattform zu schreiben.

Fix your life (and your URLs to static files in your Jinja templates)

Whenever you want to link to a static file in Jinja templates, you probably use url_for('static', filename=your_file). That’s great, but it may cause problems with your users, who may keep using a cached version of your kickass CSS file. But it doesn’t have to be that way. Cachebust your URLs for maximum convenience!

import app

app.create_jinja_environment()

def url_for_buster(endpoint, **values):
    if endpoint == 'static':
        filename = values.get('filename', None)
        if filename:
            file_path = os.path.join(app.root_path,
                                     endpoint, filename)
            values['q'] = int(os.stat(file_path).st_mtime)
    return url_for(endpoint, **values)

app.jinja_env.globals.update(url_for=url_for_buster)

Daniel, what are you doing?

Great question! I’m overwriting Jinja’s regular url_for function with a custom function that adds a parameter q to all URLs that link to the static endpoint, but that doesn’t change other URLs. The value of q is the timestamp of the linked file. That way, whenever you update the file, the parameter is changed and the browser thinks it’s a different file.

So, instead of linking /static/style.css, you’ll link to /static/style.css?q=1407762775. Nice.

R: [Irgendwas über Mozart und Wien]. Mozart wohnte doch mal in Wien?

D: Zumindest behauptet Falco das.

R: Wer ist Falco?

D: Das ist dieser Sänger? Der »Rock Me Amadeus« gesungen hat? Falco? Der Österreicher? Falco? Der King of Pop? Falco?

R: Der mit der Sonnenbrille und der roten Jacke?

D: DAS IST HEINO. WAS IST FALSCH MIT DIR.

Widowfix Filter for Jinja2

So, if you are anything like me (or Max), you hate it when the last word of a paragraph is on its own. Mostly, you don’t really have a choice. But with the following Flask snippet, you do!

It doesn’t really matter where you put it. In most cases, I’ll have a utils.py file which contains these kind of things, but because you directly register the filter with the app object, the location is not important.

from yourapp.app import app
from jinja2 import Markup

def widowfix(value):
    value = escape(value)
    splitted = value.rsplit(' ', 1)
    return Markup(' '.join(splitted))

app.jinja_env.filters['widowfix'] = widowfix

What exactly happens here?

In the first line, we import our app. This is simple.

Then, we import Markup. It takes a string as a parameter and returns a Markup object that will be treated as “safe” by Jinja. This is important, because we don’t want the   to be escaped.

We create the widowfix function. It takes a value, escapes it, splits it at the last space and puts it back together, with a   as the glue.

Finally, we register that function as a new Jinja filter with the name widowfix.

To use it in your templates, simply write

{{ my_text|widowfix }}

The last space in the string will be replaced by a non-breaking space, so the last two words will always be right next to each other. No more widows!

Update

I got a tumblr message reminding me that it may be unsafe to just not escape the whole variable. (S/he actually called it “a security nightmare”. I don’t know about that, but at least it’s dramatic.)

Yes, you probably want to escape the variable before adding the &nbsp, so I added value = escape(value) to the function. Thanks, anon!

Aktuelles Python und zumindest akzeptables SQLite auf Uberspace verwenden

Ist das hier jetzt ein Programmierblog? Unfollow!

Die Arbeit an der neuen Lesetagebuch-Version geht gut voran. Ich habe eine Volltextsuche eingebaut (endlich!) und nutze dafür die coolen FTS3/4-Funktionen, die SQLite mitliefert und für die Peewee gute Anbindungen bietet.

Gestern Abend dachte ich, dass ich den bisherigen Code mal auf dem Uberspace, auf dem auch das aktuelle Lesetagebuch läuft, installieren sollte, damit ich sehen kann, ob das so klappt, und damit ich ein paar Testlinks an Freunde rausschicken kann.

Uberspace ist hervorragend, aber nicht immer cutting edge. Das ist okay, da sie sich ja nicht den ganzen Tag mit Updates und damit verbundenen Problemen rumschlagen wollen. Aber ich hätte halt schon gerne Python 3.4, und nicht nur 3.3. (Ich habe nicht mal einen guten Grund! Einfach aus Prinzip!)

Glücklicherweise geht das. Man kompiliert einfach alles selbst! Yolo!

(Die Anleitung folgt am Ende des Artikels, denn dann müsst ihr es nicht, wie ich, zweimal machen.)

Das Lesetagebuch lief dann auch irgendwann, und ich versuchte, etwas zu suchen. Dabei reichte mir Peewee einen SQLite-Error durch (Die Funktion matchinfo() konnte nicht gefunden werden), der daher rührte, dass die Standard-SQLite-Version bei Uberspace 3.6.20 ist, und diese Funktion tatsächlich einfach nicht mitbringt.

Das Uberspace-Wiki versteckt auf der Seite über Ruby die Information, dass in /package/host/localhost/sqlite-3 eine neuere Version bereitgestellt wird. (Und zwar 3.7.15.2. Immerhin!) Yay!

Ein paar Stunden googlen später fand ich bei StackOverflow (wo sonst!) die Informationen, die ich gesucht hatte: Wie man Python sagt, welches SQLite verwendet werden soll.

Dafür muss man zum Glück nur vier Environment-Variablen setzen:

$: export PATH=/package/host/localhost/sqlite-3.7.15.2/bin:$PATH
$: export LDFLAGS='-L/package/host/localhost/sqlite-3.7.15.2/lib'
$: export CPPFLAGS='-I/package/host/localhost/sqlite-3.7.15.2/include'
$: export LD_LIBRARY_PATH=/package/host/localhost/sqlite-3.7.15.2/lib:$LD_LIBRARY_PATH

Damit sagt ihr dem Python, das ihr gleich installieren werdet, wo es zuerst nach Libraries schauen soll, die es benutzt (zumindest, soweit ich das verstanden habe). Dadurch findet es zuerst die neue SQLite-Installation.

Jetzt zum eigentlichen Spaß: Python installieren. Sucht auf der Downloadseite für den Python-Sourcecode nach dem Release, das ihr haben wollt (im folgenden 3.4.1).

Dann laden wir das mal runter, entpacken es, gehen in den Ordner, werfen die Konfiguration an, kompilieren und installieren es:

$: wget https://www.python.org/ftp/python/3.4.1/Python-3.4.1.tgz
$: tar -xzf Python-3.4.1.tgz
$: cd Python-3.4.1
$: ./configure --prefix=/home/lstgbch (warten)
$: make (lange warten)
$: make install

Das --prefix=/home/lstgbch ist wichtig, damit es nicht versucht, Python global zu installieren (was im Uberspace natürlich nicht geht), sondern in euer Homeverzeichnis rein.

Jetzt könnt ihr python3.4 in die Befehlszeile eingeben, und dann kurz in der Python-Shell testen, ob alles so ist, wie wir es haben wollen:

Python 3.4.1 (default, Jul 22 2014, 16:37:47)
> > > import sqlite3
> > > sqlite3.sqlite_version
'3.7.15.2'

Falls eure Ausgabe nicht 3.7.15.2 sagt, hat irgendwas nicht geklappt. Versucht es einfach noch ein paarmal, ich habe Python bestimmt sechsmal kompiliert, bis es geklappt hat. Diese Anleitung ist insofern auch vor allem für mich selbst, damit ich beim nächsten Mal ein bisschen schneller bin.

Update

Okay, scheinbar sind die Informationen oben nicht absolut korrekt. Ich bin nicht mal mehr sicher, ob es wirklich einen Unterschied macht, dass man die LDFLAGS und die CPPFLAGS setzt. Denn, wie gesagt, das tat ich, und gerade startete ich eine neue Session und Python wollte wieder das alte SQLite benutzen.

Was ich jetzt aber weiß: Es reicht scheinbar aus, den LD_LIBRARY_PATH so zu setzen, wie oben beschrieben. (Ich bin nicht sicher, ob das nur funktioniert, wenn Python wie beschrieben kompiliert wurde, oder ob es allgemein funktionieren würde.)

Jedenfalls solltet ihr das, der Einfachheit halber, in eure .bash_profile packen:

$: export PATH=/package/host/localhost/sqlite-3.7.15.2/bin:$PATH:$HOME/bin
$: export LD_LIBRARY_PATH=/package/host/localhost/sqlite-3.7.15.2/lib:$LD_LIBRARY_PATH