bag of tricks from iusethis

88
Bag of tricks from iusethis.com

Upload: marcus-ramberg

Post on 12-May-2015

8.944 views

Category:

Business


0 download

DESCRIPTION

practical examples of how I used Catalyst and DBIx::Class to write http://osx.iusethis.com/, a social-software site for software.

TRANSCRIPT

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 1 year

• 15.000 registered users

• 4.000 registered apps

• ~ 250.000 page views per day

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

Page 9: Bag Of Tricks From Iusethis

Design Philosophies• URLs matter!

Page 10: Bag Of Tricks From Iusethis

Design Philosophies• URLs matter!

• AJAX when it makes sense

Page 11: Bag Of Tricks From Iusethis

Design Philosophies• URLs matter!

• AJAX when it makes sense

• Support standards

Page 12: Bag Of Tricks From Iusethis

Design Philosophies• URLs matter!

• AJAX when it makes sense

• Support standards

• Provide alternative

data-formats

Page 13: Bag Of Tricks From Iusethis

Design Philosophies• URLs matter!

• AJAX when it makes sense

• Support standards

• Provide alternative

data-formats

• The user owns his data

Page 14: Bag Of Tricks From Iusethis

AJAX

Page 15: Bag Of Tricks From Iusethis

AJAH

Page 16: Bag Of Tricks From Iusethis

AJAHXMLHTTPRequest and DIV filling

Page 17: Bag Of Tricks From Iusethis

Enhance

Not Replace

Page 18: Bag Of Tricks From Iusethis

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

Page 19: Bag Of Tricks From Iusethis

NO!

Page 20: Bag Of Tricks From Iusethis

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

id=”do_some_shit”/>

Page 21: Bag Of Tricks From Iusethis

Meanwhile...

<script type=”text/javascript”>$(‘do_some_shit’).onclick=function() { // Fill that div good! return false;}// /do_some_shit is never called</script>

Page 22: 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 23: Bag Of Tricks From Iusethis

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>

Page 24: Bag Of Tricks From Iusethis

Server side Validation without submit

Page 25: Bag Of Tricks From Iusethis

<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

Page 26: Bag Of Tricks From Iusethis

And in the Controller:

Page 27: 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 28: Bag Of Tricks From Iusethis

Case Study:iusethis counter

Page 29: Bag Of Tricks From Iusethis

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

Page 30: Bag Of Tricks From Iusethis

Inline Javascript

Page 31: Bag Of Tricks From Iusethis
Page 32: Bag Of Tricks From Iusethis

Behaviour.jsCleaner - But slower

Page 33: Bag Of Tricks From Iusethis

Meanwhile, in the controller...

Page 34: Bag Of Tricks From Iusethis

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}

Page 35: Bag Of Tricks From Iusethis

$secret ?

Protect against abuse

Catalyst::Plugin::RequestToken

Page 36: Bag Of Tricks From Iusethis

Let’s look at a variant

Page 37: Bag Of Tricks From Iusethis

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

Page 38: Bag Of Tricks From Iusethis

RSS

Page 39: Bag Of Tricks From Iusethis

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

Page 40: Bag Of Tricks From Iusethis

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

Page 41: 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 42: Bag Of Tricks From Iusethis

Autodiscoveryheader.tt:

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

[% END %]

Page 43: Bag Of Tricks From Iusethis

Authentication

Page 44: Bag Of Tricks From Iusethis

DBIC AuthPlugins:

Authentication Authentication::Store::DBIC Authentication::Credential::Password

config:authentication: dbic: user_class: DBIC::Person password_field: password user_field: - screenname - email

Page 45: Bag Of Tricks From Iusethis

And in the Controller:

Page 46: Bag Of Tricks From Iusethis

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

Page 47: Bag Of Tricks From Iusethis

Basic AuthPlugins:

Authentication::Credential::HTTP

config:

authentication: http: type: basic

Page 48: Bag Of Tricks From Iusethis

And in the Controller:

Page 49: Bag Of Tricks From Iusethis

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

Page 50: Bag Of Tricks From Iusethis

OpenIDPlugins:

Authentication::Credential::OpenID

Page 51: Bag Of Tricks From Iusethis

And in the Controller:

Page 52: Bag Of Tricks From Iusethis

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

Page 53: Bag Of Tricks From Iusethis

OPMLOutline Processor Markup

Language -- is an XML format for outlines.

Page 54: 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 55: 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 56: Bag Of Tricks From Iusethis

Tags

Page 57: Bag Of Tricks From Iusethis

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

Page 58: Bag Of Tricks From Iusethis

Aggregating popular tags

Page 59: Bag Of Tricks From Iusethis

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

Page 60: Bag Of Tricks From Iusethis

Findingrelated

tags

Page 61: Bag Of Tricks From Iusethis

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

Page 62: Bag Of Tricks From Iusethis

Tag Cloud

Page 63: Bag Of Tricks From Iusethis

HTML::TagCloud

Page 64: 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 65: Bag Of Tricks From Iusethis
Page 66: Bag Of Tricks From Iusethis

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

Page 67: Bag Of Tricks From Iusethis

Caching

Page 68: Bag Of Tricks From Iusethis

Catalyst::Plugin::PageCache

Page 69: 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 70: Bag Of Tricks From Iusethis

Only for Anon

Page 71: Bag Of Tricks From Iusethis

Not POST

Page 72: Bag Of Tricks From Iusethis

Just Works

Page 73: Bag Of Tricks From Iusethis

Profile Builder

Page 74: Bag Of Tricks From Iusethis

OSX perl app

Page 75: Bag Of Tricks From Iusethis

Just core modules

Page 76: 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 77: Bag Of Tricks From Iusethis

Last Trick

Page 78: Bag Of Tricks From Iusethis

iwatchthis.com

Page 79: Bag Of Tricks From Iusethis

Random Profile

Page 80: 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 81: Bag Of Tricks From Iusethis

Another person

Page 82: 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 83: Bag Of Tricks From Iusethis

one with movies

Page 84: 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 85: Bag Of Tricks From Iusethis

not my profile

Page 86: 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 87: Bag Of Tricks From Iusethis

DBIx::ClassIt grows with you