30 December 2008

Rafał Jońca na Google App Engine, cz. 2

Obiecałem, że druga część zawierać będzie opis flatpages wykorzystywanych do wyświetlania wszystkich stron poza blogiem.



Sposób działania


Kilku dodatkowych słów wyjaśnienia wymaga system zmiany site-a, który jest sprzeżony ze zmianą języka. Zmiana języka dzięki prostemu middleware pociąga za sobą zmianę identyfikatora SITEID, a tym samym użycie innego flatpage. Oto kod middleware:


class SiteOnLocaleMiddleware(object):

def process_request(self, request):
if request.path.split('/')[0] != "admin":

settings.SITE_ID = settings.LANG_TO_SITE[request.LANGUAGE_CODE]
else:

settings.SITE_ID = 2

Pozostałe elementy mechanizmu flatpages działają jak w standardowym Django, z tym że uległy uproszczeniu, ponieważ i tak musiałem je przerobić, a nie korzystałem ze wszystkich opcji oryginalnego mechanizmu.



Kod



middleware.py, czyli FlatpageFallbackMiddleware



Ten fragment w zasadzie nie zmienił się w porówniu z oryginałem. Zmianie uległa tylko ścieżka importu widoku flatpage, więc nie będę przytaczał kodu.



models.py


Model uległ uproszczeniu i zawiera tylko najbardziej niezbędne dane dotyczące flatpage.


from appengine_django.models import BaseModel

from google.appengine.ext import db

FLATPAGE_TEMPLATES = (u'flatpages/default.html', u'flatpages/portfolio.html', 'flatpages/projects.html')

class FlatPage(BaseModel):
url = db.StringProperty('URL', required=True)

title = db.StringProperty('title', required=True)

content = db.TextProperty('content', default='')
template_name = db.StringProperty('template name', required=True, choices=FLATPAGE_TEMPLATES)

sites = db.ListProperty(db.Key, verbose_name='sites')

def __unicode__(self):
return u"%s (%s)" % (self.url, self.title)

def get_absolute_url(self):
return self.url


Proszę zwrócić uwagę na wiersz sites. To właśnie ListProperty przechowujące jako elementy klucze Key to serce mechanizmu N-M w Google App Engine. Standardowe systemy bazodanowe stosują tabelę pośredniczącą do łączenia tabel głównych. w GAE z racji braku złączeń nie byłoby to zbyt wygodne, więc listę obiektów jednej ze stron umieszcza się po drugiej stronie, najlepiej po tej, która będzie miała mniej połączeń. Jeśli użytkownik miałby należeć do wielu grup, ListProperty umieszczamy po stronie użytkownika i zapamiętujemy klucze grupy, bo w tej sytuacji lista będzie zdecydowanie mniejsza.


DataStore umożliwa filtrowanie elementów poszczególnych list. By więc pobrać wszystkich użytkowników danej grupy, wystarczy pobrać klucz grupy, a następnie zastosować go jako filtr dla pola z listą. Aby pobrać grupy użytkownika, pobieramy pole listy użytkownika, a następnie pobieramy grupy na podstawie kluczy (w DataStore pobieranie na podstawie kluczy jest najszybszą operacją). Pamiętaj, że interfesj DataStore dopuszcza podanie listy kluczy w jednym poleceniu pobierającym!



views.py


Widok to w zasadzie nieco zmodyfikowana i uproszczona wersja oryginału. Oto ona.


from flatpages.models import FlatPage
from django.template import loader, RequestContext

from django.http import HttpResponse, HttpResponseRedirect, Http404
from django.conf import settings

from sites.models import Site

def flatpage(request, url):

"""Flat page view."""
if not url.endswith('/') and settings.APPEND_SLASH:

return HttpResponseRedirect("%s/" % request.path)
if not url.startswith('/'):

url = "/" + url

f = FlatPage.all().filter('url =', url).filter('sites = ', Site.get_current().key()).get()

if f is None:
raise Http404('No FlatPage matches the given query.')

t = loader.get_template(f.template_name)

c = RequestContext(request, {

'flatpage': f,
})
response = HttpResponse(t.render(c))

return response

Jedynym fragmentem godnym zainteresowania i innym od oryginału jest:


f = FlatPage.all().filter('url =', url).filter('sites = ', Site.get_current().key()).get()

if f is None:
raise Http404('No FlatPage matches the given query.')


Zauważ, że filtracja odbywa się zarówno na podstawie adresu URL, jak i klucza site-a, więc zachodzi pierwsza z opisywanych wcześniej filtracji N-M. W odróżnieniu od ORM Django, która dla operacji get() zwraca wyjątek w przypadku nieodnalezienia wpisu, GAE zwraca None, więc obsługa jest tutaj nieco inna niż w oryginale.



admin.py


Ostatni z interesujących plików zawiera dane dla systemu administracyjnego (proszę pamiętać, że to nie jest admin z Django, ale własne rozwiązanie i z racji tego pojawia się inna składnia).


from google.appengine.ext.db import djangoforms

