11 January 2009

Blog w Google AppEngine, cz. 2

W części drugiej przedstawię widoki związane z blogiem. W odróżnieniu od modeli, zawarta tutaj logika jest znacznie prostsza (poza paginacją). W pewnych sytuacjach mogłaby być prostsza, ale przerabiają blog z oryginalnego Django chciałem zachować URL-e i interfejs z widoków generycznych korzystających z dat.


Poniżej wymagane importy dla views.py, których raczej tłumaczyć nie trzeba...


from google.appengine.ext import db

from weblog.models import Entry, Comment, EntryArchive, Tag, Comment

from weblog.forms import CommentForm
from django.views.generic import date_based
from django.http import Http404, HttpResponseRedirect

from django.views.generic.list_detail import object_list
from django.views.generic.simple import direct_to_template
from django.views.decorators.http import require_POST

from django.utils.translation import ugettext_lazy as _, ugettext
from django.views.decorators.cache import cache_page

import datetime, time

Wyświetlenie głównej strony z 5 ostatnimi wpisami raczej nie odstaje niczym od standardowego Django...


@cache_page(60)
def entry_archive_index(request):

latest = Entry.all().order('-pub_date').fetch(5)

return direct_to_template(request, 'entry_archive.html', {'latest':latest})


Wiele stron w tym przykładzie stosuje bardzo krótki cache pełnostronicowy. Przy tak niewielkim ruchu ma to nawet działanie odwrotne do założeń cachowania (więcej zapisów niż odczytów). Niemniej warto być przygotowanym na większy ruch.


Wyświetlanie listy miesięcy z wpisami w danym roku używa modelu EntryArchive i faktu, że filtr nierówności działa jak przechodzenie po słowniku. Ponieważ lista może mieć maksymalnie 12 wpisów, można ją pobrać bez martwienia się. Pamiętaj, że standardowe Django w tym miejscu używa sztuczek SQL do pobrania danych. My dzięki denormalizacji mamy ułatwiony odczyt...


def entry_archive_year(request, year):

year = int(year)
date_list = [datetime.date(int(row.year_month[0:4]), int(row.year_month[4:6]), 1)

for row in EntryArchive.all().filter('year_month >=', str(year)).filter('year_month <', str(year+1)).order('year_month').fetch(12)]

if len(date_list) == 0:
raise Http404

return direct_to_template(request, 'entry_archive_year.html',
{'date_list': date_list, 'year': year})


Pobranie wszystkich wpisów w danym miesiącu wymaga sprawdzenia, czy taki miesiąc istnieje w EntryArchive. Jeśli istnieje pobieramy 100 wpisów (raczej więcej wpisów w miesiącu mieć nie będę ;), których miesiąc jest przodkiem (kluczem nadrzędnym). Tutaj można by wprowadzić paginację, ale tego nie zrobiłem, bo średnia liczba wpisów w miesiącu jest niewielka, a prezentuję tylko ich skróty...


@cache_page(60)
def entry_archive_month(request, year, month):

archive = EntryArchive.get_by_key_name("_%s%s" % (year, month))

if archive is None:
raise Http404

object_list = Entry.all().ancestor(archive).order('-pub_date').fetch(100)

return direct_to_template(request, 'entry_archive_month.html',
{'object_list': object_list, 'month': datetime.date(int(year), int(month), 1)})


Poniższy kod pobierający konkretny wpis mógłby być prostszy, ale chciałem dopasować się do sposobu działania oryginalnego Django, czyli dany slug może pojawić się wielokrotnie, o ile nie jest to ten sam dzień. Ponieważ wyrażenie regularne nie sprawdza wszystkich możliwych sytuacji z datami, kod najpierw waliduje datę, a dopiero później poszukuje wpisu z daną datą i slugiem. Dla pobranego wpisu pobieram jego komentarze, stosując ograniczenie po kluczu przodka...


@cache_page(60)
def entry_detail(request, year, month, day, slug):

try:
date = datetime.date(*time.strptime(year+month+day, '%Y%m%d')[:3])

