niedziela, 31 października 2010

Blog całkowicie przeniesiony na pep20.net

Blog całkowicie przeniesiony na nowy adres: pep20 - blog programisty Python

środa, 28 kwietnia 2010

Symfony forms vs Django forms

Mam okazję pracować z obydwoma frameworkami i mogę je porównać w praktyce.
Django zacząłem używać jakieś dwa lata temu, a Symfony nieco wcześniej (od
wydania stabilnej wersji 1.0).

Ostatnimi czasy, z braku możliwości upgrade Symfony w projekcie, przeportowałem
mechanizm formularzy z wersji 1.1 do 1.0.20.


Piersze wrażenie

Mechanika formularzy w Symfony mocno przypomina newforms z Django. Powszechnie
wiadomo, że Fabien jest fanem Django, więc nie zdziwiło mnie zbyt specjalnie, że
wzorował się właśnie na nim.

Mamy do dyspozycji podstawową klasę formularza sfForm (django.forms.Form), oraz
klasę dedykowaną do modelu Propela sfPropelForm (django.forms.ModelForm).

Jest do dyspozycji zbiór widgetów w klasach sfWidgetForm (django.forms.widgets) do
renderingu pól oraz zestaw walidatorów w klasach sfValidator (w Django
sprawdzanie poprawności danych jest rozwiązane przez wywoływanie metod
clean_FIELDNAME() formularza, o ile zostały zdefiniowane, oraz metody clean()
każdego pola). W Symfony jest możliwość napisania własnych specyficznych widgetów
i walidatorów.

Używanie formularzy jest podobne - w widoku (sf: akcji), najczęściej przy requescie wysłanym
POST-em instancjonujemy formularz i wypelniamy go danymi z requestu, sprawdzamy
czy jest prawidlowy (sfForm::is_valid()). W przypadku sfPropelForm (ModelForm)
po walidacji wywołujemy save().


Definiowanie formularzy

W Django używamy class properties i zagnieżdżonej klasy Meta.

class MyForm(forms.Form):
  name = forms.CharField()
  birthdate = formd.DateField()
 
  class Meta:
    exclude = ('birthdate',)

W Symfony formularz konfigurowany jest w
metodzie sfForm::configure(), która uruchamiana jest domyslnie w
konstruktorze.

class MyForm extends sfForm
{
  protected function configure() {
    $this->setWidgets(array(
        'name' => new sfWidgetFormInput(array('required'=>true,)),
        'birthdate' => new sfWidgetFormDate(array('required'=>true,)),
        ));
  }
}

Ops, ale.. jak wykluczyć datę urodzin?

Google dają odpowiedź:


  • Q: How to exclude a field?

  • A: You have to remove the widget and the validator.

W Symfony zwykle kończy się to na klasie pochodnej, przeciążeniu configure() z
serią unset na widgetSchema i validators. Możliwe jest (wszystko jest możlwie
do realizacji, ale z różną efektywnością i poziomem trudności), ale trochę uciążliwe.

Django-Symfony 1:0

Uważny czytelnik zwróci uwagę, że w Symfony nie definiujemy pół formularza
(sic!) tylko oddzielnie widgety i validatory. Mimo, że istnieje klasa
sfFormField, jest ona używana już po bindingu (zawiera w sobie wartość pola - w
Django jest BoundField, które opakowuje Field i dostarcza wartość).

Jaka jest zatem kolejna wada formularzy Symfony? Nie ma zdefiniowanych klas pól
formularzy, które naturalnie łączą widgety i walidaję danych, np. CharField związane
jest domyślnie z widgetem TextInput i posiada domyślną walidację w metodzie
CharField.clean(). W Symfony trzeba dodać walidację samodzielnie:

class MyForm extends sfForm
{
  protected function configure() {
    $this->setWidgets(array(
        'name' => new sfWidgetFormInput(array('required'=>true,)),
        'birthdate' => new sfWidgetFormDate(array('required'=>true,)),
        ));
 
    $this->setValidators(array(
        'name' => new sfValidatorString(array('required'=>true)),
        ));
  }
}