from myadmin.base import BaseAdmin
from myadmin.fields import KeyListPropertyField

from flatpages.models import FlatPage as FlatPageModel
from sites.models import Site as SiteModel

ADMIN_MODELS = (
('FlatPageAdmin', u'Flat page'),
)

class FlatPageForm(djangoforms.ModelForm):
sites = KeyListPropertyField(label='Sites', query=SiteModel.all().order('name'), use_field='name')

class Meta:
model = FlatPageModel


class FlatPageAdmin(BaseAdmin):

class Meta:
change_fields = ['url', 'title']

model = FlatPageModel
form = FlatPageForm

def change(self):

return {
'header': [u'Url', u'Title'],

'data': self._from_object_to_list(FlatPageModel.all().order('title'))

}

Jeśli czytałeś poprzedni wpis, powyższa składnia nie powinna stanowić zaskoczenia. Strona listy nie jest paginowana i pobiera wszystkie wpisy, ponieważ stron statycznych mam w ilości podobnej do liczby palców, więc nie stanowi to problemu.


Tutaj również pojawia się pole formularza obsługujące N-M, które opisałem dokładniej w poprzednim wpisie.



Drobny prezent


Poniżej kod obsługujący system sitemaps z Django. Tak, ten system działa w Google App Engine bez zarzutu, o ile samemu tworzy się podklasy i zapewnia metodę items(). Nie można skorzystać ze standardowej implementacji sitemap dla flatpages.


from weblog.models import Entry

from flatpages.models import FlatPage
from django.contrib.sitemaps import Sitemap

class FlatpageSitemap(Sitemap):
def items(self):
from django.contrib.sites.models import Site

return FlatPage.all().filter('sites = ', Site.get_current().key())

class BlogSitemap(Sitemap):

def items(self):

return Entry.all().order('-pub_date')

def lastmod(self, item):

return item.pub_date

sitemaps = {
'pages': FlatpageSitemap,

'blog': BlogSitemap,
}


Podsumowanie


Strony statyczne Django w wydaniu GAE nie powinny być już dla nikogo tajemnicą. Jak widać, nie ma się czego bać :)


Opis implementacji bloga pojawi się w następnych odcinkach i to nie jednym, bo nie tylko zawiera sporo kodu, ale również najwięcej technik optymalizacyjnych: denormalizacja, tworzenie grup obiektów, pobieranie danych tylko o określonym przodku, paginacja itp. Zapraszam już wkrótce.

19 December 2008

CMS, GAE i admin Django

Założenia

  • tworzymy witrynę o dużym natężeniu ruchu
  • witryna generalnie nie zawiera treści tworzonych przez użytkownika (nie licząc ocen, komentarzy, profilu itp.)
  • za edycję treść odpowiada niewielka grupa osób (CMS)
  • chcemy użyć GAE do zapewnienia sobie skalowalności
  • chcemy użyć admina z Django + dodatki, by szybko utworzyć system CMS
  • mamy również dane statyczne liczone w GB i serwowane w TB

Propozycja

W jednym z poprzednich wpisów przedstawiłem prostego admina w GAE dla tej witryny. Daleko mu jednak do elastyczności choćby admina Django, nie wspominając o rozbudowie i dodatkach. Obecne próby migracji admina Django i uruchomienia go na GAE są coraz lepsze, ale z powodu różnic nie wszystkie elementy można łatwo przenieść.

Z przedstawionych założeń wynika, że CMS nie musi być skalowalny, bo nie jest to system z edycją treści przez użytkownika. Część publiczna w niewielkim stopniu obsługuje dane użytkownika. Za serwowanie dużych danych statycznych może w chwili obecnej odpowiadać Amazon S3 + dodatek w postaci CDN.

Wydaje się, że rozsądnym rozwiązaniem w tym momencie byłoby utworzenie systemu podwójnego:

  • część publiczną obsługuje GAE
  • część prywatną obsługuje własny serwer lub standardowy hosting

Zalety

  • CMS tworzymy w systemie relacyjnym, co daje dużą elastyczność obróbki danych i pełny wachlarz narzędzi admina Django wraz z dodatkami
  • za skalowalność systemu odpowiada Google App Engine, bo do obsługi CMS wystarczy jeden serwer i jedna baza danych (nawet jeśli 4 rdzeniowa)
  • za obsługę dużych danych statycznych odpowiada Amazon S3
  • koszty własnego System Engineering są w zasadzie zerowe, bo wszystko możemy hostować zewnętrznie
  • DataStore w GAE możemy zoptymalizować pod kątem odczytu danych - nie musi ona odpowiadać strukturze na potrzeby CMS

Wady

  • musimy napisać własną warstwę synchronizacji bazy CMS z GAE
  • musimy dwukrotnie napisać modele danych (być może nawet całkiem różne)

Wnioski

Czy warto robić coś takiego, czy jednak zmuszać się do pisania własnego systemu administracyjnego w GAE?

