Nutzerschaftsbeschimpfung

Ranty McRant hier, vielleicht auch die einzige Person, die gerade die Ohrfeige nicht mitbekommen hat, die der Chief Product Office von Netflix in Sachen Offline-Modus für Videos ausgeteilt haben soll:

Undoubtedly it adds considerable complexity to your life with Amazon Prime – you have to remember that you want to download this thing. It’s not going to be instant, you have to have the right storage on your device, you have to manage it, and I’m just not sure people are actually that compelled to do that, and that it’s worth providing that level of complexity.

In diesem Fall soll es Menschen, die smarte Telephone bedienen, durchaus auch Verantwortung in ihren Jobs übernehmen oder auch ansonsten in der Lage sind, sich Dinge zu überlegen und merken von der Komplexität einer einfach Option zum Download überfordert sein. Sollte die Aussage stimmen, muss erwidert werden, dass diese Annahme tatsächlich nur von einem Unternehmen kommen kann, dass seinen durchaus ordentlichen Inhalt in der unsäglichsten Anwendungsoberfläche nach der Erfindung der Ananas präsentiert. Also durchaus schlüssig, dass man Menschen lieber für dumm hält, als an die eigenen Designfähigkeiten zu glauben. Es ist aber ohnehin zu befürchten, dass dort nur ungeschickt vertragliche Rahmenbedingungen kaschiert werden sollen, die einen Offline-Modus verbieten. Aber dann muss es ja nicht gleich in Publikumsbeschimpfung ausarten.

Codecademy Hour of Code: Minute des Grauens

Hinter der Idee, das Programmieren möglichst einer breiten Masse zu erklären und damit auch noch möglichst früh anzufangen, stehe ich uneingeschränkt. So war ich von Codecademy, wie von vielen webbasierten Lernplattformen, sehr angetan. Gerade niedrigschwellige Kurse zu Grundlagen bekannter Programmiersprachen sind in vielen Fällen heute recht gut umsetzbar und können Hemmschwellen überwinden helfen. Wenn sie wie Codecademy zudem kostenlos sind, reißen sie einige Hürden sogar regelrecht ein.

Aber gerade bei Codecademy fehlt mir nach langer Zeit dort eine stringente didaktische Linie. Ihre Kurse sind meist fahrige Unterrichtseinheiten, die keiner erkennbaren Qualitätskontrolle unterliegen. Dabei kommt dann gerade der erwünschte Lerneffekt zu kurz, wenn Aufgaben irreführend oder nicht lösbar sind. Das Team hinter Codecademy wirbt, ganz von der eigenen Stärke überzeugt, in den höchsten Tönen vom eigenen Angebot, wirklich nachvollziehen kann ich das aber nicht. Zu oft bin ich auf Codecademy mittlerweile in schlecht geschriebene Kurse und andere Sackgassen gerannt, nur um dann zu erfahren, dass diese von ihren Entwicklerinnen und Entwicklern aus der Community verlassen wurden. Codecademy selbst hält sich dann meist zurück. Das können sie gerne machen, es ist ihr Dienst, ihr kostenloses Angebot, doch sehr im Einklang mit dem eigenen Anspruch erscheint es nicht.

Mein Eindruck ist also der einer gewissen Fahrigkeit, vielleicht sogar auch Gleichgültigkeit seitens des Dienstes. Neuestes Indiz ist die kürzlich im App Store aufgeschlagene Anwendung *Codecademy: Hour of Code". Hier kulminiert das Auseinanderklaffen von eigenem Anspruch und tatsächlicher Leistung in einer winzigen App, deren didaktischer Wert sich mir nicht erschließt.

Codecademy schließt sich dem Projekt der Hour of Code an, das sich an absolute Anfängerinnen und Anfänger richtet. Das ist allemal ein ehrenwertes Ziel, da kann ich verschmerzen, dass die App eigentlich nur ein aufgehübschter Multiple-Choice-Test in fingierter Editorumgebung ist. Ein wenig statische Eingabe kann vielleicht die Furcht nehmen, ohne dann mit absoluter Freiheit zu verwirren. Was aber nicht geht, sind die vielen Ungereimtheiten und Achtlosigkeiten, die Codecademy in der App an den Tag legt. Da soll ich dann mein Alter eingeben, kann aber genauso auch beliebige Strings eingeben. Die App feiert mein Alter von "jj" Jahren trotzdem als bahnbrechenden Erfolg in meinem kometenhaften Aufstieg als Programmierer.

