File Coverage

blib/lib/Catalyst/Action/Serialize/SimpleXLSX.pm
Criterion Covered Total %
statement 75 80 93.7
branch 23 32 71.8
condition 5 11 45.4
subroutine 9 9 100.0
pod 1 1 100.0
total 113 133 84.9


line stmt bran cond sub pod time code
1             package Catalyst::Action::Serialize::SimpleXLSX;
2 2     2   125247 use Moose;
  2         475065  
  2         19  
3             extends 'Catalyst::Action';
4 2     2   15995 use Data::Dumper;
  2         7041  
  2         158  
5 2     2   3174 use Excel::Writer::XLSX;
  2         345958  
  2         118  
6 2     2   616 use Catalyst::Exception;
  2         93012  
  2         88  
7 2     2   16 use namespace::clean;
  2         4  
  2         20  
8              
9             =head1 NAME
10              
11             Catalyst::Action::Serialize::SimpleXLSX - Serialize to Microsoft Excel 2007 .xlsx files
12              
13             =cut
14              
15             our $VERSION = "0.007";
16              
17             =head1 SYNOPSIS
18              
19             Serializes tabular data to an Excel file, with simple configuration options.
20              
21             In your REST Controller:
22              
23             package MyApp::Controller::REST;
24              
25             use parent 'Catalyst::Controller::REST';
26             use DBIx::Class::ResultClass::HashRefInflator;
27             use POSIX 'strftime';
28              
29             __PACKAGE__->config->{map}{'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'} = 'SimpleXLSX';
30              
31             sub books : Local ActionClass('REST') {}
32              
33             sub books_GET {
34             my ($self, $c) = @_;
35              
36             # Books (Sheet 1)
37             my $books_rs = $c->model('MyDB::Book')->search();
38             $books_rs->result_class('DBIx::Class::ResultClass::HashRefInflator');
39             my @books = map { [ @{$_}{qw/author title/} ] } $books_rs->all;
40              
41             # Authors (Sheet 2)
42             my $authors_rs = $c->model('MyDB::Author')->search();
43             $authors_rs->result_class('DBIx::Class::ResultClass::HashRefInflator');
44             my @authors = map { [ @{$_}{qw/first_name last_name/} ] } $authors_rs->all;
45              
46             my $entity = {
47             sheets => [
48             {
49             name => 'Books',
50             header => ['Author', 'Title'],
51             rows => \@books,
52             },
53             {
54             name => 'Authors',
55             header => ['First Name', 'Last Name'],
56             rows => \@authors,
57             },
58             ],
59             # .xlsx suffix automatically appended
60             filename => 'myapp-books-'.strftime('%m-%d-%Y', localtime)
61             };
62              
63             $self->status_ok(
64             $c,
65             entity => $entity
66             );
67             }
68              
69             In your jQuery webpage, to initiate a file download:
70              
71             <script>
72             $(document).ready(function () {
73              
74             function export_to_excel() {
75             $('<iframe ' + 'src="/item?content-type=application/vnd.openxmlformats-officedocument.spreadsheetml.sheet">').hide().appendTo('body');
76             }
77             $("#books").on("click", export_to_excel);
78              
79             });
80             </script>
81              
82              
83             Note, the content-type query param is required if you're just linking to the
84             action. It tells L<Catalyst::Controller::REST> what you're serializing the data
85             as.
86              
87             =head1 DESCRIPTION
88              
89             Your entity should be either:
90              
91             =over 4
92              
93             =item * an array of arrays
94              
95             =item * an array of arrays of arrays
96              
97             =item * a hash with the keys as described below and in the L</SYNOPSIS>
98              
99             =back
100              
101             If entity is a hashref, keys should be:
102              
103             =head2 sheets
104              
105             An array of worksheets. Either sheets or a worksheet specification at the top
106             level is required.
107              
108             =head2 filename
109              
110             Optional. The name of the file before .xlsx. Defaults to "data".
111              
112             Each sheet should be an array of arrays, or a hashref with the following fields:
113              
114             =head2 name
115              
116             Optional. The name of the worksheet.
117              
118             =head2 rows
119              
120             Required. The array of arrays of rows.
121              
122             =head2 header
123              
124             Optional, an array for the first line of the sheet, which will be in bold.
125              
126             =head2 column_widths
127              
128             Optional, the widths in characters of the columns. Otherwise the widths are
129             calculated automatically from the data and header.
130              
131             If you only have one sheet, you can put it in the top level hash.
132              
133             =cut
134              
135             has 'content_type' => (
136             is => 'ro',
137             required => 1,
138             default => sub { return 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' },
139             );
140              
141             sub execute {
142 6     6 1 10256 my $self = shift;
143 6         25 my ( $controller, $c ) = @_;
144              
145             my $stash_key = (
146             $controller->config->{'serialize'}
147             ? $controller->config->{'serialize'}->{'stash_key'}
148 6   50     80 : $controller->config->{'stash_key'}
149             )
150             || 'rest';
151              
152 6         1075 my $data = $c->stash->{$stash_key};
153              
154 6         642 open my $fh, '>', \my $buf;
155 6         74 my $workbook = Excel::Writer::XLSX->new($fh);
156 6         2801 my ( $filename, $sheets ) = $self->_parse_entity($data);
157 6         22 for my $sheet (@$sheets) {
158 9         49 $self->_add_sheet( $workbook, $sheet );
159             }
160 6         43 $workbook->close;
161              
162 6         210651 $self->_write_file( $c, $filename, $buf );
163 6         495 return 1;
164             }
165              
166             sub _write_file {
167 6     6   38 my ( $self, $c, $filename, $data ) = @_;
168              
169 6         82 $c->res->content_type( $self->content_type );
170 6         1716 $c->res->header(
171             'Content-Disposition' => "attachment; filename=${filename}.xlsx" );
172 6         1921 $c->res->output($data);
173             }
174              
175             sub _parse_entity {
176 6     6   28 my ( $self, $data ) = @_;
177              
178 6         14 my @sheets;
179 6         19 my $filename = 'data';
180              
181 6 100       37 if ( ref $data eq 'ARRAY' ) {
    50          
182 3 100       14 if ( not ref $data->[0][0] ) {
183 2         23 $sheets[0] = { rows => $data };
184             }
185             else {
186             @sheets =
187             map ref $_ eq 'HASH' ? $_
188             : ref $_ eq 'ARRAY' ? { rows => $_ }
189             : Catalyst::Exception->throw(
190 1 50       3 'Unsupported sheet reference type: ' . ref($_) ), @{$data};
  1 50       14  
191             }
192             }
193             elsif ( ref $data eq 'HASH' ) {
194 3 50       20 $filename = $data->{filename} if $data->{filename};
195              
196 3         11 my $sheets = $data->{sheets};
197 3         9 my $rows = $data->{rows};
198              
199 3 50 66     20 if ( $sheets && $rows ) {
200 0         0 Catalyst::Exception->throw('Use either sheets or rows, not both.');
201             }
202              
203 3 100       15 if ($sheets) {
    50          
204             @sheets =
205             map ref $_ eq 'HASH' ? $_
206             : ref $_ eq 'ARRAY' ? { rows => $_ }
207             : Catalyst::Exception->throw(
208 1 50       3 'Unsupported sheet reference type: ' . ref($_) ), @{$sheets};
  1 100       13  
209             }
210             elsif ($rows) {
211 2         6 $sheets[0] = $data;
212             }
213             else {
214 0         0 Catalyst::Exception->throw('Must supply either sheets or rows.');
215             }
216             }
217             else {
218 0         0 Catalyst::Exception->throw(
219             'Unsupported workbook reference type: ' . ref($data) );
220             }
221              
222 6         35 return ( $filename, \@sheets );
223             }
224              
225             sub _add_sheet {
226 9     9   33 my ( $self, $workbook, $sheet ) = @_;
227              
228 9 100       58 my $worksheet = $workbook->add_worksheet( $sheet->{name} ? $sheet->{name} : () );
229 9         2950 $worksheet->keep_leading_zeros(1);
230              
231 9         91 my ( $row, $col ) = ( 0, 0 );
232              
233 9         23 my @auto_widths;
234              
235             # Write Header
236 9 100       37 if ( exists $sheet->{header} ) {
237 4         19 my $header_format = $workbook->add_format;
238 4         186 $header_format->set_bold;
239 4         33 for my $header ( @{ $sheet->{header} } ) {
  4         15  
240 8 50 33     519 if (defined $auto_widths[$col] && $auto_widths[$col] < length $header) {
241 0         0 $auto_widths[$col] = length $header;
242             }
243 8         35 $worksheet->write( $row, $col++, $header, $header_format );
244             }
245 4         322 $row++;
246 4         11 $col = 0;
247             }
248              
249             # Write data
250 9         25 for my $the_row ( @{ $sheet->{rows} } ) {
  9         30  
251 18         40 for my $the_col (@$the_row) {
252 44 50 33     1780 if (defined $auto_widths[$col] && $auto_widths[$col] < length $the_col) {
253 0         0 $auto_widths[$col] = length $the_col;
254             }
255 44         118 $worksheet->write( $row, $col++, $the_col );
256             }
257 18         1019 $row++;
258 18         42 $col = 0;
259             }
260              
261             # Set column widths
262 9 100       39 $sheet->{column_widths} = \@auto_widths unless exists $sheet->{column_widths};
263              
264 9         18 for my $width ( @{ $sheet->{column_widths} } ) {
  9         28  
265 2         102 $worksheet->set_column( $col, $col++, $width );
266             }
267 9         131 $worksheet->set_column( 0, 0, $sheet->{column_widths}[0] );
268              
269 9         881 return $worksheet;
270             }
271              
272             =head1 AUTHOR
273              
274             Mike Baas <mbaas at cpan.org>
275              
276             =head1 ORIGINAL AUTHOR
277              
278             Rafael Kitover <rkitover at cpan.org>
279              
280             =head1 ACKNOWLEDGEMENTS
281              
282             This module is really nothing more than a tweak to L<Catalyst::Action::Serialize::SimpleExcel> that drops in L<Excel::Writer::XLSX> for compatibility with Excel 2007 and later. I just needed more rows!
283              
284             =head1 SEE ALSO
285              
286             L<Catalyst>, L<Catalyst::Controller::REST>, L<Catalyst::Action::REST>, L<Catalyst::Action::Serialize::SimpleExcel>, L<Excel::Writer::XLSX>
287              
288             =head1 REPOSITORY
289              
290             L<https://github.com/initself/Catalyst-Action-Serialize-SimpleXLSX>
291              
292             =head1 COPYRIGHT & LICENSE
293              
294             This is free software; you can redistribute it and/or modify it under the same terms as the Perl 5 programming language system itself.
295              
296             =cut
297              
298             1; # End of Catalyst::Action::Serialize::SimpleXLSX