except ValueError:
raise Http404

object = Entry.all() \
.filter('pub_date >=', datetime.datetime(date.year, date.month, date.day)) \
.filter('pub_date <=', datetime.datetime(date.year, date.month, date.day, 23, 59, 59)) \
.filter('slug =', slug).get()

if object is None:
raise Http404

comment_list = Comment.all().ancestor(object).order('-date').fetch(100)

return direct_to_template(request, 'entry_detail.html', {
'object': object,

'comment_list': comment_list,
})

Pobieranie wpisów dla danego tagu to najbardziej zaawansowany widok. Po pierwsze sprawdzam, czy istnieje tag o podanej nazwie. Mógłbym po odnalezieniu taga wyświetlić użytkownikowi łączną liczbę wpisów (licznik) do poinformowania o łącznej licznie podstron paginacji. Następnie tworzę zapytanie limitujące po określonym tagu. Zauważ, że wpis zawiera listę tagów jako teksty, ale ponieważ jest to specjalny typ listy, umożliwia on stosowanie jego zawartości do limitowania. Mając wpisy z listami ['django', 'python'] i ['django', 'www'], mogę podać, że chcę pobrać wszystkie wpisy zawierające na liście tagów django.


Widok wykorzystuje paginację, która do działania potrzebuje tylko i wyłącznie informacji o dacie publikacji (w DataStore stosowanie offsetów jest mocno ograniczone i niewydajne). Muszę uprzedzić, że poniższy kod paginacji, choć działa, to nie jest najefektywniejszy w przypadku cofania się. Istnieją również inne sposoby tworzenia paginacji. Nowsze rozwiązanie stosujące dodane w listopadzie __key__ zostało opisane tutaj. Moje podejście wymaga unikatowej właściwości daty (co nie jest problemem, bo nie dodaję dwóch wpisów w tej samej sekundzie).


Ogólnie pomysł na paginację jest prosty. Zawsze pobieramy o jeden element więcej niż chcemy wyświetlić. W ten sposób wiemy, że jest jeszcze następna strona. Do generowania adresu następnej serii wykorzystujemy unikatowy identyfikator tego dodatkowego elementu. Po przejściu do nowej serii dodajemy warunek pobierający wpisy od podanego identyfikatora. Adresy wstecz sprawiają nieco więcej problemów. Ponieważ mój sposób nie jest najefektywniejszy (pobiera w pewnej sytuacji 11 wpisów, choć paginacja jest po 5 elementów) i obecnie na Google Groups znajdują się lepsze, nie będę go tu dokładnie opisywał. Pozostawiam jednak sam kod, bo nie jest złożony. Jeśli to możliwe, w przypadku paginacji w GAE warto posiłkować się Ajaksem do pobierania dalszych podstron (patrz chociażby panel administracyjny Google App Engine), bo to najefektywniejsze rozwiązanie.


@cache_page(60)
def entry_tagged_object_list(request, tag):

PAGE_LIMIT = 5

tag = Tag.get_by_key_name(tag)

if tag is None:
raise Http404

entry_list = Entry.all().filter('tags =', tag.name)

if tag.counter > PAGE_LIMIT:
is_paginated = True

cur_page = request.GET.get('page', None)

prev_page = request.GET.get('prev', None)

if cur_page:
try:
cut_point = datetime.datetime.fromtimestamp(int(cur_page))

entry_list = entry_list.filter('pub_date <=', cut_point)
except ValueError:

is_paginated = False
if prev_page:
has_previous = True

previous = prev_page
else:
tmp = Entry.all().filter('tags =', tag.name).filter('pub_date >', cut_point).order('pub_date').fetch(PAGE_LIMIT)

if len(tmp) > 0:
previous = int(time.mktime(tmp[-1].pub_date.timetuple()))+1 # To commodate fractions of seconds

has_previous = True
else:
is_paginated = False


entry_list = entry_list.order('-pub_date').fetch(PAGE_LIMIT+1)

if len(entry_list) > PAGE_LIMIT:
extra = entry_list.pop()

