Hlavní navigace

Dvojitá alchymie

25. 10. 2013 20:46 (aktualizováno) Petr Blahoš

Podívejte se na všechny díly seriálu nebo na zdrojáky příkladu.

Před lety jsem v Pylons 1 napsal malou aplikaci, která byla vpodstatě CRUD s nějakou přidanou hodnotou. A protože nejlépe se něco naučíme tréningem, rozhodl jsem se napsat podobnou aplikaci s novými technologiemi, konkrétně frameworkem Pyramid. Ve své aplikaci jsem si sám napsal vrstvu, která zobrazovala a editovala ORM objekty. Byla jednoduchá, a umožňovala deklarativně posat, jak se má zobrazit objekt v tabulce, v detailním pohledu, jak se má editovat, a jak zobrazit případnou hierarchii objektů. Nepřipomíná vám to něco?

Kde už jsme takový seznam vlastností viděli? Aha, formalchemy. Což nám spolu s SQLalchemy dává název tohoto článku. Pokud byste chtěli navrhnout, ať použiju formalchemy_pyramid, tak vězte, že o něm vím, ale chtěl jsem zůstat na trochu nižší úrovni. Jdeme na to.

Základ je jednoduchý. Máme ORM třídu, nebo objekt. Pak máme formalchemy.Grid a formalchemy.FieldSet. formalchemy.Grid dostane pole objektů, a z nich umí vyrenredovat tabulku. formalchemy.FieldSet dostane jeden objekt a vyrenderuje formulář, pro editaci. Všechno najdete na https://github.com/petrblahos/faapp-sample. Dnes začneme step01. Nejprve, jak to rozchodit. Nejlépe bude, když si uděláte virtualenv. Unixáci jsou kompetentní, takže to ukážu jen pro Windows:

virtualenv FAAPP
cd FAAPP
scripts\activate
git clone https://github.com/petrblahos/faapp-sample
cd faapp-sample\step01
python setup.py develop
rem ted si dejte kafe
..\..\scripts\initialize_faapp_db development.ini
pserve --reload development.ini
Pokud všechno vyšlo, tak stačí nasměrovat Váš prohlížeč na localhost:6543

Dost váhám ohledně toho, jak moc jít do hloubky. Předně, pochybuju, že znáte Pyramid. Na to si ale projděte pár tutorialů, a potom Narrative Documentation. Co se týče SQLAlchemy, tak tam jsem ochoten napsat ještě míň. Začnu psát, a když se někdo ozve v komentářích, tak můžu příště něco doplnit. Jak se orientovat v této aplikaci? Nejprve model. V model/__init__.py je jenom konfigurace toho, abychom měli v request.db naši databázovou Session. Zajímá nás model/meta.py, kdežto si nadefinujeme ORM modely. Jak vidíte, mám tak 2 jednoduché tabulky s one-to-many relationship mezi nimi. Tady je snad jen dobré uvědomit si, že tyto „modely“ nejsou „bindnuty“ k žádné databázi. Dotazy spouštíme v rámci Session. Hmm, asi bych neměl psát bindnuty, ale boundnuty, že? No a v této fázi chci: Napíšu definici tabulky do model/meta.py, a to mi stačí pro vytvoření základního interface pro prohlížení, zadávání a editaci dat.

Jak nejlépe napsat česky routes? Jejich definice je v __init__.py. Máme 4:

  • top: config.add_route(‚top‘, ‚/‘)
  • list: config.add_route(„list“, „/list/{model}“, )
  • new: config.add_route(„new“, „/edit/{model}“, )
  • edit: config.add_route(„edit“, „/edit/{model}/{id}“, )
Kde je save? Uvidíte. Popíšeme si je jednu po druhé. Ve views/views.py máme jednotlivé handlery.

top

Tady chceme vypsat všechny ORM modely, abychom pak s nimi mohli dělat něco dalšího. Použijeme jednoduchou introspekci – vybereme z modulu model/meta.py všechny potomky Base.

