hab mir soeben das rails 2.1 changelog angesehen, und dort gibt es folgende nette erweiterungen.
- zimezones
- dirty tracking
- gem dependencies
- names scope
- utc-migrations
- better-caching
wie immer hat rails gute ideen, und eine sehr schoene umsetzung. aber es freut mich umso mehr, dass webtek bereites schon die haelfter der features hatte :)
timezones
in rails setzt man einen before_filter:
class ApplicationController < ActionController::Base
before_filter :set_timezone
def set_timezone
# current_user.time_zone #=> 'London'
Time.zone = current_user.time_zone
end
end
in webtek macht man das mit einem event:
event->register(
'name' => 'request-prepare-end'
'method' => sub { request->timezone(session->user->timezone) }
)
dirty tracking
hiermit kriegt man informationen welche felder in einem model neu sind (d.h. noch nicht in der db), und welche geaendert wurden (nach einem partial update). hier in rails:
article = Article.find(:first)
article.changed? #=> false
# Track changes to individual attributes with
# attr_name_changed? accessor
article.title #=> "Title"
article.title = "New Title"
article.title_changed? #=> true
# Access previous value with attr_name_was accessor
article.title_was #=> "Title"
# See both previous and current value with attr_name_change accessor
article.title_change #=> ["Title", "New Title"]
und wieder in webtek
$article = app::Model::Article->find_one;
$article->_lazy; # => [] liefert ein leeres array;
# Track changes to individual attributes with
# attr_name_changed? accessor
$article->title #=> "Title"
$article->title("New Title")
$article->_lazy; # => ['title']
# Access previous value with attr_name_was accessor
$article->{'persistent_content'}->{'title'};
# => 'Title' ... gut nicht so schoen aber machbar
# See both previous and current value with attr_name_change accessor
# das geht nicht, aber man weiss zumindest was geaendert wurde
# nach dem speichern
$article->save;
$article->_updated; # => ['title']
... zugegeben, rails macht es schoener, aber immerhin, dieses feature hatte webtek schon lange
utc-migrations
gut, das ist jetzt zwar nix besonderes, aber die webtek migrations funktionieren auch genauso. hier wieder in rails
> script/generate migration one
create db/migrate/20080402122512_one.rb
> rake db:migrate:up
...
> rake db:migrate:down
...
und webtek:
> ./webtek migrate create Test
=> create /WebTek/app/Test/scripts/migrate/20080610220557_Test.pl
> ./webtek migrate up
...
> ./webtek migrate down
...
also in perl gibt es packages:
package MyApp::Model::X;
...
sub x { ... }
soweit so gut. aber in einem modularen Framework kommt man oft mit packagenamen nicht weit, denn diese sind normalerweise in irgendeinem namespace. z.b. gib es in der MyApp application u.a. folgende Packages.
package MyApp::Model::Comment;
...
package MyApp::Model::Text;
use MyApp::Model::Comment;
...
wenn man jetzt aber modular programmiert, hat man optimalerweise die comment-funktionalitaet in einem modul gekapselt, damit man das bei einer anderen applikation auch gleich verwenden kann. nur heisst die andere applikation dann z.b. MyApp2 und somit wird auch das Comment package in dem MyApp2 namespace geladen. d.h. das Text package muesste so definiert sein.
package MyApp2::Model::Text;
use MyApp2::Model::Comment;
damit man jetzt ein modul in mehreren namespaces verwenden kann, gab es in webtek den trick mit folgender definition:
package MyApp2::Model::Text;
use app->Modul->Comment;
das app->Modul->Comment wurde dann fuer die jeweilige applikation in z.b. MyApp2::Module::Comment uebersetzt. diese logik ist aber alles andere als gut, da es dann bei folgendem aufruf:
app->Model->Comment->find_one(id=>123)
keine unterscheidung mehr zwischen dem packetnamen, und der methode die aufgerufen wird, gibt. darum heisst die neue syntax jetzt wie folgt:
package MyApp2::Model::Text;
use app::Model::Comment;
my $comment = app::Model::Comment->find_one(id=>123);
intern wird das app::Model::Comment allerdings weiter in MyApp2::Model::Comment uebersetzt (mittels einem source-filter). d.h. weiters, dass man die module zusatztlich noch in jeder applikation in einem eigenen namespace liegen (= geladen) hat.
ich hoffe es hat jetzt irgendwer verstanden, was ich gemeint hab. wenn nicht, dann bitte nachfragen.
so.. hab jetzt ein mini-test framework in webtek eingebaut:
- es gibt jetzt ein MyApp/scripts/test verzeichnis
- in diesem koennen .t files drinnen liegen (gruppierung in unterverzeichnisse ist keine problem)
- dieses test verzeichnis ist natuerlich auch in jedem modul moeglich
- ein simples test-file sieht so aus:
sub init { } #... place some code before calling the tests sub finish { } #... place some code after calling the tests sub sample_test :Test(2) { ok(1, "sample test"); is(2, 2, "sample test2"); }- zuerst gibt es die funktionen init und finish. diese funktionen werden in jedem fall zum anfang und zum ende eines test-files ausgefuehrt. diese kann man verwenden um irgendwas zu initialisieren, bzw dann wieder aufzuraeumen (db. usw.)
- dann gibt es test funktionen. diese werden durch das Test(\d+) attribute deklariert.
- in diesem attribute muss man angeben, wieviele tests in dieser funktion gemacht werden (also ok, is, like, dies_ok, ... usw aufrufe)
- alle weiteren funktionen ohne dem attribute werden nicht im rahmen der tests aufgerufen. d.h. diese koennen als hilfsfunktionen fungieren
- als assertions koennen alle funktionen aus Test::More und Test::Exception verwendet werden.
- um die tests auszufuehren wird wiedermal das webtek script verwendet. hier der aufruf mitsammt ergebnisliste:
maxs-macbook:MapToolkit max$ ./webtek test [info] run testfile /WebTek/app/MapToolkit/scripts/test/Model/X.t [info] - run test 'sample_test' with 2 tests: [info] run testfile /WebTek/app/MapToolkit/scripts/test/Page/X.t [info] - run test 'index' with 1 tests: [error] there were some failed tests, look at the details: [error] not ok 1 [error] Failed test at /WebTek/app/MapToolkit/scripts/test/Page/X.t line 9. [error] got: undef [error] expected: 'testvalue' result: - planned: 3 - successful: 2 - failed: 1
man sieht hier, dass ein test fehlerhaft war, und ganz unten ist noch eine zusammenfassung von allen tests in allen test-files.
Controller testen
um einen controller zu testen gibt es die WebTek::Engine::Test Engine. mit dieser kann man den request, response und session initialisieren. ein simpler test sieht dann aus:
sub index :Test(1) {
app->engine->prepare;
app->Page->Root->new->index;
is(response->title, 'WebTek Tests');
}
sub create :Test(4) {
#... test get
app->engine->prepare;
app->Page->Root->new->create;
is(response->title, 'WebTek Tests - Create new Test');
#... test post
app->engine->prepare(
'method' => 'post',
'params' => { 'name' => [qw( new test )] },
);
thorws_ok { app->Page->Root->new->create } 'WebTek::Exception::Redirect';
is(response->status, 302);
#... check created model
my $test = app->Model->Test->find_one('name' => 'new test');
isa_ok($test, app->Model->Test);
}
Nachtrag
so.. das war jetzt nur ein kleiner auszug, wie man testen kann. mehr weis ich selber noch nicht, da ich bis jetzt selber noch nicht getestet hab (schande ueber mich).
weiters bin ich jetzt an dem punkt angelangt, andem WebTek feature-complete ist. die aktuelle version ist 0.9.1, und hoffenlich gibt es dann bald eine version 1.0, welche ueber
- viele Tests
- und eine gute doku
verfuegt!
da es jetzt hochaufloesende bilder von oberoesterreich gibt, bin ich mal auf die suche nach meinem bus gegangen, und siehe da, auch fuendig geworden. bin hier grad bei meinen grosseltern bus reparieren. vielleicht bin sogar das kleine punkterl rechts neben dem bus ich ;)
so, seit heute unterstuetzt webtek single und concrete table inheritance.
single table inheritance (weitere infos)
create table text (
id int auto_increment,
class varchar(100),
text text,
comment text,
PRIMARY KEY (id)
);
man sieht hier, dass es eine column class gibt, welche die model-klasse definiert. hier noch der noetige perl code. wichtig ist, dass die subklassen jeweils in eigenen dateien stehen (sonnst kommt der WebTek::Loader durcheinander). mit den (optionalen) PROPERTIES funktionen kann man die models validieren.
MyApp/Model/Text.pm
use base qw( WebTek::Model );
sub PROPERTIES { 'text' => '.' }
MyApp/Model/Comment.pm
use base qw( MyApp::Model::Text );
sub PROPERTIES { 'comment' => '.' }
nun funktioniert folgender code:
> app->Model->Text->new_default('text'=>'text')->save
Model::Model::Text=HASH(0x....)
> app->Model->Comment->new_default('comment'=>'comment')->save
Model::Model::Comment=HASH(0x....)
> app->Model->Text->find_one(id=>1)
Model::Model::Text=HASH(0x....)
> app->Model->Text->find_one(id=>2)
Model::Model::Comment=HASH(0x....)
concrete table inheritance (weitere infos)
create table posts (
id int auto_increment,
text text,
PRIMARY KEY (id)
);
create table comment (
id int auto_increment,
text text,
PRIMARY KEY (id)
);
hier gibt es zwei tabellen (zusaetzlich muss noch sichergestellt werden, dass diese tabellen nicht die gleichen ids verwenden, entweder auto_increment bei einer tabelle erst bei 1000000 beginnen lassen (danke fuer den tip von adrian), oder irgendwas mit einer sequence). hier wieder der noetige perl code. weiters zu beachten ist, dass man bei den subclassen zusaetzlich zur superklasse (hier z.b. MyApp::Model::Text) noch das WebTek::Model in das @ISA array aufnimmt (sonnst initialisiert der WebTek::Loader das model nicht).
MyApp/Model/Text.pm
sub find_one_factory {
my $class = shift;
return app->Model->Post->find_one(@_)
|| app->Model->Comment->find_one(@_);
}
sub find_factory {
my $class = shift;
return app->Model->Post->find(@_)
|| app->Model->Comment->find(@_);
}
sub where_factory {
my $class = shift;
return app->Model->Post->where(@_)
|| app->Model->Comment->where(@_);
}
sub count_factory {
my $class = shift;
return app->Model->Post->count(@_)
|| app->Model->Comment->count(@_);
}
sub count_where_factory {
my $class = shift;
return app->Model->Post->count_where(@_)
|| app->Model->Comment->count_where(@_);
}
#... add more general methods
sub foo { ... }
sub text_as_uppercase { ... }
sub whatever { ... }
MyApp/Model/Post.pm
use base qw( MyApp::Model::Text WebTek::Model );
MyApp/Model/Comment.pm
use base qw( MyApp::Model::Text WebTek::Model );
so, dass ist jetzt viel komplizierter, aber man hat dafuer eine saubere trennung der datenbank. und so kann das dann verwendet werden:
> app->Model->Post->new_default('text'=>'text')->save
Model::Model::Post=HASH(0x....)
> app->Model->Comment->new_default('text'=>'comment')->save
Model::Model::Comment=HASH(0x....)
> app->Model->Text->find_one_factory(id=>1)
Model::Model::Post=HASH(0x....)
> app->Model->Text->find_one_factory(id=>2)
Model::Model::Comment=HASH(0x....)
> app->Model->Text->find_one_factory(id=>2)->text_as_uppercase
COMMENT
class table inheritance (weitere infos)
gibts nicht, ist mir zu kompliziert... :)
hab heut wieder etwas gebastelt und will euch das nicht vorenthalten. zum einen ETag support, zum anderen einen page-cache. weiters will ich hier gleich generell die caching-mechanismen in webtek erlaeutern:
Es gibt folgende arten von cache:
- macro-cache
- model-cache
- page-cache
- ETag (= caching auf client-seite)
Macro-Cache
um den output eines macros zu cachen, wird einfach das code-attribute Cache angegeben. alternativ kann auch noch eine zeit definiert werden. z.b.
sub posts :Macro :Cache { ... }
sub posts :Macro :Cache(300) { ... } # cached fuer 5min
Model-Cache
dieser cache erlaubt model-suchabfragen zu cachen. dieses gilt aber nur fuer die find_one methode. wenn man z.b. folgende abfrage cachen will:
my $user = app->Model->User->find_one('nickname' => 'max');
dann muss man folgenden code in das User-Model schreiben:
use WebTek::Cache qw( nickname );
folgende definition erlaubt folgende (gecache'te) abfragen:
use WebTek::Cache qw( id nickname street,zipcode );
...
my $user = app->Model->User->find_one('id' => 123);
my $user = app->Model->User->find_one('nickname' => 'max');
my $user = app->Model->User->find_one(
'street' => $street,
'zipcode' => $zipcode
);
wenn man das $user object aendert (sprich ein $user->update(%params) macht), wird selbstverstaendlich jeglicher cache geloescht. d.h. bei der naechsten abfrage wird das model neu von der datenbank geholt, und in den cache geschrieben. weiters ist zu beachten, wenn man den protoypen von User aendert (z.b. neue db-column), dass man auch den cache loescht, da dieser sonnst natuerlich noch die alten objekte zurueckliefert!
Page-Cache
heute neu dazugekommen ist der page-cache, welcher es erlaubt gleich ganze seiten zu cachen. hierfuer einfach das Cache attribute (wieder optional mit der zeit, defaultwert ist 1min) bei der action dazufuegen.
sub rss :Action :Cache { ... }
sub rss :Action :Cache(60) { ... }
weiters zu beachten:
- dieser cache ist hauptsaechlich dafuer da, um peaks auf eine bestimme url zu "verkraften", nicht um die halbe applikation im cache zu halten (obwohl das auch funktioniert)
- der cache funktioniert nur fuer requests welche ohne einen session->user sind, da meist bei eingeloggten requests die seite viel zu dynamisch ist, um einen cache effektiv arbeiten zu lassen.
- als (teil vom) cachekey wird die komplette URI verwendet. d.h.
/weblog/rss /weblog/rss?page=2
sind zwei verschiedene cache eintraege.
ETag's
Ein Etag ist ein unique-key fuer eine url, welchen der client bei jedem request dieser url an den server uebermittelt. der server generiert nun fuer diese url seinen eigenen key, und wenn diese zwei uebereinstimmen schickt der server ein http-status 304 (= not modified) retour. damit man diese funktionalitaet in webtek verwenden kann muessen einfach nur die abhaenigkeiten ermittelt werden, wann sich client-cache out-of-date ist. klingt vielleicht kompliziert, ist es aber meist nicht.
z.b. wenn wir ein posting anzeigen wollen:
sub index :Action :ETag($self->post->modify_time) { ... }
reicht meist schon aus, um festzustellen, ob der client noch die aktuelle version im cache hat. man kann auch einfach mehr abhaenigkeiten definieren:
sub index :Action
:ETag($self->post->modify_time)
:ETag($self->post->comment_count)
{
...
}
Nachtrag
zu guter letzt noch die antwort auf die frage wo webtek daten cached. mit dabei ist schon ein client fuer memcached, welches einfach in der config/cache.config datei aktiviert wird:
{
#... define the class which should be used for caching
'class' => 'WebTek::Cache::Memcached',
#... config settings for WebTek::Cache::Memcached
'WebTek::Cache::Memcached' => {
'servers' => [ '127.0.0.1:11211' ],
},
}
man kann auch eigene Cache klassen verwenden solange sie:
- eine subclasse von WebTek::Cache sind
- und folgende methoden besitzten
- new
- set($key, $value, $expire_time)
- add($key, $value, $expire_time)
- get($key)
- delete($key)
habe soeben die I18N von properties files auf po files umgestellt. grund dafuer ist, dass po files quasi der standard in I18N sind, und es dafuer schon coole Uebersetzungsprogramme gibt (z.b. poedit). weiters ist gleich eine routine dazugekommen, welche das uebersetzten sehr vereinfachen sollte:
- automatische message-key extraktion aus .pm und .tpl files
- message-files (= .po files) mit diesen keys erweitern, sofern sie noch nicht vorhanden sind
- erstellen von .po files fuer komplett neue sprachen
das alles geht ab jetzt mit dem webtek script, z.b.
./webtek translate de
./webtek translate de en
./webtek translate de en it
extended/erstellt die neotigen po files fuer die angegebenen sprachen. selbstverstaendlich funktioniert das auch in den modulen:
./webtek --modules comments,tags translate en
output:
update file /WebTek/app/Test/messages/en.po with 24 missing keys
update file /WebTek/app/Test/modules/comments/messages/en.po with 4 missing keys
update file /WebTek/app/Test/modules/tags/messages/en.po with 6 missing keys
in den modulen werden natuerlich nur die keys gesucht/erstellt welche auch in dem modul verwendet werden.
zu guter letzt gibt es noch ein helper-script, welches die .properties files in .po files umwandelt:
./webtek script /WebTek/extra/properties_to_po.pl
sollte alles erledigen! der code ist im trunk eingecheckt, und vielleicht noch ein bisserl beta ...
so. ich hab jetzt wiedermal einen screencast gemacht, in welchem eine weblogsoftware erstellt wird, die aehnlich dem funktionsumfang von diesem weblog ist. das weblog bekommt folgende features:
- login/logout
- artikel erstellen/aendern/loeschen/online-offline stellen
- suche nach artikeln
- tags (von tagthe.net) zum artikel speichern + tagcloud in der sidebar
- kommentare
dabei werden folgende webtek-techniken verwendet:
- pages/templates (eh klar)
- events
- paginator
- module
soviel funktionalitaet hat auch ihren preis:
- der screencast dauert 1h 20min (bin gespannt ob sich das wer antut, aber man hat dann wirklich einen guten einblick wie man eine webtek-applikation from scratch erstellt). ich sage am anfang der screencast wird ca. 30-40 min dauern, und wie das halt beim programmieren so ist, dauert immer alles doppelt so lang
- es ist leider mit einer schlechten audio-quali aufgenommen, deshalb hab ich einen leichten s-sprachfehler ;) und es ist ein bisserl dumpf -> dadurch wirkt die stimme sehr beruhigend (oder auch einschlaefernd...)
- naja, das video ist fast 200MB gross.
so will es euch nimmer laenger vorenthalten. hier ist der screencast:
http://max.xaok.org/static/weblog/webtek-weblog-demo.mov
den source-code zum nachblaettern findet man unter:
https://max.xaok.org/svn/webtek-apps/Weblog1
und um die applikation laufen zu lassen gehoert das noch ins httpd.conf file.
PerlSwitches -I/WebTek/app
PerlSwitches -I/WebTek/lib
PerlRequire /WebTek/extra/startup.pl
Alias /static/weblog /WebTek/app/Weblog1/static
<Directory /WebTek/app/Weblog1/static>
Order allow,deny
Allow from all
</Directory>
<Location /weblog>
SetHandler modperl
PerlSetVar name Weblog
PerlSetVar dir /WebTek/app/Weblog1
PerlSetVar modules comments
PerlResponseHandler Weblog1::Handler
</Location>
<Location /weblog/login>
AuthType Basic
AuthName "WebTek Weblog Demo"
AuthBasicProvider file
AuthUserFile /etc/apache2/auth-files/http.passwd
Require valid-user
</Location>
etwas kurzweile damit!
ich hab jetzt relativ viel zeit investiert, um das rendering in webtek, von string-processing und vielen regular expressions, auf in perl-code uebersetzte templates umzubauen, ... mit maessig erfolg, denn leider ist perl's string-processing viel zu schnell um dort einen wirklichen vorteil rauszuholen.
einen aehnlichen ansatz hatte schon adrian im jahre 2001, als er fuer uboot eine templating engine in C programmiert hat, ... mit genauso wenig erfolg.
ein beispiel:
man wuerde ja denken, dass gerade die standard unix commandos schnell sind, aber...
imac:DrMap max$ time find . -not -path '*.svn*' -name '*.tpl' | wc -l
198
real 0m0.233s
user 0m0.153s
sys 0m0.078s
braucht deutlich laenger als
imac:DrMap max$ time perl -e \
'foreach (`find .`) { next if /.svn/; next unless /.tpl\n$/; print }' \
| wc -l
198
real 0m0.112s
user 0m0.036s
sys 0m0.083s
also dass hat mich dann schon stutzig gemacht (ich hab beide kommandos mehrere male ausgefuehrt, damit auch ja alle sys-calls im betriebssystem-cache sind).
nochmal zum eigentlichen thema. hurra jetzt ist es so, dass webtek um 5 - 10% schnellere responsezeiten hat (hab mir min. 30% erwartet). aber das programmieren hat wiedermal viel spass gemacht. der compiler ist ein einfacher rekursiver parser mit folgender syntax:
Terminale:
==========
CHAR -> .
EQUAL -> =
DOT -> \.
PIPE -> \|
ESCAPE -> \\
QUOTE_START -> " | ' | q\{ | q\[ | q\(
QUOTE_END -> " | ' | \} | \] | \)
SPACE -> \s+
NAME -> \w+
MACRO_START -> <%
MACRO_END -> %>
NonTerminale:
=============
Template -> ( Macro | CHAR )*
Macro -> MACRO_START
SPACE
NAME ( DOT NAME )*
( Param )*
( PIPE SPACE NAME ( Param )* )*
SPACE
MACRO_END
Param -> NAME EQUAL QUOTE_START Template QUOTE_END
und wiedermal, was ich an perl so mag, mit sehr wenig code-zeilen:
- scanner: 24 zielen
- parser: 70 zielen
- aus dem syntax-tree perl code erzeugen: 100 zeilen
wers genauer wissen will, hier der Compiler und hier der alte Renderer.
einen kleinen trost gibt es. es sollte jetzt doch leicht moeglich sein weitere macro-funktionalitaet zu implementieren (wie z.b. ein loop macro)
so denn...
es ist ja allgemein bekannt, dass man in javascript nicht zwingend semicolons am ende eines statements machen muss. mancheiner denkt sich "ahh wie praktisch". und ich muss zugeben, dass ich auch dann und wann das semicolon weglass (z.b. bei debug-alerts). aber wie so oft im leben schneidet man sich mit sowas frueher oder spaeter ins eigene fleisch. naemlich dann, wenn man die js-dateien packen will, kommen auf einmal haufenweise syntax-fehler
SyntaxError: missing ; before statement
wie toll. und diese zu finden ist dann natuerlich ganz lustig, zumal die browser nicht die char-position angeben, und in einem einzeiler sowas suchen... naja.
hab mich dann auf die suche gemacht nach einem simplen command-line js compiler und bin hier fuendig geworden (sehr praktisch sowas).
zum abschluss noch ein beispiel:
function x {
return "x"
}
macht keine probleme, allerdings
var x = function() {
return "x"
}
sehr wohl, denn hier gehoert ein semicolon am ende der funktions-definitioin (eigentlich eh klar, aber wer denkt schon dran).
ps: erstaunlicherweise haben die dateien von script.aculo.us die meisten fehler dieser art beinhaltet!