[Catalyst-commits] r13880 - trunk/examples/CatalystAdvent/root/2010/pen

dhoss at dev.catalyst.perl.org dhoss at dev.catalyst.perl.org
Mon Dec 20 19:49:35 GMT 2010


Author: dhoss
Date: 2010-12-20 19:49:35 +0000 (Mon, 20 Dec 2010)
New Revision: 13880

Removed:
   trunk/examples/CatalystAdvent/root/2010/pen/cat-rest-and-jqgrid.pod
   trunk/examples/CatalystAdvent/root/2010/pen/fs-reflector-and-cat.pod
   trunk/examples/CatalystAdvent/root/2010/pen/i18n-catalyst-part2.pod
Modified:
   trunk/examples/CatalystAdvent/root/2010/pen/TODO
Log:
deleted published, updated TODO

Modified: trunk/examples/CatalystAdvent/root/2010/pen/TODO
===================================================================
--- trunk/examples/CatalystAdvent/root/2010/pen/TODO	2010-12-20 19:47:00 UTC (rev 13879)
+++ trunk/examples/CatalystAdvent/root/2010/pen/TODO	2010-12-20 19:49:35 UTC (rev 13880)
@@ -14,9 +14,9 @@
  - twitter authentication/applications + Catalyst - ** DONE **
  - .... as an Absolute Beginner (tm) could I put in a request for some newbie-friendly Cat-for-dummies entries? - victor from the mailing list
  - X-Sendfile/X-Accel-Redirect with support for agnostic file location retrieval (aka, file to send could be located on any number of replicated nodes etc.)
- - Form::Sensible::Reflector::DBIC + Catalyst (dhoss)
+ - Form::Sensible::Reflector::DBIC + Catalyst (dhoss) - ** DONE **
  - Test::DBIx::Class
- - jqgrid + Catalyst:Controller::REST (dhoss)
+ - jqgrid + Catalyst:Controller::REST (dhoss) - ** DONE **
  - AWS S3, Mosso Cloudfiles, cloud storage, deployment
  - large scale application deployment, regarding replication, load balancing, etc. 
  - DBIC pretty printers, console and plack versions
@@ -68,12 +68,12 @@
  - 13: Mosso Cloudfiles + Cat - dhoss
  - 14: DONE ("jQueryUI +Catalyst" - Sir and friends)
  - 15: DONE ("twitter" - dpetrov)
- - 16: Catalyst + jQgrid and Catalyst::Controller::REST - dhoss
- - 17: Form::Sensible::Reflector::DBIC + Cat - dhoss
+ - 16:  
+ - 17: 
  - 18
- - 19
- - 20
- - 21
+ - 19: DONE (Catalyst + jQgrid and Catalyst::Controller::REST) - dhoss
+ - 20: DONE (opsview part 2) - Ton Voon
+ - 21: DONE (Form::Sensible::Reflector::DBIC + Cat) - dhoss
  - 22
  - 23
  - 24