has_next = True
next = int(time.mktime(extra.pub_date.timetuple()))+1 # To commodate fractions of seconds

prev = int(time.mktime(entry_list[0].pub_date.timetuple()))+1 # To commodate fractions of seconds

else:
has_next = False

return direct_to_template(request, 'entry_list.html', locals())


Poniższy widok generuje listę tagów wraz z liczbą wpisów, w których się znajdują...


def all_entry_tags_list(request):
return direct_to_template(request, 'tag_list.html', extra_context={'tags_list': Tag.all().order('name')})


Ostatni fragment kodu odpowiada za zapis komentarza. Niewiele tu z GAE, więc bez dodatkowych opisów...


@require_POST
def comment_preview(request, *args):
form = CommentForm(request.POST)

if form.is_valid():
if 'post' in request.POST:

Comment.post(**form.cleaned_data)
return HttpResponseRedirect('../'+ugettext('posted')+'/')

context = form.cleaned_data
else:
context = {}

context.update({'comment_form': form})
return direct_to_template(request, 'comment_preview.html', extra_context=context)



Podsumowanie


Na tym kończę opis tworzenia niniejszej strony na GAE. Mam nadzieję, że lektura okazała się dla niektórych pomocna i pozwoli uniknąć chociaż niektórych pułapek AppEngine.


Choć ten blog został napisany z użyciem Google App Engine Django Helper, to na przyszłość polecam App Engine Patch. Ten drugi projekt ma znacznie więcej i aktywnie się rozwija, w odróżnieniu od Helpera. App Engine Patch użyję w następnym projekcie dla GAE. Być może i ten blog zostanie przeportowany, bo w dniu wczorajszym App Engine Patch doczekało się uproszczonego admina z Django (oczywiście z podobnymi ograniczeniami co wcześniejszy patch na grupie dyskusyjnej).

10 January 2009

Blog w Google AppEngine

Jak w Google AppEngine napisać własny blog z pomocą Django? Czytaj poniżej. To przedostatnia część cyklu. Przedstawię w niej modele, a w ostatniej widoki. Szablony i część administracyjną pominę, bo szablony nie zawierają nic odkrywczego, a prostego admina omówiłem we wcześniejszych częściach serii.



Modele


Zacznę od importów modeli:


from appengine_django.models import BaseModel

from google.appengine.ext import db
import datetime
from django.core import urlresolvers

from django.conf import settings


Tagi (kategorie)


Niniejszy blog stosuje bardzo prosty system tagów, ale samo zagadnienie w GAE nie należy do najprostszych. Z powodów wydajnościowych i architektonicznych (brak agregacji) nie możemy w DataStore trzymać zwykłej tablicy wiele do wielu między wpisami i tagami, a następnie zliczać statystykę tagów (kategorii). Całość zliczeń, dodawania i usuwania kategorii musi odbywać się w momencie dodawania lub usuwania wpisów. Innymi słowy, coś co w większości załatwia za nas standardowe Django lub aplikacja tagging, tutaj musimy wykonać sami. Na plus można jednak zaliczyć bardzo łatwe pobieranie danych dla bocznego podsumowania. Własny tag wykonujący to zadanie przedstawię na samym końcu.


class Tag(BaseModel):

name = db.StringProperty(required=True)
counter = db.IntegerProperty(default=0)

@classmethod
def most_popular(cls, num=5):
return Tag.all().order('-counter').fetch(num)

@classmethod
def find_differences(cls, old, new):

old_s = set(old)
new_s = set(new)

added = list(new_s - old_s)
removed = list(old_s - new_s)

return added, removed

@classmethod
def split_tags(cls, tags):

return tags.split()

@classmethod
def update_tags(cls, added=[], removed=[]):

def txn(name, inc):
tag = cls.get_by_key_name(name)

if inc:
if tag is None:
tag = cls(key_name=name, name=name)

tag.counter += 1
tag.put()
else:

if tag is None:
return
tag.counter -= 1

if tag.counter <= 0:
tag.delete()

else:
tag.put()

for name in added:

