bag of tricks from iusethis
DESCRIPTION
practical examples of how I used Catalyst and DBIx::Class to write http://osx.iusethis.com/, a social-software site for software.TRANSCRIPT
Bag of tricks from
iusethis.com
6. Who's behind iusethis? Is it a corporate thingie? Nah. Iusethis was made by Arne and Marcus. We're just these guys, you know? But we really know where our towels are.
Social Software
For Software
Status after 1 year
• 15.000 registered users
• 4.000 registered apps
• ~ 250.000 page views per day
• 20.000 revisions committed to SVN
Catalyst made such rapid development
possible
While still keeping it sane to maintain
Design Philosophies
Design Philosophies• URLs matter!
Design Philosophies• URLs matter!
• AJAX when it makes sense
Design Philosophies• URLs matter!
• AJAX when it makes sense
• Support standards
Design Philosophies• URLs matter!
• AJAX when it makes sense
• Support standards
• Provide alternative
data-formats
Design Philosophies• URLs matter!
• AJAX when it makes sense
• Support standards
• Provide alternative
data-formats
• The user owns his data
AJAX
AJAH
AJAHXMLHTTPRequest and DIV filling
Enhance
Not Replace
Example:<span><a href="" onclick= "CallBmk(); return false;" >Bookmark this</a></span>
NO!
Lynx Friendly First:<a href=”/do_some_shit”
id=”do_some_shit”/>
Meanwhile...
<script type=”text/javascript”>$(‘do_some_shit’).onclick=function() { // Fill that div good! return false;}// /do_some_shit is never called</script>
Side note:<a href=”/do_some_shit”
id=”do_some_shit”/>
<a href=”[%c.uri_for(‘/do_some_shit’)%]”
id=”do_some_shit”/>
Ditto for Forms<form action=”[%c.uri_for(‘/doit’)%]” id=”doit”/>
<script type=”text/javascript”> $(‘doit’).onsubmit=function() { // Do something neat with the form return false;} // form is never submitted</script>
Server side Validation without submit
<script type=”text/javascript”>$(‘register_screenname’).onchange = function() { var screenname=”name=”+ $(‘register_screenname’).value var updater=new Ajax.Updater( ‘register_screenname_errors’, ‘[%c.uri_for(‘/jsrpc/user_name_available’)%]’, {parameters: screenname });}</script>
iusethis forms are generated by HTML::Widget - lots of hooks through classes/ids
And in the Controller:
sub user_name_available : Local { my ($self,$c) = @_; if ($c->model('DB::Person') ->search({ screenname => $c->req->param('name') })->count()) { $c->res->body('<span>'. $c->req->param('name'). ' is already registered</span>'); } else { $c->res->body(' '); } }
Case Study:iusethis counter
<div class=”iusethis”> <div id=”iuse_[%app.short%]” class=”count”> [%app.count%]</div> <a id=”mark_[%app.id%]” href="[%c.uri_for('/app/iusethis', app.id,secret,c.user.obj.screenname)%]" title="Mark as used">i use this</a></div>
<script type=”text/javascript”>$(‘mark_[%app.id%]’).onclick=function() { new Ajax.Updater('iuse_[%app.short%]', '[%c.uri_for('/app/iusethis', app.id,secret,c.user.obj.screenname)%]', {evalScripts:true,method:'post', postBody:'count=[%use_count%]'}); return false;}</script>
Inline Javascript
Behaviour.jsCleaner - But slower
Meanwhile, in the controller...
sub iusethis : Local { my ($self,$c,$id,$secret,$screenname) = @_; my $app:Stashed = $c->model('DB::Application') ->find($id); $c->stash->{app}->add_to_iuses( {person=>$c->user_object}); $c->forward('app',[$app->short]) unless( $c->req->header('x-requested-with') && $c->req->header('x-requested-with') eq 'XMLHttpRequest'); # otherwise render ajax template}
$secret ?
Protect against abuse
Catalyst::Plugin::RequestToken
Let’s look at a variant
Email Validation sub confirm_email : Private { my ($self,$c,$user) = @_; my $item:Stashed=$user; my $seed:Stashed= md5_hex( $user->registered.$c->config->{seed}); $c->email( header => [ From => $c->config->{system_mail}, To => $user->email, Subject => 'iusethis email confirmation.' ], body => $c->view('TT')->render($c,'mailwelcome.tt'), ); my $template:Stashed='validate.tt';
RSS
sub hot_xml : Path('/hot.rss') { my ($self,$c) = @_; my $feed:Stashed; $c->forward('hot'); $c->forward('rss'); $feed->title( 'Hot apps from iusethis.com'); $feed->link( $c->uri_for('/hot')); $c->res->body($feed->as_xml);}
sub rss : Private { my ($self,$c) = @_; my $feed:Stashed= XML::Feed->new('RSS'); $feed->link($c->uri_for('/')); $feed->tagline( 'i use this. What do you use?'); my $app:Stashed; if ($apps) { while( my $app = $apps->next ) { .... # New entry } }}
Another alternative
rss.tt:<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:iusethis="http://osx.iusethis.com/ns/rss" xmlns:dc="http://purl.org/dc/elements/1.1/"> <channel> <title>[% title || 'RSS Feed from iusethis.com '%]</title>.....
Autodiscoveryheader.tt:
[% IF rss %]<link rel="alternate" title="Iusethis RSS" href="[%rss%]" type="application/rss+xml"/>
[% END %]
Authentication
DBIC AuthPlugins:
Authentication Authentication::Store::DBIC Authentication::Credential::Password
config:authentication: dbic: user_class: DBIC::Person password_field: password user_field: - screenname - email
And in the Controller:
if ($c->req->param('email')) { if ( $c->login($c->req->param('email'),$c->req->param('pass')) ) { my $root=$c->uri_for('/'); delete $c->req->params->{referer} if $c->req->params->{referer} eq $root || $c->req->params->{referer} !~ m/^$root/ || $c->req->params->{referer} eq $root."logout"; $c->res->redirect( $c->req->params->{referer} || $c->uri_for('/user',$c->user->obj->screenname)); if ($c->req->param('openid')) { $c->user->obj->openid($c->req->param('openid')); $c->user->obj->update(); } } else { my $alert:Stashed='Login failed'; }
Basic AuthPlugins:
Authentication::Credential::HTTP
config:
authentication: http: type: basic
And in the Controller:
sub auto : Private { my ( $self, $c ) = @_; return 1 if $c->action eq 'api/index'; $c->authorization_required( realm => "iusethis" ); unless ($c->req->method eq 'POST') { $c->res->body('API Requests require HTTP POST'); return 0; } return 1;}
OpenIDPlugins:
Authentication::Credential::OpenID
And in the Controller:
if ($c->authenticate_openid) { if (my $person=$c->model('DBIC::Person') ->find({openid=>$c->user->url})) { my $user=$c->get_user($person->screenname) || die "Could not make user object for " . $person->screenname."\n"; $c->set_authenticated($user); return $c->res->redirect( $c->uri_for('/user',$person->screenname)); } my $openid:Stashed=$c->user->url; $c->logout; } elsif (! @{$c->error}) { return if $c->res->redirect; $c->res->redirect($c->uri_for( '/login',{openid_failed=>1})); }};
OPMLOutline Processor Markup
Language -- is an XML format for outlines.
sub user_opml : Global { my ($self,$c,$screenname)= @_; my $user=$c->model('DBIC::Person') ->search({ screenname=>$screenname})->first; my $opml=XML::OPML::SimpleGen->new(); $opml->head(title =>'Apps used by '.$user->screenname); my $apps=$user->applications; ...
while (my $app=$apps->next) { $opml->add_outline( text => $app->name, count => $app->iuses->count, icon => $app->has_icon($c) ? $app->icon_uri($c) : $c->uri_for_img('default.png') ->as_string, xmlUrl => $c->uri_for('/appcast',$app->short) ->as_string, group => $app->uses_this($user) ->iloveit ? 'loved' : 'apps', ); } $c->res->body($opml->as_string); $c->res->content_type('text/xml'); }
Tags
CREATE TABLE tag ( id INTEGER PRIMARY KEY, name TEXT, application INT REFERENCES application );
Aggregating popular tags
sub aggregated : ResultSet { return scalar shift->search({'me.name', { -not_in => [ @banned_tags ]}}, { select=>[{count => 'id'},'name' ], as=>[qw/tagcount name/], group_by=>[qw/name/], order_by=>"count(id) DESC", page=>1, rows=>(shift||10) }); }
Findingrelated
tags
sub related : ResultSet { my ($self,$tag)=@_; return $self->search({ 'related.name'=>$tag, 'me.name',{-not_in => [@banned_tags ]}, 'me.name'=>(ref $tag ? {-not_in,$tag} : {'!=',$tag}), },{ select=>[{count => 'me.name'},'me.name' ], as=>[qw/tagcount name/], join=>'related', group_by=>[qw/me.name/], order_by=>"count(me.name) DESC", page=>1,rows=>10, }); }
Tag Cloud
HTML::TagCloud
0.33 Mon Mar 13 20:26:36 GMT 2006 - add a 'tags' method that extracts most of the logic from the html method. It also adds support for setting levels as a parameter to the constructor. It defaults to the before-hardcoded 24. (thanks to Marcus Ramberg)----
<marcus> acme++
sub get_cloud :ResultSet { my ($self,$c,$limit) = @_; my $cloud = HTML::TagCloud->new(levels=>5); my $tags = $self->aggregated($limit||75); while( my $tag=$tags->next() ) { $cloud->add(lc($tag->name),$c->uri_for('/tag', lc($tag->name)),$tag->get_column('tagcount')); } return $cloud; }
Caching
Catalyst::Plugin::PageCache
page_cache: auto_check_user: 1 set_http_headers: 1 expires: 120 no_cache_debug: 1 auto_cache: - '/top*' - '/hot*'
Only for Anon
Not POST
Just Works
Profile Builder
OSX perl app
Just core modules
@apps= map { make_short($_) } grep{ /\.(?:app|wdgt|prefPane)$/ } find_apps('/Applications'), find_apps($ENV{HOME}."/Library/PreferencePanes"), find_apps($ENV{HOME}."/Library/Widgets"); my $data='-F apps='.join(' -F apps=',@apps); my $res=`curl -s $data http://osx.iusethis.com/profile/send`;
system('open','http://osx.iusethis.com/profile/view/'.$res.'?match=1');
Last Trick
iwatchthis.com
Random Profile
sub random : Global { my ($self,$c) = @_; my $user=$c->model('DB::Person') ->search({},{ rows => 1, order_by => "rand()", })->next(); $c->res->redirect( $c->uri_for('/'.$user->login)); }
Another person
sub random : Global { my ($self,$c,$feed) = @_; my $user=$c->model('DB::Person') ->search({ login => {"-not_in"=>[$feed]}, },{ rows => 1, order_by => "rand()", })->next(); $c->res->redirect( $c->uri_for('/'.$user->login)); }
one with movies
sub random : Global { my ($self,$c,$feed) = @_; my $user=$c->model('DB::Person') ->search({ login => { '!=' => $feed}, },{ rows => 1, order_by => "rand()", join => [qw/items/], having=>{'count(items.id)' => {'>',0 }}, group_by => 'me.id'})->next(); $c->res->redirect( $c->uri_for('/'.$user->login)); }
not my profile
sub random : Global { my ($self,$c,$feed) = @_; my $user=$c->model('DB::Person') ->search({ login => {"-not_in"=>[ ($c->user_exists() ? ($feed,$c->user->obj->login) : $feed ]; },{ rows => 1, order_by => "rand()", join => [qw/items/], having=>{'count(items.id)' => {'>',0 }}, group_by => 'me.id'})->next(); $c->res->redirect( $c->uri_for('/'.$user->login)); }
DBIx::ClassIt grows with you