Diese Unachtsamkeit häufen sich in dem Kurs voll simpler Aufgaben, die wenig einsteigerfreundlich vor die Füße geworfen werden. Wirkliche Erklärungen, was dort mit meinem Dummy-Code passiert, erhalte ich nicht. Aber das wäre anscheinend auch zuviel verlangt, denn Codecademy will mich anscheinend nicht einmal damit verwirren, mir mitzuteilen, welche Sprache sie mir hier beibringen. Es passt für mich ins Bild, wenn hinter dieser App mehr der Wunsch nach Wahrnehmung der eigenen Marke als das tatsächliche Interesse an durchdachten Lernkonzepten steckt. Für mich ist das enttäuschend, denn es droht doch, dass damit dann doch manche Leute eher abgeschreckt werden. Denn sie können nicht wissen, wie wenig ihnen die App tatsächlich erklärt, so könnten sie doch die eigene Verwirrung über die App so auslegen, als wäre das alles einfach eben doch nichts für sie.

Pythonista: Zitatesammlung

Als ich morgens in der Ringbahn durch den App Store klickte, fielen mir einige Apps auf, die nichts anderes machten, als ein paar Zitate anzuzeigen. Da dachte ich mir, so etwas doch auch mal in Python klöppeln zu können. Gesagt, getan. Zumindest der Anfang ist gemacht.

# -*- coding: utf-8 -*-
from scene import *
from random import randrange, choice
 
class MyScene (Scene):
	def setup(self):
		self._changebackground()
		self._shownewquote()
		
	def _changebackground(self):
		background(randrange(50, 90)/100.0, 
		           randrange(50, 90)/100.0,
		           randrange(50, 90)/100.0)
	
	def _truncatestring(self, s, length=25):
		# Split into quote and author
		s, author = s
		# Truncate quote to fixed length, but
		# keeping word boundaries.
		output = []
		while len(s) > 0:
			if len(s) < length:
				output.append(s)
				break
			elif s[length - 1] == ' 'and len(s) > length:
				output.append(s[:length])
				s = s[length:]
			else:
				adjust = s[length:].find(' ')
				if adjust == -1:
					output.append(s)
					s = ''
				else:
					output.append(s[:length + adjust])
					s = s[length + adjust + 1:]
		# Strip all unnecessary blank spaces
		# from resulting strings.
		output = [line.strip() for line in output]
		# Righta-djust author and append to output
		w = max([len(data) for data in output])
		output.append(author.rjust(w))
		return '\n'.join(output)
		
	def _shownewquote(self):
		text(self._truncatestring(rq.next(), 20), x=self.size.w/2, y=self.size.h/2, font_name='DejaVuSansMono', font_size=12)
	
	def touch_began(self, touch):
		self._changebackground()
		self._shownewquote()
 
class RandomQuotes():
	def __init__(self):
		self.quotes = list(open('quotes.txt'))
		self.currentquote = self._newquote()
 
	def next(self):
		while True:
			next = self._newquote()
			if next != self.currentquote:
				self.currentquote = next
				break
		return self.currentquote
		
	def _newquote(self):
		 return choice(self.quotes).split('#-#')
		
if __name__ == '__main__':
	rq = RandomQuotes()
	run(MyScene())

Das Script nimmt sich aus einer weiteren Textdatei (quotes.txt), die auch auf Gist liegt, einige Zitate* und gibt sie dann aus. Bei jedem Touch auf das Display wird ein neues Zitat angezeigt und die Hintergrundfarbe geändert. Feinschliff ist noch überhaupt nicht angesetzt, dafür musste ich zu früh aus der Bahn aussteigen. Aber es tut immerhin ganz ordentlich.

* Die Zitate selbst stammen von Wikiquote.

Drafts-Rezepte XI: Sprachausgabe

Endlich wurde das Update von Pythonista durch Apples Schleusen gespült. Und wie gewohnt, kommt mit einem Update noch einmal eine Menge Qualität zu Pythonista hinzu. Unter den vielen Änderungen sind wieder so viele dabei, ich will das mal am Beispiel eines der neuen Module feiern. Die Version 1.4 von Pythonista bringt das speech-Modul, mit dem eine einfache Text-to-speech-Ausgabe unter iOS genutzt wird. Viel ist da gar nicht zu machen.

import speech
import sys

speech.say(sys.argv[1])

Die Action für Drafts ist auch nicht viel länger.

pythonista://texttospeech?action=run&argv=[[draft]]

Import-Link

