macias

turbogears.widgets.Form

Poznając Turbogears niesposób ominąć pakiet turbogears.widgets. Otóż jest to już dość duży (i ciągle rosnący) zbiór łatwych do wielokrotnego wykorzystania "kontrolek".

Tutaj pod pojęciem kontrolka nie kryje się zbyt dużo. Jest nią dowolny element strony, który możemy nazwać "samodzielnym". Toteż mamy kontrolki tabelek, obrazków z linkiem, itd. Wiele z nich jest AJAXowata, co dodaje im uroku i funkcjonalności (np. AutoCompleteField - pole tekstowe pobierające z serwera podpowiedzi w trakcie wpisywania).

Ważną częścią całej kolekcji są formularze i komponenty formularzy.

Działamy:

class DodajWizyteFields( widgets.WidgetsList ):
    pacjenci_combo = widgets.SingleSelectField(  "pacjent_id",  label="Pacjent",    validator=TableIdValidator( model.Pacjent ) )
    typy_combo     = widgets.SingleSelectField(  "typ_id",      label="Typ wizyty", validator=TableIdValidator( model.TypWizyty ) )
    kwota_text     = widgets.TextField(          "kwota",       label="Kwota",      validator=validators.Int() )
    data_text      = widgets.CalendarDatePicker( "data_wizyty", label="Data wizyty", button_text=u"Wybierz date" )

class DodajWizyteForm( widgets.ListForm ):
    fields = DodajWizyteFields()
    submit_text = "Dodaj wizyte"

SingleSelectField to nic innego niż kombo box, CalendarDatePicker to bardzo funkcjonalny Javascriptowy kalendarz, a ListForm to odmiana formularza bazujący na szkielecie <ul>..</ul>. Reszta powinna być jasna lub okazać się jasna po dalszej lekturze.

Czas na kontroler:

class Wizyty( object ):
    add_visit_form = dodaj_wizyte.DodajWizyteForm()

    @expose( template="pacjenci.templates.wizyty.dodaj_formularz" )
    def dodaj_formularz( self ):
        options = { "pacjent_id": [ ( pac.id, pac.ImieNazwiskoDataUr() ) for pac in model.Pacjent.select() ],
                        "typ_id": [ ( typ.id, typ.nazwa ) for typ in model.TypWizyty.select() ] }
        return dict( add_visit_form=self.add_visit_form,
                     options=options,
                     action="dodaj" )

Tworzymy jedną instancję formularza jako pole klasowe add_visit_form. Kontroler dodaj_formularz pobiera opcje dostępne do wyboru w polach formularza (lista pacjentów i lista typów) w formie ( id, nazwa ) i zwraca słownik zmiennych dostepnych później w szablonie:

    <box>
        <title>Dodaj wizytę:</title>
        <body>
            ${add_visit_form.display( options=options, action=action )}
        </body>
    </box>

Metoda .display() formularza generuje kod HTML odpowiadający wszystkim zdefiniowanym wcześniej polom formularza. Jedna instancja formularza jest wielokrotnego użycia, bo przy każdym żądaniu dostępne opcje pobierane są na nowo. Przyjrzyjmy się kontrolerowi dodaj:

    @expose()
    @error_handler( dodaj_formularz )
    @validate( form=add_visit_form )
    def dodaj( self, pacjent_id, typ_id, kwota, data_wizyty ):
        model.hub.hub.begin()
        try:
            p = model.Pacjent.get( pacjent_id )
            t = model.TypWizyty.get( typ_id )
            model.Wizyta( pacjent=p, typ=t, kwota=kwota, data_wizyty=data_wizyty )
        except:
            model.hub.hub.rollback()
            raise
        model.hub.hub.commit()
        redirect( turbogears.url( "lista" ) )

Dekorator @validate( form=add_visit_form ) odpowiada za odpowiednie potraktowanie parametrów żądania. Otóż zostają one zwalidowane przy pomocy walidatorów dla pól formularza (co więcej zostają one skonwertowane do odpowiednich wartości pythonowych). Dla przykładu: walidator validators.Int() użyty dla pola kwota_text sprawia, że tekst wysłany w żądaniu HTTP jako wartość parametru kwota zostaje skonwertowany do obiektu pythonowego typu int. W razie niepowodzenia (np. podano tekst "dziesięć") wchodzi w grę dekorator @error_handler( dodaj_formularz ). Sprawia on, że sterowanie przejmuje kontroler dodaj_formularz. Co więcej formularz add_visit_form użyty do skontrolowania parametrów niesie ze sobą informacje o zaistniałych błędach co jest później uwzględnione przy wyświetleniu tegoż formularza na nowo (przy odpowiednim polu pojawia się komunikat o błędzie).