Wydaje mi się, że w tym konkretnym przypadku użycia warto to rozdzielić, bo CMS i część publiczna to cały czas dwie różne sprawy, a tworzenie CMS jest najczęściej powtarzalne aż do bólu. Sytuacja jest inna, gdybyśmy próbowali stworzyć drugie Google Docs lub Facebook, czyli systemy z danymi wypełnianymi głównie przez użytkowników.

16 December 2008

Rafał Jońca na Google App Engine

Obiecałem jakiś czas temu przedstawienie szczegółów działania tej witryny na GAE. Uprzedzam, że kod nie zawsze jest optymalny i ładny, niemniej jak na pierwszą pracę tego typu, spełnia moje potrzeby i jest teoretycznie skalowalny bez przekraczania limitów na pojedyncze żądanie HTTP.



Konfiguracja


System wykorzystuje:
- Google App Engine Helper dla Django
- Django 1.0 jako zip z usuniętymi niepotrzebnymi aplikacjami i całym locale
- pytz, timezones, pygments, code_hilite, markdown; do obsługi timezone, konwersji do HTML i kolorowania składni


Wszystkie pozostałe aspekty są wykonane własnymi siłami, czasem przez przerobienie modeli i kodu z oryginalnego Django (np. flatpages i sites).


To nie jest tutorial, więc nie będę podawał wszystkich kroków, ale raczej fragmenty kodu, problemy itp. Nie opiszę więc, jak utworzyć Django ZIP, jak skonfigurować helpera itp. To wszystko jest opisane gdzieś indziej.



Panel administracyjny


Nie korzystam z panelu administracyjnego Django pod GAE, choć wiem, że są takie próby. Stworzyłem własny, bardzo prosty acz wystarczający panel administracyjny. Zacznę więc od niego, bo w rzeczywistości również powstawał jako pierwszy.


Panel jest chroniony trybem administracyjnym przez GAE bez jakiejkolwiek obsługi użytkowników, grup, uprawnień itp.



Aplikacja Django


Admin jest standardową aplikacją Django, która ze względu na prostotę/lenistwo nie do końca jest łatwa do wielokrotnego użytku, choć początkowo taki był zamysł. Niemniej dodanie obsługi nowych aplikacji jest proste.



myadmin/init.py



Plik ten zawiera informację o aplikacjach do administrowania i ma postać:


ADMIN_APPS = (
('sites', u'Site'),

('flatpages', u'Flatpage'),
('weblog', u'Blog'),

)

Czemu nie wylądowało to w settings.py? Dobre pytanie. Powinno, ale się zasiedziało. Innym od razu radzę wsadzić to tam, gdzie być powinno.



myadmin/fields.py


Plik zawiera dodatkowe pole, dzięki któremu mogę sensownie wyświetlać w formularzu listę wielokrotnego wyboru z kluczami obcymi (referencjami). Domyślnie GAE udostępnia podobne pole, ale tylko dla relacji jeden do wielu. Tutaj stosuję relację wiele do wielu, dla którego obsługę musiałem dodać sam, rozszerzając pole wielokrotnego wyboru z Django.


W GAE unika się tabel pośredniczących dla relacji wiele do wielu jak ma to miejsce w relacyjnych bazach danych. Obsługa tego byłaby często zbyt kosztowna pod kątem zapytań (pamiętajmy, że nie ma złączeń). GAE obsługuje za to umieszczanie jako pola listy elementów i wyszukiwania po nim. Zamiast więc stosować tabelę łączącą, w jednej z tabel umieszczamy listę kluczy drugiej części połączenia. Jeśli mamy tabele stron i witryn, a jedna strona może należeć do wielu witryn, umieszczamy w elemencie strony listę witryn, do których przynależy.


from google.appengine.ext.db import Key    

from django.forms.fields import MultipleChoiceField

class ModelChoiceIterator(object):
def __init__(self, field):

self.field = field
self.query = field.query

def __iter__(self):
for obj in self.query:

yield (obj.key(), getattr(obj, self.field.use_field))

class KeyListPropertyField(MultipleChoiceField):
def __init__(self, query, use_field, *args, **kwargs):

self.use_field = use_field
super(KeyListPropertyField, self).__init__(*args, **kwargs)

self.query = query

def _get_query(self):

return self._query

def _set_query(self, query):

self._query = query
self.widget.choices = self.choices

query = property(_get_query, _set_query)

def _get_choices(self):

return ModelChoiceIterator(self)

def _set_choices(self, value):

self._choices = self.widget.choices = list(value)

choices = property(_get_choices, _set_choices)

def clean(self, value):

new_value = super(KeyListPropertyField, self).clean(value)

return [Key(s) for s in new_value]


Jeśli ktoś nie wie, to kod ten stanowi zmodyfikowaną i uproszczoną wersję ModelMultipleChoiceField z Django.



myadmin/base.py


Ten plik zawiera klasę bazową do generowania opisów obsługi modelu przez admina. Traktuj to jako bardzo, bardzo uproszczoną wersję ModelAdmin z Django. Klasa zapewnia domyślne działanie oraz możliwość jego zmodyfikowania dla tworzenia listy obiektów, formularza, dodawania i usuwania.