db.run_in_transaction(txn, name, True)
for name in removed:

db.run_in_transaction(txn, name, False)

Pierwsze wyjaśnienie dla osób znających Django: modele Google AppEngine nie implementują sygnałów, więc update_tags jest wywoływane przez funkcję zapamiętania wpisu.



Model Tag istnieje tylko i wyłącznie po to, by zliczać wystąpienia tagów w celach statystycznych i móc pobierać listę tagów. Każdy tag przechowuje licznik wystąpień. Opis metod:




  • most_popular -- zwraca obiekty najbardziej popularnych tagów (używane przez pasek boczny)



  • find_differences -- informuje o różnicach w tagach między nowym i starym wpisem (stare i nowe tagi są przekazywane jako listy)



  • split_tags -- rozbija pole tekstowe na tagi (jest to osobna funkcja, bo w zamyśle miała być bardziej zaawansowana, np. obsługa cudzysłowów dla tagów wielowyrazowych)



  • update_tags -- aktualizuje liczniki tagów, dodaje nowe lub usuwa stare tagi w miarę potrzeb



Nie chcę tutaj wyjaśniać sposobu działania transakcji w DataStore, ale muszę zaznaczyć, że przy blogu dla jednej osoby edytującej powyższe transakcje są nieco nadmiarowe. Generalnie jednak inkrementacja/dekrementacja liczników w DataStore powinna zawsze następować w transakcji w podany powyżej sposób.


Zauważ, że nazwa taga trafia również jako klucz (key_name), bo klucz to jedyny w DataStore system zapewniania unikatowości. Co więcej, ułatwia on również pobieranie danych w przyszłości. Ponieważ kod był pisany przed wprowadzeniem właściwości __key__ w wyszukiwaniu, nazwa jest pamiętana dwukrotnie. W nowszym kodzie, zapamiętując nazwę w kluczu moglibyśmy pominąć osobną właściwość nazwy.



Wpisy


Kod wpisów to kolejna dawka kilku sztuczek, ale tylko część z nich jest specyficzna dla Google App Engine.


Bardzo malutka klasa EntryArchive pełni dosyć sporą rolę. Po pierwsze tworzy listę miesięcy zawierających wpisy dla bocznego panelu (pamiętaj, że w DataStore nie ma agregacji, a co dopiero agregacji po części daty). Po drugie służy jako klucz nadrzędny do wpisów w danym miesiącu co pozwala na jednym z widoków efektywnie pobierać wszystkie wpisy z danego miesiąca i paginować po nich. DataStore dopuszcza tylko jeden warunek nierówności. Używam go w trakcie paginacji, więc by ograniczyć się do jednego miesiąca i nie stosować dodatkowych zapytań, używam ograniczenia po przodku. Tworząc w DataStore obiekt możemy określić jaki inny klucz ma być jego przodkiem. Standardowo takie grupy ze wspólnym przodkiem tworzy się na potrzeby transakcji, ale można ich również użyć do obejścia pewnych ograniczeń GAE.


Poniżej przedstawiam kod, który mam nadzieję nie okaże się zbyt trudny do zrozumienia, niemniej dokumentacją Google App Engine warto mieć w tym momencie przy sobie ;)


class EntryArchive(BaseModel):
year_month = db.StringProperty(required=True)

class Entry(BaseModel):
title = db.StringProperty(required=True)

slug = db.StringProperty(required=True)
text = db.TextProperty(required=True)

text_html = db.TextProperty()
pub_date = db.DateTimeProperty(auto_now_add=True)

tags = db.StringListProperty()
comments = db.IntegerProperty(default=0)

language = db.StringProperty(choices=[code for code, name in settings.LANGUAGES], required=True)

def __unicode__(self):
return self.title

def get_absolute_url(self):
bits = ('weblog-entry', (), {

'year': self.pub_date.year,
'month': '%02d' % self.pub_date.month,

'day': '%02d' % self.pub_date.day,
'slug': self.slug})

return urlresolvers.reverse(bits[0], None, *bits[1:3])

def put(self):
from templatetags.highlight_code import highlight_code

