84
Bag of tricks from iusethis.com

Bag Of Tricks From Iusethis

Embed Size (px)

DESCRIPTION

My talk showing various techniques I used while building iusethis.com, using Catalyst DBIx::Class and TT, updated for Italian Perl Workshop

Citation preview

Page 1: Bag Of Tricks From Iusethis

Bag of tricks from

iusethis.com

Page 2: Bag Of Tricks From Iusethis
Page 3: Bag Of Tricks From Iusethis

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.

Page 4: Bag Of Tricks From Iusethis

Social Software

For Software

Page 5: Bag Of Tricks From Iusethis

Status after 3 years

• 50.000 registered users

• 10.000 registered apps

• ~ 250.000 page views per day

• 26.000 revisions committed to SVN

Page 6: Bag Of Tricks From Iusethis

Catalyst made such rapid development

possible

Page 7: Bag Of Tricks From Iusethis

While still keeping it sane to maintain

Page 8: Bag Of Tricks From Iusethis

Design Philosophies• Less is More. Keep it Simple

• URLs matter!

• AJAX when it makes sense

• Support standards

• Provide alternative

data-formats

• The user owns his data

Page 9: Bag Of Tricks From Iusethis

AJAX

Page 10: Bag Of Tricks From Iusethis

AJAHXMLHTTPRequest and DIV filling

Page 11: Bag Of Tricks From Iusethis

AJAJXMLHTTPRequest and JSON

Page 12: Bag Of Tricks From Iusethis

Enhance

Not Replace

Page 13: Bag Of Tricks From Iusethis

Example:<span><a href="" onclick= "CallBmk(); return false;" >Bookmark this</a></span>

Page 14: Bag Of Tricks From Iusethis

NO!

Page 15: Bag Of Tricks From Iusethis

Lynx Friendly First:<a href=”/do_some_shit”

id=”do_some_shit”/>

Page 16: Bag Of Tricks From Iusethis

Meanwhile...

// iusethis.js$(‘#do_some_shit’).click(function() { $.getJSON( this.getAttribute('href'), function(data) { // Fill that div good! }); return false;})// /do_some_shit is never called

Page 17: Bag Of Tricks From Iusethis

Side note:<a href=”/do_some_shit”

id=”do_some_shit”/>

<a href=”[%c.uri_for(‘/do_some_shit’)%]”

id=”do_some_shit”/>

Page 18: Bag Of Tricks From Iusethis

Ditto for Forms<form action=”[%c.uri_for(‘/doit’)%]” id=”doit”/>

// iusethis.js$(‘#doit’).submit(function() { // Do something neat with the form return false;} // form is never submitted

Page 19: Bag Of Tricks From Iusethis

Server side Validation without submit

Page 20: Bag Of Tricks From Iusethis

<script type=”text/javascript”>$('#register_screenname').change( function() { $('#register_screenname_errors') .load('[%c.uri_for('/jsrpc/sn_available')%]', { name: $('#register_screenname')[0].value });})</script>

iusethis forms are generated by HTML::FormFu - lots of hooks through classes/ids

Page 21: Bag Of Tricks From Iusethis

And in the Controller:

Page 22: Bag Of Tricks From Iusethis

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(' '); } }

Page 23: Bag Of Tricks From Iusethis

Case Study:iusethis counter

Page 24: Bag Of Tricks From Iusethis

<div class=”iusethis”> <div id=”iuse_[%app.short%]” class=”count”> [%app.count%]</div> <a id=”mark_[%app.id%]” class=”iusethis_link” 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>

Page 25: Bag Of Tricks From Iusethis

Inline Javascript

Page 26: Bag Of Tricks From Iusethis
Page 27: Bag Of Tricks From Iusethis

But that was a year agoRewritten with

JQuery:

Page 28: Bag Of Tricks From Iusethis