Walidacja pola jest zwykle specyficzna. W Symfony trzeba napisać dedykowaną
klasę walidatora. W Django implementujemy metodę clean_FIELDNAME w
klasie formularza. Można też przygotować dedykowaną klasę Field.

Django-Symfony 2:0


I18n (czyli formularze po polsku)

W Django etykiety i komunikaty walidacji muszą być opakowane w wywołanie funkcji
gettext. W komunikatach podstawowych pól używany jest ugettext_lazy. Etykiety
musimy zdefiniować ręcznie (za pomocą argumentu label przy definiowaniu pola
formularza).

class MyForm(forms.Form):
  name = forms.CharField(label=_('Name'))
  birthdate = formd.DateField(label=_('Birth date'))
 
  class Meta:
    exclude = ('birthdate',)

Po stronie Symfony zrealizowano to nieco inaczej - przez nastawienie funkcji
callbacka do funkcji translate:

  protected function configure() {
      $this->widgetSchema->getFormFormatter()->setTranslationCallable(
          array(sfContext::getInstance()->getI18N(), '__'));
      /* ... */
  }

Pełny kod formularza w Symfony:

class MyForm extends sfForm
{
  protected function configure() {
 
    $this->widgetSchema->setNameFormat('my_form[%s]');
    $this->widgetSchema->getFormFormatter()->setTranslationCallable(
        array(sfContext::getInstance()->getI18N(), '__'));
 
    $this->setWidgets(array(
        'name' => new sfWidgetFormInput(array('required'=>true,)),
        'birthdate' => new sfWidgetFormDate(array('required'=>true,)),
        ));
 
    $this->setValidators(array(
        'name' => new sfValidatorString(array('required'=>true)),
        'birthdate' => new sfValidatorDate(array('required'=>true)),
        ));
  }
}

Pamiętacie jeszcze, że w Django ten sam opis formularza składa się tylko 5 linii
kodu? :)

Mimo innego sposobu używania gettext(), to zarówno Django, jak i w Symfony dają
radę. Remis.

Django-Symfony 3:1


Domyślne dane (initial parameters)

Django:

  data = {'name': 'Stranger',}
  form = MyForm(initial=data)

Symfony:

  $form = new MyForm();
  $form->setDefaults(array('name'=>'Stranger'));

Praktycznie to samo. Jednak zauważyłem, że pomimo nastawienia wartości domyślnych w formularzu
sfPropelForm, pusta wartość pola modelu napisuje to, co przekazujemy w defaults.
Jest to dość duży problem, ale może dotyczyć tylko wersji formularzy z Symfony
1.1. Remis.

Django-Symfony 4:2


Binding

Formularze Django w konstruktorze przyjmują parametr data, do którego zwykle
przekazuje się request.POST. Analogicznie jest z przesłanymi plikami -
request.FILES. Przykład:

def my_form(request):
  if request.method == 'POST':
      form = MyForm(request.POST, request.FILES)
      if form.is_valid():
          model = form.save()
          return redirect(model)

W Symfony jest dość podobnie, aczkolwiek binding należy wykonać oddzielną
metodą bind():

public function executeMy_form() {
  $form = new MyForm();
  if($this->getRequest()->getMethod() == sfWebRequest::POST) {
      $form->bind($this->getRequestParameter('my_form'),
        $this->getRequest()->getFiles('my_form'));
      if($form->isValid()) {
          $form->save();
          $this->redirect('/somewhere/');
      }
   }
}

Należy też pamiętać o użyciu prawidłowej zmiennej z requestu - w klasie
formularza definiujemy name format (setNameFormat) i te obydwie części muszą się
zgadzać. W ten sposób złamano DRY i nie jestem, czy w Symfony da się ten problem
jakoś ominąć. Remis.

Django-Symfony 5:3


