=pod =head1 StuwerCal =head2 META =head3 Author Thomas Klausner =head3 Email domm AT plix.at =head3 URL http://domm.plix.at =head3 URL_slides http://domm.plix.at/talks/2011_riga_stuwercal =head3 Date 2011-08-16 =head3 Location Riga =head3 Event YAPC::Europe 2011 =head2 Stuwerviertel =for newslide =for img wien.jpg =for newslide =for img wien2.jpg =for newslide nice area central green BUT bad rep =for newslide street prostitution B street prostitution cheap poor people (=immigrants) vs old people plus a tiny bit of gentrification =for newslide City of Vienna started some projects to improve quality of life Intercultural dancing lessions Exhibitions of local artists Neighborhood garden =for newslide =for img garten.jpg =for newslide and I volunteered to hack some software because that's what I do... =for newslide =for img hacking.jpg =head2 StuwerCal Basic Idea: Make activities by the various actors in the Stuwerviertel visible by presenting their events on a single webpage without need for human interaction B =head2 iCal iCal is a standard to exchange information about events lots of calendar software supports it lots of blog software can export it http://calendar.google.com on the inside it looks disgusting =for newslide BEGIN:VEVENT DTSTART:20110110T170000Z DTEND:20110110T190000Z DTSTAMP:20110201T084057Z UID:5kt3l7baskmhbl6l0fb6or9oa4@google.com ATTENDEE;CUTYPE=INDIVIDUAL;ROLE=REQ-PARTICIPANT;PARTSTAT=ACCEPTED;X-NUM-GUE STS=0:mailto:pcds0caordl4h0dug15q1hnmng@group.calendar.google.com CREATED:20101014T183904Z DESCRIPTION:Arbeitskreis im Rahmen des Stadtteilmanagements Stuwerviertels\ , gemeinsame Ideen\, gemeinsame Projekte für das Stuwerviertel werden hier entwickelt. Interessenten jederzeit willkommen. LAST-MODIFIED:20101227T025818Z LOCATION:Grätzelzentrum\, Max Winter Platz 23\, 1020 Wien SEQUENCE:0 STATUS:CONFIRMED SUMMARY:Arbeitskreis Kultur\, Image\, Markt und Wirtschaft TRANSP:OPAQUE END:VEVENT =for newslide **BEGIN**:VEVENT DTSTART:20110110T170000Z DTEND:20110110T190000Z DTSTAMP:20110201T084057Z UID:5kt3l7baskmhbl6l0fb6or9oa4@google.com ATTENDEE;CUTYPE=INDIVIDUAL;ROLE=REQ-PARTICIPANT;PARTSTAT=ACCEPTED;X-NUM-GUE STS=0:mailto:pcds0caordl4h0dug15q1hnmng@group.calendar.google.com CREATED:20101014T183904Z DESCRIPTION:Arbeitskreis im Rahmen des Stadtteilmanagements Stuwerviertels\ , gemeinsame Ideen\, gemeinsame Projekte für das Stuwerviertel werden hier entwickelt. Interessenten jederzeit willkommen. LAST-MODIFIED:20101227T025818Z LOCATION:Grätzelzentrum\, Max Winter Platz 23\, 1020 Wien SEQUENCE:0 STATUS:CONFIRMED SUMMARY:Arbeitskreis Kultur\, Image\, Markt und Wirtschaft TRANSP:OPAQUE **END**:VEVENT =for newslide BEGIN:VEVENT **DTSTART**:@@20110110T170000Z@@ **DTEND**:@@20110110T190000Z@@ DTSTAMP:20110201T084057Z UID:5kt3l7baskmhbl6l0fb6or9oa4@google.com ATTENDEE;CUTYPE=INDIVIDUAL;ROLE=REQ-PARTICIPANT;PARTSTAT=ACCEPTED;X-NUM-GUE STS=0:mailto:pcds0caordl4h0dug15q1hnmng@group.calendar.google.com CREATED:20101014T183904Z DESCRIPTION:Arbeitskreis im Rahmen des Stadtteilmanagements Stuwerviertels\ , gemeinsame Ideen\, gemeinsame Projekte für das Stuwerviertel werden hier entwickelt. Interessenten jederzeit willkommen. LAST-MODIFIED:20101227T025818Z LOCATION:Grätzelzentrum\, Max Winter Platz 23\, 1020 Wien SEQUENCE:0 STATUS:CONFIRMED SUMMARY:Arbeitskreis Kultur\, Image\, Markt und Wirtschaft TRANSP:OPAQUE END:VEVENT =for newslide BEGIN:VEVENT DTSTART:20110110T170000Z DTEND:20110110T190000Z DTSTAMP:20110201T084057Z UID:5kt3l7baskmhbl6l0fb6or9oa4@google.com ATTENDEE;CUTYPE=INDIVIDUAL;ROLE=REQ-PARTICIPANT;PARTSTAT=ACCEPTED;X-NUM-GUE STS=0:mailto:pcds0caordl4h0dug15q1hnmng@group.calendar.google.com CREATED:20101014T183904Z **DESCRIPTION**:Arbeitskreis im Rahmen des Stadtteilmanagements Stuwerviertels\ , gemeinsame Ideen\, gemeinsame Projekte für das Stuwerviertel werden hier entwickelt. Interessenten jederzeit willkommen. LAST-MODIFIED:20101227T025818Z **LOCATION**:Grätzelzentrum\, Max Winter Platz 23\, 1020 Wien SEQUENCE:0 STATUS:CONFIRMED **SUMMARY**:Arbeitskreis Kultur\, Image\, Markt und Wirtschaft TRANSP:OPAQUE END:VEVENT =for newslide BEGIN:VEVENT DTSTART:20110110T170000Z DTEND:20110110T190000Z DTSTAMP:20110201T084057Z UID:5kt3l7baskmhbl6l0fb6or9oa4@google.com ATTENDEE;CUTYPE=INDIVIDUAL;ROLE=REQ-PARTICIPANT;PARTSTAT=ACCEPTED;X-NUM-GUE STS=0:mailto:pcds0caordl4h0dug15q1hnmng@group.calendar.google.com CREATED:20101014T183904Z DESCRIPTION:Arbeitskreis im Rahmen des Stadtteilmanagements Stuwerviertels**\ ,** gemeinsame Ideen**\,** gemeinsame Projekte für das Stuwerviertel werden hier entwickelt. Interessenten jederzeit willkommen. LAST-MODIFIED:20101227T025818Z LOCATION:Grätzelzentrum**\,** Max Winter Platz 23**\,** 1020 Wien SEQUENCE:0 STATUS:CONFIRMED SUMMARY:Arbeitskreis Kultur**\,** Image**\,** Markt und Wirtschaft TRANSP:OPAQUE END:VEVENT =for newslide BEGIN:VEVENT DTSTART;TZID=Europe/Vienna:20110216T190000 DTEND;TZID=Europe/Vienna:20110216T230000 **RRULE:FREQ=MONTHLY;BYDAY=3WE;WKST=MO** DTSTAMP:20110201T084057Z UID:km1dlnl4sg44h9vag064llv4gs@google.com CREATED:20100128T120505Z DESCRIPTION:Jeden dritten Mittwoch im Monat gemeinsames Musizieren im... LAST-MODIFIED:20110127T120724Z SEQUENCE:4 STATUS:CONFIRMED SUMMARY:Musiksession TRANSP:OPAQUE END:VEVENT Recurring Rule: Every third wednesday =for newslide This makes parsing iCal a little bit annoying. Luckily we have this thing called CPAN and B by Rick Frankel =head2 iCal::Parser use iCal::Parser; my $parser = iCal::Parser->new( start => '20110101', end => DateTime->now, ); =for newslide use iCal::Parser; my $parser = iCal::Parser->new( **start** => '20110101', **end** => DateTime->now, ); =for newslide use iCal::Parser; my $parser = iCal::Parser->new( start => %%'20110101'%%, end => DateTime->now, ); =for newslide use iCal::Parser; my $parser = iCal::Parser->new( start => '20110101', end => %%DateTime->now%%, ); =for newslide $parser->parse( '/some/calendar.ical' ); =for newslide $parser->parse( '/some/calendar.ical' ); $parser->parse_strings( $ical_as_string ); The two calendars are automatically merged. =for newslide my $data = $parser->calendar; C<< $data >> is a HASHREF containing C<< calendars >>, C<< events >> and C<< todos >> C<< calenders >> contains references to the different iCal-Files we parsed. I don't care about C<< todos >> But let's take a closer look at C<< events >>: =for newslide C<< events >> is a deep HASHREF, keyed by C<< year >>, C<< month >> and C<< day >>. $events->{2011}{8}{16} At the C<< day >> level we find an ARRAYREF of events, sorted by time. The events themselves are rather plain HASHREFs that mimic the name of the iCal keys: =for newslide { 'UID' => '95CCBF98-3685-11D9-8CA5-000D93C45D90', 'idref' => '7CCE8555-3516-11D9-8A43-000D93C45D90', 'DTSTAMP' => \%DateTime, 'DTEND' => \%DateTime, 'DTSTART' => \%DateTime, ... }, =for newslide The really nice thing about C<< iCal::Parser >> is that it handles recurring events for you. So if you specifiy a recurring event like 'every third Monday' this event takes up only one slot in your iCal file. But C<< iCal::Parser >> copies the data into all relevant days. This makes processing very easy, because you don't have to think about recurring events, events spanning several days, exceptions to recurring events, etc. =for newslide So one half of my aggregator was basically already finished. All I had to do was to write a smallish script that =over =item * fetches the iCal-files from the various websites, =item * let C<< iCal::Parser >> do all the hard work, =item * go through the C<< events >> hash and store the data somewhere. =back =for newslide foreach my $year ( keys %{ $data->{events} } ) { foreach my $month ( keys %{ $data->{events}{$year} } ) { foreach my $day ( keys %{ $data->{events}{$year}{$month} } ) { my $events = $data->{events}{$year}{$month}{$day}; foreach my $event ( values %$events ) { my $cal = $rs->find_or_create({ uid => join('',$year,$month,$day,$event->{UID}), calendar => $self->idref2cal->{ $event->{idref} }->id, }, { key => 'uid_unique' } ); $cal->update({ start => $event->{DTSTART}, stop => $event->{DTEND}, description => _val( $event->{DESCRIPTION} ), location => _val( $event->{LOCATION} ), summary => _val( $event->{SUMMARY} ), url => _val( $event->{URL} ), }); } } } } =head2 The Frontend Now I needed to create a small website. My initial version (which took 6 hours to build from scratch, including discovering C<< iCal::Parser >>) wrote an HTML snippet per event to the filesystem and had some JQuery assemble those. But this wasn't flexible enough, so I decided this project to be a good excuse to take a look at C<< Dancer >> =head2 Dancer Dancer calls itself a B It's similar to Mojolicous or Sinatra (which seems to have started the microframework craze..) A very minimal app looks like this: =for newslide #!/usr/bin/perl use Dancer; get '/hello/:name' => sub { return "Why, hello there " . params->{name}; }; dance; That's just one plain C script, and it's all you need. This setup is of course much simpler than e.g. Catalyst. There are some other talks on Dancer during this conference =for newslide #!/usr/bin/perl use Dancer; **get** '/hello/:name' => sub { return "Why, hello there " . params->{name}; }; dance; =for newslide #!/usr/bin/perl use Dancer; get '**/hello/:name**' => sub { return "Why, hello there " . params->{name}; }; dance; =for newslide #!/usr/bin/perl use Dancer; get '/hello/:name' => **sub** { return "Why, hello there " . params->{name}; }; dance; =for newslide #!/usr/bin/perl use Dancer; get '/hello/%%:name%%' => sub { return "Why, hello there " . **params**->{%%name%%}; }; dance; =for newslide I was a bit irritated by the lack of objects Catalyst requires B objects my ( $self, $c ) = @_; But Dancer none?? Everything is "just" a function. =for newslide params->{name}; =for newslide config->{appname} =for newslide content_type 'text/plain'; =for newslide template 'some/template.tt' { foo => 'bar' }; =head2 StuwerCal::Web Live Demo! =for newslide 169 lines in total (including blank lines etc) 74 lines are needed for the calendar navigation widget that leaves us with 95 lines of proper code... =for newslide package StuwerCal::Web; use strict; use warnings; use 5.010; use Dancer ':syntax'; use StuwerCal::ConnectDB; use DateTime; use DateTime::Format::Strptime; use Calendar::Simple; my $dp = DateTime::Format::Strptime->new( pattern=>'%Y-%m-%d', locale=>'de_AT', time_zone => 'Europe/Vienna', ); my $schema = StuwerCal::ConnectDB->connect; DateTime->DefaultLocale('de_AT'); =for newslide get '/' => sub { show_day(now()) }; get '/day' => sub { show_day(now()) }; get '/day/:day' => sub { show_day($dp->parse_datetime(params->{'day'})) }; =for newslide sub show_day { my $day = shift; my $stash = { events => $schema->resultset('Event')->per_day($day), }; make_daynav($stash,$day); make_kalender($stash,$day); make_upcoming($stash); template 'events_per_day', $stash ; } =for newslide get '/event/:id' => sub { my $stash = { event => $schema->resultset('Event')->find(params->{'id'}) }; my $event_day = $stash->{event}->start; make_daynav($stash,$event_day); make_kalender($stash,$event_day); make_upcoming($stash); template 'event', $stash; }; =for newslide get '/source' => sub { my $stash = { sources => $schema->resultset('Calendar')->list, }; make_kalender($stash); make_upcoming($stash); template 'source', $stash; }; =for newslide get '/source/:id' => sub { my $stash = { source => $schema->resultset('Calendar')->find(params->{'id'}), }; make_kalender($stash); make_upcoming($stash); template 'events_per_source', $stash; }; =for newslide get '/about' => sub { template 'about' ; }; get '/kontakt' => sub { template 'kontakt' ; }; =for newslide sub make_daynav { my ($stash, $day) = @_; $stash->{prev_day} = uri_for('/day/'.$day->clone->subtract(days=>1)->ymd('-')); $stash->{next_day} = uri_for('/day/'.$day->clone->add(days=>1)->ymd('-')); $stash->{this_day} = uri_for('/day/'.$day->ymd('-')); $stash->{this_day_label} = $day->strftime("%A, %d. %B %Y"); } =for newslide sub make_upcoming { my ($stash) = @_; $stash->{upcoming} = $schema->resultset('Event')->upcoming(now()); } =for newslide get '/blog' => sub { my $stash = { posts => $schema->resultset('Blog')->list, }; template 'blog_posts', $stash; }; get '/blog/:id' => sub { my $stash = { blog => $schema->resultset('Blog')->find(params->{'id'}), }; template 'blog', $stash; }; =for newslide Dancer comes with it's own mini-server perl -Ilib bin/app.pl =for newslide but can also be deployed using Plack plackup -s Starman app.psgi =head3 Adding content You may have noticed that there are now actions to add content. That's because only I can add new calendars or blog entries. And I don't need a UI... Instead I wrote a few scripts that set up new stuff from the command line. Much easier to skip all the nasty HTML form handling And authentification issues... =for newslide F =for newslide #!/usr/bin/env perl use strict; use warnings; use 5.010; use FindBin; use lib "$FindBin::Bin/../lib"; use StuwerCal::Script::AddBlog; StuwerCal::Script::AddBlog->new_with_options->run; =for newslide #!/usr/bin/env perl use strict; use warnings; use 5.010; use FindBin; use lib "$FindBin::Bin/../lib"; use StuwerCal::Script::AddBlog; **StuwerCal::Script::AddBlog**->new_with_options->run; =for newslide #!/usr/bin/env perl use strict; use warnings; use 5.010; use FindBin; use lib "$FindBin::Bin/../lib"; use StuwerCal::Script::AddBlog; StuwerCal::Script::AddBlog->**new_with_options**->run; =for newslide #!/usr/bin/env perl use strict; use warnings; use 5.010; use FindBin; use lib "$FindBin::Bin/../lib"; use StuwerCal::Script::AddBlog; StuwerCal::Script::AddBlog->new_with_options->**run**; =for newslide package StuwerCal::Script::AddBlog; use 5.010; use Moose; use namespace::autoclean; use MooseX::Types::Path::Class; with qw( MooseX::Getopt StuwerCal::Role::DB ); has 'file' => (is=>'ro',isa=>'Path::Class::File',required=>1,coerce=>1); sub run { my $self = shift; my %blog; my @in = $self->file->slurp(iomode => '<:encoding(UTF-8)'); # boring code that fills %blog with the files content my $blog = $self->schema->resultset('Blog')->create(\%blog); say "created blog post ".$blog->id; } =for newslide package StuwerCal::Script::AddBlog; use 5.010; use **Moose**; use namespace::autoclean; use MooseX::Types::Path::Class; with qw( MooseX::Getopt StuwerCal::Role::DB ); has 'file' => (is=>'ro',isa=>'Path::Class::File',required=>1,coerce=>1); =for newslide package StuwerCal::Script::AddBlog; use 5.010; use Moose; use namespace::autoclean; use MooseX::Types::Path::Class; with qw( MooseX::Getopt StuwerCal::Role::DB ); @@has@@ '**file**' => (is=>'ro',isa=>'Path::Class::File',required=>1,coerce=>1); =for newslide package StuwerCal::Script::AddBlog; use 5.010; use Moose; use namespace::autoclean; use MooseX::Types::Path::Class; with qw( MooseX::Getopt StuwerCal::Role::DB ); has '**file**' => (is=>'ro',isa=>'@@Path::Class::File@@',%%required%%=>1,%%coerce%%=>1); =for newslide package StuwerCal::Script::AddBlog; use 5.010; use Moose; use namespace::autoclean; use MooseX::Types::Path::Class; with qw( **MooseX::Getopt** StuwerCal::Role::DB ); has 'file' => (is=>'ro',isa=>'Path::Class::File',required=>1,coerce=>1); =for newslide package StuwerCal::Script::AddBlog; use 5.010; use Moose; use namespace::autoclean; use **MooseX::Types::Path::Class**; with qw( **MooseX::Getopt** StuwerCal::Role::DB ); has 'file' => (is=>'ro',isa=>'Path::Class::File',required=>1,coerce=>1); =for newslide package StuwerCal::Script::AddBlog; use 5.010; use Moose; use namespace::autoclean; use **MooseX::Types::Path::Class**; with qw( **MooseX::Getopt** StuwerCal::Role::DB ); has 'file' => (is=>'ro',isa=>'%%Path::Class::File%%',required=>1,coerce=>1); =for newslide ~/jobs/stuwercal$ perl bin/add_blog.pl --file some_file.txt =for newslide sub **run** { my $self = shift; my %blog; my @in = $self->file->slurp(iomode => '<:encoding(UTF-8)'); # boring code that fills %blog with the files content my $blog = $self->schema->resultset('Blog')->create(\%blog); say "created blog post ".$blog->id; } =for newslide sub run { my $self = shift; my %blog; my @in = $self->**file**->@@slurp@@(iomode => '<:encoding(UTF-8)'); # boring code that fills %blog with the files content my $blog = $self->schema->resultset('Blog')->create(\%blog); say "created blog post ".$blog->id; } =for newslide sub run { my $self = shift; my %blog; my @in = $self->file->slurp(iomode => '<:encoding(UTF-8)'); **# boring code that fills %blog with the files content** my $blog = $self->schema->resultset('Blog')->create(\%blog); say "created blog post ".$blog->id; } =for newslide sub run { my $self = shift; my %blog; my @in = $self->file->slurp(iomode => '<:encoding(UTF-8)'); # boring code that fills %blog with the files content my $blog = $self->schema->**resultset**(@@'Blog'@@)->**create**(\%blog); say "created blog post ".$blog->id; } =for newslide sub run { my $self = shift; my %blog; my @in = $self->file->slurp(iomode => '<:encoding(UTF-8)'); # boring code that fills %blog with the files content my $blog = $self->schema->resultset('Blog')->create(\%blog); **say** "@@created blog post@@ ".$blog->id; } Good way to get data into webapps if only $admins will fill it in. =head2 Dotcloud dotcloud.com is a web service that allows you to "build and deploy any application to the cloud" =for newslide =for img dotcloud.png =for newslide A few months ago they announced support for Perl via PSGI And I thought it would be a nice idea to give it a shot =head3 Install the dotcloud client ~$ sudo easy_install pip && sudo pip install dotcloud Yes, that's Python... sssssss ~$ dotcloud Enter your api key (You can find it at http://www.dotcloud.com/account/settings): =for newslide ~$ dotcloud -h Command line tool to interact with dotcloud .. lots of options =head3 Set up an application ~/jobs/stuwercal$ dotcloud create stuwercal Created application "stuwercal" =head3 Configure a service dotcloud defines an application to have several services You have to tell it about the name and types of those services via a config file called F. frontend: type: perl In this example, C is the name of the service. And it's running on C =for newslide Now you can push your code to dotcloud: ~/jobs/stuwercal$ dotcloud push stuwercal . You have to make sure to include all your prereqs in your Makefile.PL or Build.PL dotcloud will use this information to automatically fetch and install all of those prereqs After some time your application should be up an running. =head3 Configure a database service Of course I also need a database dotcloud supports several, so I of course use Postgres I added this to F frontend: type: perl psql: type: postgresql And pushed again ~/jobs/stuwercal$ dotcloud push stuwercal . =head3 Getting info ~/jobs/stuwercal$ dotcloud info stuwercal frontend: config: path: / plack_env: deployment static: static uwsgi_processes: 4 instances: 1 type: perl url: http://2281c8a9.dotcloud.com/ psql: instances: 1 type: postgresql =head3 SSH Access ~/jobs/stuwercal$ dotcloud ssh stuwercal.frontend # $SHELL dotcloud@stuwercal-default-frontend-0:~$ =head3 Accessing another service In each service there is a file called F which contains information on all other services in this application ~/jobs/stuwercal$ dotcloud run stuwercal.frontend -- less environment.json # less environment.json =for newslide { "DOTCLOUD_ENVIRONMENT": "default", "DOTCLOUD_PROJECT": "stuwercal", "DOTCLOUD_SERVICE_NAME": "frontend", "DOTCLOUD_PSQL_SQL_PASSWORD": "***", "DOTCLOUD_PSQL_SQL_LOGIN": "root", "DOTCLOUD_PSQL_SQL_URL": "pgsql://root:***@90b7e769.dotcloud.com:6871", "DOTCLOUD_DB_SQL_LOGIN": "root", "DOTCLOUD_PSQL_SQL_PORT": "6871", "DOTCLOUD_SERVICE_ID": "0", "DOTCLOUD_PSQL_SQL_HOST": "90b7e769.dotcloud.com", } =for newslide { "DOTCLOUD_ENVIRONMENT": "default", "DOTCLOUD_PROJECT": "stuwercal", "DOTCLOUD_SERVICE_NAME": "frontend", "DOTCLOUD_PSQL_SQL_PASSWORD": "***", "DOTCLOUD_PSQL_SQL_LOGIN": "root", "DOTCLOUD_PSQL_SQL_URL": "pgsql://root:***@90b7e769.dotcloud.com:6871", "DOTCLOUD_DB_SQL_LOGIN": "root", "**DOTCLOUD_PSQL_SQL_PORT**": "6871", "DOTCLOUD_SERVICE_ID": "0", "**DOTCLOUD_PSQL_SQL_HOST**": "90b7e769.dotcloud.com", } =for newslide package StuwerCal::ConnectDB; if (-e $dotcloud_env->stringify) { my $dc = decode_json($dotcloud_env->slurp); $db .= ';host=' .$dc->{'DOTCLOUD_PSQL_SQL_HOST'} .';port=' .$dc->{'DOTCLOUD_PSQL_SQL_PORT'}; $pwd = $dc->{'DOTCLOUD_PSQL_SQL_PASSWORD'}; } =for newslide There are more commands to view log files, get status info, etc see C They have a free plan (you get 2 services) and of course you can throw some money at them for more features... =head2 Wrap up iCal is ugly, but C makes it manageable Dancer is quite nice, I'm a bit irritated by the functional interface I'm not sure I would use it for a big project. dotcloud seems OK, but I haven't used the service a lot yet. =head2 Questions?