$('.iuse_link').click(function() { var number=$(this).parent().prev().children('.number') var link=$(this) $.getJSON(this.getAttribute('href'),function(data) { number.slideUp(function() { number.html(data.count) number.slideDown(); }); link.parent().toggleClass('usingthis').toggleClass('iusethis') link.attr('href',data.url) if (data.stop) { link.html('stop using') } else { link.html('i use this') } });

return false; })

Page 29: Bag Of Tricks From Iusethis

Meanwhile, in the controller...

Page 30: Bag Of Tricks From Iusethis

sub iusethis : Local { my ($self,$c,$id,$secret,$screenname) = @_; my $app; unless ($c->user_exists) { $c->stash->{message} = 'Invalid anonymous call to iusethis'; $c->detach('/app/app',[$app->short]); } $app = $c->model('DBIC::Application')->find($id); # Stripped fraud check $app->add_to_iuses({person=>$c->user->obj}); return $c->res->redirect($c->req->referer) unless ( $c->ajax ); $c->stash->{count}=$app->iuse_count->usecount; $c->stash->{stop}=1; $c->stash->{url}=$c->uri_for('/app/stopusing',$app->id,$app->secret($c->config->{seed},$c->user->obj->registered,$app->id),$screenname)->as_string; $c->forward('View::JSON');}

Page 31: Bag Of Tricks From Iusethis

$secret ?

Protect against abuse

Catalyst::Plugin::RequestToken

Page 32: Bag Of Tricks From Iusethis

Let’s look at a variant

Page 33: Bag Of Tricks From Iusethis

Email Validation sub confirm_email : Private { my ($self,$c,$user) = @_; $c->stash->{user}=$user; $c->stash->{seed}= 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'), ); $c->stash->{template}='validate.tt';

Page 34: Bag Of Tricks From Iusethis

RSS

Page 35: Bag Of Tricks From Iusethis

sub hot_xml : Path('/hot.rss') { my ($self,$c) = @_; $c->forward('hot'); $c->forward('rss'); my $feed=$c->stash->{feed}; $feed->title( 'Hot apps from iusethis.com'); $feed->link( $c->uri_for('/hot')); $c->res->body($feed->as_xml);}

Page 36: Bag Of Tricks From Iusethis

sub rss : Private { my ($self,$c) = @_; my $feed= 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 } } $c->stash->{feed}=$feed;}

Page 37: Bag Of Tricks From Iusethis

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>.....

Page 38: Bag Of Tricks From Iusethis

Autodiscoveryheader.tt:

[% IF rss %]<link rel="alternate" title="Iusethis RSS" href="[%rss%]" type="application/rss+xml"/>

[% END %]

Page 39: Bag Of Tricks From Iusethis

Authentication

Page 40: Bag Of Tricks From Iusethis

DBIC AuthRequired Plugins: Authentication Session Session::Store::DBIC Session::State::Cookie

Page 41: Bag Of Tricks From Iusethis

DBIC Configauthentication: default_realm: dbic realms: dbic: credential: class: Password password_field: password password_type: clear store: class: DBIx::Class user_class: 'DBIC::Person' id_field: screenname openid: credential: class: OpenID store: class: Null

Page 42: Bag Of Tricks From Iusethis

And in the Controller:

Page 43: Bag Of Tricks From Iusethis

