aboutsummaryrefslogtreecommitdiffstats
path: root/lib/Reaction/UI/ViewPort/ListView.pm
diff options
context:
space:
mode:
authormatthewt <matthewt@03d0b0b2-0e1a-0410-a411-fdb2f4bd65d7>2007-09-12 18:11:34 +0000
committermatthewt <matthewt@03d0b0b2-0e1a-0410-a411-fdb2f4bd65d7>2007-09-12 18:11:34 +0000
commit7adfd53f17f66ffe93763e944ed1d3fc52a369dc (patch)
tree19e599e74419b41cbbe651fd226b81e8b73551d3 /lib/Reaction/UI/ViewPort/ListView.pm
parentc728c97cb1061330e63c7cc048e768ef74988fe6 (diff)
downloadreaction-7adfd53f17f66ffe93763e944ed1d3fc52a369dc.tar.gz
reaction-7adfd53f17f66ffe93763e944ed1d3fc52a369dc.zip
moved shit to trunk
Diffstat (limited to 'lib/Reaction/UI/ViewPort/ListView.pm')
-rw-r--r--lib/Reaction/UI/ViewPort/ListView.pm465
1 files changed, 465 insertions, 0 deletions
diff --git a/lib/Reaction/UI/ViewPort/ListView.pm b/lib/Reaction/UI/ViewPort/ListView.pm
new file mode 100644
index 0000000..d5ddfba
--- /dev/null
+++ b/lib/Reaction/UI/ViewPort/ListView.pm
@@ -0,0 +1,465 @@
+package Reaction::UI::ViewPort::ListView;
+
+use Reaction::Class;
+use Data::Page;
+use Text::CSV_XS;
+use Scalar::Util qw/blessed/;
+
+class ListView is 'Reaction::UI::ViewPort', which {
+ has collection => (isa => 'DBIx::Class::ResultSet',
+ is => 'rw', required => 1);
+
+ has current_collection => (
+ isa => 'DBIx::Class::ResultSet', is => 'rw',
+ lazy_build => 1, clearer => 'clear_current_collection',
+ );
+
+ has current_page_collection => (
+ isa => 'DBIx::Class::ResultSet', is => 'rw',
+ lazy_build => 1, clearer => 'clear_current_page_collection',
+ );
+
+ has page => (
+ isa => 'Int', is => 'rw', required => 1,
+ default => sub { 1 }, trigger_adopt('page'),
+ );
+
+ has pager => (
+ isa => 'Data::Page', is => 'rw',
+ lazy_build => 1, clearer => 'clear_pager',
+ );
+
+ has per_page => (
+ isa => 'Int', is => 'rw', predicate => 'has_per_page',
+ default => sub { 10 }, trigger_adopt('page'),
+ clearer => 'clear_per_page',
+ );
+
+ has field_names => (is => 'rw', isa => 'ArrayRef', lazy_build => 1);
+
+ has field_label_map => (is => 'rw', isa => 'HashRef', lazy_build => 1);
+
+ has order_by => (
+ isa => 'Str', is => 'rw', predicate => 'has_order_by',
+ trigger_adopt('order_by')
+ );
+
+ has order_by_desc => (
+ isa => 'Int', is => 'rw', default => sub { 0 },
+ trigger_adopt('order_by')
+ );
+
+ has row_action_prototypes => (isa => 'ArrayRef', is => 'ro', lazy_build => 1);
+
+ has exclude_columns =>
+ ( is => 'rw', isa => 'ArrayRef', required => 1, default => sub{ [] } );
+
+ implements BUILD => as {
+ my ($self, $args) = @_;
+ if ($args->{unpaged}) {
+ $self->clear_per_page;
+ }
+ };
+
+ sub field_label { shift->field_label_map->{+shift}; }
+
+ implements build_pager => as {
+ my ($self) = @_;
+ return $self->current_page_collection->pager;
+ };
+
+ implements adopt_page => as {
+ my ($self) = @_;
+ $self->clear_current_page_collection;
+ $self->clear_pager;
+ };
+
+ implements adopt_order_by => as {
+ my ($self) = @_;
+ $self->clear_current_collection;
+ $self->clear_current_page_collection;
+ };
+
+ implements build_current_collection => as {
+ my ($self) = @_;
+ my %attrs;
+ if ($self->has_order_by) {
+ $attrs{order_by} = $self->order_by;
+ if ($self->order_by_desc) {
+ $attrs{order_by} .= ' DESC';
+ }
+ }
+ return $self->collection
+ ->search(undef, \%attrs);
+ };
+
+ implements build_current_page_collection => as {
+ my ($self) = @_;
+ my %attrs;
+ return $self->current_collection unless $self->has_per_page;
+ $attrs{rows} = $self->per_page;
+ return $self->current_collection
+ ->search(undef, \%attrs)
+ ->page($self->page);
+ };
+
+ implements all_current_rows => as {
+ return shift->current_collection->all;
+ };
+
+ implements current_rows => as {
+ return shift->current_page_collection->all;
+ };
+
+ implements build_field_names => as {
+ my ($self) = @_;
+ #candidate for future optimization
+ my %excluded = map { $_ => undef } @{ $self->exclude_columns };
+
+ return
+ $self->sort_by_spec( $self->column_order,
+ [ map { (($_->get_read_method) || ()) }
+ grep { !($_->has_type_constraint
+ && ($_->type_constraint->is_a_type_of('ArrayRef')
+ || eval { $_->type_constraint->name->isa(
+ 'DBIx::Class::ResultSet') })) }
+ grep { !exists $excluded{$_->name} }
+ grep { $_->name !~ /^_/ }
+ $self->current_collection
+ ->result_class
+ ->meta
+ ->compute_all_applicable_attributes
+ ] );
+ };
+
+ implements build_field_label_map => as {
+ my ($self) = @_;
+ my %labels;
+ foreach my $name (@{$self->field_names}) {
+ $labels{$name} = join(' ', map { ucfirst } split('_', $name));
+ }
+ return \%labels;
+ };
+
+ implements build_row_action_prototypes => as {
+ my $self = shift;
+ my $ctx = $self->ctx;
+ return [
+ { label => 'View', action => sub {
+ [ '', 'view', [ @{$ctx->req->captures}, $_[0]->id ] ] } },
+ { label => 'Edit', action => sub {
+ [ '', 'update', [ @{$ctx->req->captures}, $_[0]->id ] ] } },
+ { label => 'Delete', action => sub {
+ [ '', 'delete', [ @{$ctx->req->captures}, $_[0]->id ] ] } },
+ ];
+ };
+
+ implements row_actions_for => as {
+ my ($self, $row) = @_;
+ my @act;
+ my $c = $self->ctx;
+ foreach my $proto (@{$self->row_action_prototypes}) {
+ my %new = %$proto;
+ my ($c_name, $a_name, @rest) = @{delete($new{action})->($row)};
+ $new{label} = delete($new{label})->($row) if ref $new{label} eq 'CODE';
+ $new{uri} = $c->uri_for(
+ $c->controller($c_name)->action_for($a_name),
+ @rest
+ );
+ push(@act, \%new);
+ }
+ return \@act;
+ };
+
+ implements export_to_csv => as {
+ my ($self) = @_;
+ my $csv = Text::CSV_XS->new( { binary => 1 } );
+ my $output;
+ my $exporter = sub {
+ $csv->combine( @_ );
+ $output .= $csv->string."\r\n";
+ };
+ $self->export_to_data($exporter);
+ my $res = $self->ctx->res;
+ $res->content_type('text/csv');
+ my $path = $self->ctx->req->path;
+ my @parts = split(/\//, $path);
+ $res->header(
+ 'Content-disposition' => 'attachment; filename='.pop(@parts).'.csv'
+ );
+ $res->body($output);
+ };
+
+ implements export_to_data => as {
+ my ($self, $exporter) = @_;
+ $self->export_header_data($exporter);
+ $self->export_body_data($exporter);
+ };
+
+ implements export_header_data => as {
+ my ($self, $exporter) = @_;
+ my @names = @{$self->field_names};
+ my %labels = %{$self->field_label_map};
+ $exporter->( map { $labels{$_} } @names );
+ };
+
+ implements export_body_data => as {
+ my ($self, $exporter) = @_;
+ my @names = @{$self->field_names};
+ foreach my $row ($self->all_current_rows) {
+ my @row_data;
+ foreach $_ (@names) {
+ my $data = $row->$_;
+ if (blessed($data) && $data->can("display_name")) {
+ $data = $data->display_name;
+ }
+ push(@row_data, $data);
+ }
+ $exporter->( @row_data );
+ }
+ };
+
+ override accept_events => sub { ('page', 'order_by', 'order_by_desc', 'export_to_csv', super()); };
+
+};
+
+1;
+
+=head1 NAME
+
+Reaction::UI::ViewPort::ListView - Page layout block for rows of DBIx::Class::ResultSets
+
+=head1 SYNOPSIS
+
+ # Create a new ListView
+ # $stack isa Reaction::UI::FocusStack object
+ # Assuming you have a DBIC model with an Actors table
+ my $lv = $stack->push_viewport(
+ 'Reaction::UI::ViewPort::ListView',
+ collection => $ctx->model('DBIC::Actors'), # a DBIx::Class::ResultSet
+ page => 1, # 1 is default
+ per_page => 10, # 10 is default
+ field_names => [qw/name age/],
+ field_label_map => {
+ 'name' => 'Name',
+ 'age' => 'Age',
+ },
+ order_by => 'name',
+ );
+
+=head1 DESCRIPTION
+
+Use this ViewPort to display the contents of a
+L<DBIx::Class::ResultSet> as paged sets of rows. The default display
+shows 10 rows per page, unsorted.
+
+TODO: Add a filter_by which allows us to restrict the content?
+(Scenario: user has a paged display of data, user selects one value in
+a column and clicks "filter by this value", and then only rows
+containing that value are shown.
+
+=head1 ATTRIIBUTES
+
+=head2 collection
+
+This mandatory attribute must be an object derived from
+L<DBIx::Class::ResultSet> representing the search result or result
+source(Table) you wish to display in the ListView.
+
+The collection is used as the basis to create a refined set of data to
+show in the current ListView, this is stored in
+L<current_collection>. The data can further be refined and restricted
+by passing in or later changing the L<order_by> or L<page>
+attributes. The
+
+=head2 order_by
+
+A string representing the C<ORDER BY> part of the SQL statement, for
+more info see L<DBIx::Class::ResultSet/Attributes>
+
+=head2 order_by_desc
+
+By default, sorting is done in ascending order, set this to true to
+sort in descending order. Changing this attribute will cause the
+L<current_collection> to be cleared and recreated on the next access .
+
+=head2 exclude_columns
+
+
+
+=head2 page
+
+The page number of the current search result, this will default to
+1. If set explicitly on the ListView object, the current search result
+and the pager will be cleared and recreated on the next access.
+
+=head2 per_page
+
+The number of rows of data to list on each page. Changing this value
+on the ListView object will cause the L<current_page_collection> and
+the L<pager> to be cleared and recreated on the next access. This will
+default to 10 if unset.
+
+=head2 unpaged
+
+Set this to a true value if you really don't want your results shown
+in pages.
+
+=head2 field_names
+
+An array reference of field names to show in the ListView. These must
+exist as accessors in the L<DBIx::Class::ResultSource> describing the
+L<DBIx::Class::ResultSet> passed to L<collection>.
+
+If not set, this will default to the list of attributes in the
+L<DBIx::Class::ResultSource> which do not begin with an underscore,
+and don't have a type of either ArrayRef or
+C<DBIx::Class::ResultSet>. In short, all the non-private and
+non-relation attributes.
+
+=head2 field_label_map
+
+A hash reference mapping the L<field_names> to the column labels used
+to describe them in the ListView display.
+
+If not set, the label values will default to the L<field_names> with
+the initial characters capitalised and underscores turned into spaces.
+
+=head2 row_action_prototypes
+
+ row_action_prototypes => [
+ { label => 'Edit', action => sub { [ '', 'update', [ $_[0]->id ] ] } },
+ { label => 'Delete', action => sub { [ '', 'delete', [ $_[0]->id ] ] } },
+ ];
+
+Prototypes describing the actions that can be done on the rows of
+ListView data. This is an array reference of hash refs describing the
+name of each action with a C<label>, and the actual C<action> that
+takes place. The code reference stored in the C<action > will be
+called with a L<DBIx::Class::Row> object, it should return a list of a
+L<Catalyst::Controller> name, the name of an action in that
+controller, and any other parameters that need to be passed to
+it. C<label> may be a scalar value or a code reference, in the later case
+it will be called with the same parameters as C<action> and the return value
+will be used as the C<label> value.
+
+The example above shows the default actions if this attribute is not set.
+
+=head2 current_collection
+
+This contains the currently used L<DBIx::Class::ResultSet>
+representing the ListViews data, it is based on the L<collection>
+ResultSet, refined using the L<order_by> and L<order_by_desc> attributes.
+
+The current_collection will be cleared and recreated if the
+L<order_by> or L<order_by_desc> attributes are changed on the ListView
+object.
+
+=head2 current_rows
+
+=head2 all_current_rows
+
+=head2 pager
+
+A L<Data::Page> object representing the data for the current search
+result, it is cleared and reset when either L<page> or L<order_by> are
+changed.
+
+=head2 current_page_collection
+
+This contains contains a single page of the contents of the
+L<current_collection>, with the L<per_page> number of rows
+requested. If the L<page>, L<per_page>, L_order_by> or
+L<order_by_desc> attributes are changed on the ListView object, the
+current_page_collection is cleared and recreated.
+
+=head1 METHODS
+
+=head2 row_actions_for
+
+=over 4
+
+=item Arguments: none
+
+=back
+
+Returns an array reference of uris and labels representing the actions
+set in L<row_action_prototypes>. L<Catalyst/uri_for> is used to
+construct these.
+
+=head2 export_header_data
+
+=over 4
+
+=item Arguments: $exporter
+
+=back
+
+ $lv->export_head_data($exporter);
+
+C<$exporter> should be a code reference which will export lists of
+data passed to it. This method calls the C<exporter> code reference
+passing it the labels from the L<field_label_map> using the current
+set of L<field_names>.
+
+=head2 export_body_data
+
+=over 4
+
+=item Arguments: $exporter
+
+=back
+
+ $lv->export_body_data($exporter);
+
+C<$exporter> should be a code reference which will export lists of
+data passed to it. This method calls the C<exporter> code reference
+with an array of rows containing the data values of each of the
+current L<field_values>.
+
+=head2 export_to_data
+
+=over 4
+
+=item Arguments: $exporter
+
+=back
+
+ $lv->export_to_data($exporter);
+
+C<$exporter> should be a code reference which will export lists of
+data passed to it. This method calls L<export_header_data> and
+L<export_body_data> with C<exporter>.
+
+=head2 export_to_csv
+
+=over 4
+
+=item Arguments: none
+
+=back
+
+ $lv->export_to_csv();
+
+Fills the L<Catalyst::Response> body with CSV data of the
+L<current_collection> using L<export_to_data> and L<Text::CSV_XS>.
+
+=head2 field_label
+
+=over 4
+
+=item Arguments: $field_name
+
+=back
+
+Returns the label for the given C<field_name>, using L<field_label_map>.
+
+=head1 AUTHORS
+
+See L<Reaction::Class> for authors.
+
+=head1 LICENSE
+
+See L<Reaction::Class> for the license.
+
+=cut