Author Archive for Claude

Facebook Pages Notifications

logo.png You might have noticed already, if you administer community or professional pages or have developed Facebook applications, that contrary to your own personal wall you never get notified when someone post a status or write a comment on your pages?

This is a problem for most page administrators and there is a 32 pages long (and growing) thread with people complaining about this missing feature. This is a problem because without such a feature you have to periodically crawl your own pages to check if anyone posted anything (status or comment) and get the opportunity to eventually respond to it, or spam it. I guess that since they won’t get notified about this thread the Facebook people will never notice the problem…

One proposed solution to this issue is to “like” each and every status update you post on your pages wall, however, beside the fact that liking everything you post may look a bit awkward, this does not gets you notified when someone posts a new status.

I have some pages I need to watch, so missing this feature was really a problem to me. And when it itches, I scratch… Besides, I wanted to experiment with the new Facebook Graph API.

So I created this application, it’s called “Watch My Pages!” and provides users with receiving daily e-mail notifications when someone posts a status or writes a comment to their pages wall. If you like it, have a problem with it or think about a feature, just drop a message on it’s wall, I’ll get notified ;)

How to manage Google AppEngine maintenance periods

Here is a small snippet of code that I use on applications deployed on Google AppEngine to inform users that the application is in maintenance mode. It usually happen when the AppEngine team put the datastore in read-only mode for maintenance purpose but other capabilities can be tested as well.

def requires_datastore_write(view):
    def newview(request, *args, **kwargs):
        from google.appengine.api import capabilities
        datastore_write_enabled = capabilities.CapabilitySet('datastore_v3', capabilities=['write']).is_enabled()
 
        if datastore_write_enabled:
            return view(request, *args, **kwargs)
        else:
            from django.shortcuts import render_to_response
            from django.template import RequestContext
            return render_to_response('maintenance.html', context_instance=RequestContext(request))
 
    return newview

This is a python decorator and you can use it to decorate views that require write access to the datastore. For example:

@requires_datastore_write
def update(request):
    ...

You will need to create a Django template named maintenance.html to display a warning to your users. Mine looks like this:

<h2>Application Maintenance</h2>
 
<p>The LibraryThing for Facebook application is currently
in maintenance mode and some operations are temporarily unavailable.</p>
 
<p>Thanks for trying back later. Sorry for the inconvenience.</p>

Properly uploading files to Amazon S3

Here is a little script I wrote and I though ought to be shared. I use it to upload static files like images, css and javascript so that they can be served by Amazon S3 instead of the main application server (like Google App Engine).

It’s written in Python and does interesting things like compressing and minifying what needs to be. It takes 3 arguments and as 2 options:

Usage: s3uploader.py [-xm] src_folder destination_bucket_name prefix
src_folder
path to the local folder containing the static files to upload
destination_bucket_name
name of the S3 bucket to upload to (e.g. static.example.com)
prefix
a prefix to use for the destination key (kind of a folder on the destination bucket, I use it to specify a release version to defeat browser caching)
x
if set, the script will set a far future expiry for all files, otherwise the S3 default will be used (one day if I remember well)
m
if set, the script will minify css and javascript files

First you will have to install some dependencies, namely boto, jsmin and cssmin. Installation procedure will depend on your OS but on my Mac I do the following:

sudo easy_install boto
sudo easy_install jsmin
sudo easy_install cssmin

And here is the script itself:

#! /usr/bin/env python
import os, sys, boto, mimetypes, zipfile, gzip
from io import StringIO, BytesIO
from optparse import OptionParser
from jsmin import *
from cssmin import *
 
# Boto picks up configuration from the env.
os.environ['AWS_ACCESS_KEY_ID'] = 'Your AWS access key id goes here'
os.environ['AWS_SECRET_ACCESS_KEY'] = 'Your AWS secret access key goes here'
 
# The list of content types to gzip, add more if needed
COMPRESSIBLE = [ 'text/plain', 'text/csv', 'application/xml',
                'application/javascript', 'text/css' ]
 