self.text_html = highlight_code(self.text)
if not self.is_saved():

Tag.update_tags(self.tags)
else:
old = self.get(self.key())

Tag.update_tags(*Tag.find_differences(old.tags, self.tags))

super(Entry, self).put()

@classmethod
def post(cls, year, month, **kwargs):

year_month = "%d%02d" % (year, month)

entry_archive = EntryArchive.get_by_key_name(u"_"+year_month)
if entry_archive is None:

archive_key = EntryArchive(key_name=u"_"+year_month, year_month=year_month).put()

entry_archive = EntryArchive.get(archive_key)

entry = Entry(parent=entry_archive, **kwargs)

entry.put()

def delete(self):
from google.appengine.api import datastore

Tag.update_tags(removed=self.tags)
archive = EntryArchive.get_by_key_name(u"_%d%02d" % (self.pub_date.year, self.pub_date.month))

if self.all().ancestor(archive.key()).count() > 1:

archive = None
else:
archive = archive.key()

comments = [comment.key() for comment in Comment.all().ancestor(self.key())]

def txn():
for comment in comments:

datastore.Delete(comment)
super(Entry, self).delete()

if archive is not None:
datastore.Delete(archive)

db.run_in_transaction(txn)

@classmethod
def most_commented(cls, num=5):

return cls.all().filter('comments >', 0).order('-comments').fetch(num)

@classmethod
def list_of_months(cls, trim_to=100):

return [datetime.date(int(row.year_month[0:4]), int(row.year_month[4:6]), 1) for row in EntryArchive.all().order('-year_month').fetch(trim_to)]

def allow_comments(self):
if datetime.datetime.now() - datetime.timedelta(days=30) > self.pub_date:

return False
return True

Najpierw krótki opis interesujących metod, co pozwoli lepiej prześledzić cały przepływ danych:




  • __unicode__ -- tekstowa reprezentacja obiektu znana i używana w standardowym Django



  • get_absolute_url -- generowanie adresu wpisu jak w standardowym Django, używane później przez kod dla RSS, sitemap i listy



  • allow_comments -- wyłącza komentowanie po 30 dniach od dodania wpisu



  • list_of_months -- pobiera listę archiwalnych miesięcy i zwraca ją jako obiekty date



  • most_commented -- zwraca listę najczęściej komentowanych wpisów (pamiętaj, że GAE nie używa obiektów menedżerów jak standardowe Django stąd metody statyczne)



  • post -- wywoływana przy dodawaniu nowego wpisu; zapewnia dodatkową logikę tworzącą obiekt archiwum dla danego miesiąca, jeśli nie istnieje; dodatkowo przekazuje klucz tego obiektu miesiąca jako rodzica nowego wpisu



  • put -- przesłonięcie standardowej metody put zapisującej dane; przed zapisem przetwarzam tekst Markdown na wersję HTML w celu optymalizacji wyświetlania wpisów; dodatkowo wywoływana jest tu logika dodawania i aktualizacji tagów wraz z ich licznikami; zauważ, że choć nie jest to jawnie wskazane, to nie można zmienić daty istniejącego wpisu, bo nie można zmienić przodka (trzeba usunąć obiekt i utworzyć nowy w innym miejscu)



  • delete -- usuwa wpis, usuwając wcześniej jego komentarze i tag, a po usunięciu wpisu również archiwum, jeśli był to jedyny wpis w danym miesiącu.


Kilka uwag:




  • zwróć uwagę, że wpis przechowuje licznik komentarzy



  • DataStore nie usuwa kluczy podrzędnych, jeśli usuniemy przodka; również kolejność usuwania nie ma znaczenia (mógłbym najpierw usunąć wpis, a potem jego komentarze) -- pod tym kątem DataStore przypomina stare dobre MyISAM z MySQL, czyli jak chcesz integralności, to zatroszcz się o nią sam :)



  • kod usuwania zakłada, że wpisów w miesiącu nie będzie więcej niż kilkaset, podobnie jak komentarzy (limit dla GAE to w tej sytuacji 1000)



  • dodawanie obiektu archiwum miesięcy nie jest objęte transakcją, choć teoretycznie powinno (GAE dostarcza metodę create_or_get działająca w transakcji)



  • zauważ, że transakcja przy usuwaniu używa tylko kluczy -- transakcje w DataStore nie mogą wykonywać zapytań (dane muszą pobierać na podstawie kluczy), więc wszystkie potrzebne klucze trzeba zebrać przed przejściem do transakcji (system domknięć w Pythonie bardzo w tym pomaga :)



