File Coverage

blib/lib/CGI/Application/Plugin/PageLookup/Loop.pm
Criterion Covered Total %
statement 90 92 97.8
branch 21 26 80.7
condition 1 3 33.3
subroutine 8 9 88.8
pod 2 2 100.0
total 122 132 92.4


line stmt bran cond sub pod time code
1             package CGI::Application::Plugin::PageLookup::Loop;
2              
3 2     2   1897 use warnings;
  2         5  
  2         77  
4 2     2   11 use strict;
  2         5  
  2         2304  
5              
6             =head1 NAME
7              
8             CGI::Application::Plugin::PageLookup::Loop - Manage list structures in a website
9              
10             =head1 VERSION
11              
12             Version 1.8
13              
14             =cut
15              
16             our $VERSION = '1.8';
17             our $AUTOLOAD;
18              
19             =head1 DESCRIPTION
20              
21             This module manages the instantiation of list style template parameters across a website;
22             for example TMPL_LOOP in L, though one must use L for it to
23             work. For example a menu is typically implemented in HTML as
    ....
. Using this module
24             the menu can be instantiated from the database and the same data used to instantiate a human-readable
25             sitemap page. On the other hand the staff page will have list data that is only required on that page.
26             This module depends on L.
27              
28             =head1 SYNOPSIS
29              
30             In the template you might define a menu as follows (with some CSS and javascript to make it look nice):
31              
32            
33            
34            
  • 35             /">
    36            
    37            
    38            
    39            
  • 40             /">
    41            
    42            
    43            
    44            
  • 45             /">
    46            
    47            
    48            
    49            
    50            
    51            
    52            
    53            
    54            
    55            
    56            
    57              
    58             and the intention is that this should be the same on all English pages, the same on all Vietnamese pages etc etc.
    59             The use of "this." below the top levels is dictated by L which also optionally allows
    60             renaming of this implicit variable. You must register the "loop" parameter as a CGI::Application::Plugin::PageLookup::Loop object as follows:
    61              
    62             use CGI::Application;
    63             use CGI::Application::Plugin::PageLookup qw(:all);
    64             use CGI::Application::Plugin::PageLookup::Loop;
    65             use HTML::Template::Pluggable;
    66             use HTML::Template::Plugin::Dot;
    67              
    68             sub cgiapp_init {
    69             my $self = shift;
    70              
    71             # pagelookup depends CGI::Application::DBH;
    72             $self->dbh_config(......); # whatever arguments are appropriate
    73              
    74             $self->html_tmpl_class('HTML::Template::Pluggable');
    75              
    76             $self->pagelookup_config(
    77              
    78             # load smart dot-notation objects
    79             objects =>
    80             {
    81             # Register the 'values' parameter
    82             loop => 'CGI::Application::Plugin::PageLookup::Loop',
    83             },
    84              
    85             # Processing of the 'lang' parameter inside a loop requires global_vars = 1 inside the template infrastructure
    86             template_params => {global_vars => 1}
    87              
    88             );
    89             }
    90              
    91              
    92             ...
    93              
    94             The astute reader will notice that the above will only work if you set the 'global_vars' to true. After that all that remains is to populate
    95             the cgiapp_loops table with the appropriate values. To fill the above menu you might run the following SQL:
    96              
    97             INSERT INTO cgiapp_loops (lang, loopName, lineage, rank, param, value) VALUES ('en', 'menu', '', 0, 'href1', '')
    98             INSERT INTO cgiapp_loops (lang, loopName, lineage, rank, param, value) VALUES ('en', 'menu', '', 0, 'atitle1', 'Home page')
    99             INSERT INTO cgiapp_loops (lang, loopName, lineage, rank, param, value) VALUES ('en', 'menu', '', 1, 'href1', 'aboutus')
    100             INSERT INTO cgiapp_loops (lang, loopName, lineage, rank, param, value) VALUES ('en', 'menu', '', 1, 'atitle1', 'About us')
    101             INSERT INTO cgiapp_loops (lang, loopName, lineage, rank, param, value) VALUES ('en', 'menu', '', 2, 'href1', 'products')
    102             INSERT INTO cgiapp_loops (lang, loopName, lineage, rank, param, value) VALUES ('en', 'menu', '', 2, 'atitle1', 'Our products')
    103             INSERT INTO cgiapp_loops (lang, loopName, lineage, rank, param, value) VALUES ('en', 'menu', '', 3, 'href1', 'contactus')
    104             INSERT INTO cgiapp_loops (lang, loopName, lineage, rank, param, value) VALUES ('en', 'menu', '', 3, 'atitle1', 'Contact us')
    105             INSERT INTO cgiapp_loops (lang, loopName, lineage, rank, param, value) VALUES ('en', 'menu', '', 4, 'href1', 'sitemap')
    106             INSERT INTO cgiapp_loops (lang, loopName, lineage, rank, param, value) VALUES ('en', 'menu', '', 4, 'atitle1', 'Sitemap')
    107              
    108             Now suppose that you need to describe the products in more detail. Then you might add the following rows:
    109              
    110             INSERT INTO cgiapp_loops (lang, loopName, lineage, rank, param, value) VALUES ('en', 'submenu1', '2', 0, 'href2', 'wodgets')
    111             INSERT INTO cgiapp_loops (lang, loopName, lineage, rank, param, value) VALUES ('en', 'submenu1', '2', 0, 'atitle2', 'Finest wodgets')
    112             INSERT INTO cgiapp_loops (lang, loopName, lineage, rank, param, value) VALUES ('en', 'submenu1', '2', 1, 'href2', 'bladgers')
    113             INSERT INTO cgiapp_loops (lang, loopName, lineage, rank, param, value) VALUES ('en', 'submenu1', '2', 1, 'atitle2', 'Delectable bladgers')
    114             INSERT INTO cgiapp_loops (lang, loopName, lineage, rank, param, value) VALUES ('en', 'submenu1', '2', 2, 'href2', 'spodges')
    115             INSERT INTO cgiapp_loops (lang, loopName, lineage, rank, param, value) VALUES ('en', 'submenu1', '2', 2, 'atitle2', 'Exquisite spodges')
    116            
    117             Now suppose that the bladger market is hot, and we need to further subdivide our menu. Then you might add the following rows:
    118              
    119             INSERT INTO cgiapp_loops (lang, loopName, lineage, rank, param, value) VALUES ('en', 'submenu2', '2,1', 0, 'href3', 'bladgers/runcible')
    120             INSERT INTO cgiapp_loops (lang, loopName, lineage, rank, param, value) VALUES ('en', 'submenu2', '2,1', 0, 'atitle3', 'Runcible bladgers')
    121             INSERT INTO cgiapp_loops (lang, loopName, lineage, rank, param, value) VALUES ('en', 'submenu2', '2,1', 1, 'href3', 'bladgers/collapsible')
    122             INSERT INTO cgiapp_loops (lang, loopName, lineage, rank, param, value) VALUES ('en', 'submenu2', '2,1', 1, 'atitle3', 'Collapsible bladgers')
    123             INSERT INTO cgiapp_loops (lang, loopName, lineage, rank, param, value) VALUES ('en', 'submenu2', '2,1', 2, 'href3', 'bladgers/goldplated')
    124             INSERT INTO cgiapp_loops (lang, loopName, lineage, rank, param, value) VALUES ('en', 'submenu2', '2,1', 2, 'atitle3', 'Gold plated bladgers')
    125              
    126              
    127             =head1 DATABASE
    128              
    129             This module depends on only one extra table: cgiapp_loops. The lang and internalId columns join against
    130             the cgiapp_table. However the internalId column can null, making the parameter available to all pages
    131             in the same language. The key is formed by all of the columns except for the value.
    132              
    133             =over
    134              
    135             =item Table: cgiapp_loops
    136              
    137             Field Type Null Key Default Extra
    138             ------------ ------------------------------------------------------------------- ---- ---- ------- -----
    139             lang varchar(2) NO UNI NULL
    140             internalId unsigned numeric(10,0) YES UNI NULL
    141             loopName varchar(20) NO UNI NULL
    142             lineage varchar(255) NO UNI
    143             rank unsigned numeric(2,0) NO UNI 0
    144             param varchar(20) NO UNI NULL
    145             value text NO NULL
    146              
    147             =back
    148              
    149             The loopName is the parameter name of the TMPL_LOOP structure. The rank indicates which iteration of the loop
    150             this row is instantiating. The lineage is a comma separated list of ranks so that we know what part of a nested
    151             loop structure this row instantiates. For a top-level parameter this will always be the empty string.
    152              
    153             =head1 FUNCTIONS
    154              
    155             =head2 new
    156              
    157             A constructor following the requirements set out in L.
    158              
    159             =cut
    160              
    161             sub new {
    162 4     4 1 7 my $class = shift;
    163 4         9 my $self = {};
    164 4         11 $self->{cgiapp} = shift;
    165 4         9 $self->{page_id} = shift;
    166 4         11 $self->{template} = shift;
    167 4         9 $self->{name} = shift;
    168 4         10 my %args = @_;
    169 4         10 $self->{config} = \%args;
    170              
    171 4         13 bless $self, $class;
    172 4         15 return $self;
    173             }
    174              
    175             =head2 can
    176              
    177             We need to autoload methods so that the template writer can use loops without needing to know
    178             where the loops will be used. Thus 'can' must return a true value in all cases to avoid breaking
    179             L. Also 'can' is supposed to either return undef or a CODE ref. This seems the cleanest
    180             way of meeting all requirements.
    181              
    182             =cut
    183              
    184             sub can {
    185 29     29 1 1721 my $self = shift;
    186 29         39 my $loopname = shift;
    187             return sub {
    188 25     25   34 my $self = shift;
    189              
    190             # $dlineage are the "breadcrumbs" required to navigate our way through the database
    191             # and corresponds to the 'lineage' column on the cgiapp_loops table.
    192 25         29 my $dlineage = shift;
    193 25 100       49 $dlineage = "" unless defined $dlineage;
    194              
    195             # $tlineage are the "breadcrumbs" required to navigate our way through the HTML::Template structure.
    196             # It corresponds to the ARRAY ref used in $template->query(loop=> [....]) only that the
    197             # post "dot" string of the final array member (aka $loopname) is missing.
    198 25         23 my $tlineage = shift;
    199 25 100       53 $tlineage = [$self->{name}] unless defined $tlineage;
    200              
    201 25         38 my $prefix = $self->{cgiapp}->pagelookup_prefix(%{$self->{config}});
      25         96  
    202 25         42 my $page_id = $self->{page_id};
    203 25         82 my $dbh = $self->{cgiapp}->dbh;
    204              
    205             # This is what we actually want to return
    206 25         1603 my @loop;
    207              
    208             # These are temporary variables that will help us get there
    209 25         26 my $current_row = undef;
    210 25         24 my $current_rank = undef;
    211 25 100       61 $self->{work_to_be_done} = [] unless exists $self->{work_to_be_done};
    212              
    213             # First one pass over the loop
    214 25         181 my @sql = (
    215             "SELECT l.rank, l.param, l.value FROM ${prefix}loops l, ${prefix}pages p WHERE l.internalId = p.internalId AND l.loopName = '$loopname' AND l.lang = p.lang AND p.pageId = '$page_id' and l.lineage = '$dlineage' order by l.rank asc",
    216             "SELECT l.rank, l.param, l.value FROM ${prefix}loops l, ${prefix}pages p WHERE l.internalId IS NULL AND l.loopName = '$loopname' AND l.lang = p.lang AND p.pageId = '$page_id' and l.lineage = '$dlineage' order by l.rank asc");
    217 25         40 foreach my $s (@sql) {
    218 44   33     166 my $sth = $dbh->prepare($s) || croak $dbh->errstr;
    219 44 50       8418 $sth->execute || croak $dbh->errstr;
    220 44         620 while(my $hash_ref = $sth->fetchrow_hashref) {
    221              
    222 48         60 my $next_rank = $hash_ref->{rank};
    223 48         63 my $param = $hash_ref->{param};
    224 48         52 my $value = $hash_ref->{value};
    225              
    226             # rank transitions
    227 48 100       110 if (!defined $current_rank) {
        100          
    228 6         9 $current_rank = $next_rank;
    229 6         9 $current_row = {};
    230             }
    231             elsif ($current_rank < $next_rank) {
    232              
    233             # Now we need to add in any loop variables
    234 18         41 $self->__populate_lower_loops($dlineage, $tlineage, $current_row, $current_rank, $loopname);
    235              
    236             # We are finally ready to get this structure out of the door
    237 18         21 push @loop, $current_row;
    238 18         25 $current_row = {};
    239 18         70 $current_rank = $next_rank;
    240             }
    241              
    242 48         805 $current_row->{$param} = $value;
    243              
    244             }
    245 44 50       204 croak $sth->errstr if $sth->err;
    246 44         102 $sth->finish;
    247 44 100       71 if ($current_row) {
    248 6         13 $self->__populate_lower_loops($dlineage, $tlineage, $current_row, $current_rank, $loopname);
    249 6 50       18 push @loop, $current_row if %$current_row;
    250             }
    251 44 100       511 last if @loop;
    252             }
    253              
    254             # Now go back over the remaining work
    255 25         34 while(@{$self->{work_to_be_done}}) {
      46         112  
    256 21         23 my $work = shift @{$self->{work_to_be_done}};
      21         37  
    257 21         35 &$work();
    258             }
    259              
    260 25         357 return \@loop;
    261              
    262 29         182 };
    263             }
    264              
    265             =head2 AUTOLOAD
    266              
    267             We need to autoload methods so that the template writer can use loops without needing to know
    268             where the variables will be used.
    269              
    270             =cut
    271              
    272             sub AUTOLOAD {
    273 25     25   76 my $self = shift;
    274 25         106 my @method = split /::/, $AUTOLOAD;
    275 25         38 my $param = pop @method;
    276 25         49 my $c = $self->can($param);
    277 25 50       85 return &$c($self, @_) if $c;
    278 0         0 return undef;
    279             }
    280              
    281             =head2 __populate_lower_loops
    282              
    283             A private function that does what is says.
    284              
    285             =cut
    286              
    287             sub __populate_lower_loops {
    288 24     24   28 my $self = shift;
    289 24         26 my $dlineage = shift;
    290 24         24 my $tlineage = shift;
    291 24         22 my $current_row = shift;
    292 24         21 my $current_rank = shift;
    293 24         24 my $loopname = shift;
    294 24         24 my $comma = ',';
    295 24         54 my $new_dlineage = join $comma , (split /,/, $dlineage), $current_rank;
    296 24         49 my @new_tlineage = @$tlineage;
    297 24         27 my $thead = pop @new_tlineage;
    298 24         48 push @new_tlineage, "$thead.$loopname";
    299 24         79 my @new_vars = $self->{template}->query(loop=>\@new_tlineage);
    300 24         1016 foreach my $var (@new_vars) {
    301              
    302             # exclude anything that is not a loop
    303 93 100       2143 next if $self->{template}->query(name=>[@new_tlineage, $var]) eq 'VAR';
    304              
    305             # extract new loop name (following mechanics in HTML::Template::Plugin::Dot)
    306 21         727 my ($one, $the_rest) = split /\./, $var, 2;
    307 21         28 my $loopmap_name = 'this';
    308 21 50       50 $loopmap_name = $1 if $the_rest =~ s/\s*:\s*([_a-z]\w*)\s*$//;
    309              
    310             # Okay we have set up the structure but let's finish the current SQL
    311             # before populating this one
    312 21         28 my $new_loop = [];
    313 21         31 $current_row->{$the_rest} = $new_loop;
    314 21         37 my $new_tlineage = [@new_tlineage, $one];
    315 21         132 push @{$self->{work_to_be_done}}, sub {
    316 21     21   23 push @$new_loop, @{$self->$the_rest($new_dlineage, $new_tlineage)};
      21         110  
    317 21         18 };
    318             }
    319 24         793 return;
    320             }
    321              
    322              
    323             =head2 DESTROY
    324              
    325             We have to define DESTROY, because an autoloaded version would be bad.
    326              
    327             =cut
    328              
    329 0     0     sub DESTROY {
    330             }
    331              
    332             =head1 AUTHOR
    333              
    334             Nicholas Bamber, C<< >>
    335              
    336             =head1 BUGS
    337              
    338             Please report any bugs or feature requests to C, or through
    339             the web interface at L. I will be notified, and then you'll
    340             automatically be notified of progress on your bug as I make changes.
    341              
    342             =head2 AUTOLOAD
    343              
    344             AUTOLOAD is quite a fraught subject. There is probably no perfect solution. See http://www.perlmonks.org/?node_id=342804 for a sample of the issues.
    345              
    346             =head1 SUPPORT
    347              
    348             You can find documentation for this module with the perldoc command.
    349              
    350             perldoc CGI::Application::Plugin::PageLookup::Loop
    351              
    352              
    353             You can also look for information at:
    354              
    355             =over 4
    356              
    357             =item * RT: CPAN's request tracker
    358              
    359             L
    360              
    361             =item * AnnoCPAN: Annotated CPAN documentation
    362              
    363             L
    364              
    365             =item * CPAN Ratings
    366              
    367             L
    368              
    369             =item * Search CPAN
    370              
    371             L
    372              
    373             =back
    374              
    375              
    376             =head1 ACKNOWLEDGEMENTS
    377              
    378              
    379             =head1 COPYRIGHT & LICENSE
    380              
    381             Copyright 2009 Nicholas Bamber.
    382              
    383             This program is free software; you can redistribute it and/or modify it
    384             under the terms of either: the GNU General Public License as published
    385             by the Free Software Foundation; or the Artistic License.
    386              
    387             See http://dev.perl.org/licenses/ for more information.
    388              
    389              
    390             =cut
    391              
    392             1; # End of CGI::Application::Plugin::PageLookup::Loop