if ($c->req->param('email')) { if ( $c->authenticate({password => $c->req->params->{password}, 'dbix_class' => { searchargs => [ { -or => [ screenname => $c->req->params->{email}, email => $c->req->params->{email} ] } ] } })) { 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'; }

Page 44: Bag Of Tricks From Iusethis

Basic AuthThere is a plugin for new style auth

However, this is how we do it in iusethis at the moment:

Page 45: Bag Of Tricks From Iusethis

sub auto : Private { my ( $self, $c ) = @_; return 1 if $c->action eq 'api/index'; my ( $username, $password ) = $c->req->headers->authorization_basic; if ( $c->authenticate({password => $password, 'dbix_class' => { searchargs => [ { -or => [ screenname => $username, email => $username ] } ] }})) { unless ($c->req->method eq 'POST') { $c->res->body('API Requests require HTTP POST'); return 0; } return 1; } $c->res->status(401); $c->res->content_type('text/plain'); $c->res->body('Authorization required.'); $c->res->headers->push_header( 'WWW-Authenticate' => 'Basic realm=iusethis' ); return 0;}

Page 46: Bag Of Tricks From Iusethis

This API will soon be depricated in favor of

OAuth

Page 47: Bag Of Tricks From Iusethis

OpenID

Page 48: Bag Of Tricks From Iusethis

sub openid : Global { my ($self, $c ) = @_; eval { if ($c->authenticate({},'openid')) { if (my $user=$c->find_user({openid=>$c->user->{url},'dbic'})) { $c->set_authenticated($user,'dbic'); return $c->res->redirect($c->uri_for( '/feed',$user->obj->screenname)); } my $openid:Stashed=$c->user->{url}; } elsif (! @{$c->error}) { return if $c->res->redirect; $c->res->redirect($c->uri_for('/login',{openid_failed=>1})); } }; if ($@) { $@ =~ s/\sat\s\S+\sline\s\d+$//; $c->stash->{openid_alert} = $@; $c->error(0); $c->detach('login'); } }

Page 49: Bag Of Tricks From Iusethis

OPMLOutline Processor Markup

Language -- is an XML format for outlines.

Page 50: Bag Of Tricks From Iusethis

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; ...

Page 51: Bag Of Tricks From Iusethis

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'); }

Page 52: Bag Of Tricks From Iusethis

Tags

Page 53: Bag Of Tricks From Iusethis

CREATE TABLE tag ( id INTEGER PRIMARY KEY, name TEXT, application INT REFERENCES application );

Page 54: Bag Of Tricks From Iusethis

Aggregating popular tags

Page 55: Bag Of Tricks From Iusethis

# In iusethis::Schema::ResultSet::Tag sub aggregated { 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) }); }

Page 56: Bag Of Tricks From Iusethis

Findingrelated

tags

Page 57: Bag Of Tricks From Iusethis

sub related { 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, }); }

Page 58: Bag Of Tricks From Iusethis

Tag Cloud

Page 59: Bag Of Tricks From Iusethis

HTML::TagCloud

Page 60: Bag Of Tricks From Iusethis

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++

Page 61: Bag Of Tricks From Iusethis
Page 62: Bag Of Tricks From Iusethis

sub get_cloud { 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; }

Page 63: Bag Of Tricks From Iusethis

Caching

Page 64: Bag Of Tricks From Iusethis

Catalyst::Plugin::PageCache

Page 65: Bag Of Tricks From Iusethis

page_cache: auto_check_user: 1 set_http_headers: 1 expires: 120 no_cache_debug: 1 auto_cache: - '/top*' - '/hot*'

Page 66: Bag Of Tricks From Iusethis

Only for Anon

Page 67: Bag Of Tricks From Iusethis

Not POST

Page 68: Bag Of Tricks From Iusethis

Just Works

Page 69: Bag Of Tricks From Iusethis

Profile Builder

Page 70: Bag Of Tricks From Iusethis

OSX perl app

Page 71: Bag Of Tricks From Iusethis

Just core modules

Page 72: Bag Of Tricks From Iusethis

@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');

Page 73: Bag Of Tricks From Iusethis

Last Trick

Page 74: Bag Of Tricks From Iusethis

iwatchthis.com

Page 75: Bag Of Tricks From Iusethis

Random Profile

Page 76: Bag Of Tricks From Iusethis

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)); }

Page 77: Bag Of Tricks From Iusethis

Another person

Page 78: Bag Of Tricks From Iusethis

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)); }

Page 79: Bag Of Tricks From Iusethis

one with movies

Page 80: Bag Of Tricks From Iusethis

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)); }

Page 81: Bag Of Tricks From Iusethis

not my profile

Page 82: Bag Of Tricks From Iusethis

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)); }

Page 83: Bag Of Tricks From Iusethis

DBIx::ClassIt grows with you