File Coverage

blib/lib/Catalyst/Action/Serialize/SimpleExcel.pm
Criterion Covered Total %
statement 12 12 100.0
branch n/a
condition n/a
subroutine 4 4 100.0
pod n/a
total 16 16 100.0


line stmt bran cond sub pod time code
1             package Catalyst::Action::Serialize::SimpleExcel;
2              
3 1     1   25205 use strict;
  1         3  
  1         37  
4 1     1   4 use warnings;
  1         2  
  1         31  
5 1     1   5 no warnings 'uninitialized';
  1         7  
  1         37  
6 1     1   1632 use parent 'Catalyst::Action';
  1         699  
  1         6  
7             use Spreadsheet::WriteExcel ();
8             use Catalyst::Exception ();
9             use namespace::clean;
10              
11             =head1 NAME
12              
13             Catalyst::Action::Serialize::SimpleExcel - Serialize to Excel files
14              
15             =cut
16              
17             our $VERSION = '0.015';
18              
19             =head1 SYNOPSIS
20              
21             Serializes tabular data to an Excel file. Not terribly configurable, but should
22             suffice for simple purposes.
23              
24             In your REST Controller:
25              
26             package MyApp::Controller::REST;
27              
28             use parent 'Catalyst::Controller::REST';
29             use DBIx::Class::ResultClass::HashRefInflator ();
30             use POSIX 'strftime';
31              
32             __PACKAGE__->config->{map}{'application/vnd.ms-excel'} = 'SimpleExcel';
33              
34             sub books : Local ActionClass('REST') {}
35              
36             sub books_GET {
37             my ($self, $c) = @_;
38              
39             my $books_rs = $c->model('MyDB::Book')->search({}, {
40             order_by => 'author,title'
41             });
42              
43             $books_rs->result_class('DBIx::Class::ResultClass::HashRefInflator');
44              
45             my @books = map {
46             [ @{$_}{qw/author title/} ]
47             } $books_rs->all;
48              
49             my $authors_rs = $c->model('MyDB::Author')->search({}, {
50             order_by => 'last_name,middle_name,last_name'
51             });
52              
53             $authors_rs->result_class('DBIx::Class::ResultClass::HashRefInflator');
54              
55             my @authors = map {
56             [ @{$_}{qw/first_name middle_name last_name/} ]
57             } $authors_rs->all;
58              
59             my $entity = {
60             sheets => [
61             {
62             name => 'Books',
63             header => ['Author', 'Title'], # will be bold
64             rows => \@books,
65             },
66             {
67             name => 'Authors',
68             header => ['First Name', 'Middle Name', 'Last Name'],
69             rows => \@authors,
70             },
71             ],
72             # the part before .xls, which is automatically appended
73             filename => 'myapp-books-'.strftime('%m-%d-%Y', localtime)
74             };
75              
76             $self->status_ok(
77             $c,
78             entity => $entity
79             );
80             }
81              
82             In your javascript, to initiate a file download:
83              
84             // this uses jQuery
85             function export_to_excel() {
86             $('<iframe '
87             +'src="/rest/books?content-type=application%2Fvnd.ms-excel">')
88             .hide().appendTo('body');
89             }
90              
91             Note, the content-type query param is required if you're just linking to the
92             action. It tells L<Catalyst::Controller::REST> what you're serializing the data
93             as.
94              
95             =head1 DESCRIPTION
96              
97             Your entity should be either an array of arrays, an array of arrays of arrays,
98             or a hash with the keys as described below and in the L</SYNOPSIS>.
99              
100             If entity is a hashref, keys should be:
101              
102             =head2 sheets
103              
104             An array of worksheets. Either sheets or a worksheet specification at the top
105             level is required.
106              
107             =head2 filename
108              
109             Optional. The name of the file before .xls. Defaults to "data".
110              
111             Each sheet should be an array of arrays, or a hashref with the following fields:
112              
113             =head2 name
114              
115             Optional. The name of the worksheet.
116              
117             =head2 rows
118              
119             Required. The array of arrays of rows.
120              
121             =head2 header
122              
123             Optional, an array for the first line of the sheet, which will be in bold.
124              
125             =head2 column_widths
126              
127             Optional, the widths in characters of the columns. Otherwise the widths are
128             calculated automatically from the data and header.
129              
130             If you only have one sheet, you can put it in the top level hash.
131              
132             =cut
133              
134             sub execute {
135             my $self = shift;
136             my ($controller, $c) = @_;
137              
138             my $stash_key = (
139             $controller->config->{'serialize'} ?
140             $controller->config->{'serialize'}->{'stash_key'} :
141             $controller->config->{'stash_key'}
142             ) || 'rest';
143              
144             my $data = $c->stash->{$stash_key};
145              
146             open my $fh, '>', \my $buf;
147             my $workbook = Spreadsheet::WriteExcel->new($fh);
148              
149             my ($filename, $sheets) = $self->_parse_entity($data);
150              
151             for my $sheet (@$sheets) {
152             $self->_add_sheet($workbook, $sheet);
153             }
154              
155             $workbook->close;
156              
157             $self->_write_file($c, $filename, $buf);
158              
159             return 1;
160             }
161              
162             sub _write_file {
163             my ($self, $c, $filename, $data) = @_;
164              
165             $c->res->content_type('application/vnd.ms-excel');
166             $c->res->header('Content-Disposition' =>
167             "attachment; filename=${filename}.xls");
168             $c->res->output($data);
169             }
170              
171             sub _parse_entity {
172             my ($self, $data) = @_;
173              
174             my @sheets;
175             my $filename = 'data'; # default
176              
177             if (ref $data eq 'ARRAY') {
178             if (not ref $data->[0][0]) {
179             $sheets[0] = { rows => $data };
180             }
181             else {
182             @sheets = map
183             ref $_ eq 'HASH' ? $_
184             : ref $_ eq 'ARRAY' ? { rows => $_ }
185             : Catalyst::Exception->throw(
186             'Unsupported sheet reference type: '.ref($_)), @{ $data };
187             }
188             }
189             elsif (ref $data eq 'HASH') {
190             $filename = $data->{filename} if $data->{filename};
191              
192             my $sheets = $data->{sheets};
193             my $rows = $data->{rows};
194              
195             if ($sheets && $rows) {
196             Catalyst::Exception->throw('Use either sheets or rows, not both.');
197             }
198              
199             if ($sheets) {
200             @sheets = map
201             ref $_ eq 'HASH' ? $_
202             : ref $_ eq 'ARRAY' ? { rows => $_ }
203             : Catalyst::Exception->throw(
204             'Unsupported sheet reference type: '.ref($_)), @{ $sheets };
205             }
206             elsif ($rows) {
207             $sheets[0] = $data;
208             }
209             else {
210             Catalyst::Exception->throw('Must supply either sheets or rows.');
211             }
212             }
213             else {
214             Catalyst::Exception->throw(
215             'Unsupported workbook reference type: '.ref($data)
216             );
217             }
218              
219             return ($filename, \@sheets);
220             }
221              
222             sub _add_sheet {
223             my ($self, $workbook, $sheet) = @_;
224              
225             my $worksheet = $workbook->add_worksheet(
226             $sheet->{name} ? $sheet->{name} : ()
227             );
228              
229             $worksheet->keep_leading_zeros(1);
230              
231             my ($row, $col) = (0,0);
232              
233             my @auto_widths;
234              
235             # Write Header
236             if (exists $sheet->{header}) {
237             my $header_format = $workbook->add_format;
238             $header_format->set_bold;
239             for my $header (@{ $sheet->{header} }) {
240             $auto_widths[$col] = length $header
241             if $auto_widths[$col] < length $header;
242              
243             $worksheet->write($row, $col++, $header, $header_format);
244             }
245             $row++;
246             $col = 0;
247             }
248              
249             # Write data
250             for my $the_row (@{ $sheet->{rows} }) {
251             for my $the_col (@$the_row) {
252             $auto_widths[$col] = length $the_col
253             if $auto_widths[$col] < length $the_col;
254              
255             $worksheet->write($row, $col++, $the_col);
256             }
257             $row++;
258             $col = 0;
259             }
260              
261             # Set column widths
262             $sheet->{column_widths} = \@auto_widths
263             unless exists $sheet->{column_widths};
264              
265             for my $width (@{ $sheet->{column_widths} }) {
266             $worksheet->set_column($col, $col++, $width);
267             }
268             # Have to set the width of column 0 again, otherwise Excel loses it!
269             # I don't know why...
270             $worksheet->set_column(0, 0, $sheet->{column_widths}[0]);
271              
272             return $worksheet;
273             }
274              
275             =head1 AUTHOR
276              
277             Rafael Kitover, C<< <rkitover at cpan.org> >>
278              
279             =head1 BUGS
280              
281             Please report any bugs or feature requests to C<bug-catalyst-action-serialize-simpleexcel at rt.cpan.org>, or through
282             the web interface at L<http://rt.cpan.org/NoAuth/ReportBug.html?Queue=Catalyst-Action-Serialize-SimpleExcel>. I will be notified, and then you'll
283             automatically be notified of progress on your bug as I make changes.
284              
285             =head1 SEE ALSO
286              
287             L<Catalyst>, L<Catalyst::Controller::REST>, L<Catalyst::Action::REST>,
288             L<Catalyst::View::Excel::Template::Plus>, L<Spreadsheet::WriteExcel>,
289             L<Spreadsheet::ParseExcel>
290              
291             =head1 SUPPORT
292              
293             You can find documentation for this module with the perldoc command.
294              
295             perldoc Catalyst::Action::Serialize::SimpleExcel
296              
297             You can also look for information at:
298              
299             =over 4
300              
301             =item * RT: CPAN's request tracker
302              
303             L<http://rt.cpan.org/NoAuth/Bugs.html?Dist=Catalyst-Action-Serialize-SimpleExcel>
304              
305             =item * AnnoCPAN: Annotated CPAN documentation
306              
307             L<http://annocpan.org/dist/Catalyst-Action-Serialize-SimpleExcel>
308              
309             =item * CPAN Ratings
310              
311             L<http://cpanratings.perl.org/d/Catalyst-Action-Serialize-SimpleExcel>
312              
313             =item * Search CPAN
314              
315             L<http://search.cpan.org/dist/Catalyst-Action-Serialize-SimpleExcel/>
316              
317             =back
318              
319             =head1 COPYRIGHT & LICENSE
320              
321             Copyright (c) 2008-2011 Rafael Kitover
322              
323             This program is free software; you can redistribute it and/or modify it
324             under the same terms as Perl itself.
325              
326             =cut
327              
328             1; # End of Catalyst::Action::Serialize::SimpleExcel