1 package Reaction::UI::ViewPort::ListView;
6 use Scalar::Util qw/blessed/;
8 class ListView is 'Reaction::UI::ViewPort', which {
9 has collection => (isa => 'Reaction::InterfaceModel::Collection',
10 is => 'rw', required => 1);
12 has current_collection => (
13 isa => 'Reaction::InterfaceModel::Collection', is => 'rw',
14 lazy_build => 1, clearer => 'clear_current_collection',
17 has current_page_collection => (
18 isa => 'Reaction::InterfaceModel::Collection', is => 'rw',
19 lazy_build => 1, clearer => 'clear_current_page_collection',
23 isa => 'Int', is => 'rw', required => 1,
24 default => sub { 1 }, trigger_adopt('page'),
28 isa => 'Data::Page', is => 'rw',
29 lazy_build => 1, clearer => 'clear_pager',
33 isa => 'Int', is => 'rw', predicate => 'has_per_page',
34 default => sub { 10 }, trigger_adopt('page'),
35 clearer => 'clear_per_page',
38 has field_names => (is => 'rw', isa => 'ArrayRef', lazy_build => 1);
40 has field_label_map => (is => 'rw', isa => 'HashRef', lazy_build => 1);
43 isa => 'Str', is => 'rw', predicate => 'has_order_by',
44 trigger_adopt('order_by')
47 has order_by_desc => (
48 isa => 'Int', is => 'rw', default => sub { 0 },
49 trigger_adopt('order_by')
52 has row_action_prototypes => (isa => 'ArrayRef', is => 'ro', lazy_build => 1);
54 has exclude_columns =>
55 ( is => 'rw', isa => 'ArrayRef', required => 1, default => sub{ [] } );
57 implements BUILD => as {
58 my ($self, $args) = @_;
59 if ($args->{unpaged}) {
60 $self->clear_per_page;
64 sub field_label { shift->field_label_map->{+shift}; }
66 implements build_pager => as {
68 return $self->current_page_collection->pager;
71 implements adopt_page => as {
73 $self->clear_current_page_collection;
77 implements adopt_order_by => as {
79 $self->clear_current_collection;
80 $self->clear_current_page_collection;
83 implements build_current_collection => as {
87 #XXX DBICism that needs to be fixed
88 if ($self->has_order_by) {
89 $attrs{order_by} = $self->order_by;
90 if ($self->order_by_desc) {
91 $attrs{order_by} .= ' DESC';
94 return $self->collection->where(undef, \%attrs);
97 implements build_current_page_collection => as {
100 return $self->current_collection unless $self->has_per_page;
101 $attrs{rows} = $self->per_page;
102 return $self->current_collection->where(undef, \%attrs)->page($self->page);
105 implements all_current_rows => as {
106 return shift->current_collection->members;
109 implements current_rows => as {
110 return shift->current_page_collection->members;
113 implements build_field_names => as {
115 #XXX candidate for future optimization
116 my %excluded = map { $_ => undef } @{ $self->exclude_columns };
118 #XXX this abuse of '_im_class' needs to be fixed ASAP
119 my $object_class = $self->current_collection->_im_class;
120 my @fields = $object_class->meta->compute_all_applicable_attributes;
121 #eliminate excluded fields & treat names that start with an underscore as private
122 @fields = grep {$_->name !~ /^_/ && !exists $excluded{$_->name} } @fields;
123 #eliminate fields marked as collections, or fields that are arrayrefs
125 !($_->has_type_constraint &&
126 ($_->type_constraint->is_a_type_of('ArrayRef') ||
127 eval {$_->type_constraint->name->isa('Reaction::InterfaceModel::Collection')} ||
128 eval { $_->_isa_metadata->isa('Reaction::InterfaceModel::Collection') }
132 #for(grep { $_->has_type_constraint } @fields){
133 #my $tcname = $_->type_constraint->name;
134 #print STDERR $_->name, "\t", $tcname, "\n";
136 #print STDERR Dumper($_->type_constraint);
139 #order the columns all nice and pretty, and only get fields with readers, duh
140 return $self->sort_by_spec
141 ( $self->column_order, [ map { (($_->get_read_method) || ()) } @fields] );
144 implements build_field_label_map => as {
147 foreach my $name (@{$self->field_names}) {
148 $labels{$name} = join(' ', map { ucfirst } split('_', $name));
153 #XXX this has to go soon, I recommend that Objects hold a registry of their actions
154 #and that they can be queried about it somehow
155 implements build_row_action_prototypes => as {
157 my $ctx = $self->ctx;
159 { label => 'View', action => sub {
160 [ '', 'view', [ @{$ctx->req->captures}, $_[0]->__id ] ] } },
161 { label => 'Edit', action => sub {
162 [ '', 'update', [ @{$ctx->req->captures}, $_[0]->__id ] ] } },
163 { label => 'Delete', action => sub {
164 [ '', 'delete', [ @{$ctx->req->captures}, $_[0]->__id ] ] } },
168 implements row_actions_for => as {
169 my ($self, $row) = @_;
172 foreach my $proto (@{$self->row_action_prototypes}) {
174 my ($c_name, $a_name, @rest) = @{delete($new{action})->($row)};
175 $new{label} = delete($new{label})->($row) if ref $new{label} eq 'CODE';
176 $new{uri} = $c->uri_for(
177 $c->controller($c_name)->action_for($a_name),
185 implements export_to_csv => as {
187 my $csv = Text::CSV_XS->new( { binary => 1 } );
191 $output .= $csv->string."\r\n";
193 $self->export_to_data($exporter);
194 my $res = $self->ctx->res;
195 $res->content_type('text/csv');
196 my $path = $self->ctx->req->path;
197 my @parts = split(/\//, $path);
199 'Content-disposition' => 'attachment; filename='.pop(@parts).'.csv'
204 implements export_to_data => as {
205 my ($self, $exporter) = @_;
206 $self->export_header_data($exporter);
207 $self->export_body_data($exporter);
210 implements export_header_data => as {
211 my ($self, $exporter) = @_;
212 my @names = @{$self->field_names};
213 my %labels = %{$self->field_label_map};
214 $exporter->( map { $labels{$_} } @names );
217 implements export_body_data => as {
218 my ($self, $exporter) = @_;
219 my @names = @{$self->field_names};
220 foreach my $row ($self->all_current_rows) {
222 foreach $_ (@names) {
224 if (blessed($data) && $data->can("display_name")) {
225 $data = $data->display_name;
227 push(@row_data, $data);
229 $exporter->( @row_data );
233 override accept_events => sub { ('page', 'order_by', 'order_by_desc', 'export_to_csv', super()); };
241 Reaction::UI::ViewPort::ListView - Page layout block for rows of DBIx::Class::ResultSets
245 # Create a new ListView
246 # $stack isa Reaction::UI::FocusStack object
247 # Assuming you have a DBIC model with an Actors table
248 my $lv = $stack->push_viewport(
249 'Reaction::UI::ViewPort::ListView',
250 collection => $ctx->model('DBIC::Actors'), # a DBIx::Class::ResultSet
251 page => 1, # 1 is default
252 per_page => 10, # 10 is default
253 field_names => [qw/name age/],
263 Use this ViewPort to display the contents of a
264 L<DBIx::Class::ResultSet> as paged sets of rows. The default display
265 shows 10 rows per page, unsorted.
267 TODO: Add a filter_by which allows us to restrict the content?
268 (Scenario: user has a paged display of data, user selects one value in
269 a column and clicks "filter by this value", and then only rows
270 containing that value are shown.
276 This mandatory attribute must be an object derived from
277 L<DBIx::Class::ResultSet> representing the search result or result
278 source(Table) you wish to display in the ListView.
280 The collection is used as the basis to create a refined set of data to
281 show in the current ListView, this is stored in
282 L<current_collection>. The data can further be refined and restricted
283 by passing in or later changing the L<order_by> or L<page>
288 A string representing the C<ORDER BY> part of the SQL statement, for
289 more info see L<DBIx::Class::ResultSet/Attributes>
293 By default, sorting is done in ascending order, set this to true to
294 sort in descending order. Changing this attribute will cause the
295 L<current_collection> to be cleared and recreated on the next access .
297 =head2 exclude_columns
303 The page number of the current search result, this will default to
304 1. If set explicitly on the ListView object, the current search result
305 and the pager will be cleared and recreated on the next access.
309 The number of rows of data to list on each page. Changing this value
310 on the ListView object will cause the L<current_page_collection> and
311 the L<pager> to be cleared and recreated on the next access. This will
312 default to 10 if unset.
316 Set this to a true value if you really don't want your results shown
321 An array reference of field names to show in the ListView. These must
322 exist as accessors in the L<DBIx::Class::ResultSource> describing the
323 L<DBIx::Class::ResultSet> passed to L<collection>.
325 If not set, this will default to the list of attributes in the
326 L<DBIx::Class::ResultSource> which do not begin with an underscore,
327 and don't have a type of either ArrayRef or
328 C<DBIx::Class::ResultSet>. In short, all the non-private and
329 non-relation attributes.
331 =head2 field_label_map
333 A hash reference mapping the L<field_names> to the column labels used
334 to describe them in the ListView display.
336 If not set, the label values will default to the L<field_names> with
337 the initial characters capitalised and underscores turned into spaces.
339 =head2 row_action_prototypes
341 row_action_prototypes => [
342 { label => 'Edit', action => sub { [ '', 'update', [ $_[0]->id ] ] } },
343 { label => 'Delete', action => sub { [ '', 'delete', [ $_[0]->id ] ] } },
346 Prototypes describing the actions that can be done on the rows of
347 ListView data. This is an array reference of hash refs describing the
348 name of each action with a C<label>, and the actual C<action> that
349 takes place. The code reference stored in the C<action > will be
350 called with a L<DBIx::Class::Row> object, it should return a list of a
351 L<Catalyst::Controller> name, the name of an action in that
352 controller, and any other parameters that need to be passed to
353 it. C<label> may be a scalar value or a code reference, in the later case
354 it will be called with the same parameters as C<action> and the return value
355 will be used as the C<label> value.
357 The example above shows the default actions if this attribute is not set.
359 =head2 current_collection
361 This contains the currently used L<DBIx::Class::ResultSet>
362 representing the ListViews data, it is based on the L<collection>
363 ResultSet, refined using the L<order_by> and L<order_by_desc> attributes.
365 The current_collection will be cleared and recreated if the
366 L<order_by> or L<order_by_desc> attributes are changed on the ListView
371 =head2 all_current_rows
375 A L<Data::Page> object representing the data for the current search
376 result, it is cleared and reset when either L<page> or L<order_by> are
379 =head2 current_page_collection
381 This contains contains a single page of the contents of the
382 L<current_collection>, with the L<per_page> number of rows
383 requested. If the L<page>, L<per_page>, L_order_by> or
384 L<order_by_desc> attributes are changed on the ListView object, the
385 current_page_collection is cleared and recreated.
389 =head2 row_actions_for
393 =item Arguments: none
397 Returns an array reference of uris and labels representing the actions
398 set in L<row_action_prototypes>. L<Catalyst/uri_for> is used to
401 =head2 export_header_data
405 =item Arguments: $exporter
409 $lv->export_head_data($exporter);
411 C<$exporter> should be a code reference which will export lists of
412 data passed to it. This method calls the C<exporter> code reference
413 passing it the labels from the L<field_label_map> using the current
414 set of L<field_names>.
416 =head2 export_body_data
420 =item Arguments: $exporter
424 $lv->export_body_data($exporter);
426 C<$exporter> should be a code reference which will export lists of
427 data passed to it. This method calls the C<exporter> code reference
428 with an array of rows containing the data values of each of the
429 current L<field_values>.
431 =head2 export_to_data
435 =item Arguments: $exporter
439 $lv->export_to_data($exporter);
441 C<$exporter> should be a code reference which will export lists of
442 data passed to it. This method calls L<export_header_data> and
443 L<export_body_data> with C<exporter>.
449 =item Arguments: none
453 $lv->export_to_csv();
455 Fills the L<Catalyst::Response> body with CSV data of the
456 L<current_collection> using L<export_to_data> and L<Text::CSV_XS>.
462 =item Arguments: $field_name
466 Returns the label for the given C<field_name>, using L<field_label_map>.
470 See L<Reaction::Class> for authors.
474 See L<Reaction::Class> for the license.