Deleted: trunk/examples/CatalystAdvent/root/2010/pen/cat-rest-and-jqgrid.pod
===================================================================
--- trunk/examples/CatalystAdvent/root/2010/pen/cat-rest-and-jqgrid.pod	2010-12-20 19:47:00 UTC (rev 13879)
+++ trunk/examples/CatalystAdvent/root/2010/pen/cat-rest-and-jqgrid.pod	2010-12-20 19:49:35 UTC (rev 13880)
@@ -1,611 +0,0 @@
-=head1 Catalyst::Controller::REST, jQgrid, and You.
-
-Datagrids are a pretty big thing nowadays.  Every reasonable JavaScript framework has a solution for creating them, and for good reason.  You can get a really simple, data-agnostic CRUD interface up and running that has quick sorting features, pagination, bulk create/delete/update, and easy search features.  These are all extremely useful for situations in which you have large datasets you have to deal with in big chunks at a time.  Tangible scenarios include admin interfaces, store product inventory lists, email listings, etc.  This is all good and gravy, given we have a JavaScript UI solution to *displaying* the data, but how do we get data *into* it, reasonbly, and extensibly? We use my good friend L<Catalyst::Controller::REST|http://search.cpan.org/~bobtfish/Catalyst-Action-REST-0.87/lib/Catalyst/Controller/REST.pm>.  Folks have talked about this fella before, but not exactly in this capacity. See, things like flexigrid from jQuery also give you a nice grid interface, but the CRUD factor isn't quite as easy as with jQgrid.  So I'd like to take some time and explain how to use jQgrid for your datagrid instances, and show you why it's a bit easier.
-
-=head2 PACKING LIST
-
-=over 12
-
-=item Catalyst::Controller::REST
-
-    cpanm Catalyst::Controller::REST
-
-Voila.  One down, a few more to go!
-
-=item A Data Source.
-
-Ours will be a database, hooked up via L<DBIx::Class|http://search.cpan.org/dist/DBIx-Class/>.  Yours can be anything, as long as it can be coerced into the data structure we'll be talking about here shortly.
-
-=item jQgrid
-
-Put together the options you want here: L<http://www.trirand.com/blog/?page_id=6>, download, and be happy. For this specific example, jQuery UI addons, Form Editing, Cell Editing, and Inline/In Place Editing will be used, but it is up to you whether you want to include these or not.
-
-=back
-
-=head2 Putting it all Together
-
-So, I trust you have a basic Catalyst app all ready to go, in preparation and anticipation for this article, ready to go since you've been waiting with untold excitement since my last article.  Great!  Let's get going.
-
-This specific example will be a entry management "panel" for a high-traffic, many post blog.
-
-Let's create a controller that will serve as our CRUD interface to the jQgrid:
-
-    package MyApp::Controller::RESTYCrud;
-    use Moose;
-    use namespace::autoclean;
-    use Try::Tiny;
-    BEGIN { extends 'Catalyst::Controller::REST'; }
-
-    ## start of our Chained actions
-    sub base : Chained('/') PathPart('entry') CaptureArgs(0) {
-    } 
-    
-    ## initial URL pathpart, grabs all posts
-    sub index : Chained('base') PathPart('') Args(0) ActionClass('REST') {
-      my ( $self, $c ) = @_;
-      my $data = $c->req->data || $c->req->params;
-
-    ## things get a wee bit funky here, as jQgrid wants to keep it real, and keep it simple, by having a sort of
-    ## input-agnostic data structure for its grid.  Simply put, it wants a JSON representation of a table, 
-    ## with cells, rows, cell indices, sort order, number of rows, and page number.
-
-      my $order =
-        ( exists( $data->{sidx} ) && exists( $data->{sord} ) )
-        ? $data->{sidx} . " " . $data->{sord}
-        : "{ -desc => [ qw/created_at/ ] }";
-      my $rs = $c->model("Database::Entry")->search(
-        {},
-        {
-            rows => $data->{rows} || 10,
-            page => $data->{page} || 1,
-            order_by => $order
-        }
-      );
-
-    ## quick little method I wrote, not very MVC, to make the above slightly more doable in a sane fashion.
-      my @posts = $self->serialize_posts($c, $rs, $data);
-    
-    ## stuff 'em in the stash  
-      $c->stash( posts => @posts, template => 'entry/index.tt' );
-	
-    } 
-
-    ## as you know, C::C::REST needs an HTTP method defined for each action you want serialized through it.
-    ## this doesn't do much more than grab the posts that are pre-serialized (in the perl data structure sense) and serialize
-    ## them to our desired format (XML, JSON, etc.)
-    sub index_GET {
-      my ( $self, $c ) = @_;
-      my @posts = $c->stash->{posts};
-      return $self->status_ok( $c, entity => @posts );
-    }
-
-    ## simple create action, you decide how you want to present the input UI to the user
-    sub create : Chained('auth_base') PathPart('entry/new') ActionClass('REST') Args(0) {
-      my ( $self, $c ) = @_;
-
-      ## put together input UI here, a form most likely for the GET request
-
-    }
-
-    ## display the aforementioned form
-    sub create_GET {
-      my ( $self, $c ) = @_;
-
-      $self->status_ok( $c, entity => { template => 'entry/create.tt' } );
-
-    }
-
-    ## so, our first bit of CRUD. (the "C" in CRUD)
-    ## All we need to do here is let jQgrid know what action is being performed
-    ## (it checks this by seeing what the parameter "oper" says
-    ## and then actually insert the record, and return some JSON for jQgrid.
-    sub create_POST {
-
-      my ( $self, $c ) = @_;
-      my $data ||= $c->req->data || $c->req->params;
-
-      ## check to see if we're being submit from a form or from jQgrid
-      if ( $data->{'submit'} or $data->{'oper'} eq 'add' ) {
-       try {
-         ## your create logic goes here, try/catch just allows us to gracefully handle errors
-         $c->stash( status => "created", entry => $e );
-
-         ## meat and potatoes right here, 
-         ## 1. return a location, which is the request URI as a string,
-         ## 2. and some sort of identifier for the entry.  
-         ## Everything in entity => {} is serialized.
-         $self->status_created(
-                $c,
-                location => $c->req->uri->as_string,
-                entity   => {
-                    message => "Created post",
-                    entry   => $entry->entryid
-                }
-            );
-        }
-        ## something went wrong, send back a bad request response
-        ## aside: Try::Tiny puts errors caught in $_
-        catch {
-            $self->status_bad_request( $c, message => "Can not create post: $_" );
-        };
-
-    }
-
-  }
-
-  ## view post logic (the "R" in CRUD)
-  ## this should be whatever you need as far as retrieval logic goes.
-  ## this retrieves ONE entry, with a nice RESTful URI such as:
-  ## /entry/1
-  ## this is not an endpoint for Chained, thus it simply sets things up for us.
-  sub get_post : Chained('base') PathPart('') CaptureArgs(1) {
-    my ( $self, $c, $postid ) = @_;
-    my $query;
-
-    ## this is here so we can have a slightly extensible sort of
-    ## query data structure we can pass to our search solution.
-    push @{$query}, { title => $postid };
-
-    my $post = $c->model('CMS')->entry->find_by($query);
-    unless ( defined $post ) {
-        $c->error("Can't find post with that id");
-    }
-
-    $c->stash( post => $post );
-
-  }
-
-  ## "get" endpoint
-  sub view_post : Chained('get_post') PathPart('') Args(0) ActionClass('REST') {
-    my ( $self, $c ) = @_;
-    my $post = $c->stash->{'post'};
-
-    ## post "serialized" in a reasonable perl data structure
-    my $post_serialized = {
-
-        title       => $post->title,
-        body        => $post->body,
-        created_at  => $post->created_at . "",
-        author      => $post->author->userid,
-        type        => $post->type,
-        attachments => \@attachments,
-
-    };
-    $c->stash( post_serialized => $post_serialized, template => "entry/view.tt" );
-
-  }
-
-  ## REST endpoint
-  sub view_post_GET {
-    my ( $self, $c ) = @_;
-    my $post = $c->stash->{'post_serialized'};
-
-    $self->status_ok( $c, entity => {$post} );
-  }
-
-  ## the "D" in CRUD, set up an end point for this
-  sub delete_post : Chained('get_post_authed') PathPart('delete') Args(0) ActionClass('REST') {
-  }
-
-  ## delete a record.  Again, this is up to you to write.
-  sub delete_post_POST {
-    my ( $self, $c ) = @_;
-    ## need to add stuff in to make sure user can delete this post
-    my $post = $c->stash->{'post'};
-
-    $c->model('CMS')->entry->delete( $post->entryid );
-    return $self->status_ok(
-        $c,
-        entity => {
-            status => "Post unpublished",
-            post   => $post,
-            url    => $c->uri_for_action('/entry/index')
-        }
-    );
-  }
-
-  ## the "U" in CRUD
-  ## sorry, D and U got reversed in definition order, but I think you'll cope :-)
-  sub edit_post : Chained('get_post_authed') PathPart('update') Args(0) ActionClass('REST') {
-  }
-
-  sub edit_post_PUT {
-    my ( $self, $c ) = @_;
-    my $post = $c->stash->{'post'};
-    my $data = $c->req->data || $c->req->params;
-
-    $c->model('CMS')
-      ->entry->update( $post->entryid,
-        { body => $data->{body}, published => $data->{published}, title => $data->{title} } );
-    return $self->status_ok( $c, entity => { status => "Post updated", } );
-  }
-
-
-  ## this is a quick utility method, and should go in a model
-  sub serialize_posts {
-    my ( $self, $c, $rs, $data) = @_;
-    my @posts;
-    push @posts, {
-
-        'page' => $data->{page},
-
-        rows      => [],
-        'records' => $rs->count,
-
-        'total' => $rs->pager->last_page,
-
-    };
-    $c->log->debug( "pages: " . $rs->pager->last_page );
-
-    ## jQgrid wants a json structure similar to:
-    ## 
-    ## {
-    ## "page": "1",
-    ## "records": "10",
-    ## "rows": [
-    ##     {
-    ##         "id": 117,
-    ##         "cell": [
-    ##             117,
-    ##             "re:wharrrrgarrgarblll",
-    ##             "2010-12-14T04:00:42",
-    ##             "2010-12-14T04:00:42",
-    ##             "<p>you haff replied!</p>",
-    ##             1,
-    ##             "post"
-    ##         ]
-    ##      }
-    ##  ],
-    ##  "total": 11
-    ## }
-    ## 
-    ## ... which is a long-winded way of saying "I want this in row[cell[index, column1..column$n] with some
-    ## various other details concerning what will be displayed and how.
-    ## the following accomplishes putting it into a perl data structure that one of our XML or JSON serializers can 
-    ## happily deal with. 
-    while ( my $post = $rs->next ) {
-        push @{ $posts[0]->{rows} },
-          {
-            'id'   => $post->entryid,
-            'cell' => [
-                $post->entryid,         $post->title,
-                $post->created_at . "", $post->updated_at . "",
-                $post->body,            $post->published,
-                $post->type,
-            ]
-          };
-
-    }
-
-    return @posts;
-
-  }
-
-  __PACKAGE__->meta->make_immutable;
-
-  1;
-
-That is, more or less, our business logic.  A LOT of that should be put into a model, but that would be a much more involved post that is beyond the scope of the basic premise I'm going for here.
-
-=head2 The jQgrid UI 
-
-So, now we need a template (or two) that defines our grid.
-
-Here is a basic set up that should match the above code reasonably well:
-
-    $(document).ready(function() {
-   
-    var lastsel;
-    $("#tposts").jqGrid({
-        // url that will be used to POST/GET etc unless overridden
-        url: '/entry/by_user/' + [% c.user.get("userid") %],
-        datatype: "json",
-        mtype: "GET",
-        reloadAfterSubmit: true,
-        height: 400,
-        width: 680,
-        pager: $('#posts'),
-        // important, if you want to specify what's sent on the 
-        // grid event.
-        ajaxGridOptions: {
-            contentType: 'application/json'
-        },
-        // same as above, but per-row
-        ajaxRowOptions: {
-            contentType: 'application/json',
-            type: 'PUT'
-        },
-        // we want to serialize our row data to JSON before sending,
-        // so that Catalyst::Controller::REST knows how to respond
-        serializeRowData: function(data) {
-            return JSON.stringify(data);
-        },
-        // quick-and-dirty set up for your columns
-        colNames: ['Entry ID', 'Title', 'Date Created', 'Date Updated', 'Body', 'Published', 'Type'],
-        // this should essentially reflect the columns you want updated appside, for example, they should be
-        // named the same as your database columns you are updating unless you have logic that translates them
-        // properly.
-        colModel: [
-        {
-            name: 'entryid',
-            index: 'entryid',
-            width: 55
-        },
-        {
-            name: 'title',
-            index: 'title',
-            width: 80,
-            align: "right",
-            editable: true
-        },
-        {
-            name: 'created_at',
-            index: 'created_at',
-            width: 90,
-            editable: false
-        },
-        {
-            name: 'updated_at',
-            index: 'updated_at',
-            width: 100,
-            editable: false
-        },
-        // when clicked, this will turn into a textarea.
-        // cool, but I recommend setting up a modal edit window
-        // for anything that's not turning into a simple "<input type='...'"
-        // as it gets EXTREMELY cumbersome to try and edit long bits of text inside a constrained
-        // textarea.
-        {
-            name: 'body',
-            index: 'body',
-            width: 150,
-            sortable: false,
-            edittype: "textarea",
-            editoptions: {
-                rows: "10",
-                cols: "20"
-            },
-            editable: true
-        },
-        {
-            name: 'published',
-            index: 'published',
-            width: 80,
-            align: "right",
-            editable: true,
-            edittype: "select",
-            editoptions: {
-                value: "1:yes;0:no"
-            }
-        },
-        {
-            name: 'type',
-            index: 'type',
-            width: 80,
-            align: "right",
-            editable: true,
-            edittype: "select",
-            editoptions: {
-                value: "post:post;page:page"
-            }
-        }
-        ],
-        // number of rows to be displayed at once
-        rowNum: 10,
-        // steps at which you can change how many rows are shown
-        rowList: [10, 20, 30],
-        // where your arrows for pagination are rendered
-        pager: '#posts',
-        // this should be your primary key in your database, as this is the
-        // default column for sorting
-        sortname: 'entryid',
-        viewrecords: true,
-        // sort of self explanatory, descending or ascending sort order.
-        sortorder: "desc",
-        
-        // important if you want "click-and-edit" functionality.
-        // basically, check if we are "edited", if so, send that data to the server,
-        // and then restore the row, whether edited or just blurred (clicked out of)
-        onSelectRow: function(id) {
-            ajaxGridOptions: {
-                contentType: 'application/json'
-            };
-            rowid = id;
-            if (id && id !== lastsel) {
-                $('#tposts').restoreRow(lastsel);
-                lastSel = id;
-            }
-            $('#tposts').editRow(id, true, '', '', '/entry/' + rowid + '/update');
-        },
-        caption: "Your Posts"
-    });
-
-    $("#tposts").jqGrid('navGrid', '#posts', {edit: false, add: true, del: false, search: true, refresh: true},
-    {},
-    {
-        mtype: "POST",
-        reloadAfterSubmit: true,
-        url: '/entry/new',
-        modal: true
-    },
-    {},
-    {});
-     $("#tposts").jqGrid('gridResize', {});
-  });
-  // delete options, a bit different since by default jQgrid just wants to send GET/POST/PUT/DELETE requests
-  // to ONE url without major hackery.
-  $("#dedata").click(function(){ 
-	var gr = $("#tposts").jqGrid('getGridParam','selrow');
-	if( gr != null ) $("#tposts").jqGrid('delGridRow', gr, { ajaxDelOptions: { contentType: "application/json", mtype: 'POST', }, serializeRowData: function(data) {
-        return JSON.stringify(data);
-    },  "url": '/entry/' + gr + '/delete', "reloadAfterSubmit":"false" });
-	else alert("Please Select Row to delete!");
-  });
-
-
-The template with which to display all this:
-
-  <h2>Manage Entries</h2>
-  <table id="tposts" style="padding-bottom: 10px"></table>
-  <!-- this is where the grid will be rendered -->
-  <div id="posts" style="padding: 10px"></div>
-
-  <br />
-  <div>
- 
-  <!--
-        As I mentioned, jQgrid wants to send all the requests to one single URL unless you do some major hacking.
-       Therefore, I compromised and added links to create and delete records outside of the jQgrid.
-       I recommend an 'edit' link here as well, as the click and edit textarea can get really cumbersome 
-   -->
-  <div class=""><a href="[% c.uri_for_action('/entry/create', { "width"=>"50%", "height"=>"75%"}) %]" class="new-entry">Create an entry</a>
-  <div class="ui-icon-scissors"></div><a href="#" id="dedata">Delete selected row</a>
-
-  </div>
-   <div id="modalWindow" class="jqmWindow"> 
-        <div id="jqmTitle"> 
-            <button class="jqmClose"> 
-               X
-            </button> 
-            <span id="jqmTitleText">Title of modal window</span> 
-        </div> 
-        <iframe id="jqmContent" src=""> 
-        </iframe> 
-    </div>
-  <script type="text/javascript">
-  <!--
-    this should be the name of whatever you put the previous javascript in.
-  -->
-  [% PROCESS 'entry/entry_grid.js.tt' %]
-  </script>
-
-   <!-- 
-        Bonus!  Remember the modal window stuff I talked about? Here's a quick way to accomplish that:
-   -->
-
-  <script type="text/javascript">
-  // thanks to http://pixeline.be/experiments/ThickboxToJqModal/ for this
-  $(document).ready(function(){
-
-	var closeModal = function(hash)
-	    {
-	        var $modalWindow = $(hash.w);
-
-	        $modalWindow.fadeOut('2000', function()
-	        {
-	            hash.o.remove();
-	            //refresh parent
-
-	            if (hash.refreshAfterClose === 'true')
-	            {
-
-	                window.location.href = document.location.href;
-	            }
-	        });
-	    };
-	    var openInFrame = function(hash)
-	    {
-	        var $trigger = $(hash.t);
-	        var $modalWindow = $(hash.w);
-	        var $modalContainer = $('iframe', $modalWindow);
-	        var myUrl = $trigger.attr('href');
-	        var myTitle = $trigger.attr('title');
-	        var newWidth = 0, newHeight = 0, newLeft = 0, newTop = 0;
-	        $modalContainer.html('').attr('src', myUrl);
-	        $('#jqmTitleText').text(myTitle);
-	        myUrl = (myUrl.lastIndexOf("#") > -1) ? myUrl.slice(0, myUrl.lastIndexOf("#")) : myUrl;
-	        var queryString = (myUrl.indexOf("?") > -1) ? myUrl.substr(myUrl.indexOf("?") + 1) : null;
-
-	        if (queryString != null && typeof queryString != 'undefined')
-	        {
-	            var queryVarsArray = queryString.split("&");
-	            for (var i = 0; i < queryVarsArray.length; i++)
-	            {
-	                if (unescape(queryVarsArray[i].split("=")[0]) == 'width')
-	                {
-	                    var newWidth = queryVarsArray[i].split("=")[1];
-	                }
-	                if (escape(unescape(queryVarsArray[i].split("=")[0])) == 'height')
-	                {
-	                    var newHeight = queryVarsArray[i].split("=")[1];
-	                }
-	                if (escape(unescape(queryVarsArray[i].split("=")[0])) == 'jqmRefresh')
-	                {
-	                    // if true, launches a "refresh parent window" order after the modal is closed.
-
-	                    hash.refreshAfterClose = queryVarsArray[i].split("=")[1]
-	                } else
-	                {
-
-	                    hash.refreshAfterClose = false;
-	                }
-	            }
-	            // let's run through all possible values: 90%, nothing or a value in pixel
-	            if (newHeight != 0)
-	            {
-	                if (newHeight.indexOf('%') > -1)
-	                {
-
-	                    newHeight = Math.floor(parseInt($(window).height()) * (parseInt(newHeight) / 100));
-
-	                }
-	                var newTop = Math.floor(parseInt($(window).height() - newHeight) / 2);
-	            }
-	            else
-	            {
-	                newHeight = $modalWindow.height();
-	            }
-	            if (newWidth != 0)
-	            {
-	                if (newWidth.indexOf('%') > -1)
-	                {
-	                    newWidth = Math.floor(parseInt($(window).width() / 100) * parseInt(newWidth));
-	                }
-	                var newLeft = Math.floor(parseInt($(window).width() / 2) - parseInt(newWidth) / 2);
-
-	            }
-	            else
-	            {
-	                newWidth = $modalWindow.width();
-	            }
-
-	            // do the animation so that the windows stays on center of screen despite resizing
-	            $modalWindow.css({
-	                width: newWidth,
-	                height: newHeight,
-	                opacity: 0
-	            }).jqmShow().animate({
-	                width: newWidth,
-	                height: newHeight,
-	                top: newTop,
-	                left: newLeft,
-	                marginLeft: 0,
-	                opacity: 1
-	            }, 'slow');
-	        }
-	        else
-	        {
-	            // don't do animations
-	            $modalWindow.jqmShow();
-	        }
-
-	    }
-
-
-    $('#modalWindow').jqm({
-            trigger:'a.new-entry',
-            target: '#jqmContent',
-	        onHide: closeModal,
-	        onShow: openInFrame
-	});
-  });
-  </script>
-
-
-Phew!  That's pretty much the gist of it.  There are a LOT of other things you can do, but if you're lazy like me and just wanted to have this all spat out and done, that's all you need. It's a little counterintuitive at times, but I figured it out, and it's really not as bad as it could be.
-
-Anyway, enjoy!
-
-=head1 AUTHOR
-
-Devin Austin <dhoss at cpan.org>