from google.appengine.ext import db

class BaseAdmin(object):
"""Base class for all admins."""

def __init__(self, request):

self.request = request

def _from_object_to_list(self, iterator):

results = []
for obj in iterator:

row = [getattr(obj, col) for col in self.Meta.change_fields]

row[0] = (obj.key(), row[0])

results.append(row)
return results

def form(self, *args, **kwargs):

return self.Meta.form(*args, **kwargs)

def edit(self, id):
return self.Meta.model.get(id)

def remove(self, id):
return self.Meta.model.get(id)

def save(self, form, add=False):

form.save()

def delete(self, id):

from google.appengine.api import datastore
datastore.Delete(db.Key(id))


W zasadzie poza odwzorowanie danych na listę i określeniem formularza wraz z modelem nie trzeba nic więcej, by uzyskać prosty system listowania, edycji, dodawania i usuwania wybranego modelu. Przykład wykorzystania klasy bazowej znajduje się pod koniec wpisu.



myadmin/urls.py


W pliku mapowania adresów nie ma nic odkrywczego:


from django.conf.urls.defaults import *

urlpatterns = patterns('myadmin.views',
(r'^/?$', 'index'),

(r'^(?P<app>\w+)/$', 'list'),
(r'^(?P<app>\w+)/(?P<model>\w+)/$', 'change'),

(r'^(?P<app>\w+)/(?P<model>\w+)/add/$', 'add'),
(r'^(?P<app>\w+)/(?P<model>\w+)/remove/(?P<id>\w+)/$', 'delete'),

(r'^(?P<app>\w+)/(?P<model>\w+)/edit/(?P<id>\w+)/$', 'edit'),
)



Szablony i myadmin/models.py


Plik models.py jest pusty. Szablonów generalnie przytaczać nie będę, bo są naprawdę bardzo proste. Pokażę jedynie szablon edycji:


{% extends "admin/base.html" %}

{% block page_title %}{{ block.super }} - {{ application_name }}/{{ model_name }}/Edit{% endblock %}

{% block pc_main_content %}
<h2>Editing {{ application_name }}/{{ model_name }} -- {{ model_data }}</h2>

<form action="." method="post">
{{ form.as_p }}
<p><input type="submit" value="Save"></p>

</form>
{% endblock %}


myadmin/views.py


W odróżnieniu od nowego admina Django zastosowałem standardowe funkcje, a nie klasę. To najbardziej zaawansowana część systemu administracyjnego, a jednocześnie dosyć prosty kod. Najpierw przedstawię kilka funkcji pomocniczych i dekoratora, który pomaga w powtarzających się zadaniach. Część kodu dekoratora została zapożyczona z Django. Znający temat domyślą się, że mówię o końcówce.


from django.views.generic.simple import direct_to_template

from django.http import HttpResponse, HttpResponseRedirect, Http404
from django.template import loader, RequestContext

from myadmin import ADMIN_APPS

def _get_id_by_name(list_, element):

for id, elem in enumerate(list_):
if elem[0] == element:

return id

def _get_admin_for_app(app):
return __import__('%s.admin' % app).admin

def _get_model_for_app(app, model):
app_module = _get_admin_for_app(app)

return getattr(app_module, model)

def admin_decorator(f):
def wrapper(*args, **kwargs):

result = f(*args, **kwargs)
if isinstance(result, tuple):

request, app, model, context = result
else:

return result


ADMIN_MODELS = _get_admin_for_app(app).ADMIN_MODELS

dictionary = {
'apps': ADMIN_APPS,
'current_app': app,

'application_name': ADMIN_APPS[_get_id_by_name(ADMIN_APPS, app)][1],
'current_model': model,

'model_name': ADMIN_MODELS[_get_id_by_name(ADMIN_MODELS, model)][1],
}

if context is None: context = {}

for key, value in context.items():
if callable(value):

dictionary[key] = value()
else:
dictionary[key] = value

c = RequestContext(request, dictionary)
t = loader.select_template([

'admin/%s/%s_%s.html' % (app, model, f.__name__),

'admin/%s/%s.html' % (app, f.__name__),

'admin/%s.html' % f.__name__
])
return HttpResponse(t.render(c))

return wrapper

Dekorator odpowiada za przygotowanie standardowych danych dostępnych w szablonach widoków edycyjnych, a także umożliwia przesłonięci szablonów, gdyby okazało się to niezbędne dla któregokolwiek z modeli i aplikacji.


Właściwe funkcje do najkrótszych nie należą, ale nie ma tam raczej nic, co mogłoby zaskoczyć osobę znającą Django. Nie zawierają elementów specyficznych dla GAE. To jedynie kod CRUD w wersji specyficznej dla mojego admina.


def index(request):

"""Start page of admin with list of applications."""
return direct_to_template(request, 'admin/index.html', {'apps': ADMIN_APPS})