def main():
    parser = OptionParser(usage='usage: %prog [options] src_folder destination_bucket_name prefix')
    parser.add_option('-x', '--expires', action='store_true', help='set far future expiry for all files')
    parser.add_option('-m', '--minify', action='store_true', help='minify javascript files')
    (options, args) = parser.parse_args()
    if len(args) != 3:
        parser.error("incorrect number of arguments")
    src_folder = os.path.normpath(args[0])
    bucket_name = args[1]
    prefix = args[2]
 
    conn = boto.connect_s3()
    bucket = conn.get_bucket(bucket_name)
 
    namelist = []
    for root, dirs, files in os.walk(src_folder):
        if files and not '.svn' in root:
            path = os.path.relpath(root, src_folder)
            namelist += [os.path.normpath(os.path.join(path, f)) for f in files]
 
    print 'Uploading %d files to bucket %s' % (len(namelist), bucket.name)
    for name in namelist:
        content = open(os.path.join(src_folder, name))
        key = bucket.new_key(os.path.join(prefix, name))
        type, encoding = mimetypes.guess_type(name)
        type = type or 'application/octet-stream'
        headers = { 'Content-Type': type, 'x-amz-acl': 'public-read' }
        states = [type]
 
        if options.expires:
            # We only use HTTP 1.1 headers because they are relative to the time of download
            # instead of being hardcoded.
            headers['Cache-Control'] = 'max-age %d' % (3600 * 24 * 365)
 
        if options.minify and type == 'application/javascript':
            outs = StringIO()
            JavascriptMinify().minify(content, outs)
            content.close()
            content = outs.getvalue()
            if len(content) &gt; 0 and content[0] == '\n':
                content = content[1:]
            content = BytesIO(content)
            states.append('minified')
 
        if options.minify and type == 'text/css':
            outs = cssmin(content.read())
            content.close()
            content = outs
            if len(content) &gt; 0 and content[0] == '\n':
                content = content[1:]
            content = BytesIO(content)
            states.append('minified')
 
        if type in COMPRESSIBLE:
            headers['Content-Encoding'] = 'gzip'
            compressed = StringIO()
            gz = gzip.GzipFile(filename=name, fileobj=compressed, mode='w')
            gz.writelines(content)
            gz.close()
            content.close
            content = BytesIO(compressed.getvalue())
            states.append('gzipped')
 
        states = ', '.join(states)
        print '- %s =&gt; %s (%s)' % (name, key.name, states)
        key.set_contents_from_file(content, headers)
        content.close();
 
if __name__ == '__main__':
    main()

Thanks to Nico for the expiry trick :)

Spare me the talk about privacy, they’re all clueless anyway…

With all the talks and posts and whatnot about privacy on the Internet it’s easy for anyone to turn into a privacy control freak.

And I really was starting to freak out myself. After all, a good bunch of my own life is on the Net: Facebook ,Twitter, Flickr, LinkedIn, this blog, all the Google applications and all the other services I use, or I test… But this morning I received a letter, not an e-mail, a paper letter. From Google AdWords. Sent from France. In German!

I guess they just assumed that since I was living in Switzerland I was talking German, like when ebay.com redirects me to ebay.de, but I don’t speak nor read German.

And it reminded me something I learned a long time ago, when I was working for Singularis – a now defunct start-up that was collecting users preferences about TV programs: You can collect as many data as you want, if you don’t know how to use it it’s only worth the cost of the storage.

And the more you have the harder it is.

Sévices Après Vente

Dans un monde chaque jour un peu plus numérisé n’oublions pas que ce sont toujours les même vieux trucs qui fonctionnent: si vis pacem, para bellum!

!!Attention!! Ce billet est long et ennuyeux…

Ceux qui me suivent sur Twitter ou Facebook se souviendront qu’en novembre dernier (le vendredi 13 exactement) mon appartement avait été cambriolé pendant que j’étais chez le dentiste (ça fais beaucoup pour un vendredi 13). Parmi les objets qui m’avaient été volés se trouvait mon MacBook Pro, qui est mon seul et unique outil de travail.

J’avais donc rapidement besoin d’une nouvelle machine. Après avoir fait le tour des revendeurs Apple de la région pour découvrir que seules des configurations de base étaient en stock, je me rend, sans grand espoir, à la FNAC de Lausanne.

Je n’achète jamais de matériel électronique de ce prix à la FNAC – en dessous de 200,-CHF le rapport prix/rapidité de l’achat est assez favorable pour que je ne cherche pas plus loin mais au-delà j’ai toujours pu trouver moins cher ailleurs. Mais, ce samedi 14, je découvre avec bonheur que la FNAC possède en stock un MacBook Pro dont la configuration approche de très prés la configuration que je recherche: 15″, 3.06GHZ, 4Go de RAM et un disque dur de 500Go à 7200 tr/mn. Je l’achète donc, pour le prix de 3299,-CHF (moins le rabais adhérents).

Le 29 décembre dernier (un mois et demi plus tard), en allant me coucher, je décide de laisser mon MacBook allumé sur la table du salon afin qu’il puisse participer au réseau BOINC et dédier quelques cycles à la recherche extra-terrestre. Je n’ai pas d’animaux, pas d’enfants et la machine est posée sur un endroit dégagé où la ventilation n’est pas obstruée.

Le lendemain matin, ayant pris mon petit déjeuner, je m’en vais consulter mes e-mails. Étrangement, alors qu’une simple caresse suffit d’habitude, mon Mac ne veut pas se réveiller. Étonné, je vérifie que je ne l’ai pas laissé sans alimentation: non, le cordon est bien là, branché et alimenté, il ne s’agit donc pas d’un épuisement des batteries. De plus, un ronron très léger m’indique que la machine semble toujours être en marche. Je force donc un shutdown en maintenant la touche on/off enfoncée et j’entends distinctement ce petit bruit caractéristique qui signale l’arrêt d’un moteur électrique quelque part dans la machine. Je l’allume de nouveau et là un bruit de moteur se fait également entendre mais à par cela rien, l’écran reste désespérément aveugle. Après, une ou deux autres tentatives aussi infructueuses je décide d’amener la machine au SAV de la FNAC.

Continue reading ‘Sévices Après Vente’