Deleted: trunk/examples/CatalystAdvent/root/2010/pen/fs-reflector-and-cat.pod
===================================================================
--- trunk/examples/CatalystAdvent/root/2010/pen/fs-reflector-and-cat.pod	2010-12-20 19:47:00 UTC (rev 13879)
+++ trunk/examples/CatalystAdvent/root/2010/pen/fs-reflector-and-cat.pod	2010-12-20 19:49:35 UTC (rev 13880)
@@ -1,242 +0,0 @@
-=head2 One Definition, One Place: Form::Sensible::Reflector::DBIC and Catalyst
-
-You've seen the form building frameworks.  You've been there, done that, and hated it.  But what irks you most is that you have to define your forms in multiple places, when you have a data source that has PLENTY of information to give you a quick, albeit simple, form.  Well, I share your frustration, and although I can't say it makes me like forms any more,  L<Form::Sensible::Reflector::DBIC|http://search.cpan.org/dist/Form-Sensible-Reflector-DBIC/> was written by yours truly to ease a LOT of the pain associated with creating forms.
-
-=head2 Where Do I use this Magical Beast?
-
-Well, most of the time, I write my forms in standard, boring old HTML.  This works great, and I can use L<Data::Manager|http://search.cpan.org/dist/Data-Manager/> for my data validation quite easily, but what if I have a project I want done yesterday, that has GIGANTIC forms that I don't feel like going through and creating by hand?  This is a perfect scenario for  L<Form::Sensible::Reflector::DBIC|http://search.cpan.org/dist/Form-Sensible-Reflector-DBIC/>.  In short, if you want rapid prototyping that can actually stick around and be extensible for a while, this module's for you.
-
-=head2 What you Need
-
-=over 12
-
-=item A Catalyst Application
-
-That's what you're here for, right? :-)
-
-=item DBIx::Class
-
-My favorite ORM, but Form::Sensible::Reflector provides a base reflector class you could write a reflector for just about anything.
-This is what will be used specifically, as it gave me the best options data structure-wise from database<->forms.
-
-=item L<Form::Sensible::Reflector::DBIC|http://search.cpan.org/dist/Form-Sensible-Reflector-DBIC/>
-
-This will be doing most of the work, at least generating the form.
-
-=back
-
-=head2 Getting Started
-
-Okay.  So we have a scenario where we have an existing database, an existing DBIx::Class schema, but we need a quick interface we can set up and customize very, very quickly.
-
-Let's start with our database schema (for convenience sake, we'll just use one table as an example):
-
-    package My::Form::Sensible::App::Schema::Result::Entry;
-
-    use base qw/DBIx::Class::Core/;
-
-    __PACKAGE__->load_components(qw/ InflateColumn::DateTime TimeStamp  /);
-    __PACKAGE__->table('entries');
-      
-    __PACKAGE__->add_columns(
-    entryid => {
-        data_type         => 'integer',
-        is_nullable       => 0,
-        is_auto_increment => 1,
-    },
-    title => {
-        data_type   => 'varchar',
-        size        => 150,
-        is_nullable => 0,
-    },
-    body => {
-        data_type   => "text",
-        is_nullable => 0,
-    },
-    author => {
-        data_type    => 'integer',
-        is_nullable  => 0,
-        render_hints => { field_type => "hidden" },
-    },
-    published => {
-        data_type    => 'bool',
-        is_nullable  => 0,
-        default      => 0,
-    },
-    created_at => {
-        data_type     => 'datetime',
-        is_nullable   => 1,
-        set_on_create => 1,
-    },
-    updated_at => {
-        data_type     => 'datetime',
-        is_nullable   => 1,
-        set_on_create => 1,
-        set_on_update => 1,
-    },
-    type => {
-        data_type   => 'enum',
-        is_nullable => 0,
-        is_enum     => 1,
-        extra       => { list => [qw/post page/] },
-    },
-    parent => {
-        data_type   => 'int',
-        is_nullable => 1,
-        default     => 0,
-    },
-    path => {
-        data_type   => 'varchar',
-        size        => '100',
-        is_nullable => 1,
-        default     => '1',
-    },
-    
- );
-
-  __PACKAGE__->add_unique_constraint( [qw/ title /] );
-
-  __PACKAGE__->set_primary_key('entryid');
-
-  1;
-
-This is a typical, no frills DBIC schema representation of a table.  We need to add a few things to tell FS::Reflector::DBIC how to render things:
-
-   package  My::Form::Sensible::App::Schema::Result::Entry;
-
-   use base qw/DBIx::Class::Core/;
-
-   __PACKAGE__->load_components(qw/ InflateColumn::DateTime TimeStamp  /);
-   __PACKAGE__->table('entries');
-
-   __PACKAGE__->add_columns(
-    entryid => {
-        data_type         => 'integer',
-        is_nullable       => 0,
-        is_auto_increment => 1,
-    },
-    title => {
-        data_type   => 'varchar',
-        size        => 150,
-        is_nullable => 0,
-    },
-    display_title => {
-        data_type   => "varchar",
-        size        => 250,
-        is_nullable => 0,
-        render_hints => { field_type => "hidden" },
-    },
-    body => {
-        data_type   => "text",
-        is_nullable => 0,
-    },
-    author => {
-        data_type    => 'integer',
-        is_nullable  => 0,
-        render_hints => { field_type => "hidden" },
-    },
-    published => {
-        data_type    => 'bool',
-        is_nullable  => 0,
-        default      => 0,
-        render_hints => { on_label => "yes", off_label => "no", on_value => 1, off_value => 0 },
-    },
-    created_at => {
-        data_type     => 'datetime',
-        is_nullable   => 1,
-        set_on_create => 1,
-        render_hints  => { field_type => "hidden" },
-    },
-    updated_at => {
-        data_type     => 'datetime',
-        is_nullable   => 1,
-        set_on_create => 1,
-        set_on_update => 1,
-        render_hints  => { field_type => "hidden" },
-    },
-    type => {
-        data_type   => 'enum',
-        is_nullable => 0,
-        is_enum     => 1,
-        extra       => { list => [qw/post page/] },
-        render_hints =>
-          { options => [ { name => 'page', value => 'page' }, { name => 'post', value => 'post' } ] }
-       
-    },
-    parent => {
-        data_type   => 'int',
-        is_nullable => 1,
-        default     => 0,
-        render_hints => { field_type => "hidden" },
-    },
-    path => {
-        data_type   => 'varchar',
-        size        => '100',
-        is_nullable => 1,
-        default     => '1',
-        render_hints => { field_type => "hidden" },
-    },
-    
- );
-
- __PACKAGE__->add_unique_constraint( [qw/ title /] );
-
- __PACKAGE__->set_primary_key('entryid');
-
- 1;
-
-So, basically, if we don't want something showing up in our form, we mark it as hidden.  With things like an "enum", it's going to show up as a C<< <select> >> drop down, and we want to make sure we get the fields named correctly.
-
-So now, let's get a Catalyst controller up and going with our final reflector code:
-
-    ## this could be any action in your app, this exact one is just a for instance:
-    sub create : Chained('auth_base') PathPart('entry/new')  Args(0) {
-        my ( $self, $c ) = @_;
-
-        my $reflector = Form::Sensible::Reflector::DBIC->new;
-
-        my $form =
-          $reflector->reflect_from( $c->model('Database::Entry'), { form => { name => 'entry' } } );
-        my $renderer = Form::Sensible->get_renderer('HTML');
-        $c->stash( form => $renderer->render($form), form_object => $form, post_to => '/entry/new' );    # hate this
-
-    }
-
-
-The template to display this:
-
-    [% form.complete( post_to, 'POST') %]
-
-And that's really it.  Basically, I've tried to keep the SQL data types relatively intuitive.  If a column is supposed to take a textfield's worth of data, it's probably a textfield.  If it's got options, you get to decide what it should be.  At this point, things like C<enum> are C<< <select> >> fields. Form::Sensible is very flexible in allowing you to specify what you want to look like what, so post-creation, you can even munge the fields to look like what you want.
-
-We get a form like this: 
-
-=begin html
-
-<img src="/static/images/2010/form-sensible-reflector-dbic-example.png" alt="" />
-
-=end html
-
-You'll notice that I've used L<TinyMCE|http://moxiecode.com/products_tinymce.php> for the main textarea.  What's great about Form::Sensible is it creates sensible divs for your CSS to be styled, and since TinyMCE just uses the DOM, you just point it at whatever id or element you want styled as a WYSIWYG editor.
-
-
-That's really it folks.  Form::Sensible::Reflector::DBIC only does a few things: 
-
-=over 12
-
-=item *
-Set up a bridge between Form::Sensible and DBIx::Class
-
-=item * 
-Allow you to have form definitions in one place, as opposed to two or three, like other systems might have you do.
-
-=item *
-Quickly display your form without much fuss.
-
-=back
-
-Define your tables, tinker, display.  That's all.  I hate forms, let's make it painless :-)
-
-=head2 AUTHOR
-
-Devin Austin <dhoss at cpan.org>