def list(request, app):
"""List of models for one application that have admin pages."""
return direct_to_template(request, 'admin/list.html', {

'current_app': app,
'application_name': ADMIN_APPS[_get_id_by_name(ADMIN_APPS, app)][1],

'models': _get_admin_for_app(app).ADMIN_MODELS,
'apps': ADMIN_APPS

})

@admin_decorator
def change(request, app, model):

"""List of objects for one application model."""
admin_cls = _get_model_for_app(app, model)
admin_obj = admin_cls(request)

return request, app, model, {
'admin_object': admin_obj,

'model_data': admin_obj.change(),
}

@admin_decorator
def edit(request, app, model, id):

"""Shows the edit form for model and saves the model."""
admin_cls = _get_model_for_app(app, model)
admin_obj = admin_cls(request)

object = admin_obj.edit(id)
if object is None:

raise Http404()

if request.method == 'POST':

form = admin_obj.form(request.POST, instance=object)

if form.is_valid():
admin_obj.save(form)
return HttpResponseRedirect('../../')

context = {'form': form}
else:
context = {'form': admin_obj.form(instance=object)}

context.update({
'admin_object': admin_obj,
'model_data': object

})
return request, app, model, context

@admin_decorator
def add(request, app, model):
"""Shows the add form for model and saves the model."""

admin_cls = _get_model_for_app(app, model)
admin_obj = admin_cls(request)

if request.method == 'POST':
form = admin_obj.form(request.POST)

if form.is_valid():
admin_obj.save(form, add=True)

return HttpResponseRedirect('../')
context = {'form': form}

else:
context = {'form': admin_obj.form()}

context.update(admin_object=admin_obj)
return request, app, model, context

@admin_decorator
def delete(request, app, model, id):

"""Shows the remove form for model and removes the data on POST."""
admin_cls = _get_model_for_app(app, model)
admin_obj = admin_cls(request)

object = admin_obj.remove(id)
if object is None:

raise Http404()

if request.method == 'POST':

admin_obj.delete(id)
return HttpResponseRedirect('../../')

context = {
'admin_object': admin_obj,
'model_data': object

}
return request, app, model, context



Obsługa witryn (Sites)


Na zakończenie przykład użycia panelu administracyjnego na przykładzie najprostszego ze stosowanych modeli, czyli obsługi witryn.



sites/models.py


Poniższy kod to w zasadzie przerobiona i zmodyfikowana na potrzeby GAE wersja modelu Sites z Django. Zmienione zostały obiekty pól, nazwy metod zapisujących (put() zamiast save()). To jednak za mało...


W Django bardzo dużo aplikacji korzysta z mechanizmu witryn, między innymi sitemap, RSS i flatpages. To ostatnie i tak musiałem utworzyć własne, więc nie był to znaczący problem, ale dwóch pierwszych nie chciałem modyfikować. Z tego powodu model zawiera kilka dodatkowych elementów:




  • element objects udający menedżera i zapewniający metodę get_current()




  • dodanie metody get_current() jako metody statycznej klasy,



  • dodanie w module obiektu RequestSite -- wymaga tego jeden z importów w aplikacji dla RSS




  • wpis Site._meta.installed = True bez którego mechanizm RSS rozkłada się jak długi -- modele z GAE nie stosują klasy Meta!


Oto i cały kod:


from appengine_django.models import BaseModel

from google.appengine.ext import db

SITE_CACHE = {}

class RequestSite(object):

def __init__(self, request):
pass

class Manager(object):

def get_current(self):
return Site.get_current()

class Site(BaseModel):
id = db.IntegerProperty('site identifier', required=True)

domain = db.StringProperty('domain name', required=True)

name = db.StringProperty('display name', required=True)

objects = Manager()

def __unicode__(self):

return self.name

def put(self):
try:

del(SITE_CACHE[self.id])
except KeyError:
pass
super(Site, self).put()

def delete(self):
id = self.id

super(Site, self).delete()
try:
del(SITE_CACHE[id])

except KeyError:
pass

@classmethod
def get_current(cls):

"""
Returns the current ``Site`` based on the SITE_ID in the
project's settings. The ``Site`` object is cached the first
time it's retrieved from the database.
"""
from django.conf import settings

try:
sid = settings.SITE_ID
except AttributeError:

from django.core.exceptions import ImproperlyConfigured
raise ImproperlyConfigured("You're using the Django \"sites framework\" without having set the SITE_ID setting. Create a site in your database and set the SITE_ID setting to fix this error.")

try:
current_site = SITE_CACHE[sid]
except KeyError:

current_site = cls.all().filter('id =', sid).get()

SITE_CACHE[sid] = current_site
return current_site

@classmethod

def clear_cache(cls):
"""Clears the ``Site`` object cache."""
global SITE_CACHE
SITE_CACHE = {}

Site._meta.installed = True


sites/admin.py


Pozostało już tylko zdefiniować moduł admin.py zapewniający obsługę aplikacji w panelu administracyjnym. Zauważ, że choć wymaganego kodu jest więcej niż przy standardowym Django, to jednak nadal niewiele.


from google.appengine.ext.db import djangoforms

