CatalystX-ListFramework

 view release on metacpan or  search on metacpan

lib/CatalystX/ListFramework.pm  view on Meta::CPAN

package CatalystX::ListFramework;
use HTML::Widget;
use File::Slurp;
use Data::Dumper;
use Carp;

use strict;
use warnings;
our $VERSION = '0.5';
require 5.8.1;

=head1 NAME

CatalystX::ListFramework - foundations for displaying and editing lists (CRUD) in a Catalyst application

=head1 SYNOPSIS

    package MyApp::Controller::Foo;
    use base 'Catalyst::Controller';
    use CatalystX::ListFramework;
    
    sub listandsearch :Local {
        my ($self, $c, $kind) = @_;
        my $lf = CatalystX::ListFramework->new($kind, $c);
        my $restrict = {};
        $lf->stash_listing('myview', 'myprefix', $restrict);
        $c->stash->{template} = 'list-and-search.tt';
    }

    sub get :Local {
        my ($self, $c, $kind, $id) = @_;
        my $lf = CatalystX::ListFramework->new($kind, $c);
        $lf->stash_infoboxes({'me.id' => $id}); 
        $c->stash->{kind} = $kind;
        $c->stash->{id} = $id;  # the update form adds this to the URL
        $c->stash->{template} = 'detail.tt';
    }
    
    sub update :Local {
        my ($self, $c, $kind, $id) = @_;
        my $lf = CatalystX::ListFramework->new($kind, $c);
        $lf->update_from_query({'me.id' => $id}); 
        $c->res->redirect("/listandsearch/$kind");
    }
    
    sub create :Local {
        my ($self, $c, $kind) = @_;
        my $lf = CatalystX::ListFramework->new($kind, $c);
        my $id = $lf->create_new; 
        $c->res->redirect("/get/$kind/$id");
    }

=head1 DESCRIPTION

Displaying tabulated lists of database records, updating those records and
creating new ones is a common task in Catalyst applications.
This class supplies such lists, and forms to edit such records, to a set of
templates, using simple definition files and your L<DBIx::Class> Catalyst
model. A search form is also supplied, which can include JSON-powered
ExtJS comboboxes (see L<http://www.extjs.com/>).

To run the included demo application, grab a copy of ExtJS, then

    cd t/
    ln -s /path/to/extjs/ static/extjs-1.1
    lib/script/testapp_server.pl

then

    firefox http://localhost:3000/start
    
Please see L<BUGS> about some SQLite issues with the demo app.
The noninteractive test suite is

    perl live-test.pl
    

=head1 DEFINITION FILES

ListFramework is driven by a set of definition files, found under C<formdef/>, one pair per schema class (table).
These are divided into 'master' files and 'site' files and are named I<kind>.form.
Files under C<master/> describe a I<kind>'s source, what fields it has available, and how it is
associated with other schema classes.
Files under C<site/> describe how the data is displayed on the page.
This division, and the naming, implies that a vendor (you) could supply the master files, while a particular
installation could use customised site files to suit their needs.

These are best understood by looking at the example files.

=head2 Files under /master

The sections in these files are:

=over

=item title

A title, displayed on various screens.

lib/CatalystX/ListFramework.pm  view on Meta::CPAN

    
    foreach my $box (keys %{$self->{formdef}->{infoboxes}}) {
        my @info;
        my $columns = $self->{formdef}->{infoboxes}->{$box};

        # Copy metadata (headings etc) from 'columns' (maybe in a related form file)
        $self->copy_metadata_from_columns($columns);

        # Make calls on the row object to fill the various columns
        my $processed_row = $self->rowobject_to_columns($db_row, $columns); # a hashref with col_id => celldata

        foreach my $col (@$columns) {
            my %object_info;
            if (ref($processed_row->{$col->{id}})) {  # if it's a '.OBJECT' entry, i.e. to select a belongs_to thing
                %object_info = (
                                primary_key  => $col->{primary_key},  # master formdef columns line for OBJECT sets this
                                form_type    => $col->{form_type},  # some duplication here. This is set by new()
                                stringified  => "$processed_row->{$col->{id}}", # use overloaded ""
                                is_object    => 1,
                               );
            }
            my $is_editable = 1;
            # anything that's not a simple single field or that has a not_editable key isn't editable
            $is_editable = 0 if ($col->{field} && (ref($col->{field}) || $col->{not_editable} || $col->{field} !~ m{^\w+$}));
            
            push @info, {
                          name       => $col->{heading},
                          id         => $col->{id},
                          value      => $processed_row->{$col->{id}},
                          options    => $col->{options},
                          type       => $col->{type},
                          is_editable=> $is_editable,
                          %object_info,
                        };
        }

        # If no column ids were specified for this box, just stash the DB object and let the template do what it likes.
        if (scalar(@info)) {
            $box_data->{$box} = \@info;
        }
        else {
            $box_data->{$box} = $db_row;
        }
        my $title = ucfirst($box);
        $title =~ tr/_/ /;
        push @$box_metadata, {id=>$box, title=>$title};
    }
    
    if (ref($self->{formdef}->{infobox_order})) {
        @$box_metadata = sort { $self->{formdef}->{infobox_order}->{$a->{id}}
                                  <=>
                                $self->{formdef}->{infobox_order}->{$b->{id}}
                              } @$box_metadata;
    }
    $c->stash->{box_metadata} = $box_metadata;
    $c->stash->{box_data} = $box_data;

    return 1;
}