Częste problemy/tips

  • Własny rendering formularzy. W Symfony musicie pamiętać o jawnym używaniu gettext(),
    jeśli nie używacie metod render*() pól/formularza.


  • Dostosowywanie formularzy do potrzeb projektu. Django daje duże możliwości
    dzięki przestrzeniom nazw Pythona i konfiguracji urls/settings. Zwykle
    w projekcie dodaje się dedykowaną aplikację z modułem zawierającym
    dostosowane formularze i najczęściej zmienia się konfigurację urls (w
    reusable apps mamy zwykle do dyspozycji parametr form_class w widokach).
    Symfony, przynajmniej w wersji 1.0.X, nie daje mechanizmu zbliżonego do
    urls, chociaż można użyć factories
    albo innych rozwiązań (ja stosowałem własny kontener IoC). Warto o tym
    pamiętać, bo we wdrożeniach bardzo często dostosowuje się właśnie formularze.


  • Odpowiednik klasy ModelForm w Symfony nie potrafi "w locie" zbudować
    definicji formularza. Bazowe klasy dla poszczególnych modeli muszą być
    wygenerowane (sic!). Zatem użyteczność sfPropelForm bez wygenerowanych
    bazowych klas jest niewielka i ogranicza się do zaimplementowanej metody save().



Fantazje Fabiena

Na koniec, jako ciekawostkę, dodam krótkie podsumowanie tego, co Fabien zaimplementował będąc albo
przemęczonym, albo na kacu ;)

  • schemat widgetów/validatorów (kontener/dict) dziedziczy bezpośrednio po klasie
    Widget/Validator (znakomity przykład do czego nie używać dziedziczenia)


  • forms embedding (formularze w formularzach) - przyznam, że nie mogę tego
    pojąć. Nie mamy formsetow, ale możemy zagnieżdżać formularze w sobie.


  • form merging (łączenie formularzy) - nie widze zastosowania (formularz
    hermetyzuje logikę, sprawdzanie poprawności i sposób prezentacji - po co
    łączyć, użyjmy kilku formularzy lub Formsetu)


  • I18N w kontekście translacji (gettext) - zahermetyzowane wewnątrz formularza,
    lecz z możliwością wymiany callable na dowolny; trudne/każdorazowe definiowanie
    domyślnego callable; gettext powinien być używany, nie zawarty wewnątrz
    klasy formularza



Podsumowanie

Formularze w Symfony są, a to już lepsze niż nic, które było w wersji 1.0.X.
Ostateczny wynik rozgrywki Django-Symfony 5:3. Jeśli ktoś nie używał formularzy
w Symfony, to gorąco polecam. Jeśli ktoś ma dylemat co wybrać, polecam Django z
racji szerszych możliwości i skrócenia czasu realizacji zadań.

sobota, 27 marca 2010

Instalacja sterownika drukarki Canon IP1900 na Arch Linux 64bit