from myadmin.base import BaseAdmin

from sites.models import Site as SiteModel

ADMIN_MODELS = (

('SiteAdmin', u'Site'),
)

class SiteForm(djangoforms.ModelForm):

class Meta:
model = SiteModel

class SiteAdmin(BaseAdmin):

class Meta:
change_fields = ['id', 'name', 'domain']

model = SiteModel
form = SiteForm

def change(self):

return {
'header': [u'Id', u'Name', u'Domain'],

'data': self._from_object_to_list(SiteModel.all().order('name'))

}


I to by było na tyle


Na tym zakończę pierwszą część. W części drugiej pokrótce przedstawię strony statyczne (flatpages), które działają dwujęzycznie. Nie ma tu wielkiej magii, ale warto pokazać system wiele do wielu w wydaniu GAE.

15 December 2008

Kod, refaktoryzacja i banki

Wszyscy od dłuższego czasu obserwujemy efekty działania niektórych amerykańskich banków. Znamy też powód tej finansowej czkawki, którym jest zaklinanie rzeczywistości, ale od początku...

Jesteśmy bankiem, który radzi sobie jako tako, ale jako zarządzający chcemy mieć się czym pochwalić, więc zaczynami szukać nowych możliwości. Ludzi o dobrej zdolności kredytowej szybko nie przybędzie, więc może by tak pójść na skróty i udzielać kredytów coraz mniej pewnym. Generujemy genialne wyniki sprzedaży, ale na tym nie kończymy, bo z racji dobrych wyników, nakręcamy spiralę i udziałowcy chcą jeszcze więcej. Chcemy zgarnąć premię, więc robimy jeszcze więcej. Kogo to obchodzi, że wszystko jest ładne tylko na papierze. Premia na koncie jest już teraz. Potem pojawia się jeden niespłacający, potem następny, załamanie na rynku i panika... a wystarczyło pielęgnować swoje instrumenty finansowe i pamiętać, że chęć ogromnych zysków to i ogromne ryzyko. Niby w świecie finansjery znane nawet początkującym, ale jak to bywa w rzeczywistości mogliśmy się przekonać.

Teraz przejdźmy do naszego podwórka, czyli programowania i kodu, bo kto powiedział, że musimy uczyć się tylko na błędach we własnej branży ;)

Mamy niewielki, stabilny i dobrze napisany kod, ale pojawia się okazja do ogromnego zarobku po dodaniu kilku ficzerów w krótkim czasie. Dodajmy je i zbieramy forsę, ale czasu na posprzątanie w kodzie już nie było. Spokojnie -- myślimy sobie -- zrobimy to później. Ale nie ma później, przychodzi inny klient, kładzie forsę, a my chcemy ją dostać i... dorabiamy nowe ficzery. Idzie nieco trudniej, ale dajemy jakoś radę. Sytuacja potarza się ze 2 lub 3 razy. Udziałowcy zachwyceni, ale na kod się już patrzeć nie da, bo błaga o przepisanie. Przychodzi kolejny klient, kładzie forsę, podaje termin, zgadzamy się i... nie dajemy rady. Lecimy z karą, błędami, nowi klienci zaczynają nas omijać, bo jesteśmy niepewni i dopiero co startująca konkurencja bez bagażu zrobi to za pół ceny w 1/3 czasu. Czas zwijać interes lub sporo wydać na przebudowę własnego systemu. Na zastrzyk ze strony rządu raczej nie mamy co liczyć, oprogramowanie to nie bank.

A wystarczyło być szczerym z klientem i samym sobą, czyli zapewnić wysoką jakość kodu po projekcie, nie rzucać się na każdą okazję, a jeśli nawet to dobrze wyliczyć zyski i straty. Brak refaktoryzacji zmniejsza naszą szybkość reakcji w przyszłości, czyli nasze przyszłe zyski. Oczywiście nie mówię tu o przesadnej refaktoryzacji, bo określone pieniądze z projektów już teraz są również bardzo ważne, ale o równowadze. Drobne opóźnienia taktyczne w zapewnieniu wysokiej jakości są poprawne, jeśli jesteśmy tego świadomi, znamy konsekwencje i chcemy to później szybko spłacić. Nie wierzmy tak jak niektórzy prezesi banków, że magia istnieje i konsekwencji nie będzie.

11 December 2008

Scrum - visibility

Scrum has much to offer when it comes to project visibility, especially when the only thing you need is just Google Spreadsheet or Microsoft Excel and 5 minutes a day for an update.

Product Backlog and Release Burndown

First element of visibility is the Product Backlog available to everyone. It not only tells what needs to be done in next months or years, but it also shows the cost (relative amount of work as estimates) of each story (feature). Having that information and also the team velocity (in Story Points), you can create Release Burndown.

The Release Burndown shows when the features will be completed (using current priority order). If you add or remove story or change estimates, you can easily see the change in date. In Scrum we agree on the constant, high quality, so if the team is exactly the same each time (it should be), variation in team speed per sprint shouldn't be big. It is a very powerful tool. In standard watefallish solutions you do not know that you are late until half or more of the project is done. In Scrum you know it more or less just after first Sprint and you can take steps to hit the deadline (dropping some features, hiring another team, but not dropping the quality).

