strona główna

Archive for October, 2008

Warsztaty Django: zapisy otwarte

Tuesday, October 28th, 2008

Od wczoraj można już zapisywać się na pierwszą edycję warsztatów "Tworzenie aplikacji w Django". Liczba uczestników ograniczona do 12 osób, decyduje kolejność zgłoszeń. Pierwsze cztery miejsca już zajęte, więc nie warto zwlekać – jeśli temat Cię interesuje, to zapisz się już teraz.

Szczegóły, jak zwykle, na stronie warsztatów.

Djazzpora

Monday, October 27th, 2008

Wszystko wskazuje na to, że przegapiłem w tym roku kilkanaście świetnych koncertów na Chłodnej.

Wczoraj w ramach cyklu Djazzpora zagrali tam Paweł Szamburski, Patryk Zakrocki i zaśpiewała Asia "Asja" Czajkowska. Dwóch pierwszych już słyszałem wcześniej (zapewniali m.in. oprawę muzyczną Utworu sentymentalnego na czterech aktorów Montowni) i nie zawiodłem się. To niesamowite, co można osiągnąć przy użyciu klarnetu, altówki i odrobiny elektroniki (oktawer, dwie loop stations, chyba tyle).

Koncert był nagrywany, więc mam nadzieję że będzie gdzieś dostępny. Na razie Szamburski solo, też na Chłodnej:

Zwróćcie uwagę: solo. Tak brzmi świetnie wykorzystane loop station.

Odkryciem tygodnia jest dla mnie wokalistka i zespół, do którego trafiłem teraz dzięki jej nazwisku: Alamut:

Impressive. Koncertują rzadko, ale zdarza się.

Django: powiadomienia

Saturday, October 25th, 2008

Wczoraj przyszło mi do głowy, że powiadomienia o błędach to jednocześnie jeden z bardziej przydatnych i najmniej widocznych elementów Django. To dzięki nim zdarza mi się poprawić błędy i wysłać do użytkownika informację, że jakaś strona działa już poprawnie nawet jeśli nie chciało mu się zgłaszać usterki.

Na przykład:

 
from django.shortcuts import render_to_response
 
def gather_user_data(user):
    # in reality this would be more complex
    return {'email': user.email}
 
def profile_view(request):
    data = {'user': request.user}
    data.update(gather_user_data(request.user))
    return render_to_response('profile_view.html',
                              dictionary = data)
 

Widok profile_view zawiera błąd ujawniający się tylko w niektórych sytuacjach. Jeśli trafi na niego ktoś na serwerze produkcyjnym, zobaczy standardowe 500 Internal server error, a Django automatycznie wyśle do mnie taki raport:

 
Traceback (most recent call last):
 
  File "/home/marcink/checkout/django-trunk/django/core/handlers/base.py",
  line 86, in get_response
    response = callback(request, *callback_args, **callback_kwargs)
 
  File "/home/marcink/checkout/utils/blogowe/errtest/../errtest/errapp/views.py",
  line 8, in profile_view
    data.update(gather_user_data(request.user))
 
  File "/home/marcink/checkout/utils/blogowe/errtest/../errtest/errapp/views.py",
  line 4, in gather_user_data
    return {'email': user.email}
 
AttributeError: 'AnonymousUser' object has no attribute 'email'
 
< wsgirequest GET:< QueryDict: {}>,
POST:< querydict : {}>,
COOKIES:{'django_log_selectedLevel': '0',
 'django_log_showLocation': '1',
 'sessionid': '915922a5710cf210e2f5fae47e0f8900'},