Komentarze


Po całym wcześniejszym kodzie komentarze to niemalże ostoja spokoju. W zasadzie jedyną trudnością jest tutaj potrzeba aktualizacji licznika komentarzy danego wpisu. Dodatkowo ponownie stosuję grupę encji. Wszystkie komentarze wpisu są podgrupą wpisu, co umożliwia mi poprawne działanie transakcji, która dodaje komentarz i zwiększa licznik.


Dodatkowa uwaga dotycząca grup encji i transakcji. W tym systemie klucz stosuje hierarchię EntryArchive->Entry->Comment. W GAE transakcja blokuje całą grupę, więc dodanie komentarza, choć jest zabezpieczone pod kątem transakcji, blokuje wszystkie wpisy z miesiąca, czyli znacznie więcej niż powinno. W tym przypadku byłem tego świadom i nie zamierzam tego zmieniać, bo liczba konfliktów transakcyjnych jest na tym blogu minimalna. Co więcej, GAE stosuje optymistyczne blokady, więc znaczenie mają tylko zapisy. Problemy z prezentowanymi transakcjami rozpoczęłyby się, gdyby komentarze napływały do systemu częściej niż kilkanaście razy na sekundę. Jeśli wykluczymy atak, taka sytuacja nie zdarza się nawet na najbardziej popularnych blogach.


Po tym wprowadzeniu mogę przedstawić kod modelu komentarzy bez opisywania jego metod.


class Comment(BaseModel):
author = db.StringProperty(required=True)

date = db.DateTimeProperty(auto_now_add=True)
comment = db.TextProperty(required=True)

entry = db.ReferenceProperty(Entry, required=True)

def __unicode__(self):
return u'%s - %s' % (self.author, self.date)

@classmethod
def post(cls, **kwargs):
from templatetags.highlight_code import highlight_code

kwargs['comment'] = highlight_code(kwargs['comment'], True)

if isinstance(kwargs['entry'], Entry):
entry_key = kwargs['entry'].key()

else:
entry_key = kwargs['entry']

def txn():

entry = Entry.get(entry_key)
entry.comments += 1

entry.put()
comment = cls(parent=entry, **kwargs)

comment.put()

db.run_in_transaction(txn)

def delete(self):

entry_key = self.parent_key()

def txn():

entry = Entry.get(entry_key)
entry.comments -= 1

entry.put()
super(Comment,self).delete()

db.run_in_transaction(txn)


Znacznik szablonowy sitebar


Obiecałem jeszcze jako bonus przedstawić znacznik szablonowy generujący dane dla bocznego paska. Prezentuję go z jednego powodu -- poniższy kod używa trzech pojedynczych zapytań, by pobrać wszystkie dane bez jakiejkolwiek agregacji i złączeń! Trud wcześniejszych liczników i sztuczek czasem się spłaca :)


from django import template

from weblog.models import Entry, Tag
from django.core.cache import cache

register = template.Library()

@register.inclusion_tag('sidebar.html')

def sidebar():
value = cache.get('sidebar')

if value is None:
value = {

'most_commented': Entry.most_commented(num=5),
'months_list': Entry.list_of_months(),

'tag_list': Tag.most_popular(5)
}
cache.set('sidebar', value, 300)
return value



Podsumowanie