Sprint Backlog and Sprint Burndown

Similar situation is with Sprint Backlog. Within a Sprint you know exactly what you will be doing, what is your goal and general effort needed to accomplish the goal. Every day on Daily Scrum the remaining effort is updated, so each day the team and the whole company know if everything is OK or there is something to worry about. You cannot and should not hide any problems. Everything is visible. If for example there is no progress by 2 or 3 days, the CEO can come and ask what is the problem (probably earlier Scrum Master will inform people outside the team about the problem), and how he can help. Clear visibility and building trust, no problem hiding.

The Sprint Burndown updated daily is the visual sign of the progress. It is so simple that anyone can understand it. Updating it is as simple as counting work remaining at current day and draw a line (or let the Excel do it).

The benefits

  • CEO/CTO is happy because he can watch the progress every day and react quickly when something is wrong.
  • Product Owner is happy because he has an easy tool to see then the work will be completed. He can also experiment with priorities and with what if (for example adding a second team or removing a story). He instantly (just after estimating) see the impact on date or cost of new stories. Everything without expensive tools - even Google Spreadsheet in browser can do that).
  • The team know where it is within the sprint and what is on the horizon.
  • The Scrum Master has good historical and current data about the speed and can defend the team from client asking to work faster (the client do not know that asking for lower quality by rushing things is like shooting in own foot in long run).

9 December 2008

Scrum - the Pull System

In my previous post Introduction to Scrum I said that the team estimates the effort that is needed to complete the stories and he choose amount of work to be done in the next sprint. Please notice that it is uncommon in many projects.

In standard project the client says on which date he wants the project to be completed, shows the needed functionality and manager tries to slice the requirements and assign them to the exact developers - you will be doing this, then this and then that. He will probably ask about how much it take but if he won't be satisfied with the answer, he will ask you to work harder, be smarter, stay in office longer... whatever to get the project map not looking as something impossible. To see what is wrong with that approach, ask several questions:

  • Do you think in this approach the quality of produced code will be on the constant, high level?
  • Do you think the developer will feel satisfaction of his work, will be really motivated to do it right and will do the work in optimal way?
  • Do you think that developers will be smarter, especially working long hours?
  • Do you think the client will be satisfied that you silently drop the quality or when this is an internal project that you are mining the well being of the company in the long run?

I guess you have answered NO to many of those questions, so why so many projects still not using pull system? Hard question. I'm not going to answer it now. I'll just show the benefits of the pull system in Scrum.

  • If someone says that he will do something he is more motivated to do it than when someone else will say that he must do it.
  • Client or Product Owner must explain why he needs the story, so he must know what he wants. If developers will say that they still do not understand what he wants, they can choose another story. This way only well prepared stories will be done in the next sprint ensuring less slowdowns and questions in sprint.
  • The quality will be constant because the only way for client to get more done is to drop functionality, rearrange stories or pay for another team.
  • The team will work in sustainable peace, so no burn-out.
  • Everyone in the team is responsible for selected work, because they all agreed on it, so better team work, more knowledge sharing etc.
  • You know at the beginning of the sprint that something cannot be done, not in the middle or at the end.

Now we can co back to hard question. Why so many projects still do not use the pull system?

  • They think that dropping quality has no cost.
  • They think that development is a defined process (assembly line). In reality it is empirical process (uses creativity).

Imagine you want to order a painting, a complicated and big one. You also have limited amount of money and time. You go to a good painter and tell him the impossible deadline. Do you want him to silently agree and after some time give you a picture but in reality a crap (because doing it right is not possible in mentioned timeframe) or want him to say to you at the beginning that it is impossible and ask for change of requirements or deadline?

Do it for your projects too...

8 December 2008

Introduction to Scrum

Before we even start to talk about Scrum (it is not a acronym!), let me introduce the word agile. Many people nowadays heard about it, many say and think they are using it, but not everyone really know what it means to be agile. That's why this introduction is so important. Word agile and the movement started in 2001. Almost everyone in the IT industry have seen or read the agile manifesto text. It is shown on many occasions (presentations, workshops, articles), because it is short and simple. That is why many think that it is everything they need and it really hurts the agility.

If you want to think about agility, please read at least the agile principles. They give you better understanding that agile is not near cowboy coding and loosely management and working software in manifesto is about something that really (not barely) works. If you read it, you will find many places where it informs not only about management and interaction, but also about quality of code. When you get to know Scrum, you will see how well it aligns with the manifesto and principles. Also notice that Scrum alone is not addressing all principles. For some of them you need to get a closer look at the best engineering practices. That is OK, because Scrum is just a framework, not a silver bullet. It do not contain answers to everything you want to fix, because solution to the problem depends on who is asking.

Scrum in the nutshell