Dieses kleine Skript macht natürlich nicht mehr als die Sprachausgabe – und das auch nur in der Systemsprache. Ich war zu faul, auf die Schnelle noch eine Option für die Sprache zu basteln. Ich muss ja auch noch den Rest des Updates kennenlernen.

Drafts-Rezepte X: Serverstatus bei Bitbucket

Letzte Woche hatte ich Githubs Serverstatus über Drafts abgefragt, beim Konkurrenten Bitbucket ließ man sich natürlich nicht lumpen. Es gibt keine eigene API für den Status der Server, aber Bitbucket hat einen RSS-Feed, den wir natürlich auswerten können. Da hatte ich immerhin mal Gelegenheit, das Feedparser-Modul von Python auszuprobieren.

from console import alert
import feedparser
from sys import exit
from urllib import urlopen, quote
import webbrowser

def error_dialog(title, message):
	'''A diaolog box for error messages.'''
	try:
		alert(title, message)
	except KeyboardInterrupt:
		pass
	webbrowser.open('drafts://')
	exit(message)

def request():
	'''a request to bitbuckets status feed.'''
	api_url_base = 'http://feeds.feedburner.com/BitBucketServerStatus'
	try:
		response = urlopen(api_url_base)
	except IOError:
		error_dialog('Connection Error', 'Unable to perform request.')
	if response.getcode() != 200:
		error_dialog('Error', 'Status code: {0} - Message: {1}'.format(response.getcode(), response.read()))
	return response.read()

def bitbucket_status():
	'''get information and admin messages for bitbucket servers from bitbucket feed.'''
	bitbucket_feed = request()
	feed = feedparser.parse(bitbucket_feed)
	output = '''
{title}
================
Last entries:
	'''.format(title=feed['feed']['title'])
	for entry in feed['entries']:
		output += '\n{date}: {comment}\n'.format(date=entry['title'],
		                                         comment=entry['summary_detail']['value'])
	output += '\nFor further information: http://status.bitbucket.org/'
	webbrowser.open('drafts://x-callback-url/create?text={0}'.format(quote(output)))

if __name__ == '__main__':
	bitbucket_status()

Gist

So richtig schön sieht die Ausgabe in Drafts aus, wenn die Anzeige über die Markdown-Action erfolgt. Das habe ich im obigen Script beim Callback zu Drafts noch nicht integriert. Aber das ließe sich bewerkstelligen.

Die Action, die in Drafts alles auslöst, sieht so aus.

pythonista://bitbucketstatus?action=run

Import-Link

Drafts-Rezepte IX: Serverstatus bei Github

Da auch den guten Leuten von github mal die Server wegbrechen, gibt es dort eine kleine API. Es ist nur eine klitzekleine Schnittstelle, aber sie wirft raus, wie es gerade und in letzter Zeit um die Erreichbarkeit von github steht. Also basteln wir uns doch mal ein kleines Script, mit dem wir die Daten abrufen können.

from console import alert
from json import loads
from sys import exit
from urllib import urlopen, quote
import webbrowser

def error_dialog(title, message):
    '''A diaolog box for error messages.'''
    try:
        alert(title, message)
    except KeyboardInterrupt:
        pass
    webbrowser.open('drafts://')
    exit(message)

def formatdate(date):
    '''bring the date into shape'''
    return date[:-1].replace('T', ' ')

def request(method):
    '''a request to github's status API.'''
    api_url_base = 'https://status.github.com/api/{method}'
    try:
        response = urlopen(api_url_base.format(method=method))
    except IOError:
        error_dialog('Connection Error', 'Unable to perform request.')
    if response.getcode() != 200:
        error_dialog('Error', 'Status code: {0} - Message: {1}'.format(response.getcode(), response.read()))
    return loads(response.read())

def github_status():
    '''get information and admin messages for github servers.'''
    status = request('status.json')
    messages = request('messages.json')
    output = '''
GITHUB STATUS
================
{date}: {status}

Last messages:
    '''.format(date=formatdate(status['last_updated']),
               status=status['status'])
    for item in messages:
        output += '\n{date}: {status}\n{comment}\n'.format(date=formatdate(item['created_on']),
                                                             status=item['status'],
                                                             comment=item['body'])
    output += '\nFor further information: https://status.github.com/'
    webbrowser.open('drafts://x-callback-url/create?text={0}'.format(quote(output)))

if __name__ == '__main__':
    github_status()

Gist

Die Action in Drafts ist auch keine Neuerung mehr.

pythonista://githubstatus?action=run