META:{'COLORTERM': 'gnome-terminal',
 'CONTENT_LENGTH': '',
 'CONTENT_TYPE': 'text/plain',
 'DBUS_SESSION_BUS_ADDRESS': 'unix:abstract=/tmp/dbus-BibuFgosrz,guid=c50cabac45d33af5b6936d0049024c87',
 'DESKTOP_SESSION': 'default',
 'DISPLAY': ':0.0',
 'DJANGO_DIR': '/home/marcink/checkout/django-trunk',
 'DJANGO_SETTINGS_MODULE': 'errtest.settings',
 'FCGI_PORT': '9014',
 'GATEWAY_INTERFACE': 'CGI/1.1',
 'GDMSESSION': 'default',
 'GDM_LANG': 'en_US.UTF-8',
 'GDM_XSERVER_LOCATION': 'local',
 'GNOME_DESKTOP_SESSION_ID': 'Default',
 'GNOME_KEYRING_PID': '7904',
 'GNOME_KEYRING_SOCKET': '/tmp/keyring-glnuCS/socket',
 'GTK_RC_FILES': '/etc/gtk/gtkrc:/home/marcink/.gtkrc-1.2-gnome2',
 'HISTCONTROL': 'ignoreboth',
 'HOME': '/home/marcink',
 'HTTP_ACCEPT': 'text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5',
 'HTTP_ACCEPT_CHARSET': 'ISO-8859-1,utf-8;q=0.7,*;q=0.7',
 'HTTP_ACCEPT_ENCODING': 'gzip,deflate',
 'HTTP_ACCEPT_LANGUAGE': 'en-us,en;q=0.5',
 'HTTP_CACHE_CONTROL': 'max-age=0',
 'HTTP_CONNECTION': 'keep-alive',
 'HTTP_COOKIE': 'sessionid=915922a5710cf210e2f5fae47e0f8900; django_log_showLocation=1; django_log_selectedLevel=0',
 'HTTP_HOST': '127.0.0.1:5003',
 'HTTP_KEEP_ALIVE': '300',
 'HTTP_USER_AGENT': 'Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.1.17) Gecko/20080922 Ubuntu/7.10 (gutsy) Firefox/2.0.0.17',
 'JAVA_HOME': '/home/marcink/devel/jre1.6.0/',
 'LANG': 'en_US.UTF-8',
 'LD_LIBRARY_PATH': '/home/marcink/checkout/ldev_comicspot/comicspot/../other',
 'LESSCLOSE': '/usr/bin/lesspipe %s %s',
 'LESSOPEN': '| /usr/bin/lesspipe %s',
 'LOGNAME': 'marcink',
 'LS_COLORS': 'no=00:fi=00:di=01;34:ln=01;36:pi=40;33:so=01;35:do=01;35:bd=40;33;01:cd=40;33;01:or=40;31;01:su=37;41:sg=30;43:tw=30;42:ow=34;42:st=37;44:ex=01;32:*.tar=01;31:*.tgz=01;31:*.arj=01;31:*.taz=01;31:*.lzh=01;31:*.zip=01;31:*.z=01;31:*.Z=01;31:*.gz=01;31:*.bz2=01;31:*.deb=01;31:*.rpm=01;31:*.jar=01;31:*.jpg=01;35:*.jpeg=01;35:*.gif=01;35:*.bmp=01;35:*.pbm=01;35:*.pgm=01;35:*.ppm=01;35:*.tga=01;35:*.xbm=01;35:*.xpm=01;35:*.tif=01;35:*.tiff=01;35:*.png=01;35:*.mov=01;35:*.mpg=01;35:*.mpeg=01;35:*.avi=01;35:*.fli=01;35:*.gl=01;35:*.dl=01;35:*.xcf=01;35:*.xwd=01;35:*.flac=01;35:*.mp3=01;35:*.mpc=01;35:*.ogg=01;35:*.wav=01;35:',
 'OLDPWD': '/home/marcink',
 'OTHER_DIR': '/home/marcink/checkout/ldev_comicspot/comicspot/../other',
 'OUTER_DIR': '/home/marcink/checkout/ldev_comicspot/comicspot/..',
 'PATH': '/home/marcink/bin:/home/marcink/local/bin:/home/marcink/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games',
 'PATH_INFO': u'/profile/',
 'PROJECT_BASE_DIR': '/home/marcink/checkout/ldev_comicspot/comicspot',
 'PROJECT_DIR': '/home/marcink/checkout/ldev_comicspot/comicspot/comicspot',
 'PROJECT_NAME': 'comicspot',
 'PWD': '/home/marcink/checkout/utils/blogowe/errtest',
 'PYTHONPATH': '/home/marcink/checkout/django-trunk',
 'QUERY_STRING': '',
 'REMOTE_ADDR': '127.0.0.1',
 'REMOTE_HOST': '',
 'REQUEST_METHOD': 'GET',
 'RUN_MAIN': 'true',
 'SCRIPT_NAME': u'',
 'SERVER_NAME': 'raven.loc',
 'SERVER_PORT': '5003',
 'SERVER_PROTOCOL': 'HTTP/1.1',
 'SERVER_SOFTWARE': 'WSGIServer/0.1 Python/2.5.1',
 'SESSION_MANAGER': 'local/raven:/tmp/.ICE-unix/7907',
 'SHELL': '/bin/bash',
 'SHLVL': '1',
 'SSH_AGENT_PID': '7947',
 'SSH_AUTH_SOCK': '/tmp/ssh-lBRzQZ7907/agent.7907',
 'TERM': 'xterm',
 'TZ': 'America/Chicago',
 'USER': 'marcink',
 'USERNAME': 'marcink',
 'WINDOWID': '39852701',
 'WINDOWPATH': '7',
 'XAUTHORITY': '/tmp/.gdmELMXJU',
 'XDG_DATA_DIRS': '/usr/local/share/:/usr/share/:/usr/share/gdm/',
 'XDG_SESSION_COOKIE': '90de2c2a7bf31a95a76c4900471ce700-1224887428.976645-1927173936',
 '_': '/usr/bin/python',
 'wsgi.errors': < open file '< stderr>', mode 'w' at 0xb7dc30b0>,
 'wsgi.file_wrapper': < class 'django.core.servers.basehttp.FileWrapper'>,
 'wsgi.input': < socket ._fileobject object at 0x854a72c>,
 'wsgi.multiprocess': False,
 'wsgi.multithread': True,
 'wsgi.run_once': False,
 'wsgi.url_scheme': 'http',
 'wsgi.version': (1, 0)}>
 