@view_config(route_name='top', renderer='/top.mako')
def top(request):
    models = []
    for (name, ent) in meta.__dict__.iteritems():
        if name.startswith("_"):
            continue
        if "Base"==name:
            continue
        try:
            if issubclass(ent, meta.Base):
                models.append(name)
        except:
            pass
    return { 'models': models, }

list

List čeká jméno modelu jako parametr. Uděláme z něho třídu, a předhodíme do Grid, což je něco z formalchemy, co umí vyrenderovat tabulku. Všimněte si, že Grid chci readonly. Můžete si zkusit, co udělá, když nebude readonly, ale předem upozorňuju, že Vaše změny nemá kdo uložit.

@view_config(route_name="list", renderer="/list.mako")
def list(request):
    model = meta.__dict__.get(request.matchdict["model"])
    grid = Grid(model, request.db.query(model).all(), )
    grid.configure(readonly=True)
    return { "q": request.db.query(model).all(), "grid": grid, }

Proč dělám ten dotaz ještě jednou? Mrkněte na šablonu templates/list.mako:

...
<ul>
% for i in q:
<li>
<a href="${ request.route_url("edit", model=request.matchdict["model"], id=i.id) }">
${ i.id }</a> ${ i }
% endfor
</ul>
<a href="${ request.route_url("new", model=request.matchdict["model"]) }">
${ _("New") }</a>
<hr>
<table>
    ${ grid.render() |n}
</table>

Ano. Kromě toho, že dělám grid.render() si ještě vypíšu jednotlivé záznamy ručně, protože zatím neumím grid donutit, aby mi udělala nějaké odkazy na editaci. Takže když chci editovat, použiju zatím ty ručně vytvořené odkazy.

edit a new

V definici rout edit a new vidíte, že se liší jenom tím, že new nemá „ocas“ s id. Handler těch rout necháme dohromady. Pokud v request.matchdict najdeme to id, tak je to edit, jinak je to new. V každém případě najdeme model, pokud máme id, tak nejdeme i databázový záznam a uděláme s ním formalchemy.FieldSet. Ten si pak necháme v šabloně vykreslit, tentokrát už ne read-only.

@view_config(route_name="edit", request_method="GET", renderer="/edit.mako")
@view_config(route_name="new", request_method="GET", renderer="/edit.mako")
def edit(request):
    model = meta.__dict__.get(request.matchdict["model"])
    if "id" in request.matchdict:
        obj = request.db.query(model).filter(model.id==request.matchdict["id"]).first()
        fs = FieldSet(obj)
    else:
        fs = FieldSet(model, session=request.db)
    return { 'fs': fs }

Jak se teda dělá ten save? Visí na úplně stejné routě – edit nebo new. Pyramid umožňuje při konfiguraci handleru nastavit nějaké predikáty, takže ten náš předchozí edit se vyvolá, když přijde GET. Když přijde POST, tak se vyvolá tento save:

@view_config(route_name="edit", request_method="POST", renderer="/edit.mako")
@view_config(route_name="new", request_method="POST", renderer="/edit.mako")
def saveedit(request):
    model = meta.__dict__.get(request.matchdict["model"])
    if "id" in request.matchdict:
        obj = request.db.query(model).filter(model.id==request.matchdict["id"]).first()
        fs = FieldSet(obj, request=request)
    else:
        fs = FieldSet(model, session=request.db, request=request)
    if fs.validate():
        request.db.add(fs.model)
        fs.sync()
    else:
        return { "fs": fs }
    return HTTPFound(location=request.route_url("list", model=request.matchdict["model"]))
Vidíte, že jsem do něj přidal validaci, a jestliže prošla, tak prostě uložím a přesměruju na list. Commit se provede automaticky při skončení handleru díky pyramid_tm – Pyramid Transaction Manager.

Příště budeme trošku zjednodušovat, a začneme upravovat obsah toho, co formalchemy vyrenderuje. Slovo závěrem: Pokud snad někoho to, co píšu zajímá, a chtěli byste něco víc rozvést, tak napište do komentáře.

Sdílet