Nie będę zdziwiony, jeśli po tym tekście i analizie kodu osoby przyzwyczajone do relacyjnych baz danych będą siwe i nigdy nie spojrzą na Google AppEngine. Choć powyższego kodu nie ma dużo, to napisanie go w sposób w miarę efektywny dla GAE wymaga analizy rozkładu zapisów i odczytów, obejrzenia kilku prezentacji z Google IO i dokładnego przeczytania dokumentacji. Choć początkowy nakład pracy może być spory, po pewnym czasie można wyodrębnić pewne wzorce korzystania z DataStore. Mam nadzieję, że przedstawienie tu części z nich, choć nie zawsze idealnych (bo to moja pierwsza złożona przymiarka do GAE), pomoże innym w zrozumieniu ograniczeń Google App Engine i przestawieniu się na inny sposób myślenia.

4 January 2009

Czas podsumowań

Trochę spóźnione to podsumowanie, ale zawsze. Skupie się na sprawach zawodowych i programistycznych, bo tu w 2008 roku wydarzyło się sporo:

  • odejście ze Spill Group
  • przygotowanie tutoriala o prostym portalu gier w Django
  • pierwsza praca jako programista Python/Django (Open-E) i jak na razie jedyna ;)
  • pierwszy test jako Scrum Master zespołu
  • pomoc przy tłumaczeniu django.pl
  • udział w RuPy 2008 (widok Zeda Shawa (autor Mongrela dla Rails) piszącego w Pythonie bezcenny ;)
  • wewnątrzfirmowy warsztat Scrum prowadzony przez Jeffa Sutherlanda (współtwórca Scrum)
  • zakończenie jak na razie współpracy z Helion S.A.
  • przejście do SPIL GAMES
  • prezentacja o GAE i Django na PyCon PL 2008
  • modyfikacja bloga i uruchomienie go na Google App Engine
  • trening ScrumMastera i uzyskanie tytułu Certified Scrum Master
  • wreszcie aktywność na blogu :)

Co planuję na 2009:

  • poznać język Erlang
  • wymyślić i napisać użyteczną aplikację/narzędzie open source dla Google App Engine + Django
  • wprowadzić Pythona i Django w firmie
  • pogłębiać wiedzę o agile i lean przez jej pełniejsze praktykowanie
  • przyjrzeć się ponownie Pylonsom
  • dalej blogować

Pluf - Django dla PHP 5

Dzisiaj znalazłem interesujący framework (Pluf) dla tych, dla których Django obecnie nie jest dopuszczalną opcją z racji braku Pythona w środowisku produkcyjnym.

Pluf to framework dla PHP 5.2 wzorujący się na Django. Piszę wzorujący, bo wiele elementów jest podobnych, choć nie identycznych. Choć wiele elementów jest uproszczonych, to możemy tam znaleźć:

  • prosty ORM, który zwraca obiekty! (wspominam, bo CakePHP wzorowany na Rails tego nie czyni a moim zdaniem to podstawa)
  • szablony z dziedziczeniem podobne do tych z Django (choć sam format to mniej więcej Smarty, więc historia zatacza koło ;)
  • obsługa wzorca MTV (Model-Template-View)
  • prosta migracja bazy danych
  • obsługa formularzy w stylu new forms, także tworzonych z modelu!
  • obsługa gettext
  • obsługa testów
  • obsługa cache (plikowy i memcached)
  • obsługa middleware (sesja, GA, Debug, Tidy)
  • użytkownicy, grupy i uprawnienia

Ogólnie jestem pod wrażeniem, bo jako swego czasu główny programista wewnętrznego frameworka w PHP wiem, że nie jest lekko. Sama funkcjonalność jest ciekawa, ale czytelność takiego kodu niestety mniej. Nie z winy autorów, bo w PHP trudno pozbyć się tablic asocjacyjnych na każdym kroku (brak w PHP argumentów nazwanych, a introspekcja PHP 5 rodem z Javy nie zachęca do eksperymentów z obiektami w tej materii).

Tak, jak Railsowcy, którzy utknęli w PHP mają swojego CakePHP, tak Djangowcy mają Pluf. Dokumentacja Plufa jeszcze mocno utyka, ale dzięki zgodności tylko z PHP 5.2 przyjemność użytkowania powinna być dobra.