Import-Link

Seitdem Drafts auch Actions ausführt, wenn ein Draft keine Zeichen enthält, muss auch nur die Action gefeuert werden. Daraufhin rödelt Pythonista etwas rum, wirft dann aber das Ergebnis als Text in einen neuen Draft aus. Darin stehen dann in knapper Form die Informationen.

Drafts-Rezepte VIII: Down or just me?

Ein ganz kleines Rezept, um mal schnell zu checken, ob eine Seite wirklich down sein könnte oder der Fehler irgendwo auf meiner Seite liegt. Ich hatte das leidige Thema diese Woche zu oft, da war es ganz gut, mit Drafts schnell losschlagen zu können. Um den Service in Safari zu öffnen, sieht das alles so aus:

http://www.isup.me/[[draft]]

Will ich es in Chrome öffnen, ist das auch schnell erledigt:

googlechrome://www.isup.me/[[draft]]

Die Idee sollt also klar sein. Ist euer Browser nicht dabei sein, ist sein Handle vielleicht hier zu finden.

Will ich jetzt mal sehen, ob kultprok down ist, würde ich inDrafts einfach

kultprok.de

eingeben und dann die Action feuern. Okay, das ist natürlich reichlich dämliches Beispiel, weil ich zuverlässigere Wege habe, um festzustellen, ob mein Server noch läuft. Aber na ja, der Gedanke zählt.

Drafts-Rezepte VII: Postleitzahlen mit Zippopotamus II

Der zweite Teil zu Postleitzahlen mit Zippopotamus geht in die entgegengesetzte Richtung. Statt nun also eine Postleitzahl zu ermitteln, geht es nun um die Städte oder Stadtteile hinter eine Postleitzahl.

Das Script ist wieder genauso einfach wie das erste. In Drafts muss eine neue Notiz nur die folgende Syntax einhalten:

[PLZ],[LÄNDERKÜRZEL]

Beispielsweise würde die folgende Eingabe der berühmten Postleitzahl eine Markdown-Liste in Drafts öffnen.

90210 , us

Auch hier kommt ein kleiner Haken ins Spiel, auch wenn er nicht ganz so lästig ist. Denn die API von Zippopotamus erlaubt es derzeit nicht, das Land nicht anzugeben. Unter Umständen müssen wir also mehr Infos angeben, als wir haben. Das Python-Script sieht derzeit so aus:

# -*- coding: utf-8 -*-
from console import alert
from json import loads
from sys import argv, exit
from urllib import urlopen, quote
import webbrowser

def error_dialog(title, message):
    '''
    a diaolog box for error messages.
    '''
    try:
        alert(title, message)
    except KeyboardInterrupt:
        pass
    webbrowser.open('drafts://')
    exit(message)

def handle_data(data):
    '''
    process json response from zippopotamus.
    will return a markdown list of items.
    '''
    city_json = loads(data)
    output = ''
    for item in city_json['places']:
        output += '- Ort: {place}, Region: {state}\n'.format(place=item['place name'], state=item['state'])
    return output

def get_by_postalcode(data):
    '''
    get all possible cities for a postal code in
    the given country.
    '''
    api_url_base = 'http://api.zippopotam.us/{country}/{postcode}'
    try:
        postcode_, country_= [item.strip() for item in data.split(',')]
    except Exception as err:
        error_dialog(str(err.__class__), err.message)

    try:
        response = urlopen(api_url_base.format(country=country_, postcode=postcode_))
    except IOError:
        error_dialog('Connection Error', 'Unable to perform request.')
    if response.getcode() == 200:
        postcode_data = handle_data(response.read())
        webbrowser.open('drafts://x-callback-url/create?text={0}'.format(quote(postcode_data)))
    else:
        error_dialog('Error', 'Status code: {0} - Message: {1}'.format(response.getcode(), response.read()))

if __name__ == '__main__':
    get_by_postalcode(argv[1])

Gist

Die dazugehörige Action für Drafts ist:

pythonista://city-by-postalcode?action=run&argv=[[draft]]

Import-Link

Drafts-Rezepte VI: Postleitzahlen mit Zippopotamus I

Den kleinen Gag kann ich mir nicht verkneifen. Ich mache zu Zippopotamus, einem guten, kleinen Dienst für Postleitzahlen in einer Vielzahl von Ländern, zwei Teile. Damit artet die Kleinteiligkeit aus wie der überbordende Song-Kosmos von Coheed & Cambria.