Po pierwsze – dowiaduję się o problemie bez żadnej akcji ze strony użytkownika. Po drugie – dostaję mnóstwo informacji o błędzie: opis wyjątku (tutaj – brak atrybutu email w AnonymousUser), dokładnie wskazane miejsce wystąpienia razem z pełnym stosem wywołań funkcji, zawartość parametrów przekazanych przez GET i POST i zawartość zmiennych środowiskowych. W praktyce zwykle wystarcza to do złożenia testu pozwalającego na powtórzenie i usunięcie błędu.

Niechętnie

Sunday, October 19th, 2008

Wybraliśmy się z Anią i tatą na koncert do Przestrzeni STSW. "GGGHOSTS live noise jazz festival" – opis wyglądał interesująco, do tego za darmo, dobry pretekst żeby zobaczyć Przestrzeń. Wychodzimy, wsiadamy, jedziemy.

Na miejscu chwila zamieszania ("- Lubelska 50/52? - Nie, raczej 30/32. - Na pewno? - Telefon do przyjaciółki"), znajdujemy budynek, znajdujemy mało rzucające się w oczy strzałki wskazujące drogę. Dziedziniec, ludzie wychodzący z szeroko otwartych drzwi, "Przepraszam, czy tu jest koncert?", "Tak, na górze grają", wchodzimy, jest koncert, jest muzyka, jest "kurcze, to jest niesamowite" muzyka. Mocny, energiczny, ciekawie zaaranżowany, świetny jazz.

Tylko, jak się później okazało, to nie ten koncert. Ot, serendipity w praktyce.

Trafiliśmy do Komuny Otwock, na występ zespołu Niechęć.

Dla mnie to odkrycie roku, dawno nie słyszałem tak dobrze brzmiącej grupy. Jeśli znacie kogoś, kto jeszcze w Polsce gra coś podobnego – bardzo prosiłbym o informację w komentarzu albo na email.

„Django - 3 lata pracy i wszystko, co mamy, to marne 1.0″

Monday, October 13th, 2008

Z okazji wydania Django 1.0 na ostatnim Bootstrapie opowiedziałem o najważniejszych zmianach, jakie zaszły w tej ramówce podczas tych trzech lat, które upłynęły od momentu otwarcia kodu. Slajdy z mojej prezentacji:

Reklama: już wkrótce odbędzie się pierwsza edycja warsztatów Django – najszybszy sposób na poznanie możliwości tego środowiska i rozpoczęcie pracy.

Rutkowski vs Rutkowski

Sunday, October 5th, 2008

Pisałem kiedyś, że Testosteron to najzabawniejsze przedstawienie, jakie znam.

To już nieaktualne.

