1 # Copyright 2002-2007 Interchange Development Group and others
3 # This program is free software; you can redistribute it and/or modify
4 # it under the terms of the GNU General Public License as published by
5 # the Free Software Foundation; either version 2 of the License, or
6 # (at your option) any later version. See the LICENSE file for details.
8 # $Id: report_table.tag,v 1.5 2007-03-30 23:40:57 pajamian Exp $
10 UserTag report-table addAttr
11 UserTag report-table Documentation <<EOD
13 By Chris Wenham of Synesmedia, Inc. - www.synesmedia.com
14 This software is distributed under the terms of the GNU Public License.
15 Version 1.2, November 20, 2003.
17 Generate an HTML table based on the results of a query, with bells and
18 whistles. Can do horizontal (colspan) and vertical (rowspan) subheaders,
19 apply any Interchange filter or widget to any column, add a CSS class to
20 any column, link cell contents (and add parameters to the link based on
21 any column in the query results), add virtual columns based on internal
22 variables (such as the line number), and skip rows based on an array of
24 Good for making quick tables, sophisticated reports, and easy forms.
26 Synopsis and minimum syntax
30 query="SELECT * FROM addresses"
31 columns="address city state zip"
37 <form action="[process]">
40 query="SELECT * FROM addresses"
41 columns="state city address sales"
68 <td colspan="4" align="right">
69 <input type="hidden" name="rows" value="[scratch report_table_linecount]" $Vend::Xtrailer>
70 <input type="submit" value="Save addresses" $Vend::Xtrailer>
76 This last example could give you something like this:
78 +-------------------------------------------------------+
79 | state | city | address | sales |
80 |-------+-----------+-----------------------------------|
81 | NY | Levittown | Zip code: 11756 |
82 | | |-----------------------------------|
83 | | | [123 Return Lane_____] | $240.12 |
84 | | | [321 Raspberry Lane__] | $43.52 |
85 | |-----------+-----------------------------------|
86 | | Bellmore | Zip code: 11710 |
87 | | |-----------------------------------|
88 | | | [23 Merrick Road_____] | $354.06 |
89 | | | [43 Bellmore Ave_____] | $11.34 |
90 |-------+-----------+-----------------------------------|
91 | PA | Anytown | Zip code: 23456 |
92 | | |-----------------------------------|
93 | | | [63 Some Street______] | $771.35 |
94 |-------------------------------------------------------|
95 | [ Save addresses ] |
96 +-------------------------------------------------------+
99 The columns to include in the report are passed in the "columns"
102 Column definitions are defined in a perl hash of hash references.
103 The tag will display only the columns you specify, and in that order.
104 Pagination is not supported, but you can easily construct the logic for
105 that outside of the report-table tag, and then use OFFSET and LIMIT in
108 Vertical headers (state and city in this example) are always sorted
109 to the left of the table, but they can be nested to any level. The tag
110 does not support vertical headers within the scope of a horizontal
113 Horizontal headers can also be nested to any level. You might want to
114 pass a "class" value in the column definition so you can style them
115 later and make it easier to tell them apart.
116 NOTE: Columns used for horizontal headers should *not* be included in
117 the "columns" parameter of the report-table tag. Defining them in
118 column_defs is sufficient.
120 Advanced column definitions
122 The following parameters are supported for the column definitions.
124 title => 'Column Header'
125 The tag will default to the database column name, but you
126 can override it with a title. All titles are put in <th>
127 tags at the top of each column, or in the case of
128 horizontal subheaders they're put just before the value
129 (eg: "Zip code: 11756" from above)
132 Indicates that this column is a header, and whether it's
133 vertical ('vert') or horizontal ('horiz').
134 Headers are generated every time the value in that column
135 changes between rows. Let's say that the following are
136 the rows returned by the query:
138 NY,Levittown,11756,123 Return Lane
139 NY,Levittown,11756,321 Raspberry Lane
140 NY,Bellmore,11710,23 Merrick Road
142 If city was a header, then it would spit out "Levittown"
143 first, then two rows later spit out "Bellmore".
145 NOTE: To make headers work properly, you must sort by those
146 columns in your query, or you may get redundant headers.
148 prefix => '$', postfix => '%'
149 Something to insert just before and after the value. Will
150 appear after the title in a horizontal header, and outside any
153 filter => 'digits_dot'
154 Any Interchange filter. Will be applied to the cell value
155 before it's put into any link or widget.
158 Any Interchange form widget. The widget will be passed the
159 contents of the cell as the default value. The name of the
160 form widget will be the column name plus the line number.
161 Eg: "address_1", "address_2", and so-on.
162 You can pass any addtional parameter supported by the [widget]
163 tag (such as rows and cols) by prefixing them with "widget_".
164 EG: "widget_cols => '30'".
166 Any column can be a widget, even vertical and horizontal
170 Will give you <td class="currency"> for each cell in that
173 align => 'right', valign => 'top'
174 Sets the alignment of each cell in the column. Vertical headers
175 are valign="top" by default, but this can override.
178 Set the column width.
180 link => 'show_customer'
182 link_key => 'cust_id'
183 Link a cell's contents using Interchange's [page] tag, and
184 optionally passing a parameter based on any column in the
185 query results. So let's say "cust_id" is a column returned in
186 the database query, but not actually displayed in the result.
187 The cells in your customer column could be linked to the
188 "show_customer" page, passing the value of "cust_id" in a
189 parameter named "id". Like this:
190 http://www.store.com/cgi-bin/catalog/show_customer?id=523
192 NOTE: You can't use a link and a widget at the same time. If you
193 set the 'link' parameter, any widget in the same column def will
197 What to use instead if the cell is empty for that row. For
198 tables with borders set, you might want to use a nonbreaking
199 space ( ), or 0.00 for currency columns, or whatever.
200 NOTE: The tag can't tell the difference between an empty cell
203 dynamic => 'linecount'
204 Indicates a column that does not draw its data from the query
205 results, but from an internal value. Most of these aren't
206 terribly useful, but 'linecount' is good for adding line numbers.
207 Dynamic values can be used with links, widgets and filters, but
208 they can't be used as subheaders. Available dynamic values are:
211 The absolute current row from the query results. Is not
212 affected by the row_toggle parameter (described later).
216 The current row, including any used by horizontal
221 The current data line. Does not include lines used by
222 horizontal subheaders.
226 1 if we're on an odd numbered line, 0 if we're on an
231 row_toggle="1,1,1,1,1,1,0,1,1,0,1"
232 This is a comma separated list of toggles ('1' or '0') that
233 can be used to make the report skip individual rows in the
234 results. The number of toggles must either equal the number
235 of results from the query, or the remainder will be skipped.
236 Eg: passing row_toggle="1,1,0,1,1,1" and a query that returns
237 six rows will give you a five-row report, where the third
238 row from the results had been skipped. If the query returns
239 more than six rows, then the remainder will be skipped.
241 (Ideally, what you should probably do is just modify your
242 query so it doesn't return those rows anyway, but this feature
243 was added for a special application.)
245 row_hidden_id="address_id"
246 The name of a column in the query results to use in a
247 type="hidden" form element. This is for forms that need to pass
248 the database key's value for each row, and is added just before
249 the first data cell, like this:
251 <tr><input type="hidden" name="id_1" value="523"/><td...
253 The number appended after "id_" in the name is the linecount,
254 and will match the number appended to the name of any other
255 widgets on the same row.
258 If you want the value of horizontal subheaders to stand on
259 their own (without a title), then set title_horiz="0".
260 Otherwise the tag will use the database name or title of
264 By default, the scope of a horizontal header does not cross
265 the scope of a vertical header. It looks confusing and
266 doesn't follow the typical way subheaders are used. So when
267 a vertical header goes out of scope, it resets all the
268 horizontal headers so they begin anew with the next row.
269 Example: Some zip codes cross city boundaries, so the
270 "Levittown" vertical header could end, but the next address
271 might still be in the "11756" zip code. By default, the
272 report table will simply run the "Zip code: 11756" header
273 again before the next row.
274 If you don't want it to do this, meaning you want the scope of
275 horizontal headers to cross the scope of vertical headers,
276 then pass reset_horiz="0".
278 display_colheaders="0"
279 When set to zero, don't bother to display the column headers.
281 no_results="<tr><td>Woah dude, nothing to see!</td></tr>"
282 Override the default message when there are no results from
288 Outputs XHMTL compliant markup*.
290 This tag will not generate the <table> tags in the final HTML because
291 it's trivial to add those yourself, and it was designed to be used in
292 cases where the table might not be "finished" even when the report-table
293 tag was (such as when you're using it to create a form).
295 The column headers row will be written with <tr class="headers">.
297 Every odd-numbered row will be written with <tr class="odd">.
299 The total number of columns it will use will always be the same as what
300 you pass in the "columns" parameter*. Even when the query returns no
301 results, it will still return one complete row with an apropriate
302 colspan (unless overridden by the no_results parameter).
304 * Except if you use a widget that doesn't output XHTML.
306 ** Except if you were naughty and listed a column that is later defined
307 as a horizontal header, then it will get stripped out. You shouldn't list
308 horizontal headers in the colums="" parameter. Simply defining them in
309 column_defs is sufficient.
314 The following temporary scratch variables are set prior to tag completion.
316 [scratch report_table_rowcount]
317 The total number of rows created by the tag. This includes rows
318 used up by horizontal subheaders, and the column header row.
320 [scratch report_table_linecount]
321 Total number of data rows returned by the tag, NOT including rows
322 used by horizontal subheaders or the column headers. Useful if
323 you're using widgets and your mv_nextpage needs to know how many
326 [scratch report_table_colspan]
327 Total number of columns it used.
332 To get a blank column:
334 columns="city state zip x customer"
338 empty_cell => ' '
344 UserTag report-table Version $Revision: 1.5 $
345 UserTag report-table Routine <<EOR
347 my ($def,$datum,$linecount,$record) = @_;
349 #Debug("prep_cell datum: $datum");
352 if ($def->{filter}) {
353 $datum = $Tag->filter({ op => $def->{filter}, }, $datum);
357 my $page_parms = { href => $def->{link}, };
358 if ($def->{link_parm}) {
359 $page_parms->{form} = $def->{link_parm} .'='. $record->{$def->{link_key}};
361 $cell = $Tag->page($page_parms);
364 } elsif ($def->{widget}) {
365 if ($def->{widget} =~ /^checkonly$/) {
366 # This was a quick hack to support standalone checkboxes
367 # for "delete/edit checked rows" type forms.
370 $checked = ' checked="checked"';
372 $cell = '<input type="checkbox" name="'. $def->{colname} .'_'. $linecount ."\" value=\"1\"$checked $Vend::Xtrailer>";
374 my $widget_name = $def->{colname} .'_'. $linecount;
375 # We need to bludgeon Interchange over the head with the proper value
376 # becuase set,default,value, and passed are ignored when there's an
378 $::Values->{$widget_name} = $datum;
379 $cell = $Tag->widget($widget_name, {
380 type => $def->{widget},
382 attribute => $def->{widget_attribute},
383 db => $def->{widget_db},
384 field => $def->{widget_field},
385 extra => $def->{widget_extra},
386 cols => $def->{widget_cols},
387 rows => $def->{widget_rows},
388 delimiter => $def->{widget_delimiter},
389 key => $def->{widget_key},
390 year_begin => $def->{widget_year_begin},
391 year_end => $def->{widget_year_end},
392 filter => $def->{widget_filter},
393 set => $def->{widget_set},
400 $cell = $def->{prefix} . $cell . $def->{postfix};
402 #Debug("prep_cell returning: $cell");
408 my ($def,$rowspan,$colspan) = @_;
411 push @tag_parms, "colspan=\"$colspan\"" if $colspan;
412 push @tag_parms, "rowspan=\"$rowspan\"" if $rowspan;
413 push @tag_parms, "class=\"$def->{class}\"" if $def->{class};
414 push @tag_parms, "width=\"$def->{width}\"" if $def->{width};
415 push @tag_parms, "valign=\"$def->{valign}\"" if $def->{valign};
416 push @tag_parms, "align=\"$def->{align}\"" if $def->{align};
418 my $type = $def->{header} ? 'th' : 'td';
421 return "<$type ". join( ' ', @tag_parms) .'>';
428 #Debug("Entering report-table");
429 # Options gathering ------------------------------------------
432 my @columns = split ' ', $opt->{columns};
433 my @row_toggle = split ',', $opt->{row_toggle};
435 if ($opt->{reset_horiz} eq '') {
436 $opt->{reset_horiz} = 1;
439 if ($opt->{title_horiz} eq '') {
440 $opt->{title_horiz} = 1;
443 if ($opt->{colheaders} eq '') {
444 $opt->{colheaders} = 1;
447 #Debug("Gathered options. Query is: ". $opt->{query});
449 # Data structure preparation ---------------------------------
451 my @subheader_cols = ();
453 my (%cols,$column_defs);
454 if ($opt->{column_defs}) {
455 $column_defs = eval( $opt->{column_defs} );
456 %cols = %{$column_defs};
458 foreach my $col (@columns) {
459 $cols{$col}->{title} = $col;
465 foreach my $col (@columns) {
466 if ($cols{$col}->{header}) {
467 # Horizontal headers should never be in the 'columns' list
468 if ($cols{$col}->{header} eq 'vert') {
469 $cols{$col}->{pos} = $headpos;
471 push @subheader_cols, $col;
472 push @vertheads, $col;
473 $cols{$col}->{valign} ||= 'top';
479 foreach my $col (keys(%cols)) {
480 $cols{$col}->{colname} = $col;
481 $cols{$col}->{title} ||= $col;
482 if ($cols{$col}->{header} =~ /horiz/) {
483 push @subheader_cols, $col;
487 # ----------------------------------------------------------##
490 my $db = ::database_exists_ref('products');
491 my $results = $db->query({ sql => $opt->{query}, hashref => 'results' });
493 # Output column headers --------------------------------------
494 if (($results) and (@{$results}) and ($opt->{colheaders})) {
495 $output .= '<tr class="headers">';
497 foreach my $c (@vertheads) {
498 $output .= "<th>$cols{$c}->{title}</th>";
500 foreach my $c (@columns) {
501 $output .= "<th>$cols{$c}->{title}</th>";
503 $output .= "</tr>\n";
506 if (!(($results) and (@{$results}))) {
507 return $opt->{no_results} || '<tr><td colspan="'. (scalar(@columns) + scalar(@vertheads)) .'">No results</td></tr>';
509 # ----------------------------------------------------------##
511 # Process results --------------------------------------------
513 my @vh_stack = (); # Stack of vertical headers we're working on
517 for (my $i = 0; $i < scalar(@{$results}); $i++) {
519 next if !$row_toggle[$i];
521 my $record = $results->[$i];
524 #Debug("Row: ". ::uneval($record));
526 # Dynamic values that can be used as column data
529 rowcount => $rowcount,
530 rownumber => $linecount,
531 linecount => $linecount,
532 parity => $linecount % 2 ? 1 : 0,
535 $row->{dynamic} = \%dynamic;
537 foreach my $subhead (@subheader_cols) {
538 if ($record->{$subhead} ne $cols{$subhead}->{value}) {
539 if ($cols{$subhead}->{header} ne 'vert') {
540 $row->{html} = cell_open_tag($cols{$subhead},0,$#columns + 1);
542 if ($opt->{title_horiz}) {
543 $row->{html} .= $cols{$subhead}->{title} .' ';
545 my $datum = $record->{$subhead};
546 $row->{html} .= prep_cell($cols{$subhead},$datum,$linecount,$record) .'</th>';
547 $cols{$subhead}->{value} = $record->{$subhead};
549 # Vertical headers must be inserted at the end, because that's
550 # the only time we know what the rowspan is going to be.
551 # So we keep track of them with a stack and a notation in the
554 if ($cols{$vh->{column}}->{pos} >= $cols{$subhead}->{pos}) {
555 while (($old->{column} ne $subhead) and (@vh_stack)) {
556 $old = pop @vh_stack;
557 $old->{end} = $rowcount;
558 $cols{$old->{column}}->{value} = '';
559 #::Debug("Popped vh_stack. Old is: ". ::uneval($old));
562 if ($opt->{reset_horiz}) {
563 # Don't let horizontal headers apply across vertical headers
564 foreach my $tmp (@subheader_cols) {
565 if ($cols{$tmp}->{header} eq 'horiz') {
566 $cols{$tmp}->{value} = '';
570 my $datum = $record->{$subhead};
572 content => prep_cell($cols{$subhead},$datum,$linecount,$record),
576 push @vh_stack, $new;
577 #::Debug("vh_stack now: ". ::uneval(\@vh_stack));
578 unshift @{$row->{'vert_headers'}}, $new;
579 $cols{$subhead}->{value} = $record->{$subhead};
590 if ($opt->{row_hidden_id}) {
591 $row->{id} = $record->{$opt->{row_hidden_id}};
593 foreach my $col (@columns) {
594 $row->{html} .= cell_open_tag($cols{$col});
597 if ($cols{$col}->{dynamic}) {
598 $datum = $dynamic{$cols{$col}->{dynamic}};
600 $datum = $record->{$col};
602 if ((!$datum) and ($cols{$col}->{empty_cell})) {
603 $datum = $cols{$col}->{empty_cell};
606 $row->{html} .= prep_cell($cols{$col},$datum,$linecount,$record);
608 $row->{html} .= '</td>';
615 # ----------------------------------------------------------##
618 # Do post-processing table assembly --------------------------
619 foreach my $row (@rows) {
620 my $html = $row->{'html'};
621 if ($row->{'vert_headers'}) {
622 foreach my $vert (@{$row->{'vert_headers'}}) {
623 my $end = $vert->{end} || $rowcount;
624 my $cell = cell_open_tag($cols{$vert->{column}},$end - $vert->{begin});
625 $cell .= $vert->{content};
627 $html = $cell . $html;
631 if ($row->{dynamic}->{parity}) {
632 $odd = ' class="odd"';
635 my $name = $opt->{row_hidden_id} .'_'. $row->{dynamic}->{linecount};
636 $id = "<input type=\"hidden\" name=\"$name\" value=\"$row->{id}\" $Vend::Xtrailer>";
638 $output .= "<tr$odd>$id$html</tr>\n";
640 # ----------------------------------------------------------##
642 # Set some side-effect scratch variables
643 if ($opt->{colheaders}) { $rowcount++; }
644 $Tag->tmp('report_table_rowcount',$rowcount);
645 $Tag->tmp('report_table_linecount',$linecount - 1);
646 $Tag->tmp('report_table_colspan',(scalar(@columns) + scalar(@vertheads)));