Das Script ist wieder sehr einfach. In Drafts muss eine neue Notiz nur die folgende Syntax einhalten:

[STADT], [REGIONSKÜRZEL], [LÄNDERKÜRZEL]

Beispielsweise würde die folgende Eingabe die berühmte Postleitzahlen für die Stadt als Markdown-Liste in Drafts öffenen.

beverly hills, ca, us

Leider kommt diese Sache nicht ohne einen kleinen Haken aus. Denn die API von Zippopotamus erlaubt es derzeit nicht, die Region und das Land nicht anzugeben. Unter Umständen müssen wir also mehr Infos angeben, als wir haben. Ich bin dabei, mir dafür eine Lösung zu überlegen. Das Python-Script sieht derzeit so aus:

# -*- coding: utf-8 -*-
from console import alert
from json import loads
from sys import argv, exit
from urllib import urlopen, quote
import webbrowser

def error_dialog(title, message):
    '''
    a diaolog box for error messages.
    '''
    try:
        alert(title, message)
    except KeyboardInterrupt:
        pass
    webbrowser.open('drafts://')
    exit(message)

def handle_data(data):
    '''
    process json response from zippopotamus.
    will return a markdown list of items.
    '''
    city_json = loads(data)
    output = ''
    for item in city_json['places']:
        output += '- City: {place}, Post code: {postcode}\n'.format(place=item['place name'], postcode=item['post code'])
    return output

def get_by_city(data):
    '''
    get all possible post codes for a city in
    the given country.
    '''
    api_url_base = 'http://api.zippopotam.us/{country}/{state}/{city}'
    try:
        city_, state_, country_= [item.strip() for item in data.split(',')]
    except Exception as err:
        error_dialog(str(err.__class__), err.message)

    try:
        response = urlopen(api_url_base.format(country=country_, state=state_, city=city_))
    except IOError:
        error_dialog('Connection Error', 'Unable to perform request.')
    if response.getcode() == 200:
        city_data = handle_data(response.read())
        webbrowser.open('drafts://x-callback-url/create?text={0}'.format(quote(city_data)))
    else:
        error_dialog('Error', 'Status code: {0} - Message: {1}'.format(response.getcode(), response.read()))

if __name__ == '__main__':
    get_by_city(argv[1])

Gist

Die dazugehörige Action für Drafts ist hier:

pythonista://postalcode-by-city?action=run&argv=[[draft]]

Import-Link

Timely Alarm Clock: Form auf Höhe der Funktion

Muss ich über Timely noch wirklich viele Worte verlieren, muss ich? Eine halbe Woche mindestens bin ich zu spät dran, also eine Ewigkeit, in der Lob kübelweise über diesen Wecker für Android ausgeschüttet wurde. Da rechtfertigt nur ein Verriss oder eine kleine, pointierte Kritik, um jetzt noch mit ein paar Sätzen über die App rauszukommen. Weit gefehlt, ich kann nur in den Chor einstimmen.

Vor einiger Zeit schrieb ich noch von Rise auf iOS, dem Timely in vielen Punkten ähnlich ist, in den meisten sogar überlegen. Optisch ist Timely um Welten ansehnlicher als jede durchschnittliche Android-App. Nicht nur das, Timely ist schön. Flaches Design, ja wir spüren den Zeitgeist, mit subtilen Effekten. Der Nutzbarkeit steht das nie im Weg. Timely ist ein Muster an intuitiven Bedienelementen, ich arbeite wie im Traum mit dem Wecker, der Uhr, Timer und Stoppuhr. Ich schwebe durch die App, keine Übertreibung.

Kostenlos ist Timely in seiner grundlegenden Form, weitere Themes, Weckersynchronisation über mehrere Geräte hinweg und vor allem einige fantastische Wecktöne lassen sich einzeln oder als Paket hinzu kaufen. Nicht aber, ohne dass die meisten Zusatzinhalte vorher in einer Testphase für einige Tage kostenlos zur Verfügung zu stehen. So fair ist Timely, alle Kosten sind transparent, ich wünsche mir, dass es sich auszahlt. Und selbst wenn ich auf der kostenlosen Schiene hätte bleiben wollen, ich hätte einen fantastischen Wecker gehabt. Timely ist ohne böse Überraschungen eine grandiose App, aber auch ein Blick in eine Zukunft fernab der biederen Nacktmulche von Software, die ich mir schweren Herzens auf den Android schob. Android kann schön sein, kombiniert mit solch durchdachter Funktionalität sogar ästhetisch.