Pozostałe walidatory zapewniają, że jeśli znaleźliśmy się w kontrolerze dodaj, to znaczy, że wszystkie parametry wejściowe są w całkowitym porządku (i odpowiedniego typu). Zatem ograniczamy się do dodania odpowiedniego rekordu do bazy danych i oddajemy sterowanie kontrolerowi lista.

Pozostaje jeszcze wyjaśnić jak utworzyć nowy walidator (pozostaje do wyjaśnienia dużo więcej, ale...). W naszy przypadku to TableIdValidator:

class TableIdValidator( validators.FancyValidator ):
    messages = { 'not_found': "There is no record with this ID",
                   'not_int': "ID format incorrect. It should be an integer." }

    def __init__( self, sqlobject_table ):
        self.used_table = sqlobject_table

    def validate_python( self, value, state ):
        try:
            self.used_table.get( value )
        except SQLObjectNotFound:
            raise validators.Invalid( self.message( 'not_found', state ), value, state )

    def _to_python( self, value, state ):
        try:
            id = int( value )
        except ValueError:
            raise validators.Invalid( self.message( 'not_int', state ), value, state )
        return id

Dziedziczymy po validators.FancyValidator i dostarczamy metody .validate_python() i ._to_python() (w naszym przypadku akurat te i tylko te). ._to_python() odpowiada za konwersję z napisu do odpowiedniego typu pythonowego (u nas int), a .validate_python() sprawdza, czy uzyskany obiekt pythonowy jest zgodny z naszymi oczekiwaniami (czy jest poprawnym kluczem w odpowiedniej tabeli). Komunikat z jakim rzucamy Invalid ..... pojawia się na formularzu w przypadku wystąpienia tego z przypuszczalnych błędów - nic dodać nic ująć.

Na życzenie zamieszczam skrinszoty: Przykład formularza Formularz z wyświetlonym błędem

Skomentuj (2) 2006-09-17 22:58:01 Edytuj

macias

Na czasie być [nie tak dobrze].

Pomiędzy letnimi obowiązkami domowymi (przetwory z owoców, patroszenie grzybów, ...) i pracą znalazłem chwilę czasu na kontynuację zabawy z frejmłorkami łebowymi (konkretnie Pythonowymi a najkonkretniej TurboGears). Jakiś czas temu natknąłem się na framework o magicznej nazwie Nevow. Moje dotychczasowe przeżycia z Turbogears są wystarczająco pozytywne, aby nie przesiadać się całkowicie - najbardziej zastanawiający okazał się podzbiór Nevow: system szablonów Stan. Krótkie poszukiwania i bingo - TurboStan, który jest prostym adapterem Stan'a do TurboGears.

Tyle po wprowadzeniu. Teraz sedno.

Próba instalacji:

# easy_install TurboStan
Coś nie tak z Twisted z której Nevow (jako zależność TurboStan'a) korzysta jako serwera sieciowego. Szukam i updejtuję i po dobrych 20 minutach rozwiązanie: zamiast skoncentrować się na twisted i instalować wersje niestabilne itd. wystarczyło zainstalować moduł twisted-web od którego de facto Nevow był zależny ("wina" Gentoo :) - rozbili twisted na podmoduły).

Idę dalej. TurboStan zainstalowany. W międzyczasie (tak, wiem: nie ma takiego słowa) próbuję uruchomić mój stary projekt robiąc uprzednio to co zwykle w takim momencie:

$ cd ~/turbogears-dev
$ svn up
$ su
Password:
# easy_install .
$ cd -
Stąd tytuł - akurat jeśli chodzi o aktualnego trunka TurboGears, to do najstabilniejszych on nie należy. O czym niebawem się przekonałem i zakończyłem na tarczy (oczywiście na razie).
Uruchomienie projektu:
$ ./start-pacjenci.py
Traceback (most recent call last):
  File "./start-pacjenci.py", line 5, in ?
    import turbogears
  File "/usr/lib/python2.4/site-packages/TurboGears-1.1a0-py2.4.egg/turbogears/__init__.py", line 6, in ?
    from turbogears.controllers import expose, flash, validate, redirect, 
  File "/usr/lib/python2.4/site-packages/TurboGears-1.1a0-py2.4.egg/turbogears/controllers.py", line 14, in ?
    from turbogears import view, database, errorhandling
  File "/usr/lib/python2.4/site-packages/TurboGears-1.1a0-py2.4.egg/turbogears/view/__init__.py", line 1, in ?
    from turbogears.view.base import *
  File "/usr/lib/python2.4/site-packages/TurboGears-1.1a0-py2.4.egg/turbogears/view/base.py", line 340, in ?
    _load_engines()
  File "/usr/lib/python2.4/site-packages/TurboGears-1.1a0-py2.4.egg/turbogears/view/base.py", line 338, in _load_engines
    engines[entrypoint.name] = engine(stdvars, engine_options)
  File "/usr/lib/python2.4/site-packages/TurboStan-0.8.6-py2.4.egg/turbostan/stansupport.py", line 113, in __init__
    ns [ 'tg' ] = self.get_extra_vars ( ) [ 'tg' ]
  File "/usr/lib/python2.4/site-packages/TurboGears-1.1a0-py2.4.egg/turbogears/view/base.py", line 307, in stdvars
    tg_js="/" + turbogears.startup.webpath + "tg_js",
AttributeError: 'module' object has no attribute 'startup'
$ ls -l //usr/lib/python2.4/site-packages/TurboGears-1.1a0-py2.4.egg/turbogears/
razem 338
...
-rw-r--r-- 1 root root 10604 wrz  7 22:01 startup.py
...
No i bądź mądry, skąd taki błąd. Zaraz napiszę, ale wcześniej spróbuj dojść do tego sam(a).
Otóż prosto: do próby użycia 'podmodułu' startup doszło w trakcie inicjowania modułu turbogears, ale przed jego załadowaniem startup'a w wyniku tej inicjacji. Rodzaj 'cyklicznej' zależności.

Zatem patch i dalej:

$ diff -u stansupport_old.py stansupport.py
--- stansupport_old.py  2006-09-07 23:44:53.000000000 +0200
+++ stansupport.py      2006-09-07 23:28:19.000000000 +0200
@@ -105,6 +105,9 @@
             self.basedir = os.path.join ( os.getcwd ( ), self.basedir )
         log.info ( "templates root = %s" % self.basedir )

+        self._default_ns = None
+
+    def _prepare_default_ns( self ):
         ns = { }
         ns.update ( __import__ ( 'nevow.tags', ns, ns, [ '__all__' ] ).__dict__ )
         ns.update ( __import__ ( 'nevow.entities', ns, ns, [ '__all__' ] ).__dict__ )
@@ -180,6 +183,8 @@
             pretty = False

         if format == 'html':
+            if self._default_ns == None:
+                self._prepare_default_ns()
             ns.update ( self._default_ns )
         else: # import user-defined Stan tags and flatteners
             ns.update ( __import__ ( '%s.tags' % format, ns, ns, [ '__all__' ] ).__dict__ )
$ ./start-pacjenci.py
2006-09-07 23:46:14,597 cherrypy.msg INFO CONFIG: Server parameters:
2006-09-07 23:46:14,599 cherrypy.msg INFO CONFIG:   server.environment: development
2006-09-07 23:46:14,600 cherrypy.msg INFO CONFIG:   server.log_to_screen: True
2006-09-07 23:46:14,601 cherrypy.msg INFO CONFIG:   server.log_file:
2006-09-07 23:46:14,602 cherrypy.msg INFO CONFIG:   server.log_tracebacks: True
2006-09-07 23:46:14,603 cherrypy.msg INFO CONFIG:   server.log_request_headers: True
2006-09-07 23:46:14,604 cherrypy.msg INFO CONFIG:   server.protocol_version: HTTP/1.0
2006-09-07 23:46:14,605 cherrypy.msg INFO CONFIG:   server.socket_host:
2006-09-07 23:46:14,606 cherrypy.msg INFO CONFIG:   server.socket_port: 8080
2006-09-07 23:46:14,607 cherrypy.msg INFO CONFIG:   server.socket_file:
2006-09-07 23:46:14,608 cherrypy.msg INFO CONFIG:   server.reverse_dns: False
2006-09-07 23:46:14,609 cherrypy.msg INFO CONFIG:   server.socket_queue_size: 5
2006-09-07 23:46:14,610 cherrypy.msg INFO CONFIG:   server.thread_pool: 10
2006-09-07 23:46:14,643 turbogears.visit INFO Visit Tracking starting
2006-09-07 23:46:14,651 turbogears.visit.sovisit INFO Succesfully loaded "pacjenci.model.VisitIdentity"
2006-09-07 23:46:14,652 turbogears.visit INFO Visit filter initialised
 1/QueryOne:  SELECT tbl_name FROM sqlite_master WHERE type='table' AND tbl_name = 'tg_visit'
 1/QueryR  :  SELECT tbl_name FROM sqlite_master WHERE type='table' AND tbl_name = 'tg_visit'
Unhandled exception in thread started by <bound method Server._start of <cherrypy._cpserver.Server object at 0xb7b7450c>>
Traceback (most recent call last):
  File "/usr/lib/python2.4/site-packages/CherryPy-2.2.1-py2.4.egg/cherrypy/_cpserver.py", line 78, in _start
    Engine._start(self)
  File "/usr/lib/python2.4/site-packages/CherryPy-2.2.1-py2.4.egg/cherrypy/_cpengine.py", line 108, in _start
    func()
  File "/usr/lib/python2.4/site-packages/TurboGears-1.1a0-py2.4.egg/turbogears/startup.py", line 219, in startTurboGears
    ext.start_extension()
  File "/usr/lib/python2.4/site-packages/TurboGears-1.1a0-py2.4.egg/turbogears/visit/api.py", line 68, in start_extension
    create_extension_model()
  File "/usr/lib/python2.4/site-packages/TurboGears-1.1a0-py2.4.egg/turbogears/visit/api.py", line 87, in create_extension_model
    _manager.create_model()
  File "/usr/lib/python2.4/site-packages/TurboGears-1.1a0-py2.4.egg/turbogears/visit/sovisit.py", line 34, in create_model
    TG_Visit.createTable(ifNotExists=True)
  File "/usr/lib/python2.4/site-packages/SQLObject-0.7.1dev_r1860-py2.4.egg/sqlobject/main.py", line 1320, in createTable
    if ifNotExists and conn.tableExists(cls.sqlmeta.table):
  File "/usr/lib/python2.4/site-packages/SQLObject-0.7.1dev_r1860-py2.4.egg/sqlobject/sqlite/sqliteconnection.py", line 201, in tableExists
    result = self.queryOne("SELECT tbl_name FROM sqlite_master WHERE type='table' AND tbl_name = '%s'" % tableName)
  File "/usr/lib/python2.4/site-packages/SQLObject-0.7.1dev_r1860-py2.4.egg/sqlobject/dbconnection.py", line 760, in queryOne
    return self._dbConnection._queryOne(self._connection, s)
  File "/usr/lib/python2.4/site-packages/SQLObject-0.7.1dev_r1860-py2.4.egg/sqlobject/dbconnection.py", line 342, in _queryOne
    self._executeRetry(conn, c, s)
  File "/usr/lib/python2.4/site-packages/SQLObject-0.7.1dev_r1860-py2.4.egg/sqlobject/dbconnection.py", line 298, in _executeRetry
    return cursor.execute(query)
pysqlite2.dbapi2.Warning: You can only execute one statement at a time.
To wynik rozpoczętych niedawno prac nad rozwątkowaniem głównych części rdzenia TG. Z drugiej strony ORM z którego korzystam (SQLObject) z wątkami sobie nie radzi i jest problem.
Po chwili paniki, myśl: Na pewno jak założę nowy projekt, to się wygeneruje model ze świeższych szablonów i będzie lepiej (choć trochę lepiej).
$ tg-admin quickstart
Enter project name: hihi
Enter package name [hihi]:
Do you need Identity (usernames/passwords) in this project? [no]
Selected and implied templates:
  TurboGears#tgbase      tg base template
  TurboGears#turbogears  web framework

Variables:
  egg:         hihi
  identity:    none
  package:     hihi
  project:     hihi
  sqlalchemy:  False
Creating template tgbase
Creating directory ./hihi
  Recursing into +einame+.egg-info
    Creating ./hihi/hihi.egg-info/
...
  Copying setup.py_tmpl to ./hihi/setup.py
  Copying start-+package+.py_tmpl to ./hihi/start-hihi.py
Running /usr/bin/python setup.py egg_info
Error (exit code: 1)
Traceback (most recent call last):
  File "setup.py", line 2, in ?
    from turbogears.finddata import find_package_data
  File "/usr/lib/python2.4/site-packages/TurboGears-1.1a0-py2.4.egg/turbogears/__init__.py", line 5, in ?
    from turbogears import config
  File "/usr/lib/python2.4/site-packages/TurboGears-1.1a0-py2.4.egg/turbogears/config.py", line 3, in ?
    from cherrypy import config
ImportError: No module named cherrypy

Traceback (most recent call last):
  File "/usr/bin/tg-admin", line 7, in ?
    sys.exit(
  File "/usr/lib/python2.4/site-packages/TurboGears-1.1a0-py2.4.egg/turbogears/command/base.py", line 353, in main
    command.run()
  File "/usr/lib/python2.4/site-packages/TurboGears-1.1a0-py2.4.egg/turbogears/command/quickstart.py", line 195, in run
    command.run(cmd_args)
  File "/usr/lib/python2.4/site-packages/PasteScript-0.9.7-py2.4.egg/paste/script/command.py", line 210, in run
    result = self.command()
  File "/usr/lib/python2.4/site-packages/PasteScript-0.9.7-py2.4.egg/paste/script/create_distro.py", line 129, in command
    cwd=output_dir)
  File "/usr/lib/python2.4/site-packages/PasteScript-0.9.7-py2.4.egg/paste/script/command.py", line 565, in run_command
    raise OSError("Error executing command %s" % cmd)
OSError: Error executing command /usr/bin/python
Tajemnicze zniknięcie modułu cherrypy (serwera HTTP, z którego korzysta TurboGears) pozostaje dla mnie niewyjaśnione. Instaluję na nowo. I zakładam nowy projekt:
$ tg-admin quickstart
Enter project name: hihi
Enter package name [hihi]:
Do you need Identity (usernames/passwords) in this project? [no]
Selected and implied templates:
  TurboGears#tgbase      tg base template
  TurboGears#turbogears  web framework

Variables:
  egg:         hihi
  identity:    none
  package:     hihi
  project:     hihi
  sqlalchemy:  False
Creating template tgbase
Creating directory ./hihi
  Recursing into +einame+.egg-info
    Creating ./hihi/hihi.egg-info/
...
  Copying setup.py_tmpl to ./hihi/setup.py
  Copying start-+package+.py_tmpl to ./hihi/start-hihi.py
Running /usr/bin/python setup.py egg_info
Error (exit code: 1)
Traceback (most recent call last):
  File "setup.py", line 2, in ?
    from turbogears.finddata import find_package_data
  File "/usr/lib/python2.4/site-packages/TurboGears-1.1a0-py2.4.egg/turbogears/__init__.py", line 6, in ?
    from turbogears.controllers import expose, flash, validate, redirect, 
  File "/usr/lib/python2.4/site-packages/TurboGears-1.1a0-py2.4.egg/turbogears/controllers.py", line 9, in ?
    import kid
  File "/usr/lib/python2.4/site-packages/kid-0.9.3-py2.4.egg/kid/__init__.py", line 27, in ?
    from kid.pull import ElementStream, Element, SubElement, Fragment, 
  File "/usr/lib/python2.4/site-packages/kid-0.9.3-py2.4.egg/kid/pull.py", line 11, in ?
    from kid.et import *  # ElementTree
  File "/usr/lib/python2.4/site-packages/kid-0.9.3-py2.4.egg/kid/et.py", line 41, in ?
    import xml.etree.ElementTree as ET
ImportError: No module named etree.ElementTree

Traceback (most recent call last):
  File "/usr/bin/tg-admin", line 7, in ?
    sys.exit(
  File "/usr/lib/python2.4/site-packages/TurboGears-1.1a0-py2.4.egg/turbogears/command/base.py", line 353, in main
    command.run()
  File "/usr/lib/python2.4/site-packages/TurboGears-1.1a0-py2.4.egg/turbogears/command/quickstart.py", line 195, in run
    command.run(cmd_args)
  File "/usr/lib/python2.4/site-packages/PasteScript-0.9.7-py2.4.egg/paste/script/command.py", line 210, in run
    result = self.command()
  File "/usr/lib/python2.4/site-packages/PasteScript-0.9.7-py2.4.egg/paste/script/create_distro.py", line 129, in command
    cwd=output_dir)
  File "/usr/lib/python2.4/site-packages/PasteScript-0.9.7-py2.4.egg/paste/script/command.py", line 565, in run_command
    raise OSError("Error executing command %s" % cmd)
OSError: Error executing command /usr/bin/python
znów coś z modułami, ale tym razem dziwniej: niby nie ma 'podmodułu' etree.ElementTree modułu xml ze standard library - coś jest nieźle skopane, albo... Mignęło cos pod czachą... Patrzę i mnie zmyło: PEP-0356, bo nie ma ebuilda do python'a 2.5, a to w nim wbudowano do stdlib ElementTree, a już na tyle późno, że zdążę tylko napisać na Jabbie o tym fenomenalnym wieczorze.

c.d.n
W następnych odcinkach: o TurboStanie, IronPythonie i masie innych. Stay tuned.

Skomentuj (0) 2006-09-08 00:13:04 Edytuj

Kategorie

Blogroll  |  Edytuj

Linki  |  Edytuj

Feeds