Miałem kilkanaście minut rozgryzania, zanim udało mi się zainstalować sterownik. Dlatego w krótkich punktach napiszę, jak przez ten proces przebrnąć sprawnie.

  1. Zaktualizuj system do najnowszej wersji

    pacman -Syu

  2. Zainstaluj pakiet cups-canon-3.00 znajdujący się w repozytorium AUR.
  3. Uwaga! Podczas instalacji (np. za pomocą yaourt) musiałem wyłączyć kilka zależności, gdyż pacman nie mógł ich rozwiązać (dwa pakiety znajdują się w AUR). Wyłączyłem wszystkie biblioteki lib32 po prostu kasując je z wiersza dependencies.
  4. Nie zapomnij zainstalować bibliotek lib32:
    • lib32-popt z AUR
    • lib32-libpng12 z AUR
    • pacman -S lib32-libxml2 
    • pacman -S lib32-libpng  lib32-gtk2
  5. Po kompilacji i instalacji powinieneś mieć plik /usr/bin/cifip1900. Sprawdź:

    marcin]# file /usr/bin/cifip1900
    /usr/bin/cifip1900: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.2.5, stripped

  6. Sprawdź także, czy nie brakuje innych bibliotek:

    marcin]# ldd32 /usr/bin/cifip1900

    Jeśli wszystko gra, powinieneś mieć output podobny do:


    linux-gate.so.1 =>  (0xf775f000)

    libcnbpcmcm346.so => /usr/lib/libcnbpcmcm346.so (0xf772a000)

    libcnbpess346.so => /usr/lib/libcnbpess346.so (0xf76da000)

    libm.so.6 => /opt/lib32/lib/libm.so.6 (0xf76b4000)

    libdl.so.2 => /opt/lib32/lib/libdl.so.2 (0xf76b0000)

    libtiff.so.3 => /opt/lib32/usr/lib/libtiff.so.3 (0xf7656000)

    libpng12.so.0 => /opt/lib32/usr/lib/libpng12.so.0 (0xf7631000)

    libcnbpcnclapi346.so => /usr/lib/libcnbpcnclapi346.so (0xf762d000)

    libcnbpcnclbjcmd346.so => /usr/lib/libcnbpcnclbjcmd346.so (0xf7629000)

    libcnbpcnclui346.so => /usr/lib/libcnbpcnclui346.so (0xf7623000)

    libpopt.so.0 => /opt/lib32/usr/lib/libpopt.so.0 (0xf7618000)

    libc.so.6 => /opt/lib32/lib/libc.so.6 (0xf74d1000)

    libpthread.so.0 => /opt/lib32/lib/libpthread.so.0 (0xf74b8000)

    /lib/ld-linux.so.2 (0xf7760000)

    libjpeg.so.8 => /opt/lib32/usr/lib/libjpeg.so.8 (0xf7483000)

    libz.so.1 => /opt/lib32/usr/lib/libz.so.1 (0xf746e000) 

  7. Zainstaluj CUPS i wsparcie HAL

    pacman -S cups hal-cups-utils

  8. Dodając drukarkę do CUPS wybierz tę znalezioną przez HAL, a następnie wskaż plik PPD.


    Odpowiedni plik PPD znajdziesz poleceniem:

    pacman -Ql cups-canon-3.00 | grep ppd


Strona testowa powinna wydrukować się poprawnie.
Powodzenia!

wtorek, 20 października 2009

Fotogaleria w Django

Kilka dni temu zakończyłem wdrożenie projektu galerii fotograficznej. Realizacja była bardzo przyjemna. Użyłem:
Sercem galerii jest aplikacja mojego autorstwa, którą wzbogaciłem o własne widoki CRUD (na bazie widoków generycznych), gdyż panel administracyjny Django w swej niezmodyfikowanej formie jest niewygodny dla przeciętnego użytkownika. Szczerze mówiąc rozwijałem to oprogramowanie tylko dlatego, żeby poszerzyć umiejętności programowania reusable apps. Aplikacja jest wyposażona w replikator albumów i zdjęć z galerii Coppermine. Replikacja odbywa się za pomocą xml-rpc (po stronie Coppermine napisałem prosty rpc server w Pythonie).

Nie jestem jeszcze zdecydowany na uwolnienie kodu mojej galerii - najpierw zweryfikuję istniejące aplikacje, które zapewne są bardziej rozbudowane. Natomiast skłaniam się ku wydaniu paczki importującej fotki i albumy z Coppermine.

Przy okazji realizacji tego projektu wydałem aplikację django-imageprocessor. Można powiedzieć, że jest to lżejsza wersja django-imagekit. Imageprocessor ewoluował od czasów Pylons, a nawet PHP (od ponad roku). Na chwile obecną usunąłem większość kodu - okazał się zbędny.

django-imageprocessor zastosowałem do generowania miniatur oraz dużych zdjęć ze znakami wodnymi. Czym się rożni od pozostałych implementacji generatorów miniatur? Głównie możliwością zarejestrowania presetów pod unikalnymi nazwami, za pomocą których będzie odbywał się rendering docelowych obrazków. W realizowanym projekcie użyłem presetu z procesorem skalującym obrazek źródłowy do 800x600 z nałożeniem znaku wodnego, oraz helpera do generowania miniatur (dedykowany template tag). Imageprocessor jest jeszcze w alpha stage (szczególnie boli brak testów). Mimo wszystko zachęcam do zapoznania się z aplikacją i czekam na Wasze opinie.

Dla zainteresowanych efektem finalnym: http://obiettiva.pl/

niedziela, 4 października 2009

Arch linux - aktualizacja Postgresql 8.3 do 8.4