Wczoraj w piwnicy Chłodnej 25 odbyła się premiera one man show w wykonaniu Rafała Rutkowskiego (1/7 obsady Testosteronu), "To nie jest kraj dla wielkich ludzi" – półtorej godziny, dobrze ponad dwadzieścia ról, ślub, pogrzeb, przedszkole, WKU, gabinet ginekologa, wszystko w niewielkiej sali pełnej ludzi śmiejących się na całe gardło.

Django na Bootstrapie

Saturday, October 4th, 2008

Już w przyszłym tygodniu Bootstrap 8.10. Andy Budd opowie o testach użyteczności (“Guerilla Usability Testing with Silverback”), a ja ponarzekam na Django ("Django - 3 lata prac i wszystko, co mamy, to marne 1.0").

Serdecznie zapraszamy.

El Monito: zmiany

Saturday, October 4th, 2008

lawnmower.jpg

Miesiąc temu opublikowałem ankietę na temat El Monito; dziękuję bardzo wszystkim, którzy poświęcili czas na jej wypełnienie (41 osób).

Podsumowanie wyników: większość z Was monitoruje od 2 do 5 serwisów, w większości własnych i niekomercyjnych (66% odpowiedzi), chociaż komercyjnych też jest całkiem dużo (44% odpowiedzi). Preferowana cena to, o dziwo, 0zł (55%) :)

Na razie taka zostanie. Będą inne zmiany, z czego już niedługo coś, co pozwoli mi na odsianie nieaktywnych kont: konieczność zalogowania się do serwisu przynajmniej raz na dwa tygodnie. Będzie mniej wygodnie, ale niestety – w przeciwieństwie do większości serwisów, w El Monito nieużywane konta z włączonym monitoringiem zajmują i miejsce, i czas, i kable sieciowe.

Obrazek z SXC

Skróty: render_to_response

Wednesday, October 1st, 2008

Twórcy Django są bardzo skuteczni w eliminowaniu miejsc, które wymagałyby złamania zasady DRY. Tym dziwniejszy jest wyjątek render_to_response: teoretycznie skrót, w praktyce – funkcja wymagająca codziennego wpisywania prawie identycznego kodu.

Wygląda to tak:

 
from django.shortcuts import render_to_response
from django.template import RequestContext
def some_view(request):
    return render_to_response('testapp/some_view.html',
                              context_instance = RequestContext(request),
                              dictionary = {'title': 'A very simple view',
                                            'text': 'Some text here'})
 

Parametr context_instance w praktyce zawsze wygląda tak samo. Nazwa szablonu często, chociaż nie zawsze, przypomina nazwę funkcji obsługującej dany adres. Parametr dictionary bywa na tyle rozbudowany, że najczęściej buduje się go wcześniej, żeby uniknąć bardzo długich lub sztucznie podzielonych linii. Łączny poziom powtarzalności jest bardzo wyraźny po napisaniu kilku-kilkunastu widoków, zwłaszcza jeśli część z nich zawiera więcej niż jedną ścieżkę kończącą się wywołaniem render_to_response.

A przecież to może wyglądać tak:

 
@with_template
def simple_view(request):
    return {'title': 'A very simple view',
            'text': 'Some text here'}
 

W którymś momencie ta powtarzalność zirytowała mnie na tyle, że poświęciłem trochę czasu na napisanie odpowiedniego dekoratora. Dla lenistwa.

Działa tak:

  • jeśli funkcja zwraca HttpResponse, to dekorator nic nie zmienia – przekazuje ten wynik wyżej,
  • w przeciwnym wypadku zakłada, że wynikiem jest słownik (lub coś, co słownik udaje). W takiej sytuacji przekazuje go do render_to_response, zakładając że wymagany szablon to "nazwa_aplikacji/nazwa_funkcji.html", a context_instance to standardowy RequestContext.
  • dodatkowo, funkcja może zmienić nazwę szablonu: wystarczy, że w słowniku zwróci element 'template_name'.

Przykłady:

 
# plik testapp/views.py
 
@with_template
def simple_view(request):
    return {'title': 'A very simple view',
            'text': 'Some text here'}
 
@with_template
def simple_view_with_locals(request):
    title = 'Another simple view'
    text = 'Some text here'
    return locals()
 
@with_template
def complex_view(request):
    if request.REQUEST.get('redirect_me'):
        return HttpResponseRedirect('http://google.com/')
    else:
        template_name = 'testapp/some_other_template.html'
        return locals()
 
@with_template('testapp/yet_another_template.html')
def view_with_template_override(request):
    title = 'Another simple view'
    text = 'Some text here'
    return locals()
 
