Successfully reported this slideshow.
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, ...
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 co...
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
• T...
AJAX
AJAH
AJAH
XMLHTTPRequest and DIV filling
Enhance
  Not Replace
Example:
<span><a href=quot;quot; onclick=
quot;CallBmk(); return false;quot;
>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 fals...
Side note:
<a href=”/do_some_shit”
  id=”do_some_shit”/>

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

<script type=”text/javascript”>
$(‘doit’).onsubmit=func...
Server side Validation
   without submit
<script type=”text/javascript”>
$(‘register_screenname’).onchange =
function() {
  var screenname=”name=”+
     $(‘registe...
And in the
Controller:
sub user_name_available : Local {
  my ($self,$c) = @_;
  if ($c->model('DB::Person')
     ->search({
         screenname ...
Case Study:
iusethis counter
<div class=”iusethis”>
  <div id=”iuse_[%app.short%]” class=”count”>
    [%app.count%]</div>
  <a id=”mark_[%app.id%]” hre...
Inline
Javascript
Behaviour.js
Cleaner - But slower
Meanwhile, in
the controller...
sub iusethis : Local {
      my ($self,$c,$id,$secret,$screenname)    = @_;
      my $app:Stashed = $c->model('DB::Applica...
$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:St...
RSS
sub hot_xml : Path('/hot.rss') {
  my ($self,$c) = @_;
  my $feed:Stashed;
  $c->forward('hot');
  $c->forward('rss');
  $...
sub rss : Private {
  my ($self,$c) = @_;
  my $feed:Stashed=
    XML::Feed->new('RSS');
  $feed->link($c->uri_for('/'));
...
Another alternative
rss.tt:
<?xml version=quot;1.0quot; encoding=quot;UTF-8quot;?>
<rss version=quot;2.0quot;
xmlns:iuseth...
Autodiscovery
                 header.tt:
                 [% IF rss %]
 <link rel=quot;alternatequot; title=quot;Iusethis...
Authentication
DBIC Auth
Plugins:

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

config:
a...
And in the
Controller:
if ($c->req->param('email')) {
	 	 if ( $c->login($c->req->param('email'),$c->req->param
('pass')) ) {
	 	 	 my $root=$c->...
Basic Auth
Plugins:

    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(...
OpenID
Plugins:

    Authentication::Credential::OpenID
And in the
Controller:
if ($c->authenticate_openid) {
       if (my $person=$c->model('DBIC::Person')
            ->find({openid=>$c->user->url})...
OPML
Outline Processor Markup
 Language -- is an XML
   format for outlines.
sub user_opml : Global {
  my ($self,$c,$screenname)= @_;
  my $user=$c->model('DBIC::Person')
     ->search({
       scre...
while (my $app=$apps->next) {
      $opml->add_outline(
      text    => $app->name,
      count   => $app->iuses->count,
...
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=>...
Finding
related
  tags
sub related : ResultSet {
    my ($self,$tag)=@_;
    return $self->search({
         'related.name'=>$tag,
         'me.n...
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 ad...
sub get_cloud :ResultSet {
   my ($self,$c,$limit) = @_;
   my $cloud = HTML::TagCloud->new(levels=>5);
   my $tags = $sel...
Caching
Catalyst::Plugin::PageCache
page_cache:
    auto_check_user: 1
    set_http_headers: 1
    expires: 120
    no_cache_debug: 1
    auto_cache:
        ...
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{...
Last
Trick
iwatchthis
  .com
Random
 Profile
sub random : Global {
   my ($self,$c) = @_;
   my $user=$c->model('DB::Person')
     ->search({},{
       rows     => 1,
...
Another
person
sub random : Global {
   my ($self,$c,$feed) = @_;
   my $user=$c->model('DB::Person')
     ->search({
       login => {qu...
one with
 movies
sub random : Global {
   my ($self,$c,$feed) = @_;
   my $user=$c->model('DB::Person')
     ->search({
       login => { '...
not my
profile
sub random : Global {
    my ($self,$c,$feed) = @_;
    my $user=$c->model('DB::Person')
      ->search({
         login =...
DBIx::Class
 It grows
 with you
Questions?
marcus@iusethis.com
Bag Of Tricks From Iusethis
Bag Of Tricks From Iusethis
Bag Of Tricks From Iusethis
Upcoming SlideShare
Loading in …5
×

Bag Of Tricks From Iusethis

9,424 views

Published on

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

Published in: Business, Technology
  • Be the first to comment

Bag Of Tricks From Iusethis

  1. 1. Bag of tricks from iusethis.com
  2. 2. 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.
  3. 3. Social Software For Software
  4. 4. Status after 1 year • 15.000 registered users • 4.000 registered apps • ~ 250.000 page views per day • 20.000 revisions committed to SVN
  5. 5. Catalyst made such rapid development possible
  6. 6. While still keeping it sane to maintain
  7. 7. Design Philosophies
  8. 8. Design Philosophies • URLs matter!
  9. 9. Design Philosophies • URLs matter! • AJAX when it makes sense
  10. 10. Design Philosophies • URLs matter! • AJAX when it makes sense • Support standards
  11. 11. Design Philosophies • URLs matter! • AJAX when it makes sense • Support standards • Provide alternative data-formats
  12. 12. Design Philosophies • URLs matter! • AJAX when it makes sense • Support standards • Provide alternative data-formats • The user owns his data
  13. 13. AJAX
  14. 14. AJAH
  15. 15. AJAH XMLHTTPRequest and DIV filling
  16. 16. Enhance Not Replace
  17. 17. Example: <span><a href=quot;quot; onclick= quot;CallBmk(); return false;quot; >Bookmark this</a></span>
  18. 18. NO!
  19. 19. Lynx Friendly First: <a href=”/do_some_shit” id=”do_some_shit”/>
  20. 20. Meanwhile... <script type=”text/javascript”> $(‘do_some_shit’).onclick=function() { // Fill that div good! return false; }// /do_some_shit is never called </script>
  21. 21. Side note: <a href=”/do_some_shit” id=”do_some_shit”/> <a href=”[%c.uri_for(‘/ do_some_shit’)%]” id=”do_some_shit”/>
  22. 22. 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>
  23. 23. Server side Validation without submit
  24. 24. <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
  25. 25. And in the Controller:
  26. 26. 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(' '); } }
  27. 27. Case Study: iusethis counter
  28. 28. <div class=”iusethis”> <div id=”iuse_[%app.short%]” class=”count”> [%app.count%]</div> <a id=”mark_[%app.id%]” href=quot;[%c.uri_for ('/app/iusethis', app.id,secret,c.user.obj.screenname)%]quot; title=quot;Mark as usedquot;>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>
  29. 29. Inline Javascript
  30. 30. Behaviour.js Cleaner - But slower
  31. 31. Meanwhile, in the controller...
  32. 32. 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 }
  33. 33. $secret ? Protect against abuse Catalyst::Plugin::RequestToken
  34. 34. Let’s look at a variant
  35. 35. 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';
  36. 36. RSS
  37. 37. 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); }
  38. 38. 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 } } }
  39. 39. Another alternative rss.tt: <?xml version=quot;1.0quot; encoding=quot;UTF-8quot;?> <rss version=quot;2.0quot; xmlns:iusethis=quot;http://osx.iusethis.com/ ns/rssquot; xmlns:dc=quot;http://purl.org/dc/ elements/1.1/quot;> <channel> <title>[% title || 'RSS Feed from iusethis.com '%]</title> .....
  40. 40. Autodiscovery header.tt: [% IF rss %] <link rel=quot;alternatequot; title=quot;Iusethis RSSquot; href=quot;[%rss%]quot; type=quot;application/rss+xmlquot;/> [% END %]
  41. 41. Authentication
  42. 42. DBIC Auth Plugins: Authentication Authentication::Store::DBIC Authentication::Credential::Password config: authentication: dbic: user_class: DBIC::Person password_field: password user_field: - screenname - email
  43. 43. And in the Controller:
  44. 44. 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.quot;logoutquot;; $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'; }
  45. 45. Basic Auth Plugins: Authentication::Credential::HTTP config: authentication: http: type: basic
  46. 46. And in the Controller:
  47. 47. sub auto : Private { my ( $self, $c ) = @_; return 1 if $c->action eq 'api/index'; $c->authorization_required( realm => quot;iusethisquot; ); unless ($c->req->method eq 'POST') { $c->res->body('API Requests require HTTP POST'); return 0; } return 1; }
  48. 48. OpenID Plugins: Authentication::Credential::OpenID
  49. 49. And in the Controller:
  50. 50. if ($c->authenticate_openid) { if (my $person=$c->model('DBIC::Person') ->find({openid=>$c->user->url})) { my $user=$c->get_user($person->screenname) || die quot;Could not make user object for quot; . $person->screenname.quot;nquot;; $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})); } };
  51. 51. OPML Outline Processor Markup Language -- is an XML format for outlines.
  52. 52. 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; ...
  53. 53. 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'); }
  54. 54. Tags
  55. 55. CREATE TABLE tag ( id INTEGER PRIMARY KEY, name TEXT, application INT REFERENCES application );
  56. 56. Aggregating popular tags
  57. 57. 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=>quot;count(id) DESCquot;, page=>1, rows=>(shift||10) }); }
  58. 58. Finding related tags
  59. 59. 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=>quot;count(me.name) DESCquot;, page=>1,rows=>10, }); }
  60. 60. Tag Cloud
  61. 61. HTML::TagCloud
  62. 62. 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++
  63. 63. 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; }
  64. 64. Caching
  65. 65. Catalyst::Plugin::PageCache
  66. 66. page_cache: auto_check_user: 1 set_http_headers: 1 expires: 120 no_cache_debug: 1 auto_cache: - '/top*' - '/hot*'
  67. 67. Only for Anon
  68. 68. Not POST
  69. 69. Just Works
  70. 70. Profile Builder
  71. 71. OSX perl app
  72. 72. Just core modules
  73. 73. @apps= map { make_short($_) } grep{ /.(?:app|wdgt|prefPane)$/ } find_apps('/Applications'), find_apps($ENV{HOME}.quot;/Library/ PreferencePanesquot;), find_apps($ENV{HOME}.quot;/Library/Widgetsquot;); 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');
  74. 74. Last Trick
  75. 75. iwatchthis .com
  76. 76. Random Profile
  77. 77. sub random : Global { my ($self,$c) = @_; my $user=$c->model('DB::Person') ->search({},{ rows => 1, order_by => quot;rand()quot;, })->next(); $c->res->redirect( $c->uri_for('/'.$user->login)); }
  78. 78. Another person
  79. 79. sub random : Global { my ($self,$c,$feed) = @_; my $user=$c->model('DB::Person') ->search({ login => {quot;-not_inquot;=>[$feed]}, },{ rows => 1, order_by => quot;rand()quot;, })->next(); $c->res->redirect( $c->uri_for('/'.$user->login)); }
  80. 80. one with movies
  81. 81. sub random : Global { my ($self,$c,$feed) = @_; my $user=$c->model('DB::Person') ->search({ login => { '!=' => $feed}, },{ rows => 1, order_by => quot;rand()quot;, join => [qw/items/], having=>{'count(items.id)' => {'>',0 }}, group_by => 'me.id'})->next(); $c->res->redirect( $c->uri_for('/'.$user->login)); }
  82. 82. not my profile
  83. 83. sub random : Global { my ($self,$c,$feed) = @_; my $user=$c->model('DB::Person') ->search({ login => {quot;-not_inquot;=>[ ($c->user_exists() ? ($feed,$c->user->obj->login) : $feed ]; },{ rows => 1, order_by => quot;rand()quot;, join => [qw/items/], having=>{'count(items.id)' => {'>',0 }}, group_by => 'me.id'})->next(); $c->res->redirect( $c->uri_for('/'.$user->login)); }
  84. 84. DBIx::Class It grows with you
  85. 85. Questions? marcus@iusethis.com

×