diff --git a/docs/changelog/7.x.x.txt b/docs/changelog/7.x.x.txt index 6bd40ff9c..53eaf4ead 100644 --- a/docs/changelog/7.x.x.txt +++ b/docs/changelog/7.x.x.txt @@ -17,6 +17,7 @@ - fixed #11107: linked image with caption - fixed #10914: Shop: No email notifications sent when the cart has net value 0 - fixed #11126: WebGUI database has varchar fields + - fixed #10989: DataForm List: No pagination 7.8.1 - mark $session->datetime->time as deprecated and remove its use from core code diff --git a/lib/WebGUI/Asset/Wobject/DataForm.pm b/lib/WebGUI/Asset/Wobject/DataForm.pm index 98ad15708..85f84def5 100644 --- a/lib/WebGUI/Asset/Wobject/DataForm.pm +++ b/lib/WebGUI/Asset/Wobject/DataForm.pm @@ -31,6 +31,7 @@ use WebGUI::Utility; use WebGUI::Group; use WebGUI::AssetCollateral::DataForm::Entry; use WebGUI::Form::SelectRichEditor; +use WebGUI::Paginator; use JSON (); our @ISA = qw(WebGUI::Asset::Wobject); @@ -697,9 +698,10 @@ A hash reference. New template variables will be appended to it. =cut sub getListTemplateVars { - my $self = shift; - my $var = shift; - my $i18n = WebGUI::International->new($self->session,"Asset_DataForm"); + my $self = shift; + my $session = $self->session; + my $var = shift; + my $i18n = WebGUI::International->new($session,"Asset_DataForm"); $var->{"back.url"} = $self->getFormUrl; $var->{"back.label"} = $i18n->get('go to form'); my $fieldConfig = $self->getFieldConfig; @@ -713,7 +715,9 @@ sub getListTemplateVars { } @{ $self->getFieldOrder }; $var->{field_loop} = \@fieldLoop; my @recordLoop; - my $entryIter = $self->entryClass->iterateAll($self); + my $p = WebGUI::Paginator->new($session); + $p->setDataByCallback(sub { return $self->entryClass->iterateAll($self, { offset => $_[0], limit => $_[1], }); }); + my $entryIter = $p->getPageIterator(); while ( my $entry = $entryIter->() ) { my $entryData = $entry->fields; my @dataLoop; @@ -734,9 +738,9 @@ sub getListTemplateVars { %dataVars, "record.ipAddress" => $entry->ipAddress, "record.edit.url" => $self->getFormUrl("func=view;entryId=".$entry->getId), - "record.edit.icon" => $self->session->icon->edit("func=view;entryId=".$entry->getId, $self->get('url')), + "record.edit.icon" => $session->icon->edit("func=view;entryId=".$entry->getId, $self->get('url')), "record.delete.url" => $self->getUrl("func=deleteEntry;entryId=".$entry->getId), - "record.delete.icon" => $self->session->icon->delete("func=deleteEntry;entryId=".$entry->getId, $self->get('url'), $i18n->get('Delete entry confirmation')), + "record.delete.icon" => $session->icon->delete("func=deleteEntry;entryId=".$entry->getId, $self->get('url'), $i18n->get('Delete entry confirmation')), "record.username" => $entry->username, "record.userId" => $entry->userId, "record.submissionDate.epoch" => $entry->submissionDate->epoch, @@ -746,6 +750,7 @@ sub getListTemplateVars { }; } $var->{record_loop} = \@recordLoop; + $p->appendTemplateVars($var); return $var; } diff --git a/lib/WebGUI/AssetCollateral/DataForm/Entry.pm b/lib/WebGUI/AssetCollateral/DataForm/Entry.pm index 71bc86f08..04b9d4596 100644 --- a/lib/WebGUI/AssetCollateral/DataForm/Entry.pm +++ b/lib/WebGUI/AssetCollateral/DataForm/Entry.pm @@ -207,7 +207,7 @@ sub getId { #------------------------------------------------------------------- -=head2 iterateAll ( $asset ) +=head2 iterateAll ( $asset, [ $options ] ) This class method returns an iterator set to iterate over all entries for a Dataform. @@ -215,13 +215,33 @@ This class method returns an iterator set to iterate over all entries for a Data A reference to a Dataform object. +=head3 $options + +A hashreference of options. + +=head4 offset + +The record number to start the iterator at. Defaults to 0 if not set. + +=head4 limit + +The number of records for the iterator to return. Defaults to a very large number if not set. + =cut sub iterateAll { - my $class = shift; - my $asset = shift; - my $sth = $asset->session->dbSlave->read("SELECT `DataForm_entryId`, `userId`, `username`, `ipAddress`, `submissionDate`, `entryData` FROM `DataForm_entry` WHERE `assetId` = ? ORDER BY `submissionDate` DESC", [$asset->getId]); - my $sub = sub { + my $class = shift; + my $asset = shift; + my $options = shift; + my $sql = "SELECT SQL_CALC_FOUND_ROWS `DataForm_entryId`, `userId`, `username`, `ipAddress`, `submissionDate`, `entryData` FROM `DataForm_entry` WHERE `assetId` = ? ORDER BY `submissionDate` DESC LIMIT ?,?"; + my $placeHolders = [ $asset->getId ]; + push @{ $placeHolders }, exists $options->{offset} ? $options->{offset} : 0; + push @{ $placeHolders }, exists $options->{limit} ? $options->{limit} : 1234567890; + my $slave = $asset->session->dbSlave; ##Use the same slave to calculate the number of rows + my $sth = $slave->read($sql, $placeHolders); + my $allRows = $slave->quickScalar('SELECT FOUND_ROWS()'); + my $sub = sub { + return $allRows if $_[0] eq 'rowCount'; if (defined wantarray) { my $properties = $sth->hashRef; if ($properties) { diff --git a/lib/WebGUI/Help/Asset_DataForm.pm b/lib/WebGUI/Help/Asset_DataForm.pm index 06e763014..900a58fa8 100644 --- a/lib/WebGUI/Help/Asset_DataForm.pm +++ b/lib/WebGUI/Help/Asset_DataForm.pm @@ -141,6 +141,9 @@ our $HELP = { { namespace => "Asset_Template", tag => "template variables" }, + { namespace => "WebGUI", + tag => "pagination template variables" + }, ], fields => [], variables => [ diff --git a/lib/WebGUI/Paginator.pm b/lib/WebGUI/Paginator.pm index 4cbf6fab9..1b3dd560c 100644 --- a/lib/WebGUI/Paginator.pm +++ b/lib/WebGUI/Paginator.pm @@ -63,7 +63,7 @@ Private method which retrieves a data set from a database and replaces whatever This method should only ever be called by the public setDataByQuery method and is only called in the case that dynamicPageNumberKey is set. -The public setDataByQuery method is not capable of efficiently handling requests that dyncmically set the page number by value +The public setDataByQuery method is not capable of efficiently handling requests that dynamically set the page number by value due to the fact that only one page of results is ever returned. In this method, all the results are returned making this possible. =head3 query @@ -405,7 +405,7 @@ sub getPage { =head2 getPageData ( [ pageNumber ] ) -Returns the data from the page specified as an array reference. +Returns the data from the specified page as an array reference. =head3 pageNumber @@ -414,40 +414,41 @@ Defaults to the page you're currently viewing. This is mostly here as an overrid =cut sub getPageData { - my $self = shift; - my $pageNumber = shift || $self->getPageNumber; - my $allRows = $self->{_rowRef}; - - my $pageCount = $self->getNumberOfPages; - return [] if ($pageNumber > $pageCount); + my $self = shift; + my $pageNumber = shift || $self->getPageNumber; + my $allRows = $self->{_rowRef}; + + my $pageCount = $self->getNumberOfPages; + return [] if ($pageNumber > $pageCount); if($self->{_setByQuery}) { #Return the cached page return $allRows if($pageNumber == $self->getPageNumber); return []; - } + } - #Handle setByArrayRef or the old setDataByQuery method - my @pageRows = (); + #Handle setByArrayRef or the old setDataByQuery method + my @pageRows = (); my $rowsPerPage = $self->{_rpp}; my $pageStartRow = ($pageNumber*$rowsPerPage)-$rowsPerPage; my $pageEndRow = $pageNumber*$rowsPerPage; for (my $i=$pageStartRow; $i<$pageEndRow; $i++) { - $pageRows[$i-$pageStartRow] = $allRows->[$i] if ($i <= $#{$self->{_rowRef}}); - } - return \@pageRows; + $pageRows[$i-$pageStartRow] = $allRows->[$i] if ($i <= $#{$self->{_rowRef}}); + } + return \@pageRows; } #------------------------------------------------------------------- -=head2 getPageNumber ( ) +=head2 getPageIterator ( ) -Returns the current page number. If no page number can be found then it returns 1. +Returns the iterator that was created by setDataByCallback =cut -sub getPageNumber { - return $_[0]->{_pn}; +sub getPageIterator { + my $self = shift; + return $self->{_iteratorObj}; } #------------------------------------------------------------------- @@ -458,7 +459,7 @@ Returns links to all pages in this paginator. =head3 limit -An integer representing the maximum number of page links to return. Defaultly all page links will be returned. +An integer representing the maximum number of page links to return. By default, all page links will be returned. =cut @@ -523,6 +524,18 @@ sub getPageLinks { } +#------------------------------------------------------------------- + +=head2 getPageNumber ( ) + +Returns the current page number. If no page number can be found then it returns 1. + +=cut + +sub getPageNumber { + return $_[0]->{_pn}; +} + #------------------------------------------------------------------- =head2 getPreviousPageLink ( ) @@ -589,13 +602,13 @@ By default the page number will be determined by looking at $self->session->form =cut sub new { - my $class = shift; - my $session = shift; - my $currentURL = shift; - my $rowsPerPage = shift || 25; - my $formVar = shift || "pn"; - my $pn = shift || $session->form->process($formVar) || 1; - bless {_session=>$session, _url => $currentURL, _rpp => $rowsPerPage, _formVar => $formVar, _pn => $pn}, $class; + my $class = shift; + my $session = shift; + my $currentURL = shift; + my $rowsPerPage = shift || 25; + my $formVar = shift || "pn"; + my $pn = shift || $session->form->process($formVar) || 1; + bless {_session=>$session, _url => $currentURL, _rpp => $rowsPerPage, _formVar => $formVar, _pn => $pn}, $class; } #------------------------------------------------------------------- @@ -651,6 +664,42 @@ sub setDataByArrayRef { } +#------------------------------------------------------------------- + +=head2 setDataByCallback ( callback ) + +Provide the paginator with data by giving it a callback. This interface does not support +having alphabetical keys ala C because the data is never stored in +the Paginator object. + +=head3 callback + +A callback to invoke that returns an iterator. The callback method should +accept two optional parameters, an offset to start, and the rows per page +to return. The iterator should return the total number of rows in +the query, without limits, when the first argument it is passed is 'rowCount'. + +=cut + +sub setDataByCallback { + my $self = shift; + my $callback = shift; + + my $pageNumber = $self->getPageNumber; + my $rowsPerPage = $self->{_rpp}; + my $start = ( ($pageNumber - 1) * $rowsPerPage ); + + my $obj = $callback->($start, $rowsPerPage); + $self->{_totalRows} = $obj->('rowCount'); + + $self->{_iteratorObj} = $obj; + $self->{_setByQuery} = 0; + $self->{_setByArrayRef} = 0; + $self->{_setByCallback} = 1; + return ''; +} + + #------------------------------------------------------------------- =head2 setDataByQuery ( query [, dbh, unconditional, placeholders, dynamicPageNumberKey, dynamicPageNumberValue ] ) diff --git a/t/Asset/Wobject/DataForm/viewList.t b/t/Asset/Wobject/DataForm/viewList.t index 3f198735b..77cd73ba2 100644 --- a/t/Asset/Wobject/DataForm/viewList.t +++ b/t/Asset/Wobject/DataForm/viewList.t @@ -29,7 +29,7 @@ my $df = WebGUI::Asset->getImportNode($session)->addChild( { className => 'WebGUI::Asset::Wobject::DataForm', } ); -WebGUI::Test->tagsToRollback( WebGUI::VersionTag->getWorking( $session ) ); +addToCleanup( WebGUI::VersionTag->getWorking( $session ) ); # Add fields to the dataform $df->createField( "name", { type => "text", } ); @@ -49,18 +49,20 @@ my @entryProperties = ( } ); +my $birthday = WebGUI::Test->webguiBirthday; + my @entries = (); for my $properties (@entryProperties) { my $entry = $df->entryClass->newFromHash( $df, $properties ); + $entry->submissionDate(WebGUI::DateTime->new($session, $birthday++)); $entry->save; push @entries, $entry; - sleep 1; } #---------------------------------------------------------------------------- # Tests -plan tests => 2; # Increment this number for each test you create +plan tests => 6; # Increment this number for each test you create #---------------------------------------------------------------------------- # Test getListTemplateVars @@ -184,8 +186,63 @@ cmp_deeply( 'getListTemplateVars is complete and correct', ); +is($tmplVar->{'pagination.pageCount'}, 1, '... and has pagination variables'); -#---------------------------------------------------------------------------- -# Cleanup +#------------------------------------- +#Shove in a bunch of data to test pagination + +my @quoteDb = ( + { name => "Red", message => "That tall drink of water", }, + { name => "Norton", message => "Do you enjoy working in the laundry?", }, + { name => "Andy", message => "They say it has no memory", }, + { name => "Boggs", message => "Hey, we all need friends in here", }, + { name => "Andy", message => "It's my life. Don't you understand?", }, + + { name => "Red", message => "Rehabilitated? Well, now let me see.", }, + { name => "Red", message => "I know what *you* think it means, sonny.", }, + { name => "Red", message => "I know what *you* think it means, sonny.", }, + { name => "Andy", message => "How can you be so obtuse?", }, + { name => "Red", message => "The man likes to play chess; let's get him some rocks. ", }, + + { name => "Brooks", message => "Easy peasy japanesey.", }, + { name => "Hadley", message => "What is your malfunction?", }, + { name => "Red", message => "Hope is a dangerous thing. Hope can drive a man insane. ", }, + { name => "Red", message => "They send you here for life, and that's exactly what they take.", }, + { name => "Red", message => "Truth is, I don't want to know. Some things are best left unsaid.", }, + + { name => "Andy", message => "That's the beauty of music.", }, + { name => "Red", message => "I played a mean harmonica as a younger man.", }, + { name => "Tommy", message => "I don't read so good.", }, + { name => "Andy", message => "You don't read so *well*.", }, + { name => "Red", message => "Murder, same as you.", }, + + { name => "Norton", message => "Salvation lies within.", }, + { name => "Andy", message => "Remember Red, hope is a good thing.", }, + { name => "Hadley", message => "Drink up while it's cold, ladies.", }, + { name => "Red", message => "We sat and drank with the sun on our shoulders and felt like free men.", }, + { name => "Andy", message => "You see that's tax deductible, you can write that off. ", }, + + { name => "Norton", message => "Lord! It's a miracle!", }, + { name => "Red", message => "I don't have her stuffed down the front of my pants right now, I'm sorry to say, but I'll get her.", }, + { name => "Andy", message => "Get busy living, or get busy dying.", }, + { name => "Brooks", message => "The world went and got itself in a big damn hurry.", }, + { name => "Andy", message => "Everybody's innocent in here. Didn't you know that?", }, + +); + +for my $quote (@quoteDb) { + my $entry = $df->entryClass->newFromHash( $df, $quote ); + $entry->submissionDate(WebGUI::DateTime->new($session, $birthday++)); + $entry->save; + push @entries, $entry; +} + +$tmplVar = $df->getListTemplateVars({}); +is @{ $tmplVar->{record_loop} }, 25, 'list variables are paginated'; +ok $tmplVar->{'pagination.pageCount.isMultiple'}, 'pagination: has multiple pages'; + +$session->request->setup_body({ pn => 2, }); +$tmplVar = $df->getListTemplateVars({}); +is @{ $tmplVar->{record_loop} }, 7, '7 entries in the 2nd page'; #vim:ft=perl diff --git a/t/Paginator.t b/t/Paginator.t index f1c16d90d..f3ea0b1bf 100644 --- a/t/Paginator.t +++ b/t/Paginator.t @@ -25,7 +25,7 @@ use Data::Dumper; my $session = WebGUI::Test->session; -plan tests => 26; # increment this value for each test you create +plan tests => 32; # increment this value for each test you create my $startingRowNum = 0; my $endingRowNum = 99; @@ -107,6 +107,12 @@ is('100', $p->getPage(5), '(101) page 5 stringification okay'); is($p->getPageNumber, 1, 'Default page number is 1'); ##Additional page numbers are specified at instantiation +######################################################################## +# +# getPageLinks with limits +# +######################################################################## + $expectedPages = [ map { +{ 'pagination.text' => ( $_ + 1 ), 'pagination.range' => ( 25 * $_ + 1 ) . "-" . ( $_ * 25 + 25 <= $endingRowNum + 1 ? $_ * 25 + 25 : $endingRowNum + 1 ), # First row number - Last row number @@ -115,12 +121,6 @@ $expectedPages = [ map { +{ $expectedPages->[0]->{'pagination.activePage'} = 'true'; -######################################################################## -# -# getPageLinks with limits -# -######################################################################## - cmp_deeply( ($p->getPageLinks)[0], $expectedPages, @@ -217,3 +217,65 @@ cmp_deeply( \@pageWindow, 'set of last 10 pages selected correctly, (20/20)', ); + +######################################################################## +# +# iterator based paginator +# +######################################################################## + +my $callback = sub { + my ($start, $rowsPerPage) = @_; + my $state = $start * 2; + my $counter = 0; + my $iterator = sub { + return 50 if $_[0] eq 'rowCount'; + return if ($counter >= $rowsPerPage); + $state += 2; + ++$counter; + return if $state > 50; + return $state; + }; + return $iterator; +}; + +my $p1 = WebGUI::Paginator->new($session, '/neighborhood', 5); +$p1->setDataByCallback($callback); +my $pIterator = $p1->getPageIterator; +isa_ok($pIterator, 'CODE', 'getPageIterator'); +is($pIterator->('rowCount'), 50, 'generated iterator returns the correct maximum number of rows'); +is($p1->getNumberOfPages, 10, 'getNumberOfPages works with an iterator'); + +cmp_deeply(drainIterator($pIterator, 10), [2, 4, 6, 8, 10], 'setDataByCallback: iterator returned page 1 data'); + +$p1->setPageNumber(2); +$p1->setDataByCallback($callback); +$pIterator = $p1->getPageIterator; + +cmp_deeply(drainIterator($pIterator, 10), [12, 14, 16, 18, 20], '... iterator returned page 2 data'); + +$expectedPages = [ map { +{ + 'pagination.text' => ( $_ + 1 ), + 'pagination.range' => ( 5 * $_ + 1 ) . "-" . ( ($_+1) * 5 ), # First row number - Last row number + 'pagination.url' => ( $_ != 1 ? '/neighborhood' . '?pn=' . ( $_ + 1 ) : '' ), # Current page has no URL + } } (0..9) ]; + +$expectedPages->[1]->{'pagination.activePage'} = 'true'; + +cmp_deeply( + ($p1->getPageLinks())[0], + $expectedPages, + 'getPageLinks works with a paginator' +); + +sub drainIterator { + my $iterator = shift; + my $terminalCount = shift; + my $pageData = []; + my $infiniteLoopCount = 0; + while (defined(my $item = $pIterator->())) { + push @{ $pageData }, $item; + last if ++$infiniteLoopCount >= $terminalCount; + } + return $pageData; +}