Bug 18433: Allow to select results to export in item search
authorJulian Maurice <julian.maurice@biblibre.com>
Fri, 14 Apr 2017 13:45:04 +0000 (15:45 +0200)
committerMartin Renvoize <martin.renvoize@ptfs-europe.com>
Wed, 19 Feb 2020 16:07:58 +0000 (16:07 +0000)
This adds a column of checkboxes in the results table to be able to select the
items to be exported

Test plan:
1. Go to item search and click 'Search'
2. Without checking any checkbox, verify that the export still works (it should
   export all results)
3. Tick some checkboxes and re-export, verify that only selected items are
   exported

Signed-off-by: Marc VĂ©ron <veron@veron.ch>
Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Jonathan Druart <jonathan.druart@bugs.koha-community.org>
Signed-off-by: Martin Renvoize <martin.renvoize@ptfs-europe.com>

catalogue/item-export.pl [new file with mode: 0755]
catalogue/itemsearch.pl
koha-tmpl/intranet-tmpl/prog/en/includes/catalogue/itemsearch_item.csv.inc
koha-tmpl/intranet-tmpl/prog/en/includes/catalogue/itemsearch_item.json.inc
koha-tmpl/intranet-tmpl/prog/en/modules/catalogue/itemsearch.tt

diff --git a/catalogue/item-export.pl b/catalogue/item-export.pl
new file mode 100755 (executable)
index 0000000..ef6aaa4
--- /dev/null
@@ -0,0 +1,64 @@
+#!/usr/bin/perl
+
+# Copyright 2017 BibLibre
+#
+# 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 Modern::Perl;
+
+use CGI;
+
+use C4::Auth;
+use C4::Output;
+
+my $cgi = new CGI;
+
+my ($template, $borrowernumber, $cookie) = get_template_and_user({
+    template_name => 'catalogue/itemsearch_csv.tt',
+    query => $cgi,
+    type => 'intranet',
+    authnotrequired => 0,
+    flagsrequired   => { catalogue => 1 },
+});
+
+my @itemnumbers = $cgi->multi_param('itemnumber');
+my $format = $cgi->param('format') // 'csv';
+
+my @items = Koha::Items->search({ itemnumber => { -in => \@itemnumbers } });
+
+if ($format eq 'barcodes') {
+    print $cgi->header({
+        type => 'text/plain',
+        attachment => 'barcodes.txt',
+    });
+
+    foreach my $item (@items) {
+        print $item->barcode . "\n";
+    }
+    exit;
+}
+
+$template->param(
+    results => \@items,
+);
+
+print $cgi->header({
+    type => 'text/csv',
+    attachment => 'items.csv',
+});
+for my $line ( split '\n', $template->output ) {
+    print "$line\n" unless $line =~ m|^\s*$|;
+}
index a145149..7acde61 100755 (executable)
@@ -97,13 +97,7 @@ my ($template, $borrowernumber, $cookie) = get_template_and_user({
     flagsrequired   => { catalogue => 1 },
 });
 
-my $mss = Koha::MarcSubfieldStructures->search({ frameworkcode => '', kohafield => 'items.notforloan', authorised_value => [ -and => {'!=' => undef }, {'!=' => ''}] });
-my $notforloan_values = $mss->count ? GetAuthorisedValues($mss->next->authorised_value) : [];
-
-$mss = Koha::MarcSubfieldStructures->search({ frameworkcode => '', kohafield => 'items.location', authorised_value => [ -and => {'!=' => undef }, {'!=' => ''}] });
-my $location_values = $mss->count ? GetAuthorisedValues($mss->next->authorised_value) : [];
-
-$mss = Koha::MarcSubfieldStructures->search({ frameworkcode => '', kohafield => 'items.itemlost', authorised_value => [ -and => {'!=' => undef }, {'!=' => ''}] });
+my $mss = Koha::MarcSubfieldStructures->search({ frameworkcode => '', kohafield => 'items.itemlost', authorised_value => [ -and => {'!=' => undef }, {'!=' => ''}] });
 my $itemlost_values = $mss->count ? GetAuthorisedValues($mss->next->authorised_value) : [];
 
 $mss = Koha::MarcSubfieldStructures->search({ frameworkcode => '', kohafield => 'items.withdrawn', authorised_value => [ -and => {'!=' => undef }, {'!=' => ''}] });