First, good news. Scrum is simple. Second, bad news. It is not easy. Many have fallen implementing the simple rules mentioned below. It is easy to explain, but very hard to get working right. You may also think that because it is simple, how much power it may have. A lot! Some companies stopped stopped to use it because they couldn't stand the disfunctions it was exposing all the time. You have to have a real desire to change. If you do it, you can count on at least 2-3 productivity gains and be driven by business value.

The elements

Scrum uses only three roles: Team, Scrum Master (SM) and Product Owner (PO); two lists: Product Backlog (PB) and Sprint Backlog (SB; there may also be a Impediment Backlog); and three meetings: Sprint Planning, Daily Scrum and Sprint Review. There is also two graphs: Sprint Burndown and Product Burndown. Simple?

Lists

Product Backlog is a prioritised list of stories (functionalities and non-functional requirements). Each story is a short description of the functionality, tells why it is needed and contains work effort estimates done by the team. Everyone can add a story to it but only PO is responsible for prioritisation.

Sprint Backlog is a list of stories and tasks selected by the team to be done in the current iteration (named sprint).

Roles

Product Owner is responsible for the Product Backlog (PB), its proper prioritisation and good shape. It is a client or its representative.

Scrum Master facilitates all meetings, teaches PO and the team Scrum rules and shields the team from interruptions. Also enforces compliance with the rules (for example to not show on demo something that is not really done).

Team is self-organising, cross-functional group that takes the stories and produces working software every sprint.

The cycle

The sprint takes from 1 to 4 weeks. It begins with Sprint Planning meeting consisting of two parts: on first, PO explains the stories and if they are not estimated, the team do the estimation, PO may reorder the PB seeing estimates. This first part ends with the team selecting the stories to be done on sprint. This takes no more than 2-4 hours. On the second part of the meeting the team breaks the stories in to smaller tasks. It also should take longer than 2-4 hours.

On every day of the sprint there is a Daily Scrum meeting taking max 15 minutes on which every team member answer three questions: - What did he do yesterday? - What is going to do today? - Does he have any problems?

Client or his representative is more or less available for questions or explanations within sprint, because some things may need clarification. In general you do not want to create the document for something that can be shown or explained when talking.

The sprint ends with Review Meeting that takes no more than 4 hours. The meeting also consist two parts: on first, the teams shows what accomplished, but it is always only a done, working software (no presentations). The PO at this meeting can ask for changes etc. that are transformed into new stories. The second part is the retrospective when the team is inspecting his behaviour and thinks about ways to improve itself.

That is all folks!

Simple, isn't it? You may start thinking if it works because of its simplicity. Yes, exactly because of the simplicity. Do you really think that 200 pages of rules work better? I do not think so. Also because everything is simple and not constrained there is much room for the inspect & adapt.

In the next entry I'll start to explain on some practical examples why it works. I'll also try to show some examples of waterfall thinking that stops you from being agile. Waterfall thinking is much harder to stop than waterfall process, so it is many times lurking in your mind any you may not even be aware of it.

7 December 2008

PHP goni inne języki

Dzisiaj natknąłem się na artykuł o nowościach w PHP 5.3. Dobrze, że udało im się przepchnąć dwa istotne elementy (przestrzenie nazw i funkcje lambda/domknięcia). Postęp znaczący! Problem jednak w tym, że te slogany ładnie wyglądają na papierze, ale niestety czasem sprawią problemy, bo diabeł tkwi w szczegółach. Dwa przykłady:

  • importowanie przestrzeni nazw bez przedrostków działa dla klas, ale nie dla funkcji i stałych :(
  • funkcje lambda są zmiennymi ($ na początku), ale standardowe funkcje tak nie mają, więc jest ewidentne rozdwojenie wymuszające pisanie $test() zamiast test() :(

Może wyglądam na czepialskiego, ale to elementy mocno przeszkadzające, jeśli chce się na przykład utworzyć spójną bibliotekę o czystym API.

Funkcje lambda i domknięcia w Pythonie 2.x (na pocieszenie coś, czego w PHP jeszcze nie ma):

# Obiekty mogą zachowywać się jak funkcje...
# Pojawi się w PHP 5.3 (metoda __invoke)
class FunClass(object):

def __init__(self):
self._times_called = 0

def __call__(self, test1, test2):
self._times_called += 1
return test1 + test2

fun2 = FunClass()
print fun2._times_called
print fun2(1, 1)
print fun2._times_called

# Funkcje obsługują domknięcia...
# Pojawi się w PHP 5.3, jako dodatek do funkcji lambda.
def fun3(test1):
def fun3a(test2):
return test1 + test2
return fun3a

real_fun3 = fun3(3)
print real_fun3(5)

# Funkcje mogą mieć właściwości...
#
def fun1(test1, test2):
return test1 + test2

fun1.safe = False

if getattr(fun1, 'safe', False):
print fun1(1, 1)
else:
print "%s is not safe" % fun1.__name__

PS. Znających Pythona proszę o pominięcie ciszą stwierdzenia z artykułu, że "The biggest benefit namespaces will provide is shortening of long classnames.".