From d20b75c5365cd390e415faa0e35af339c6aac1be Mon Sep 17 00:00:00 2001
From: Mason James <mtj@kohaaloha.com>
Date: Thu, 28 Apr 2016 23:09:45 +1200
Subject: [PATCH] Bug 10756 - Carousel Display of New Titles on OPAC home page

test plan...
1/ import some bibs into your database for testing (important)

 $ wget http://www.loc.gov/catdir/cpso/RDAtest/extra_bib_marc.zip
 $ unzip extra_bib_marc.zip
 $ bulkmarcimport.pl -b -m ISO2709 -file ./formal_extra_all_final.bib

2/ apply patches

3/ execute atomic update to add syspref and 'Carousel' table

 $ perl ./installer/data/mysql/atomicupdate/Bug-10756-carousel.pl

4/ open OPAC homepage in web-browser, observe no change

5/ set 'OpacCarousel' syspref to 'display'

6/ open OPAC homepage in web-browser, observe a new carousel :0)

extra QA tests...

7/ run 5 carousel tests
 $ prove ./t/db_dependent/Carousel.t

8/ run qa-tool on patchset
 $ qa -c 2

Signed-off-by: Magnus Enger <magnus@libriotech.no>
Followed the test plan, everything worked as expected. The front page
displays a nice coverflow widget.

More parameters to control the widget would be nice, but that could
be added later.

On first running this I got a nasty 500 error: "Can't call method
"is_valid" on an undefined value at /home/magnus/scripts/kohaclone/Koha/Carousel.pm
line 128." But I think I found a cure for that and will propose a
followup patch.
---
 Koha/Carousel.pm                                   | 278 +++++++++++++++++++++
 Koha/Schema/Result/Carousel.pm                     |  79 ++++++
 debian/koha-common.cron.daily                      |   2 +-
 .../data/mysql/atomicupdate/Bug-10756-carousel.pl  |  20 ++
 .../prog/en/modules/admin/preferences/opac.pref    |   7 +
 .../bootstrap/en/includes/opac-bottom.inc          |  16 ++
 .../opac-tmpl/bootstrap/en/modules/opac-main.tt    |  18 ++
 misc/cronjobs/cleanup_database.pl                  |  27 +-
 opac/opac-main.pl                                  |   9 +
 t/db_dependent/Carousel.t                          | 243 ++++++++++++++++++
 10 files changed, 696 insertions(+), 3 deletions(-)
 create mode 100755 Koha/Carousel.pm
 create mode 100644 Koha/Schema/Result/Carousel.pm
 create mode 100755 installer/data/mysql/atomicupdate/Bug-10756-carousel.pl
 create mode 100755 t/db_dependent/Carousel.t