sub update_from_query {  # Update a record. Probably called from an infobox screen
    my ($self, $search) = @_;
    my $c = $self->{c};
    my $rs = $c->model($self->{formdef}->{model})->search($search, {});  # NB: no joins. We'll assume we're looking locally for an id
    my $db_row_obj = $rs->first;
    unless (ref $db_row_obj) { confess "No such object found"; }
    
    # All editable fields must be listed in the infobox section
    my $all_cols;
    foreach my $box (keys %{$self->{formdef}->{infoboxes}}) {
        my $columns = $self->{formdef}->{infoboxes}->{$box};
        push @$all_cols, @$columns;
    }
    $self->copy_metadata_from_columns($all_cols);

    foreach my $col (@$all_cols) {
        next if ($col->{field} && (ref($col->{field}) || $col->{not_editable} || $col->{field} !~ m{^\w+$}));
        if (my $new_value = $c->req->params->{$col->{id}}) {
            my $row_in_wanted_table = $db_row_obj;
            my $row_in_parent_table = undef; # this is the one we must update if we're being sent an OBJECT id
            {
                # Obtain the row object on which to call update.
                my $col_id = $col->{id};
                while ($col_id =~ m{^(\w+)\.(.+)}) { # work along the abc.def.ghi relationships til we get to the final row obj we want
                    $row_in_parent_table = $row_in_wanted_table;
                    $row_in_wanted_table = eval("\$row_in_parent_table->$1");
                    if ($@) { die "Eval of row->$1 failed"; }
                    $col_id = $2;
                }
            }
            if ($col->{id} =~ m{\.OBJECT$}) { # called from stash_infoboxes and requesting the relationship be updated by id
                my $foreign_model = $col->{model}; # filled-in by new() above
                $col->{id} =~ m{(\w+)\.OBJECT$};
                my $rel = $1;
                #warn "Fetching id $new_value from $foreign_model to update $rel";
                (my $new_foreign_row) = $c->model($foreign_model)->find($new_value);
                die "Row for new relationship setting not found" if (!ref $new_foreign_row);
                $row_in_parent_table->update_from_related($rel, $new_foreign_row);
            }
            else {
                if (defined $col->{type}) {
                    eval "\$new_value = \&CatalystX::ListFramework::Helpers::Types::inverse$col->{type}(\"$new_value\", \$c, \$self->{formdef})";
                    if ($@) {die "Type-helper call failed: $@";}
                }
                # Do the update
                eval("\$row_in_wanted_table->$col->{field}(\$new_value); \$row_in_wanted_table->update;");
                if ($@) { die "Error while updating row: $@"; }
            }
        }
    }
}

sub create_new {
    my ($self, $columnvalues, $dont_set_rels) = @_;
    my $c = $self->{c};
    my $columns = $self->{formdef}->{columns};
    my $relationships = $self->{formdef}->{uses};
    
    # Do the create(). Use default_value if specified in a column hash, and set 'belongs_to's to the 1st row out of the hat.
    my $create_hash = $columnvalues;
    foreach my $col (keys %$columns) {



( run in 0.748 second using v1.01-cache-2.11-cpan-39bf76dae61 )