Po aktualizacji systemu server postgres nie startuje wskutek niekompatybilności formatu plików danych. Skrócony przepis na aktualizację:

  1. zainstaluj poprzednią wersję postgresql
    pacman -U /var/cache/pacman/pgk/postgresql-8.3-X.pkg.tar.gz
  2. uruchom usługę, wykonaj dump bazy:
    pg_dumpall -oU postgres >~/postgres_83.dump.sql

  3. zastopuj usługę
    pg_ctl stop
  4. zmień nazwę katalogu z danymi wersji 8.3
    mv /var/lib/postgres/data /var/lib/postgres/data_8.3
    mkdir /var/lib/postgres/data
    chown postgres:postgrss /var/lib/postgres/data
    chmod 0700 /var/lib/postgres/data
  5. wykonaj upgrade postgresql, np. przez pacman -Syu
  6. wykonaj init katalogu danych
    initdb -D /var/lib/postgres/data
  7. sprawdź/uaktualnij konfigurację - niektóre opcje nie są kompatybilne
    vimdiff /var/lib/postgres/data_8.3/pg_hba.conf /var/lib/postgres/data/pg_hba.conf
    vimdiff /var/lib/postgres/data_8.3/postgresql.conf /var/lib/postgres/data/postgresql.conf
  8. uruchom usługę postgres
  9. zaimportuj backup
    psql -U postgres -d postgres -f ~/postgres_83.dump.sql

Więcej informacji:

środa, 22 lipca 2009

Django forms tips

Rendering bez widgetów - dane pól formularzy

W ostatnim czasie mocno ingerowałem w formularze aplikacji django.contrib.admin
i potrzebowałem kilku rozwiązań, które nie bardzo pasowały do konwencji field<->widget.
Potrzebowałem wyświetlić miniatury zdjęć produktów w formularzu inline.
Rozwiązanie, które stosowałem dotychczas (custom widget) nie do końca mnie satysfakcjonowało.


class AdminImageFieldWithThumbWidget(FileInput):

def __init__(self, thumb_width=50, thumb_height=50):
self.width = thumb_width
self.height = thumb_height
super(AdminImageFieldWithThumbWidget, self).__init__({})

def render(self, name, value, attrs=None):
thumb_html = ''
if value and hasattr(value, "url"):
thumb_html = '<a href="http://www.blogger.com/%s"><img src="http://www.blogger.com/%s" width="%s" /></a>' % (value.url, value.url, self.width, self.height)
return mark_safe("%s%s" % (thumb_html,
super(AdminImageFieldWithThumbWidget, self).render(name, value, attrs)))


Wadą rozwiązania opartego na widget`cie jest osadzenie miniatury między label a file-input.
Potrzebowałem wydostać się poza div`a klasy form-row. Stanąłem przed problemem wydobycia danych formularza.

Próbowałem kolejno:


  • form.myfield - zwraca output renderowania przez widget

  • form.myfield.as_text (i pochodne) - używa metody as_widget() do renderowania pola

  • form.myfield.value - nie istnieje, intensywnie pracują nad łatą #10247

  • form.myfield.data - jest to property read-only, w moim przypadku nie działało

Do czasu zakończenia ticketu #10427 dobrym rozwiązaniem jest dostęp do initial data formularza za pomocą:

form.initial.myfield
Trzeba pamiętać, że zmienna form.initial zawiera dane początkowe formularza.


Nie zapomnij o non-form errors

Jeśli rendering formularza przeniosłeś do własnego szablonu, nie zapomnij o wyświetleniu błędów walidacji nie związanych z polami formularza (np. własna walidacja). Unikniesz czasu straconego na debugowanie walidacji.


{% for error in form.non_form_errors %}
<li>{{ error }}</li>
{% endfor %}


Wyłączanie pól formularza

Django svn/1.0.2 zawiera irytujący błąd, który uniemożliwia wyłączanie niektórych pól z formularza, jeśli je redefiniowano za pomocą class properties klasy dziedziczącej. Pisałem o tym szerzej nieco wcześniej. Jeśli masz problem z deaktywacją wybranych pól za pomocą właściwości exclude, użyj w klasie bazowej formularza własnej metaklasy.