diff --git a/Koha/Carousel.pm b/Koha/Carousel.pm
new file mode 100755
index 0000000..ec6a7f7
--- /dev/null
+++ b/Koha/Carousel.pm
@@ -0,0 +1,278 @@
+package Koha::Carousel;
+
+# Copyright 2016 Mason James
+#
+# This file is part of Koha.
+#
+# Koha is free software; you can redistribute it and/or modify it under the
+# terms of the GNU General Public License as published by the Free Software
+# Foundation; either version 3 of the License, or (at your option) any later
+# version.
+#
+# Koha is distributed in the hope that it will be useful, but WITHOUT ANY
+# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+# A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with Koha; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+use strict;
+use warnings;
+use C4::Koha;
+use C4::Biblio;
+use C4::Debug;
+
+use LWP::Simple;
+use List::Util qw(shuffle );
+use Business::ISBN;
+
+if ( $ENV{DEBUG} ) {
+    use Time::HiRes qw/gettimeofday tv_interval/;
+    use Carp;
+}
+
+use vars qw($VERSION @ISA @EXPORT);
+
+BEGIN {
+    $VERSION = 1.00;
+
+    require Exporter;
+    @ISA = qw( Exporter );
+
+    push @EXPORT, qw(
+      &GetRecentBibs
+    );
+}
+
+=head1 NAME
+
+Koha::Carousel - A module to handle subroutines for the 'new items' carousel
+
+=head1 DESCRIPTION
+
+This module has 1 subroutine,
+
+    GetRecentBibs() returns a hashref of bibs with a matching amazon bookcover url
+
+=head1 SYNOPSIS
+
+    use Koha::Carousel
+
+    # give me a hashref of 10 random new bibs
+    my $new_bibs_href =  GetRecentBibs();
+
+    # give me a hashref of 20 random new bibs
+    my $new_bibs_href =  GetRecentBibs('20');
+
+=cut
+
+sub GetRecentBibs {
+
+    my $caro_num = shift;
+
+    # speed stats for debugging.
+    my $tt0;
+    my $tt1;
+    if ( $ENV{DEBUG} ) {
+        $tt0 = [gettimeofday];
+    }
+
+    # set vars = 0, to shush warnings
+    my ( $amz_hit_cnt, $amz_miss_cnt, $dupes, $cache_hits, $fetches, $hits,
+        $cache_misses )
+      = (0) x 7;
+    my ( @bibs_stage1, @bibs_stage2, @bibs_stage3 );
+
+    my $ua = new LWP::UserAgent( 'max_redirect' => 0 );
+
+    # 3 different limits, for narrowing the list of bib results
+    my $stage1_limit = 300;
+    my $stage2_limit = 150;
+    my $stage3_limit = $caro_num ? $caro_num : 10;
+
+# Initial SQL query to get recently added bibs
+# we choose a 'reasonable' limit here, to ensure we don't make our dataset too small
+
+    my $bib_rs = Koha::Database->new()->schema->resultset('Biblio')->search(
+        { 'biblioitems.isbn' => { '!=', undef }, },
+        {
+            join      => 'biblioitems',
+            '+select' => ['biblioitems.isbn'],
+            '+as'     => ['isbn'],
+
+            rows => $stage1_limit,
+            desc => 'datecreated',
+        }
+    );
+
+    # run a loop to get $stage1_limit bibs with checksum verified ISBNs.
+    my $i;
+    while ( my $r1 = $bib_rs->next ) {
+
+        # test checksum of ISBN10
+        my $isbn_tmp = $r1->get_column('isbn');
+        my $isbn;
+
+        my @ibs = split( /\|/, $isbn_tmp );
+        foreach my $ii (@ibs) {
+
+            # strip trailing (comments) from isbn
+            $ii =~ s/\(.*$//g;
+
+            my $isbn_obj = Business::ISBN->new($ii);
+            next unless $isbn_obj;
+
+            my $isbn10;
+            $isbn10 = $isbn_obj->as_isbn10;
+            if ( $isbn10->is_valid() ) {
+                $isbn = $isbn10->as_string( [] );
+                last;
+            }
+        }
+
+        next unless $isbn;
+
+        my $bib = {
+            biblionumber => $r1->biblionumber,
+            title        => $r1->title,
+            author       => $r1->author,
+            isbn         => $isbn,
+        };
+
+        push @bibs_stage1, $bib;
+
+    }    # end while
+
+    # run a loop to remove any dupes
+    $i = 0;
+    foreach my $bib (@bibs_stage1) {
+
+        # check for dups before adding
+        my $dupe = 0;
+        foreach my $b (@bibs_stage2) {
+            if ( $bib->{isbn} eq $b->{isbn} ) {
+                $dupe = 1;
+                $dupes++;
+                last;
+            }
+        }
+        next if $dupe == 1;
+        push @bibs_stage2, $bib;
+        last if ++$i == $stage2_limit;
+    }    # end while
+
+    #randomise our bibs
+    @bibs_stage2 = shuffle @bibs_stage2;
+
+   # loop, until we get enough cover images
+   # add each amazon lookup attempt to the carousel db table, for future caching
+
+    for my $bib (@bibs_stage2) {
+
+        # check for an existing ISBN row, in caro table
+        my $caro_rs =
+          Koha::Database->new()->schema->resultset('Carousel')
+          ->search( { 'isbn' => { '=', $bib->{'isbn'} }, }, { rows => '1' } );
+
+        my $cnt = $caro_rs->count;
+
+        my $rs = $caro_rs->next;
+        my $amz_miss;
+        my $image_url;
+
+        # if not db match, do amazon lookup....
+        if ( $cnt == 0 ) {
+
+            # no row in table, so fetch from amazon
+            $image_url =
+                'http://images.amazon.com/images/P/'
+              . $bib->{isbn}
+              . '.01._THUMBZZZ.jpg';
+
+            my $req      = HTTP::Request->new( 'GET', $image_url );
+            my $res      = $ua->request($req);
+            my $res_size = $res->headers->{'content-length'};
+
+            $fetches++;
+
+            #     warn $res_size;
+            # 43 bytes returned, means lookup was a miss :'(
+            $amz_miss = 1 if ( $res_size == 43 );
+
+            # insert lookup result into table
+            $caro_rs->create(
+                {
+                    isbn      => $bib->{'isbn'},
+                    image_url => ( $amz_miss ? undef : $image_url )
+                }
+            );
+
+            if ($amz_miss) {
+                $amz_miss_cnt++;
+                next;
+            }
+            else {
+                # else, got a match from amazon
+                $amz_hit_cnt++;
+            }
+
+        }    # end
+             # else, got result from db..
+        else {
+
+            # but no image, so skip :/
+            unless ( $rs->get_column('image_url') ) {
+                $cache_misses++;
+                next;
+            }
+            else {
+                # db result has image, so use ;)
+                $image_url = $rs->get_column('image_url');
+                $cache_hits++;
+            }
+        }    # end for
+
+        # remove glitchy trailing chars from title/author for display /:,
+        foreach ( $bib->{title}, $bib->{author} ) {
+            next unless $_;    # shush warns
+            s|/$||;
+            s|:$||;
+            s|,$||;
+            s/[ \t]+$//g;
+        }
+
+        $bib->{image_url} = $image_url;
+        $bib->{id}        = $hits;
+
+        push @bibs_stage3, $bib;
+
+        $hits++;
+        last if $hits == $stage3_limit;
+    }
+
+    # if debug, print some extra stats
+    if ( $ENV{DEBUG} ) {
+        $tt1 = [gettimeofday];
+        carp 'carousel: load time ' . tv_interval( $tt0, $tt1 );
+        carp "carousel: db_hits: $cache_hits, amz_fetches:$fetches";
+        carp
+"amz_hit:$amz_hit_cnt, amz_miss:$amz_miss_cnt, dupes:$dupes, hits: $hits, misses: $cache_misses, needs: $stage3_limit";
+    }
+
+    return \@bibs_stage3;
+}
+
+=head1 EXPORT
+
+None by default.
+
+=head1 AUTHOR
+
+Mason James, E<lt>mason@calyx.net.auE<gt>
+
+=cut
+
+1;
+
+__END__
diff --git a/Koha/Schema/Result/Carousel.pm b/Koha/Schema/Result/Carousel.pm
new file mode 100644
index 0000000..3cd2581
--- /dev/null
+++ b/Koha/Schema/Result/Carousel.pm
@@ -0,0 +1,79 @@
+use utf8;
+package Koha::Schema::Result::Carousel;
+
+# Created by DBIx::Class::Schema::Loader
+# DO NOT MODIFY THE FIRST PART OF THIS FILE
+
+=head1 NAME
+
+Koha::Schema::Result::Carousel
+
+=cut
+
+use strict;
+use warnings;
+
+use base 'DBIx::Class::Core';
+
+=head1 TABLE: C<carousel>
+
+=cut
+
+__PACKAGE__->table("carousel");
+
+=head1 ACCESSORS
+
+=head2 isbn
+
+  data_type: 'varchar'
+  is_nullable: 0
+  size: 30
+
+=head2 image_url
+
+  data_type: 'varchar'
+  is_nullable: 1
+  size: 255
+
+=head2 timestamp
+
+  data_type: 'timestamp'
+  datetime_undef_if_invalid: 1
+  default_value: current_timestamp
+  is_nullable: 0
+
+=cut
+
+__PACKAGE__->add_columns(
+  "isbn",
+  { data_type => "varchar", is_nullable => 0, size => 255 },
+  "image_url",
+  { data_type => "varchar", is_nullable => 0, size => 255 },
+  "timestamp",
+  {
+    data_type => "timestamp",
+    datetime_undef_if_invalid => 1,
+    default_value => \"current_timestamp",
+    is_nullable => 0,
+  },
+);
+
+=head1 PRIMARY KEY
+
+=over 4
+
+=item * L</isbn>
+
+=back
+
+=cut
+
+__PACKAGE__->set_primary_key("isbn");
+
+
+# Created by DBIx::Class::Schema::Loader v0.07025 @ 2014-11-15 02:47:15
+# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:WlpGsPHQes9PIDaN4/6zaA
+
+
+# You can replace this text with custom code or comments, and it will be preserved on regeneration
+1;
diff --git a/debian/koha-common.cron.daily b/debian/koha-common.cron.daily
index cbd7eed..9830840 100644
--- a/debian/koha-common.cron.daily
+++ b/debian/koha-common.cron.daily
@@ -21,7 +21,7 @@ koha-foreach --enabled --email /usr/share/koha/bin/cronjobs/advance_notices.pl -
 koha-foreach --enabled /usr/share/koha/bin/cronjobs/membership_expiry.pl -c
 koha-foreach --enabled /usr/share/koha/bin/cronjobs/holds/cancel_expired_holds.pl >/dev/null 2>&1
 koha-foreach --enabled /usr/share/koha/bin/cronjobs/services_throttle.pl > /dev/null 2>&1
-koha-foreach --enabled /usr/share/koha/bin/cronjobs/cleanup_database.pl --sessions --zebraqueue 10 --list-invites
+koha-foreach --enabled /usr/share/koha/bin/cronjobs/cleanup_database.pl --sessions --zebraqueue 10 --list-invites --carousel
 koha-foreach --enabled --noemail /usr/share/koha/bin/cronjobs/cleanup_database.pl --mail
 koha-foreach --enabled /usr/share/koha/bin/cronjobs/holds/auto_unsuspend_holds.pl > /dev/null 2>&1
 koha-foreach --enabled /usr/share/koha/bin/cronjobs/automatic_renewals.pl
diff --git a/installer/data/mysql/atomicupdate/Bug-10756-carousel.pl b/installer/data/mysql/atomicupdate/Bug-10756-carousel.pl
new file mode 100755
index 0000000..724f9f0
--- /dev/null
+++ b/installer/data/mysql/atomicupdate/Bug-10756-carousel.pl
@@ -0,0 +1,20 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+use C4::Context;
+my $dbh = C4::Context->dbh;
+
+$dbh->do(
+q|
+CREATE TABLE `carousel` (
+  `isbn` varchar(255) NOT NULL,
+  `image_url` varchar(255) NULL,
+  `timestamp` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
+  PRIMARY KEY (`isbn`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8 |
+);
+
+  $dbh->do("INSERT INTO `systempreferences` (variable,value,explanation,options,type) VALUES('OpacCarousel','0','Display the new-items carousel on OPAC home page.','','YesNo')");
+
+print "Upgrade done (Bug 10756 - Carousel Display of New Titles on OPAC)\n";
diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/opac.pref b/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/opac.pref
index f092767..9f85f19 100644
--- a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/opac.pref
+++ b/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/opac.pref
@@ -490,6 +490,13 @@ OPAC:
                   no: Don't display
             - the acquisition details on OPAC detail pages.
         -
+            - pref: OPACCarousel
+              default: 0
+              choices:
+                  yes: Display
+                  no: Don't display
+            - the 'new items' carousel on OPAC home page.
+        -
             - "Use the following as the OPAC ISBD template:"
             - pref: OPACISBD
               type: textarea
diff --git a/koha-tmpl/opac-tmpl/bootstrap/en/includes/opac-bottom.inc b/koha-tmpl/opac-tmpl/bootstrap/en/includes/opac-bottom.inc
index 9e7a15c..7c14881 100644
--- a/koha-tmpl/opac-tmpl/bootstrap/en/includes/opac-bottom.inc
+++ b/koha-tmpl/opac-tmpl/bootstrap/en/includes/opac-bottom.inc
@@ -189,6 +189,22 @@ $.widget.bridge('uitooltip', $.ui.tooltip);
     </script>
 [% END %]
 
+[% IF OpacCarousel %]
+    <script type="text/javascript" src="[% interface %]/[% theme %]/lib/contentflow/contentflow.js"></script>
+    <script type="text/javascript">
+    //<![CDATA[
+    $(document).ready(function() {
+      var cflow2 = new ContentFlow('cflow2', {
+        reflectionHeight : 0.2 ,
+        reflectionGap : 0.0,
+        scaleFactor : 2.0,
+      });
+      cflow2.init()
+    });
+    //]]>
+    </script>
+[% END %]
+
 [% IF OPACLocalCoverImages %]
     <script type="text/javascript" src="[% interface %]/[% theme %]/js/localcovers.js"></script>
     <script type="text/javascript">
diff --git a/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-main.tt b/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-main.tt
index 798c90e..387207d 100644
--- a/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-main.tt
+++ b/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-main.tt
@@ -66,6 +66,24 @@
         [% END %]
 
         [% IF ( OpacMainUserBlock ) %]<div id="opacmainuserblock">[% OpacMainUserBlock %]</div>[% END %]
+
+        [% IF ( caro_bibs  ) %]
+        <div id="cflow1" class="span6 offset3">
+        <center><h3>Recently added items</h3></center>
+          <div id="cflow2" class="ContentFlow" style="background: hidden;">
+            <div class="flow">
+            [% FOREACH caro_bibs IN caro_bibs %]
+                <div class="item" href="/cgi-bin/koha/opac-detail.pl?bib=[% caro_bibs.biblionumber %]" >
+                  <img class="content" src="[% caro_bibs.image_url %]" />
+                  <div class="caption">[% caro_bibs.title %][% IF ( caro_bibs.author  ) %] by [% caro_bibs.author %][% END %]</div>
+                </div>
+            [% END %]
+            </div>
+            <div class="globalCaption"></div>
+          </div>
+        </div>
+        [% END %]
+
         </div> <!-- / .span 7/9 -->
 
         [% IF ( ( Koha.Preference( 'opacuserlogin' ) == 1 ) || OpacNavRight ) %]
diff --git a/misc/cronjobs/cleanup_database.pl b/misc/cronjobs/cleanup_database.pl
index bb54256..f5013c6 100755
--- a/misc/cronjobs/cleanup_database.pl
+++ b/misc/cronjobs/cleanup_database.pl
@@ -23,6 +23,7 @@ use constant DEFAULT_ZEBRAQ_PURGEDAYS             => 30;
 use constant DEFAULT_MAIL_PURGEDAYS               => 30;
 use constant DEFAULT_IMPORT_PURGEDAYS             => 60;
 use constant DEFAULT_LOGS_PURGEDAYS               => 180;
+use constant DEFAULT_CAROUSEL_PURGEDAYS           => 90;
 use constant DEFAULT_SEARCHHISTORY_PURGEDAYS      => 30;
 use constant DEFAULT_SHARE_INVITATION_EXPIRY_DAYS => 14;
 use constant DEFAULT_DEBARMENTS_PURGEDAYS         => 30;
@@ -43,7 +44,7 @@ use C4::Accounts;
 
 sub usage {
     print STDERR <<USAGE;
-Usage: $0 [-h|--help] [--sessions] [--sessdays DAYS] [-v|--verbose] [--zebraqueue DAYS] [-m|--mail] [--merged] [--import DAYS] [--logs DAYS] [--searchhistory DAYS] [--restrictions DAYS] [--all-restrictions] [--fees DAYS]
+Usage: $0 [-h|--help] [--sessions] [--sessdays DAYS] [-v|--verbose] [--zebraqueue DAYS] [-m|--mail] [--merged] [--import DAYS] [--logs DAYS] [--searchhistory DAYS] [--restrictions DAYS] [--all-restrictions] [--fees DAYS] [--carousel DAYS]
 
    -h --help          prints this help message, and exits, ignoring all
                       other options
@@ -76,9 +77,11 @@ Usage: $0 [-h|--help] [--sessions] [--sessdays DAYS] [-v|--verbose] [--zebraqueu
                          days.  Defaults to 14 days if no days specified.
    --restrictions DAYS   purge patrons restrictions expired since more than DAYS days.
                          Defaults to 30 days if no days specified.
-    --all-restrictions   purge all expired patrons restrictions.
+   --all-restrictions   purge all expired patrons restrictions.
    --del-exp-selfreg  Delete expired self registration accounts
    --del-unv-selfreg DAYS  Delete unverified self registrations older than DAYS
+   --carousel DAYS    purge entries from carousel older than DAYS days.
+                      Defaults to 90 days if no days specified.
 USAGE
     exit $_[0];
 }
@@ -92,6 +95,7 @@ my $mail;
 my $purge_merged;
 my $pImport;
 my $pLogs;
+my $pCarousel;
 my $pSearchhistory;
 my $pZ3950;
 my $pListShareInvites;
@@ -113,6 +117,7 @@ GetOptions(
     'z3950'           => \$pZ3950,
     'logs:i'          => \$pLogs,
     'fees:i'          => \$fees_days,
+    'carousel:i'      => \$pCarousel,
     'searchhistory:i' => \$pSearchhistory,
     'list-invites:i'  => \$pListShareInvites,
     'restrictions:i'  => \$pDebarments,
@@ -125,6 +130,7 @@ GetOptions(
 $sessions          = 1                                    if $sess_days                  && $sess_days > 0;
 $pImport           = DEFAULT_IMPORT_PURGEDAYS             if defined($pImport)           && $pImport == 0;
 $pLogs             = DEFAULT_LOGS_PURGEDAYS               if defined($pLogs)             && $pLogs == 0;
+$pCarousel         = DEFAULT_CAROUSEL_PURGEDAYS           if defined($pCarousel)         && $pCarousel == 0;
 $zebraqueue_days   = DEFAULT_ZEBRAQ_PURGEDAYS             if defined($zebraqueue_days)   && $zebraqueue_days == 0;
 $mail              = DEFAULT_MAIL_PURGEDAYS               if defined($mail)              && $mail == 0;
 $pSearchhistory    = DEFAULT_SEARCHHISTORY_PURGEDAYS      if defined($pSearchhistory)    && $pSearchhistory == 0;
@@ -142,6 +148,7 @@ unless ( $sessions
     || $pImport
     || $pLogs
     || $fees_days
+    || $pCarousel
     || $pSearchhistory
     || $pZ3950
     || $pListShareInvites
@@ -238,6 +245,12 @@ if ($pZ3950) {
     print "Done with purging Z39.50 records from import tables.\n" if $verbose;
 }
 
+if ($pCarousel) {
+    print "Purging records from carousel tables.\n" if $verbose;
+    PurgeCarousel();
+    print "Done with purging records from carousel tables.\n" if $verbose;
+}
+
 if ($pLogs) {
     print "Purging records from action_logs.\n" if $verbose;
     $sth = $dbh->prepare(
@@ -363,6 +376,16 @@ sub PurgeZ3950 {
     $sth->execute() or die $dbh->errstr;
 }
 
+sub PurgeCarousel {
+    $sth = $dbh->prepare(
+        q{
+            DELETE FROM carousel
+            WHERE timestamp <  date_sub(curdate(), INTERVAL ? DAY)
+        }
+    );
+    $sth->execute($pCarousel) or die $dbh->errstr;
+}
+
 sub PurgeDebarments {
     require Koha::Patron::Debarments;
     my $days = shift;
diff --git a/opac/opac-main.pl b/opac/opac-main.pl
index c79afb4..aeb8c61 100755
--- a/opac/opac-main.pl
+++ b/opac/opac-main.pl
@@ -25,6 +25,7 @@ use C4::Output;
 use C4::NewsChannels;    # GetNewsToDisplay
 use C4::Languages qw(getTranslatedLanguages accept_language);
 use C4::Koha qw( GetDailyQuote );
+use Koha::Carousel;
 
 my $input = new CGI;
 my $dbh   = C4::Context->dbh;
@@ -72,4 +73,12 @@ if (C4::Context->preference('OPACNumbersPreferPhrase')) {
         $template->param('numbersphr' => 1);
 }
 
+if ( C4::Context->preference('OpacCarousel') ) {
+    our $caro_bibs = GetRecentBibs(10);
+    $template->param(
+        caro_bibs    => $caro_bibs,
+        OpacCarousel => 1
+    );
+}
+
 output_html_with_http_headers $input, $cookie, $template->output;
diff --git a/t/db_dependent/Carousel.t b/t/db_dependent/Carousel.t
new file mode 100755
index 0000000..d1f3761
--- /dev/null
+++ b/t/db_dependent/Carousel.t
@@ -0,0 +1,243 @@
+#!/usr/bin/perl
+
+# This file is part of Koha.
+#
+# Copyright 2016 CALYX Group
+#
+# Koha is free software; you can redistribute it and/or modify it
+# under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# Koha is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Koha; if not, see <http://www.gnu.org/licenses>.
+
+use Modern::Perl;
+
+use Test::More tests => 5;
+use Test::MockModule;
+
+use MARC::Record;
+use t::lib::Mocks qw( mock_preference );
+
+BEGIN {
+    use_ok('C4::Biblio');
+    use_ok('Koha::Carousel');
+}
+
+my $dbh = C4::Context->dbh;
+
+$dbh->{AutoCommit} = 0;
+$dbh->{RaiseError} = 1;
+
+# Mock a context
+my $context = new Test::MockModule('C4::Context');
+mock_marcfromkohafield();
+
+# create 3 sets of ISBN run to tests on...
+# 1 set of invalid isbns
+my @isbns_bad = qw |
+  1111
+  2222
+  333
+  444
+  5555
+  666
+  777
+  88
+  99
+  1100000
+
+  |;
+
+# 1 set of mixed valid and invalid isbns
+my @isbns_mixed = qw |
+  2846542082
+  2848301384
+  2848672870
+  2953079971
+  2953636501
+  11111
+  2222
+  33333
+  4444444
+  55555
+
+  |;
+
+# 1 set of valid isbns
+my @isbns_good = qw |
+  2846542082
+  2848301384
+  2848672870
+  2953079971
+  2953636501
+  3110251345
+  3406606067
+  3531167707
+  3531174142
+  3531174827
+
+  |;
+
+my $marcflavour;
+
+# run some tests for MARC21, UNIMARC and NORMARC bibsets
+sub run_tests {
+
+    $marcflavour = shift;
+
+    # clean the bib tables for the before starting the tests, with auto-commit off
+    my $sth;
+    $sth = $dbh->do("delete from items");
+    $sth = $dbh->do("delete from biblioitems");
+
+    $sth = $dbh->do("delete from biblio");
+    $sth = $dbh->do("delete from caro_isbn_url");
+
+    # Undef C4::Biblio::inverted_field_map to avoid problems introduced
+    # by caching in TransformMarcToKoha
+    undef $C4::Biblio::inverted_field_map;
+    t::lib::Mocks::mock_preference( 'marcflavour', $marcflavour );
+
+    # add some bibs with bad isbns
+    our $caro_bibs_bad = GetRecentBibs(10);
+
+    # test for 10 invalid ISBNs, expects 0 successful results
+    is(
+        scalar @$caro_bibs_bad,
+        0,
+        'Number of invalid bibs returned is '
+          . scalar @$caro_bibs_bad
+          . ', expected 0'
+    );
+
+
+    foreach my $i (@isbns_mixed) { create_bib($i); }
+    our $caro_bibs_mixed = GetRecentBibs(10);
+
+    # test for 10 mixed ISBNs, expects 5 successful results
+    is(
+        scalar @$caro_bibs_mixed,
+        5,
+        'Number of mixed bibs returned is '
+          . scalar @$caro_bibs_mixed
+          . ', expected 5'
+    );
+
+    # test for 10 valid ISBNs, expects 10 successful results
+    foreach my $i (@isbns_good) { create_bib($i); }
+    our $caro_bibs_good = GetRecentBibs(10);
+
+    # then... add some more bibs with good isbns
+    is(
+        scalar @$caro_bibs_good,
+        10,
+        'Number of valid bibs returned is '
+          . scalar @$caro_bibs_good
+          . ', expected 10'
+    );
+}
+
+# mock a needed MARC subfield, for testing
+sub mock_marcfromkohafield {
+
+    $context->mock(
+        'marcfromkohafield',
+        sub {
+            my ($self) = shift;
+
+            # if we are testing MARC21 or NORMARC, use these subfields...
+            if (   C4::Context->preference('marcflavour') eq 'MARC21'
+                || C4::Context->preference('marcflavour') eq 'NORMARC' )
+            {
+                return {
+                    '' => {
+                        'biblio.title'                 => [ '245', 'a' ],
+                        'biblio.biblionumber'          => [ '999', 'c' ],
+                        'biblioitems.isbn'             => [ '020', 'a' ],
+                        'biblioitems.issn'             => [ '022', 'a' ],
+                        'biblioitems.biblioitemnumber' => [ '999', 'd' ]
+                    }
+                };
+            }
+
+            # or, if we are testing UNIMARC, use these subfields...
+            elsif ( C4::Context->preference('marcflavour') eq 'UNIMARC' ) {
+
+                return {
+                    '' => {
+                        'biblio.title'                 => [ '200', 'a' ],
+                        'biblio.biblionumber'          => [ '999', 'c' ],
+                        'biblioitems.isbn'             => [ '010', 'a' ],
+                        'biblioitems.issn'             => [ '011', 'a' ],
+                        'biblioitems.biblioitemnumber' => [ '090', 'a' ]
+                    }
+                };
+            }
+        }
+    );
+}
+
+# create a simple mocked bib
+sub create_bib {
+
+    my $isbn  = shift;
+    my $title = 'some title';
+
+    # Generate a record with just the ISBN
+    my $marc_record = MARC::Record->new;
+    my $isbn_field = create_isbn_field( $isbn, $marcflavour );
+    $marc_record->append_fields($isbn_field);
+
+    # Add title
+    my $fld = ( $marcflavour eq 'UNIMARC' ) ? '200' : '245';
+    my $title_field = MARC::Field->new( $fld, '', '', 'a' => $title );
+    $marc_record->append_fields($title_field);
+
+    # Add the record to the DB
+    my ( $biblionumber, $biblioitemnumber ) = AddBiblio( $marc_record, '' );
+    my $data = GetBiblioData($biblionumber);
+
+}
+
+# Add an isbn subfield to an item
+sub create_isbn_field {
+    my ( $isbn, $marcflavour ) = @_;
+
+    my $isbn_field = ( $marcflavour eq 'UNIMARC' ) ? '010' : '020';
+    my $field = MARC::Field->new( $isbn_field, '', '', 'a' => $isbn );
+
+    return $field;
+}
+
+# do tests on 3 different MARC formats
+subtest 'MARC21' => sub {
+    plan tests => 3;
+    run_tests('MARC21');
+
+    $dbh->rollback;
+};
+
+subtest 'UNIMARC' => sub {
+    plan tests => 3;
+    run_tests('UNIMARC');
+
+    $dbh->rollback;
+};
+
+subtest 'NORMARC' => sub {
+    plan tests => 3;
+    run_tests('NORMARC');
+
+    $dbh->rollback;
+};
+
+done_testing();
+
+1;
-- 
1.9.1