@@ -227,38 +221,10 @@ if (scalar keys %params > 0) {
     }
 
     if ($results) {
-        # Get notforloan labels
-        my $notforloan_map = {};
-        foreach my $nfl_value (@$notforloan_values) {
-            $notforloan_map->{$nfl_value->{authorised_value}} = $nfl_value->{lib};
-        }
-
-        # Get location labels
-        my $location_map = {};
-        foreach my $loc_value (@$location_values) {
-            $location_map->{$loc_value->{authorised_value}} = $loc_value->{lib};
-        }
-
-        # Get itemlost labels
-        my $itemlost_map = {};
-        foreach my $il_value (@$itemlost_values) {
-            $itemlost_map->{$il_value->{authorised_value}} = $il_value->{lib};
-        }
-
-        # Get withdrawn labels
-        my $withdrawn_map = {};
-        foreach my $wd_value (@$withdrawn_values) {
-            $withdrawn_map->{$wd_value->{authorised_value}} = $wd_value->{lib};
-        }
-
         foreach my $item (@$results) {
             my $biblio = Koha::Biblios->find( $item->{biblionumber} );
             $item->{biblio} = $biblio;
             $item->{biblioitem} = $biblio->biblioitem->unblessed;
-            $item->{status} = $notforloan_map->{$item->{notforloan}};
-            if (defined $item->{location}) {
-                $item->{location} = $location_map->{$item->{location}};
-            }
         }
     }
 