@login_required
@with_template
def authenticated_view(request):
    text = 'Welcome, ' + request.user.username
    return locals()
 

Można też po prostu zwrócić locals() – słownik zawierający wszystkie zmienne lokalne. Możliwe jest też, jak w ostatnim przykładzie, użycie innych dekoratorów, na przykład login_required.

Dekorator with_template ma też dodatkową, dużą zaletę: ułatwia wprowadzenie konwencji, zgodnie z którą nazwa szablonu wynika z nazwy aplikacji i funkcji. Konwencje, ogólnie, to dobra rzecz. Ta konkretna sprawia, że oszczędza się czas który normalnie poświęca się na decyzję jak nazwać szablon, wpisanie jego nazwy oraz późniejsze przeglądanie kodu żeby dowiedzieć się, jakiego szablonu należy się spodziewać pod danym adresem.

Owszem, można – jak zauważył ktoś podczas europythonowej prezentacji – dopisać informację o nazwie szablonu do dokumentacji funkcji, ale konwencje działają jak makra lub funkcje w dokumentacji: określa się je w jednym miejscu i stosuje w wielu, bez potrzeby zaglądania do tekstu.

 
# plik testapp/urls.py
 
from django.conf.urls.defaults import *
 
urlpatterns = patterns('testapp.views',
    (r'^normal/', 'normal_view'),
    (r'^simple/', 'simple_view'),
    (r'^complex/', 'complex_view'),
    (r'^authenticated/', 'authenticated_view'),
    (r'^view_with_template_override/', 'view_with_template_override'),
)
 

I trochę testów:

 
# plik testapp/tests.py
 
class WithTemplateTestCase(TestCase):
    def test_views(self):
        response = self.client.get('/app/simple/')
        self.assertTemplateUsed(response, 'testapp/simple_view.html')
 
        response = self.client.get('/app/complex/')
        self.assertTemplateUsed(response, 'testapp/some_other_template.html')
 
        response = self.client.get('/app/complex/', data = {'redirect_me': '1'})
        self.assertEqual(response.status_code, 302)
 
        response = self.client.get('/app/view_with_template_override/')
        self.assertTemplateUsed(response, 'testapp/yet_another_template.html')
 
        from django.contrib.auth.models import User
        user, created = User.objects.get_or_create(username = 'marcink')
        user.set_password('marcink')
        user.save()
 
        self.client.login(username='marcink', password = 'marcink')
        response = self.client.get('/app/authenticated/')
        self.assertTemplateUsed(response, 'testapp/authenticated_view.html')
 

A sam dekorator wygląda tak:

 
import re
 
from django.http import HttpResponse
from django.shortcuts import render_to_response
from django.template import RequestContext
try:
    # functools appeared in Python 2.5
    from functools import wraps, update_wrapper
except ImportError:
    # luckily for us, Django already backported it
    from django.utils.functional import wraps, update_wrapper
 
def with_template(arg):
    """
    A view decorator that handles rendering.
 
    If the view returns a HttpResponse, it is passed intact; otherwise
    the returned value is passed as dictionary to render_to_response.
 
    Usage samples:
 
    @with_template
    def view_func(request, ...):
        return ...
 
    @with_template('custom/template/name.html')
    def other_view_func(request, ...):
        return ...
    """
 
    class TheWrapper(object):
        def __init__(self, default_template_name):
            self.default_template_name = default_template_name
 
        def __call__(self, func):
            def decorated_func(request, *args, **kwargs):
                ret = func(request, *args, **kwargs)
                if isinstance(ret, HttpResponse):
                    return ret
                return render_to_response(ret.get('template_name',
                                                  self.default_template_name),
                                          context_instance=RequestContext(request),
                                          dictionary=ret)
            update_wrapper(decorated_func, func)
            return decorated_func
 
    if not callable(arg):
        return TheWrapper(arg)
    else:
        app_name = re.search('([^.]+)[.]views', arg.__module__).group(1)
        default_template_name = ''.join([app_name, '/', arg.__name__, '.html'])
        return TheWrapper(default_template_name)(arg)
 

Pełen kod można znaleźć tutaj: testowy projekt with_template

Aktualizacja 2008/10/13: zgodnie z sugestią urbana dodałem możliwość przekazania nazwy szablonu jako parametru dekoratora.