Deleted: trunk/examples/CatalystAdvent/root/2010/pen/i18n-catalyst-part2.pod
===================================================================
--- trunk/examples/CatalystAdvent/root/2010/pen/i18n-catalyst-part2.pod	2010-12-20 19:47:00 UTC (rev 13879)
+++ trunk/examples/CatalystAdvent/root/2010/pen/i18n-catalyst-part2.pod	2010-12-20 19:49:35 UTC (rev 13880)
@@ -1,216 +0,0 @@
-=head1 Internationalising Catalyst, Part 2
-
-=head2 SYNOPSIS
-
-In part one (L<http://www.catalystframework.org/calendar/2010/9>), we showed you how to add internationalisation to your Catalyst application. Now
-we'll show you some of the hints and tips that we've learnt while developing Opsview (L<http://opsview.com>).
-
-=head2 KEY NAMES
-
-We like the message key names to give an idea of where in the 
-application this message is used. Although xgettext.pl will
-list the file it was found and the line number, you don't really want your translators to have to dig their way
-through the code to understand. So given two pieces of information - the key and the default English text -
-this should be enough for a translator to change the text appropriately.
-
-We use keys like:
-
-  ui.menu.section.status
-  ui.event.title
-  ui.admin.contact.edit.label.confirmPassword
-  ui.admin.edit.button.submitChanges
-  messages.access.noAuthToStatusApi
-
-We deliberately use camelCase as this parses a bit better than the usual perl multi_word_phrases convention.
-
-=head2 GENERATED KEYS
-
-As c.loc() just needs a string parameter, you can pass in variables. For instance, we have the list of
-menu items stored in a configuration file, so our code has:
-
-  c.loc("ui.menu.section.$ident")
-
-This is caught by xgettext.pl, but you can just ignore it in i_default.po. But what about the actual strings?
-
-The xgettext routine to find 
-the list of keys will remove any keys that it cannot find in the controllers or template files - this is
-a feature, otherwise you will just keep growing the list of translation strings and translators will be 
-annoyed that they are translating strings that are no longer in use.
-
-As you need to store the list of possible strings somewhere, we decided to create a dummy 
-template file to hold the keys.  In Opsview, we create a root/dummy template with:
-
-  [%
-  c.loc("ui.menu.section.status")
-  c.loc("ui.menu.entry.hostgrouphierarchy")
-  ...
-  %]
-
-This lists all the possible options. The xgettext routine will then find it in this dummy file 
-and then it is just another string to translate. 
-
-=head2 VARIABLES
-
-Variables work as you would expect. The only thing to watch out for is the default way of 
-specifying variables is via [_1] in the perl code, but the .po files will have %1.
-
-An example in Opsview is in the template as:
-
-  c.loc("ui.state.service.downtime.help.schedulesDowntime [_1] [_2]", object.name, object.hostname )
-
-This generates an entry of:
-
-  msgid "ui.state.service.downtime.help.schedulesDowntime %1 %2"
-  msgstr "Schedules downtime for the service %1 on host %2"
-
-=head2 FILTERING
-
-This advice applies for all generated pages. However, it is good practise for translation strings otherwise
-a poor translation could break your application.
-
-In part one (L<http://www.catalystframework.org/calendar/2010/9>), there was a template filter called escape_js_string. 
-Template Toolkit also ships with an html filter.
-Based on where your strings appear in the web page, they should be escaped appropriately to stop invalid characters
-from affecting your code. For instance:
-
-  <script type="text/javascript">alert("[% c.loc("ui.help.welcomeText") | escape_js_string %]")</script>
-  <a href="http://opsview.com" 
-    alt="[% c.loc("ui.help.opsviewLink") | html %]"
-    onclick="alert('[% c.loc("ui.help.confirmLink") | escape_js_string | html %]')">
-  [% c.loc("ui.help.clickMe") | html %]
-  </a>
-
-The text in the <script> tag needs to be escaped (ui.help.welcomeText could have </script> within it). The alt tag
-needs to be escaped by the html filter. Finally, the onclick needs to be filtered by javascript first, then by html
-because it is within the context of an HTML element.
-
-Doing this will stop bad translation strings from causing unintended side effects.
-
-Sometimes you may need to put html in the strings. In these cases, we make sure the key contains 
-the word "html", so that translators know that we are expecting html in the text - 
-beware, you are at the mercy of your translators!
-
-=head2 HELPER METHODS
-
-We have 2 helper methods in Opsview/Web.pm.
-
-=over 4
-
-=item translate
-
-This always returns the i_default version of a certain string. This is commonly used for our audit log entries, 
-which need to be in a specific language as it is not a per user setting.
-
-  sub translate {
-    my $c = shift;
-    Opsview::Web::I18N::i_default->maketext(@_);
-  }
-
-We specifically chose C<translate> because this is one of the keywords that xgettext.pl picks up, so it 
-automatically gets into your po files.
-
-If you use this consistently in your application and introduce a system langauge setting, then you can change
-all your translations at once to a different language.
-
-=item ifloc
-
-This returns undef if translated version is the same as the initial string - this means no translation was found.
-
-  sub ifloc {
-    my $c           = shift;
-    my $id          = $_[0];
-    my $translation = $c->loc(@_);
-    return undef if ( $translation eq $id );
-    return $translation;
-  }
-
-We use this in situations where variables are used which we won't ever have a chance of knowing about.
-For example, in Opsview you define notifications by email, mobile or RSS, but someone could extend it
-with Jabber or IRC or Facebook.
-
-  c.ifloc("ui.admin.notificationmethod.variable.$variable_lc") || String.replace("_"," ").capital;
-
-We only add entries in our dummy template for the variables we support, but using ifloc allows a user
-to add their own message string.
-
-=back
-
-=head2 GETTING A LIST OF SUPPORTED LANGUAGES
-
-Catalyst::Plugin::I18N has a method called installed_languages, which will return a hashref with 
-all the languages it supports. It does this by scanning for MyApp/I18N/*.po files, and then using
-I18N::LangTags::List to convert the name to the language name.
-
-So a new translation file just needs to be dropped into the correct location and on application restart
-the list of supported languages increases!
-
-=head2 GENERATE AUTOMATIC TRANSLATIONS!
-
-When you are starting with internationalisation, you need to know that your application is working as you
-expect, but you (or at least I) can only speak English. Use this script to get Google to help you - it will 
-pass the English phrase and then populate the msgstr with the response from Google's translate service.
-This is quite awesome the first time you run it, until you get a native speaker to tell you that the
-phrases are completely wrong. But it's good for testing!
-
-Thanks to the fantastic WebService::Google::Language module.
-
-Script available here: L<https://secure.opsera.com/wsvn/wsvn/opsview/tags/release-3.9.1/opsview-web/utils/auto_translate>.
-
-=head2 RENAMING A STRING KEY
-
-Occasionally we need to rename a key to make more sense. Use this script which will rename the key
-and do it for all language files - this saves your translators from further headache!
-
-Script available here: L<https://secure.opsera.com/wsvn/wsvn/opsview/tags/release-3.9.1/opsview-web/utils/rename_string>.
-
-=head2 CHECKING FOR MISSING STRINGS
-
-There's a small perl script called validate_i_default to confirm there are no strings untranslated in
-i_default.po. You can add exceptions to this script, for strings that are not meant to be translated, usually auto generated strings.
-
-Script available here: L<https://secure.opsera.com/wsvn/wsvn/opsview/tags/release-3.9.1/opsview-web/utils/validate_i_default>.
-
-=head2 UPDATING STRINGS IN OTHER LANGUAGES
-
-We have another perl script called add_new_strings, which adds new strings in i_default.po to all the other
-language files with a default value of "". This means that translators can pick up their language file and 
-most translation programs will list the strings with no translations, making it easier to 
-identify the outstanding work.
-
-Locale::Maketext::Simple will also discard any msgid where the msgstr is empty, so there's no cost there.
-
-Script available here: L<https://secure.opsera.com/wsvn/wsvn/opsview/tags/release-3.9.1/opsview-web/utils/add_new_strings>.
-
-=head2 WRAP UP THE CHECKS INTO A MAKEFILE RULE
-
-The last two above can be added into a makefile rule:
-
-  gettext:
-    xgettext.pl -P perl=* -P tt2=* --output=lib/Opsview/Web/I18N/messages.pot --directory=lib/ --directory=root/
-    msgmerge --no-location --no-wrap --no-fuzzy-matching --update lib/Opsview/Web/I18N/i_default.po lib/Opsview/Web/I18N/messages.pot
-    # This is quick, so run it everytime
-    $(MAKE) gettext-test
-    # Check for missing strings
-    utils/validate_i_default
-    # Update all po files with new strings
-    utils/add_new_strings
-
-  gettext-test:
-    for i in lib/Opsview/Web/I18N/*.po; do msgfmt --output=/dev/null $$i || exit 1; done
-
-So now our workflow is reduced down to:
-
-  Add a string
-  Add the i_default.po version
-  Run make gettext
-  Commit
-
-=head2 CONCLUSION
-
-Adding language support shouldn't be hard, and we've reduced it as much as we can. So go forth and spread the Catalyst love in many different languages!
-
-=head2 AUTHOR
-
-Ton Voon <ton.voon at opsview.com>
-
-=cut




More information about the Catalyst-commits mailing list