@@ -289,13 +255,6 @@ if (scalar keys %params > 0) {
 # Display the search form
 
 my @branches = map { value => $_->branchcode, label => $_->branchname }, Koha::Libraries->search( {}, { order_by => 'branchname' } );
-my @locations;
-foreach my $location (@$location_values) {
-    push @locations, {
-        value => $location->{authorised_value},
-        label => $location->{lib} // $location->{authorised_value},
-    };
-}
 my @itemtypes;
 foreach my $itemtype ( Koha::ItemTypes->search ) {
     push @itemtypes, {
@@ -304,23 +263,10 @@ foreach my $itemtype ( Koha::ItemTypes->search ) {
     };
 }
 
-$mss = Koha::MarcSubfieldStructures->search({ frameworkcode => '', kohafield => 'items.ccode', authorised_value => [ -and => {'!=' => undef }, {'!=' => ''}] });
-my $ccode_avcode = $mss->count ? $mss->next->authorised_value : 'CCODE';
-my $ccodes = GetAuthorisedValues($ccode_avcode);
-my @ccodes;
-foreach my $ccode (@$ccodes) {
-    push @ccodes, {
-        value => $ccode->{authorised_value},
-        label => $ccode->{lib},
-    };
-}
-
-my @notforloans;
-foreach my $value (@$notforloan_values) {
-    push @notforloans, {
-        value => $value->{authorised_value},
-        label => $value->{lib},
-    };
+my @ccodes = Koha::AuthorisedValues->get_descriptions_by_koha_field({ kohafield => 'items.ccode' });
+foreach my $ccode (@ccodes) {
+    $ccode->{value} = $ccode->{authorised_value},
+    $ccode->{label} = $ccode->{lib},
 }
 
 my @itemlosts;
@@ -350,10 +296,8 @@ foreach my $field (@items_search_fields) {
 
 $template->param(
     branches => \@branches,
-    locations => \@locations,
     itemtypes => \@itemtypes,
     ccodes => \@ccodes,
-    notforloans => \@notforloans,
     itemlosts => \@itemlosts,
     withdrawns => \@withdrawns,
     items_search_fields => \@items_search_fields,
index f52e3a6..63aaee8 100644 (file)
@@ -1,7 +1,7 @@
 [%- USE Branches -%]
 [%- USE Koha -%]
 [%- USE ItemTypes -%]
-[% USE AuthorisedValues %]
+[%- USE AuthorisedValues -%]
 [%- SET biblio = item.biblio -%]
 [%- SET biblioitem = item.biblioitem -%]
-"[% biblio.title | html %] [% IF ( Koha.Preference( 'marcflavour' ) == 'UNIMARC' && biblio.author ) %]by [% END %][% biblio.author | html %]", "[% (biblioitem.publicationyear || biblio.copyrightdate) | html %]", "[% biblioitem.publishercode | html %]", "[% AuthorisedValues.GetByCode( 'CCODE', item.ccode ) | html %]", "[% item.barcode | html %]", "[% item.itemcallnumber | html %]", "[% Branches.GetName(item.homebranch) | html %]", "[% Branches.GetName(item.holdingbranch) | html %]", "[% item.location | html %]", "[% ItemTypes.GetDescription(item.itype) | html %]", "[% item.stocknumber | html %]", "[% item.status | html %]","[% AuthorisedValues.GetByCode( 'LOST', item.itemlost ) || "" | html %]","[% AuthorisedValues.GetByCode( 'WITHDRAWN', item.withdrawn ) || "" | html %]", "[% (item.issues || 0) | html %]"
+"[% biblio.title | html %] [% IF ( Koha.Preference( 'marcflavour' ) == 'UNIMARC' && biblio.author ) %]by [% END %][% biblio.author | html %]", "[% (biblioitem.publicationyear || biblio.copyrightdate) | html %]", "[% biblioitem.publishercode | html %]", "[% AuthorisedValues.GetByCode( 'CCODE', item.ccode ) | html %]", "[% item.barcode | html %]", "[% item.itemcallnumber | html %]", "[% Branches.GetName(item.homebranch) | html %]", "[% Branches.GetName(item.holdingbranch) | html %]", "[% AuthorisedValues.GetDescriptionByKohaField(frameworkcode => biblio.frameworkcode, kohafield => 'items.location', authorised_value => item.location) | html %]", "[% ItemTypes.GetDescription(item.itype) | html %]", "[% item.stocknumber | html %]", "[% AuthorisedValues.GetDescriptionByKohaField(frameworkcode => biblio.frameworkcode, kohafield => 'items.notforloan', authorised_value => item.notforloan) | html %]","[% AuthorisedValues.GetByCode( 'LOST', item.itemlost ) || "" | html %]","[% AuthorisedValues.GetByCode( 'WITHDRAWN', item.withdrawn ) || "" | html %]", "[% (item.issues || 0) | html %]"
index 88ba548..6b17cd6 100644 (file)
@@ -2,11 +2,14 @@
 [%- USE Koha -%]
 [%- USE To -%]
 [%- USE ItemTypes -%]
-[% USE AuthorisedValues %]
+[%- USE AuthorisedValues -%]
 [%- biblio = item.biblio -%]
 [%- biblioitem = item.biblioitem -%]
 [
   "[% FILTER escape_quotes = replace('"', '\"') ~%]
+    <input type="checkbox" name="itemnumber" value="[% item.itemnumber %]"/>
+  [%~ END %]",
+  "[% FILTER escape_quotes ~%]
     <a href="/cgi-bin/koha/catalogue/detail.pl?biblionumber=[% biblio.biblionumber | uri %]" title="Go to record detail page">[% biblio.title | html %]</a>[% IF ( Koha.Preference( 'marcflavour' ) == 'UNIMARC' && biblio.author ) %] by[% END %] [% biblio.author | html %]
   [%~ END %]",
   "[% (biblioitem.publicationyear || biblio.copyrightdate) | html %]",
   "[% item.itemcallnumber | html %]",
   "[% Branches.GetName(item.homebranch) | html %]",
   "[% Branches.GetName(item.holdingbranch) | html %]",
-  "[% item.location | html %]",
+  "[% AuthorisedValues.GetDescriptionByKohaField( frameworkcode => biblio.frameworkcode, kohafield => 'items.location', authorised_value => item.location) | html %]",
   "[% ItemTypes.GetDescription(item.itype) | html %]",
   "[% item.stocknumber | html %]",
-  "[% item.status | html %]",
+  "[% AuthorisedValues.GetDescriptionByKohaField( frameworkcode => biblio.frameworkcode, kohafield => 'items.notforloan', authorised_value => item.notforloan) | html %]",
   "[% AuthorisedValues.GetByCode( 'LOST', item.itemlost ) || "" | html %]",
   "[% AuthorisedValues.GetByCode( 'WITHDRAWN', item.withdrawn ) || "" | html %]",
   "[% (item.issues || 0) | html %]",
index ee1b43e..a9ad87d 100644 (file)
@@ -1,6 +1,7 @@
 [% USE raw %]
 [% USE To %]
 [% USE Asset %]
+[% USE AuthorisedValues %]
 
 [% BLOCK form_label %]
   [% SWITCH label %]
     [%- To.json(escaped) | $raw -%]
 [%- END -%]
 
+[% notforloans = AuthorisedValues.GetDescriptionsByKohaField({ kohafield = 'items.notforloan' }) %]
+[% FOREACH nfl IN notforloans %]
+    [% nfl.value = nfl.authorised_value %]
+    [% nfl.label = nfl.lib %]
+[% END %]
+
+[% locations = AuthorisedValues.GetDescriptionsByKohaField({ kohafield = 'items.location' }) %]
+[% FOREACH loc IN locations %]
+    [% loc.value = loc.authorised_value %]
+    [% loc.label = loc.lib %]
+[% END %]
+
 [%# Page starts here %]
 
 [% SET footerjs = 1 %]
         function submitForm($form) {
             var tr = ''
                 + '    <tr>'
+                + '      <th id="items_checkbox"></th>'
                 + '      <th id="items_title">' + _("Title") + '</th>'
                 + '      <th id="items_pubdate">' + _("Publication date") + '</th>'
                 + '      <th id="items_publisher">' + _("Publisher") + '</th>'
                     $('#item-search-block').show();
                 });
 
+            function exportItems(format) {
+              var itemnumbers = [];
+              $('#results').find('input[name="itemnumber"]:checked').each(function() {
+                itemnumbers.push($(this).val());
+              });
+              if (itemnumbers.length) {
+                var href = '/cgi-bin/koha/catalogue/item-export.pl?format=' + format;
+                href += '&itemnumber=' + itemnumbers.join('&itemnumber=');
+                location = href;
+              } else {
+                $('#format-' + format).prop('checked', true);
+                $('#itemsearchform').submit();
+                $('#format-html').prop('checked', true);
+              }
+            }
+
             var csvExportLink = $('<a>')
                 .attr('href', '#')
-                .html(_("Export results to CSV"))
-                .addClass('btn btn-default btn-xs')
+                .html("CSV")
                 .on('click', function(e) {
                     e.preventDefault();
-                    $('#format-csv').prop('checked', true);
-                    $('#itemsearchform').submit();
-                    $('#format-html').prop('checked', true);
+                    exportItems('csv');
                 });
             var barcodesExportLink = $('<a>')
                 .attr('href', '#')
-                .html(_("Export results to barcodes file"))
-                .addClass('btn btn-default btn-xs')
+                .html(_("Barcodes file"))
                 .on('click', function(e) {
                     e.preventDefault();
-                    $('#format-barcodes').prop('checked', true);
-                    $('#itemsearchform').submit();
-                    $('#format-html').prop('checked', true);
+                    exportItems('barcodes');
               });
 
-            var editSearchAndExportLinks = $('<p>')
-                .append(editSearchLink)
-                .append(' | ')
-                .append(csvExportLink)
-                .append(' ')
-                .append(barcodesExportLink);
+            var exportButton = $('<div>')
+              .addClass('btn-group')
+              .append($('<button>')
+                  .addClass('btn btn-default btn-xs dropdown-toggle')
+                  .attr('id', 'export-button')
+                  .attr('data-toggle', 'dropdown')
+                  .attr('aria-haspopup', 'true')
+                  .attr('aria-expanded', 'false')
+                  .html(_("Export all results to") + ' <span class="caret"></span>'))
+              .append($('<ul>')
+                  .addClass('dropdown-menu')
+                  .append($('<li>').append(csvExportLink))
+                  .append($('<li>').append(barcodesExportLink)));
+
+            var selectVisibleRows = $('<a>')
+              .attr('href', '#')
+              .append('<i class="fa fa-check"></i> ')
+              .append(_("Select visible rows"))
+              .on('click', function(e) {
+                  e.preventDefault();
+                  $('#results input[type="checkbox"]').prop('checked', true).change();
+              });
+            var clearSelection = $('<a>')
+              .attr('href', '#')
+              .append('<i class="fa fa-remove"></i> ')
+              .append(_("Clear selection"))
+              .on('click', function(e) {
+                  e.preventDefault();
+                  $('#results input[type="checkbox"]').prop('checked', false).change();
+              });
+            var exportLinks = $('<p>')
+              .append(selectVisibleRows)
+              .append(' ')
+              .append(clearSelection)
+              .append(' | ')
+              .append(exportButton);
 
             var results_heading = $('<div>').addClass('results-heading')
                 .append("<h1>" + _("Item search results") + "</h1>")
                 .append($('<p>').append(advSearchLink))
-                .append(editSearchAndExportLinks);
+                .append($('<p>').append(editSearchLink))
+                .append(exportLinks);
             $('#results-wrapper').empty()
                 .append(results_heading)
                 .append(table);
                     });
                 },
                 'sDom': '<"top pager"ilp>t<"bottom pager"ip>r',
+                'aaSorting': [[1, 'asc']],
                 'aoColumns': [
+                    { 'sName': 'checkbox', 'bSortable': false },
                     { 'sName': 'title' },
                     { 'sName': 'publicationyear' },
                     { 'sName': 'publishercode' },
                     { 'sName': 'itemlost' },
                     { 'sName': 'withdrawn' },
                     { 'sName': 'issues' },
-                    { 'sName': 'checkbox', 'bSortable': false }
+                    { 'sName': 'actions', 'bSortable': false }
                 ],
                 "sPaginationType": "full_numbers"
             })).columnFilter({
                 'sPlaceHolder': 'head:after',
                 'aoColumns': [
+                    null,
                     { 'type': 'text' },
                     { 'type': 'text' },
                     { 'type': 'text' },
                     null
                 ]
             });
+
+            $('#results').on('change', 'input[type="checkbox"]', function() {
+              var countSelected = $(this).parents('table').find('input:checked').length;
+              var caret = ' <span class="caret">';
+              if (countSelected > 0) {
+                $('#export-button').html(_("Export selected results to") + caret);
+              } else {
+                $('#export-button').html(_("Export all results to") + caret);
+              }
+            });
         }
         var Sticky;
         $(document).ready(function () {