Bug 11425: Add item search form in staff interface
authorJulian Maurice <julian.maurice@biblibre.com>
Thu, 4 Jul 2013 14:52:58 +0000 (16:52 +0200)
committerTomas Cohen Arazi <tomascohen@gmail.com>
Tue, 4 Nov 2014 22:08:12 +0000 (19:08 -0300)
Item search is available at catalogue/itemsearch.pl (link is in
catalogue/search.pl)
It only uses SQL (not Zebra)
* Use DataTables and server-side processing to be able to filter on
  individual columns after the first search is done.
* Allow to export results in CSV
* With Javascript disabled, search form still works (and CSV export too)

There is the possibility to define "Custom search fields" in a new admin
page admin/items_search_fields.pl (link is in admin/admin-home.pl)
A custom item search field is defined by:
* a name: its unique identifier
* a label: the text displayed to the user
* a MARC field/subfield: the field/subfield to query (it uses
  ExtractValue)
* an authorised values list (optional): if defined the list is displayed
  in the search form

New Perl dependency: Template::Plugin::JSON::Escape

Test plan:
1/ Apply the patch and run updatedatabase.pl
2/ Go to advanced search (staff interface), then click on "Go to item
search"
3/ Play with the search form! :)
In the 3rd fieldset you can add as many fields as you want and combine them with
boolean operators (AND, OR). You can use SQL jokers characters (%, _)
You can output to screen (in a DataTables table) or to a CSV file.
4/ In the DataTables table, play with filters and try sorting columns.
5/ Disable Javascript (with Firefox: extensions NoScript or YesScript,
or in about:config 'javascript.enabled' = false
6/ Reload the search page and do some searches on screen output. (there
is no sorting or filtering features, but there is still pagination)
7/ Try again CSV output.
8/ You can re-enable Javascript.
9/ Go to Administration > Items search fields
10/ Add a new field. Example for title (in UNIMARC):
  Name: title
  Label: Title
  MARC field: 200
  MARC subfield: a
  Authorised values category: None
(add another field with an authorised values category to see the
difference).
11/ As you are there try to update and delete some fields.
12/ Go back to items search form. You can see in the 3rd fieldset that
your fields have appeared in the selects.
13/ Try searching on them.
14/ I think you're done :)

Signed-off-by: Bernardo Gonzalez Kriegel <bgkriegel@gmail.com>
Work as described. Good new option.
No koha-qa errors

Signed-off-by: Kyle M Hall <kyle@bywatersolutions.com>
24 files changed:
C4/Installer/PerlDependencies.pm
C4/Items.pm
C4/SQLHelper.pm
Koha/Item/Search/Field.pm [new file with mode: 0644]
acqui/check_uniqueness.pl
admin/items_search_field.pl [new file with mode: 0755]
admin/items_search_fields.pl [new file with mode: 0755]
catalogue/itemsearch.pl [new file with mode: 0755]
installer/data/mysql/kohastructure.sql
installer/data/mysql/updatedatabase.pl
koha-tmpl/intranet-tmpl/prog/en/css/itemsearchform.css [new file with mode: 0644]
koha-tmpl/intranet-tmpl/prog/en/includes/admin-items-search-field-form.inc [new file with mode: 0644]
koha-tmpl/intranet-tmpl/prog/en/includes/admin-menu.inc
koha-tmpl/intranet-tmpl/prog/en/includes/catalogue/itemsearch_item.csv.inc [new file with mode: 0644]
koha-tmpl/intranet-tmpl/prog/en/includes/catalogue/itemsearch_item.inc [new file with mode: 0644]
koha-tmpl/intranet-tmpl/prog/en/includes/catalogue/itemsearch_item.json.inc [new file with mode: 0644]
koha-tmpl/intranet-tmpl/prog/en/includes/catalogue/itemsearch_items.inc [new file with mode: 0644]
koha-tmpl/intranet-tmpl/prog/en/modules/admin/admin-home.tt
koha-tmpl/intranet-tmpl/prog/en/modules/admin/items_search_field.tt [new file with mode: 0644]
koha-tmpl/intranet-tmpl/prog/en/modules/admin/items_search_fields.tt [new file with mode: 0644]
koha-tmpl/intranet-tmpl/prog/en/modules/catalogue/advsearch.tt
koha-tmpl/intranet-tmpl/prog/en/modules/catalogue/itemsearch.csv.tt [new file with mode: 0644]
koha-tmpl/intranet-tmpl/prog/en/modules/catalogue/itemsearch.json.tt [new file with mode: 0644]
koha-tmpl/intranet-tmpl/prog/en/modules/catalogue/itemsearch.tt [new file with mode: 0644]

index a6a5bd9..09be2cd 100644 (file)
@@ -552,6 +552,11 @@ our $PERL_DEPS = {
         'required' => '1',
         'min_ver'  => '0.03',
     },
+    'Template::Plugin::JSON::Escape' => {
+        'usage'    => 'Core',
+        'required' => '1',
+        'min_ver'  => '0.02',
+    },
     'DBD::Mock' => {
         'usage'    => 'Core',
         'required' => '1',
index fde9d95..ab9c4e1 100644 (file)
@@ -35,6 +35,7 @@ use DateTime::Format::MySQL;
 use Data::Dumper; # used as part of logging item record changes, not just for
                   # debugging; so please don't remove this
 use Koha::DateUtils qw/dt_from_string/;
+use C4::SQLHelper qw(GetColumns);
 
 use vars qw($VERSION @ISA @EXPORT);
 
@@ -85,6 +86,7 @@ BEGIN {
        GetAnalyticsCount
         GetItemHolds
 
+        SearchItemsByField
         SearchItems
 
         PrepareItemrecordDisplay
@@ -2590,39 +2592,194 @@ sub GetItemHolds {
     return $holds;
 }
 
-# Return the list of the column names of items table
-sub _get_items_columns {
-    my $dbh = C4::Context->dbh;
-    my $sth = $dbh->column_info(undef, undef, 'items', '%');
-    $sth->execute;
-    my $results = $sth->fetchall_hashref('COLUMN_NAME');
-    return keys %$results;
+=head2 SearchItemsByField
+
+    my $items = SearchItemsByField($field, $value);
+
+SearchItemsByField will search for items on a specific given field.
+For instance you can search all items with a specific stocknumber like this:
+
+    my $items = SearchItemsByField('stocknumber', $stocknumber);
+
+=cut
+
+sub SearchItemsByField {
+    my ($field, $value) = @_;
+
+    my $filters = [ {
+            field => $field,
+            query => $value,
+    } ];
+
+    my ($results) = SearchItems($filters);
+    return $results;
+}
+
+sub _SearchItems_build_where_fragment {
+    my ($filter) = @_;
+
+    my $where_fragment;
+    if (exists($filter->{conjunction})) {
+        my (@where_strs, @where_args);
+        foreach my $f (@{ $filter->{filters} }) {
+            my $fragment = _SearchItems_build_where_fragment($f);
+            if ($fragment) {
+                push @where_strs, $fragment->{str};
+                push @where_args, @{ $fragment->{args} };
+            }
+        }
+        my $where_str = '';
+        if (@where_strs) {
+            $where_str = '(' . join (' ' . $filter->{conjunction} . ' ', @where_strs) . ')';
+            $where_fragment = {
+                str => $where_str,
+                args => \@where_args,
+            };
+        }
+    } else {
+        my @columns = GetColumns('items');
+        push @columns, GetColumns('biblio');
+        push @columns, GetColumns('biblioitems');
+        my @operators = qw(= != > < >= <= like);
+        my $field = $filter->{field};
+        if ( (0 < grep /^$field$/, @columns) or (substr($field, 0, 5) eq 'marc:') ) {
+            my $op = $filter->{operator};
+            my $query = $filter->{query};
+
+            if (!$op or (0 == grep /^$op$/, @operators)) {
+                $op = '='; # default operator
+            }
+
+            my $column;
+            if ($field =~ /^marc:(\d{3})(?:\$(\w))?$/) {
+                my $marcfield = $1;
+                my $marcsubfield = $2;
+                my $xpath;
+                if ($marcfield < 10) {
+                    $xpath = "//record/controlfield[\@tag=\"$marcfield\"]";
+                } else {
+                    $xpath = "//record/datafield[\@tag=\"$marcfield\"]/subfield[\@code=\"$marcsubfield\"]";
+                }
+                $column = "ExtractValue(marcxml, '$xpath')";
+            } else {
+                $column = $field;
+            }
+
+            if (ref $query eq 'ARRAY') {
+                if ($op eq '=') {
+                    $op = 'IN';
+                } elsif ($op eq '!=') {
+                    $op = 'NOT IN';
+                }
+                $where_fragment = {
+                    str => "$column $op (" . join (',', ('?') x @$query) . ")",
+                    args => $query,
+                };
+            } else {
+                $where_fragment = {
+                    str => "$column $op ?",
+                    args => [ $query ],
+                };
+            }
+        }
+    }
+
+    return $where_fragment;
 }
 
 =head2 SearchItems
 
-    my $items = SearchItems($field, $value);
+    my ($items, $total) = SearchItemsByField($filters, $params);
 
-SearchItems will search for items on a specific given field.
-For instance you can search all items with a specific stocknumber like this:
+Perform a search among items
 
-    my $items = SearchItems('stocknumber', $stocknumber);
+$filters is a reference to an array of filters, where each filter is a hash with
+the following keys:
+
+=over 2
+
+=item * field: the name of a SQL column in table items
+
+=item * query: the value to search in this column
+
+=item * operator: comparison operator. Can be one of = != > < >= <= like
+
+=back
+
+A logical AND is used to combine filters.
+
+$params is a reference to a hash that can contain the following parameters:
+
+=over 2
+
+=item * rows: Number of items to return. 0 returns everything (default: 0)
+
+=item * page: Page to return (return items from (page-1)*rows to (page*rows)-1)
+               (default: 1)
+
+=item * sortby: A SQL column name in items table to sort on
+
+=item * sortorder: 'ASC' or 'DESC'
+
+=back
 
 =cut
 
 sub SearchItems {
-    my ($field, $value) = @_;
+    my ($filter, $params) = @_;
+
+    $filter //= {};
+    $params //= {};
+    return unless ref $filter eq 'HASH';
+    return unless ref $params eq 'HASH';
+
+    # Default parameters
+    $params->{rows} ||= 0;
+    $params->{page} ||= 1;
+    $params->{sortby} ||= 'itemnumber';
+    $params->{sortorder} ||= 'ASC';
+
+    my ($where_str, @where_args);
+    my $where_fragment = _SearchItems_build_where_fragment($filter);
+    if ($where_fragment) {
+        $where_str = $where_fragment->{str};
+        @where_args = @{ $where_fragment->{args} };
+    }
 
     my $dbh = C4::Context->dbh;
-    my @columns = _get_items_columns;
-    my $results = [];
-    if(0 < grep /^$field$/, @columns) {
-        my $query = "SELECT $field FROM items WHERE $field = ?";
-        my $sth = $dbh->prepare( $query );
-        $sth->execute( $value );
-        $results = $sth->fetchall_arrayref({});
+    my $query = q{
+        SELECT SQL_CALC_FOUND_ROWS items.*
+        FROM items
+          LEFT JOIN biblio ON biblio.biblionumber = items.biblionumber
+          LEFT JOIN biblioitems ON biblioitems.biblioitemnumber = items.biblioitemnumber
+    };
+    if (defined $where_str and $where_str ne '') {
+        $query .= qq{ WHERE $where_str };
     }
-    return $results;
+
+    my @columns = GetColumns('items');
+    push @columns, GetColumns('biblio');
+    push @columns, GetColumns('biblioitems');
+    my $sortby = (0 < grep {$params->{sortby} eq $_} @columns)
+        ? $params->{sortby} : 'itemnumber';
+    my $sortorder = (uc($params->{sortorder}) eq 'ASC') ? 'ASC' : 'DESC';
+    $query .= qq{ ORDER BY $sortby $sortorder };
+
+    my $rows = $params->{rows};
+    my @limit_args;
+    if ($rows > 0) {
+        my $offset = $rows * ($params->{page}-1);
+        $query .= qq { LIMIT ?, ? };
+        push @limit_args, $offset, $rows;
+    }
+
+    my $sth = $dbh->prepare($query);
+    my $rv = $sth->execute(@where_args, @limit_args);
+
+    return unless ($rv);
+    my ($total_rows) = $dbh->selectrow_array(q{ SELECT FOUND_ROWS() });
+
+    return ($sth->fetchall_arrayref({}), $total_rows);
 }
 
 
index b867094..55204eb 100644 (file)
@@ -56,6 +56,7 @@ BEGIN {
        UpdateInTable
        GetPrimaryKeys
         clear_columns_cache
+    GetColumns
 );
        %EXPORT_TAGS = ( all =>[qw( InsertInTable DeleteInTable SearchInTable UpdateInTable GetPrimaryKeys)]
                                );
@@ -296,6 +297,38 @@ sub _get_columns {
     return $hashref->{$tablename};
 }
 
+=head2 GetColumns
+
+    my @columns = GetColumns($tablename);
+
+Given a tablename, returns an array of columns names.
+
+=cut
+
+sub GetColumns {
+    my ($tablename) = @_;
+
+    return unless $tablename;
+    return unless $tablename =~ /^[a-zA-Z0-9_]+$/;
+
+    # Get the database handle.
+    my $dbh = C4::Context->dbh;
+
+    # Pure ANSI SQL goodness.
+    my $sql = "SELECT * FROM $tablename WHERE 1=0;";
+
+    # Run the SQL statement to load STH's readonly properties.
+    my $sth = $dbh->prepare($sql);
+    my $rv = $sth->execute();
+
+    my @data;
+    if ($rv) {
+        @data = @{$sth->{NAME}};
+    }
+
+    return @data;
+}
+
 =head2 _filter_columns
 
 =over 4
diff --git a/Koha/Item/Search/Field.pm b/Koha/Item/Search/Field.pm
new file mode 100644 (file)
index 0000000..6b1e137
--- /dev/null
@@ -0,0 +1,107 @@
+package Koha::Item::Search::Field;
+
+use Modern::Perl;
+use base qw( Exporter );
+
+our @EXPORT_OK = qw(
+    AddItemSearchField
+    ModItemSearchField
+    DelItemSearchField
+    GetItemSearchField
+    GetItemSearchFields
+);
+
+use C4::Context;
+
+sub AddItemSearchField {
+    my ($field) = @_;
+
+    my ( $name, $label, $tagfield, $tagsubfield, $av_category ) =
+      @$field{qw(name label tagfield tagsubfield authorised_values_category)};
+
+    my $dbh = C4::Context->dbh;
+    my $query = q{
+        INSERT INTO items_search_fields (name, label, tagfield, tagsubfield, authorised_values_category)
+        VALUES (?, ?, ?, ?, ?)
+    };
+    my $sth = $dbh->prepare($query);
+    my $rv = $sth->execute($name, $label, $tagfield, $tagsubfield, $av_category);
+
+    return ($rv) ? $field : undef;
+}
+
+sub ModItemSearchField {
+    my ($field) = @_;
+
+    my ( $name, $label, $tagfield, $tagsubfield, $av_category ) =
+      @$field{qw(name label tagfield tagsubfield authorised_values_category)};
+
+    my $dbh = C4::Context->dbh;
+    my $query = q{
+        UPDATE items_search_fields
+        SET label = ?,
+            tagfield = ?,
+            tagsubfield = ?,
+            authorised_values_category = ?
+        WHERE name = ?
+    };
+    my $sth = $dbh->prepare($query);
+    my $rv = $sth->execute($label, $tagfield, $tagsubfield, $av_category, $name);
+
+    return ($rv) ? $field : undef;
+}
+
+sub DelItemSearchField {
+    my ($name) = @_;
+
+    my $dbh = C4::Context->dbh;
+    my $query = q{
+        DELETE FROM items_search_fields
+        WHERE name = ?
+    };
+    my $sth = $dbh->prepare($query);
+    my $rv = $sth->execute($name);
+
+    my $is_deleted = $rv ? int($rv) : 0;
+    if (!$is_deleted) {
+        warn "DelItemSearchField: Field '$name' doesn't exist";
+    }
+
+    return $is_deleted;
+}
+
+sub GetItemSearchField {
+    my ($name) = @_;
+
+    my $dbh = C4::Context->dbh;
+    my $query = q{
+        SELECT * FROM items_search_fields
+        WHERE name = ?
+    };
+    my $sth = $dbh->prepare($query);
+    my $rv = $sth->execute($name);
+
+    my $field;
+    if ($rv) {
+        $field = $sth->fetchrow_hashref;
+    }
+
+    return $field;
+}
+
+sub GetItemSearchFields {
+    my $dbh = C4::Context->dbh;
+    my $query = q{
+        SELECT * FROM items_search_fields
+    };
+    my $sth = $dbh->prepare($query);
+    my $rv = $sth->execute();
+
+    my @fields;
+    if ($rv) {
+        my $fields = $sth->fetchall_arrayref( {} );
+        @fields = @$fields;
+    }
+
+    return @fields;
+}
index 8deb195..d8f29fa 100755 (executable)
@@ -43,7 +43,7 @@ my @value = $input->param('value[]');
 my $r = {};
 my $i = 0;
 for ( my $i=0; $i<@field; $i++ ) {
-    my $items = C4::Items::SearchItems($field[$i], $value[$i]);
+    my $items = C4::Items::SearchItemsByField($field[$i], $value[$i]);
 
     if ( @$items ) {
         push @{ $r->{$field[$i]} }, $value[$i];
diff --git a/admin/items_search_field.pl b/admin/items_search_field.pl
new file mode 100755 (executable)
index 0000000..a544959
--- /dev/null
@@ -0,0 +1,63 @@
+#!/usr/bin/perl
+# Copyright 2013 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;
+use C4::Koha;
+
+use Koha::Item::Search::Field qw(GetItemSearchField ModItemSearchField);
+
+my $cgi = new CGI;
+
+my ($template, $borrowernumber, $cookie) = get_template_and_user({
+    template_name => 'admin/items_search_field.tt',
+    query => $cgi,
+    type => 'intranet',
+    authnotrequired => 0,
+    flagsrequired   => { catalogue => 1 },
+});
+
+my $op = $cgi->param('op') || '';
+my $name = $cgi->param('name');
+
+if ($op eq 'mod') {
+    my %vars = $cgi->Vars;
+    my $field = { name => $name };
+    my @params = qw(label tagfield tagsubfield authorised_values_category);
+    @$field{@params} = @vars{@params};
+    if ( $field->{authorised_values_category} eq '' ) {
+        $field->{authorised_values_category} = undef;
+    }
+    $field = ModItemSearchField($field);
+    my $updated = ($field) ? 1 : 0;
+    print $cgi->redirect('/cgi-bin/koha/admin/items_search_fields.pl?updated=' . $updated);
+    exit;
+}
+
+my $field = GetItemSearchField($name);
+my $authorised_values_categories = C4::Koha::GetAuthorisedValueCategories();
+
+$template->param(
+    field => $field,
+    authorised_values_categories => $authorised_values_categories,
+);
+
+output_html_with_http_headers $cgi, $cookie, $template->output;
diff --git a/admin/items_search_fields.pl b/admin/items_search_fields.pl
new file mode 100755 (executable)
index 0000000..1ebf243
--- /dev/null
@@ -0,0 +1,81 @@
+#!/usr/bin/perl
+# Copyright 2013 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;
+use C4::Koha;
+
+use Koha::Item::Search::Field qw(AddItemSearchField GetItemSearchFields DelItemSearchField);
+
+my $cgi = new CGI;
+
+my ($template, $borrowernumber, $cookie) = get_template_and_user({
+    template_name => 'admin/items_search_fields.tt',
+    query => $cgi,
+    type => 'intranet',
+    authnotrequired => 0,
+    flagsrequired   => { catalogue => 1 },
+});
+
+my $op = $cgi->param('op') || '';
+
+if ($op eq 'add') {
+    my %vars = $cgi->Vars;
+    my $field;
+    my @params = qw(name label tagfield tagsubfield authorised_values_category);
+    @$field{@params} = @vars{@params};
+    if ( $field->{authorised_values_category} eq '' ) {
+        $field->{authorised_values_category} = undef;
+    }
+    $field = AddItemSearchField($field);
+    if ($field) {
+        $template->param(field_added => $field);
+    } else {
+        $template->param(field_not_added => 1);
+    }
+} elsif ($op eq 'del') {
+    my $name = $cgi->param('name');
+    my $rv = DelItemSearchField($name);
+    if ($rv) {
+        $template->param(field_deleted => 1);
+    } else {
+        $template->param(field_not_deleted => 1);
+    }
+} else {
+    my $updated = $cgi->param('updated');
+    if (defined $updated) {
+        if ($updated) {
+            $template->param(field_updated => 1);
+        } else {
+            $template->param(field_not_updated => 1);
+        }
+    }
+}
+
+my @fields = GetItemSearchFields();
+my $authorised_values_categories = C4::Koha::GetAuthorisedValueCategories();
+
+$template->param(
+    fields => \@fields,
+    authorised_values_categories => $authorised_values_categories,
+);
+
+output_html_with_http_headers $cgi, $cookie, $template->output;
diff --git a/catalogue/itemsearch.pl b/catalogue/itemsearch.pl
new file mode 100755 (executable)
index 0000000..cb025a5
--- /dev/null
@@ -0,0 +1,298 @@
+#!/usr/bin/perl
+# Copyright 2013 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 JSON;
+
+use C4::Auth;
+use C4::Output;
+use C4::Items;
+use C4::Biblio;
+use C4::Branch;
+use C4::Koha;
+use C4::ItemType;
+
+use Koha::Item::Search::Field qw(GetItemSearchFields);
+
+my $cgi = new CGI;
+my %params = $cgi->Vars;
+
+my $format = $cgi->param('format');
+my ($template_name, $content_type);
+if (defined $format and $format eq 'json') {
+    $template_name = 'catalogue/itemsearch.json.tt';
+    $content_type = 'json';
+
+    # Map DataTables parameters with 'regular' parameters
+    $cgi->param('rows', $cgi->param('iDisplayLength'));
+    $cgi->param('page', ($cgi->param('iDisplayStart') / $cgi->param('iDisplayLength')) + 1);
+    my @columns = split /,/, $cgi->param('sColumns');
+    $cgi->param('sortby', $columns[ $cgi->param('iSortCol_0') ]);
+    $cgi->param('sortorder', $cgi->param('sSortDir_0'));
+
+    my @f = $cgi->param('f');
+    my @q = $cgi->param('q');
+    push @q, '' if @q == 0;
+    my @op = $cgi->param('op');
+    my @c = $cgi->param('c');
+    foreach my $i (0 .. ($cgi->param('iColumns') - 1)) {
+        my $sSearch = $cgi->param("sSearch_$i");
+        if ($sSearch) {
+            my @words = split /\s+/, $sSearch;
+            foreach my $word (@words) {
+                push @f, $columns[$i];
+                push @q, "%$word%";
+                push @op, 'like';
+                push @c, 'and';
+            }
+        }
+    }
+    $cgi->param('f', @f);
+    $cgi->param('q', @q);
+    $cgi->param('op', @op);
+    $cgi->param('c', @c);
+} elsif (defined $format and $format eq 'csv') {
+    $template_name = 'catalogue/itemsearch.csv.tt';
+
+    # Retrieve all results
+    $cgi->param('rows', 0);
+} else {
+    $format = 'html';
+    $template_name = 'catalogue/itemsearch.tt';
+    $content_type = 'html';
+}
+
+my ($template, $borrowernumber, $cookie) = get_template_and_user({
+    template_name => $template_name,
+    query => $cgi,
+    type => 'intranet',
+    authnotrequired => 0,
+    flagsrequired   => { catalogue => 1 },
+});
+
+my $notforloan_avcode = GetAuthValCode('items.notforloan');
+my $notforloan_values = GetAuthorisedValues($notforloan_avcode);
+
+if (scalar keys %params > 0) {
+    # Parameters given, it's a search
+
+    my $filter = {
+        conjunction => 'AND',
+        filters => [],
+    };
+
+    foreach my $p (qw(homebranch location itype ccode issues datelastborrowed)) {
+        if (my @q = $cgi->param($p)) {
+            if ($q[0] ne '') {
+                my $f = {
+                    field => $p,
+                    query => \@q,
+                };
+                if (my $op = $cgi->param($p . '_op')) {
+                    $f->{operator} = $op;
+                }
+                push @{ $filter->{filters} }, $f;
+            }
+        }
+    }
+
+    my @c = $cgi->param('c');
+    my @fields = $cgi->param('f');
+    my @q = $cgi->param('q');
+    my @op = $cgi->param('op');
+
+    my $f;
+    for (my $i = 0; $i < @fields; $i++) {
+        my $field = $fields[$i];
+        my $q = shift @q;
+        my $op = shift @op;
+        if (defined $q and $q ne '') {
+            if ($i == 0) {
+                $f = {
+                    field => $field,
+                    query => $q,
+                    operator => $op,
+                };
+            } else {
+                my $c = shift @c;
+                $f = {
+                    conjunction => $c,
+                    filters => [
+                        $f, {
+                            field => $field,
+                            query => $q,
+                            operator => $op,
+                        }
+                    ],
+                };
+            }
+        }
+    }
+    push @{ $filter->{filters} }, $f;
+
+    # Yes/No parameters
+    foreach my $p (qw(damaged itemlost)) {
+        my $v = $cgi->param($p) // '';
+        my $f = {
+            field => $p,
+            query => 0,
+        };
+        if ($v eq 'yes') {
+            $f->{operator} = '!=';
+            push @{ $filter->{filters} }, $f;
+        } elsif ($v eq 'no') {
+            $f->{operator} = '=';
+            push @{ $filter->{filters} }, $f;
+        }
+    }
+
+    if (my $itemcallnumber_from = $cgi->param('itemcallnumber_from')) {
+        push @{ $filter->{filters} }, {
+            field => 'itemcallnumber',
+            query => $itemcallnumber_from,
+            operator => '>=',
+        };
+    }
+    if (my $itemcallnumber_to = $cgi->param('itemcallnumber_to')) {
+        push @{ $filter->{filters} }, {
+            field => 'itemcallnumber',
+            query => $itemcallnumber_to,
+            operator => '<=',
+        };
+    }
+
+    my $search_params = {
+        rows => $cgi->param('rows') // 20,
+        page => $cgi->param('page') || 1,
+        sortby => $cgi->param('sortby') || 'itemnumber',
+        sortorder => $cgi->param('sortorder') || 'asc',
+    };
+
+    my ($results, $total_rows) = SearchItems($filter, $search_params);
+    if ($results) {
+        # Get notforloan labels
+        my $notforloan_map = {};
+        foreach my $nfl_value (@$notforloan_values) {
+            $notforloan_map->{$nfl_value->{authorised_value}} = $nfl_value->{lib};
+        }
+
+        foreach my $item (@$results) {
+            $item->{biblio} = GetBiblio($item->{biblionumber});
+            ($item->{biblioitem}) = GetBiblioItemByBiblioNumber($item->{biblionumber});
+            $item->{status} = $notforloan_map->{$item->{notforloan}};
+        }
+    }
+
+    $template->param(
+        filter => $filter,
+        search_params => $search_params,
+        results => $results,
+        total_rows => $total_rows,
+        search_done => 1,
+    );
+
+    if ($format eq 'html') {
+        # Build pagination bar
+        my $url = $cgi->url(-absolute => 1);
+        my @params;
+        foreach my $p (keys %params) {
+            my @v = $cgi->param($p);
+            push @params, map { "$p=" . $_ } @v;
+        }
+        $url .= '?' . join ('&', @params);
+        my $nb_pages = 1 + int($total_rows / $search_params->{rows});
+        my $current_page = $search_params->{page};
+        my $pagination_bar = pagination_bar($url, $nb_pages, $current_page, 'page');
+
+        $template->param(pagination_bar => $pagination_bar);
+    }
+}
+
+if ($format eq 'html') {
+    # Retrieve data required for the form.
+
+    my $branches = GetBranches();
+    my @branches;
+    foreach my $branchcode (keys %$branches) {
+        push @branches, {
+            value => $branchcode,
+            label => $branches->{$branchcode}->{branchname},
+        };
+    }
+    my $locations = GetAuthorisedValues('LOC');
+    my @locations;
+    foreach my $location (@$locations) {
+        push @locations, {
+            value => $location->{authorised_value},
+            label => $location->{lib},
+        };
+    }
+    my @itemtypes = C4::ItemType->all();
+    foreach my $itemtype (@itemtypes) {
+        $itemtype->{value} = $itemtype->{itemtype};
+        $itemtype->{label} = $itemtype->{description};
+    }
+    my $ccode_avcode = GetAuthValCode('items.ccode') || '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 @items_search_fields = GetItemSearchFields();
+
+    my $authorised_values = {};
+    foreach my $field (@items_search_fields) {
+        if (my $category = ($field->{authorised_values_category})) {
+            $authorised_values->{$category} = GetAuthorisedValues($category);
+        }
+    }
+
+    $template->param(
+        branches => \@branches,
+        locations => \@locations,
+        itemtypes => \@itemtypes,
+        ccodes => \@ccodes,
+        notforloans => \@notforloans,
+        items_search_fields => \@items_search_fields,
+        authorised_values_json => to_json($authorised_values),
+    );
+}
+
+if ($format eq 'csv') {
+    print $cgi->header({
+        type => 'text/csv',
+        attachment => 'items.csv',
+    });
+    print $template->output;
+} else {
+    output_with_http_headers $cgi, $cookie, $template->output, $content_type;
+}
index 865f930..12fb181 100644 (file)
@@ -3461,6 +3461,23 @@ CREATE TABLE IF NOT EXISTS columns_settings (
     PRIMARY KEY(module, page, tablename, columnname)
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
 
+--
+-- Table structure for table 'items_search_fields'
+--
+
+DROP TABLE IF EXISTS items_search_fields;
+CREATE TABLE items_search_fields (
+  name VARCHAR(255) NOT NULL,
+  label VARCHAR(255) NOT NULL,
+  tagfield CHAR(3) NOT NULL,
+  tagsubfield CHAR(1) NULL DEFAULT NULL,
+  authorised_values_category VARCHAR(16) NULL DEFAULT NULL,
+  PRIMARY KEY(name),
+  CONSTRAINT items_search_fields_authorised_values_category
+    FOREIGN KEY (authorised_values_category) REFERENCES authorised_values (category)
+    ON DELETE SET NULL ON UPDATE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
 /*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */;
 /*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
 /*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;
index 0dd917e..7d214d3 100755 (executable)
@@ -8986,6 +8986,25 @@ if ( CheckVersion($DBversion) ) {
     SetVersion($DBversion);
 }
 
+$DBversion = "3.17.00.XXX";
+if ( CheckVersion($DBversion) ) {
+    $dbh->do(q{
+        CREATE TABLE IF NOT EXISTS items_search_fields (
+          name VARCHAR(255) NOT NULL,
+          label VARCHAR(255) NOT NULL,
+          tagfield CHAR(3) NOT NULL,
+          tagsubfield CHAR(1) NULL DEFAULT NULL,
+          authorised_values_category VARCHAR(16) NULL DEFAULT NULL,
+          PRIMARY KEY(name),
+          CONSTRAINT items_search_fields_authorised_values_category
+            FOREIGN KEY (authorised_values_category) REFERENCES authorised_values (category)
+            ON DELETE SET NULL ON UPDATE CASCADE
+        ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+    });
+    print "Upgrade to $DBversion done (Bug 11425: Add items_search_fields table)\n";
+    SetVersion($DBversion);
+}
+
 =head1 FUNCTIONS
 
 =head2 TableExists($table)
diff --git a/koha-tmpl/intranet-tmpl/prog/en/css/itemsearchform.css b/koha-tmpl/intranet-tmpl/prog/en/css/itemsearchform.css
new file mode 100644 (file)
index 0000000..34c76de
--- /dev/null
@@ -0,0 +1,44 @@
+.form-field {
+  margin: 5px 0;
+}
+
+.form-field > * {
+  vertical-align: middle;
+}
+
+.form-field-label {
+  display: inline-block;
+  text-align: right;
+  width: 10em;
+}
+
+.form-field-conjunction[disabled] {
+  visibility: hidden;
+}
+
+.form-field-radio > * {
+  vertical-align: middle;
+}
+
+.form-actions {
+  margin-top: 20px;
+}
+
+th.active {
+  padding-right: 21px;
+  background-repeat: no-repeat;
+  background-position: 100% 50%;
+}
+
+th.sort-asc {
+  background-image: url('../../img/asc.gif');
+}
+
+th.sort-desc {
+  background-image: url('../../img/desc.gif');
+}
+
+th select {
+  width: 100%;
+  font-weight: normal;
+}
diff --git a/koha-tmpl/intranet-tmpl/prog/en/includes/admin-items-search-field-form.inc b/koha-tmpl/intranet-tmpl/prog/en/includes/admin-items-search-field-form.inc
new file mode 100644 (file)
index 0000000..995e1f6
--- /dev/null
@@ -0,0 +1,58 @@
+<ul>
+  <li>
+    <label for="name">Name</label>
+    [% IF field %]
+      <input type="text" name="name" value="[% field.name %]" disabled="disabled">
+      <input type="hidden" name="name" value="[% field.name %]">
+    [% ELSE %]
+      <input type="text" name="name" />
+    [% END %]
+  </li>
+  <li>
+    <label for="label">Label</label>
+    [% IF field %]
+      <input type="text" name="label" value="[% field.label %]" />
+    [% ELSE %]
+      <input type="text" name="label" />
+    [% END %]
+  </li>
+  <li>
+    <label for="tagfield">MARC field</label>
+    <select name="tagfield">
+      [% FOREACH tagfield IN ['001'..'999'] %]
+        [% IF field && field.tagfield == tagfield %]
+          <option value="[% tagfield %]" selected="selected">[% tagfield %]</option>
+        [% ELSE %]
+          <option value="[% tagfield %]">[% tagfield %]</option>
+        [% END %]
+      [% END %]
+    </select>
+  </li>
+  <li>
+    <label for="tagsubfield">MARC subfield</label>
+    <select name="tagsubfield">
+      [% codes = [''] %]
+      [% codes = codes.merge([0..9], ['a'..'z']) %]
+      [% FOREACH tagsubfield IN codes %]
+        [% IF field && field.tagsubfield == tagsubfield %]
+          <option value="[% tagsubfield %]" selected="selected">[% tagsubfield %]</option>
+        [% ELSE %]
+          <option value="[% tagsubfield %]">[% tagsubfield %]</option>
+        [% END %]
+      [% END %]
+    </select>
+  </li>
+  <li>
+    <label for="authorised_values_category">Authorised values category</label>
+    <select name="authorised_values_category">
+      <option value="">- None -</option>
+      [% FOREACH category IN authorised_values_categories %]
+        [% IF field && field.authorised_values_category == category %]
+          <option value="[% category %]" selected="selected">[% category %]</option>
+        [% ELSE %]
+          <option value="[% category %]">[% category %]</option>
+        [% END %]
+      [% END %]
+    </select>
+  </li>
+</ul>
index a86aab2..281bb54 100644 (file)
@@ -45,6 +45,7 @@
     <li><a href="/cgi-bin/koha/admin/classsources.pl">Classification sources</a></li>
     <li><a href="/cgi-bin/koha/admin/matching-rules.pl">Record matching rules</a></li>
     <li><a href="/cgi-bin/koha/admin/oai_sets.pl">OAI sets configuration</a></li>
+    <li><a href="/cgi-bin/koha/admin/items_search_fields.pl">Items search fields</a></li>
 </ul>
 
 <h5>Acquisition parameters</h5>
diff --git a/koha-tmpl/intranet-tmpl/prog/en/includes/catalogue/itemsearch_item.csv.inc b/koha-tmpl/intranet-tmpl/prog/en/includes/catalogue/itemsearch_item.csv.inc
new file mode 100644 (file)
index 0000000..fd6e835
--- /dev/null
@@ -0,0 +1,4 @@
+[%- USE Branches -%]
+[%- biblio = item.biblio -%]
+[%- biblioitem = item.biblioitem -%]
+"[% biblio.title |html %] by [% biblio.author |html %]", "[% biblioitem.publicationyear |html %]", "[% biblioitem.publishercode |html %]", "[% biblioitem.collectiontitle |html %]", "[% item.barcode |html %]", "[% item.itemcallnumber |html %]", "[% Branches.GetName(item.homebranch) |html %]", "[% Branches.GetName(item.holdingbranch) |html %]", "[% item.location |html %]", "[% item.stocknumber |html %]", "[% item.status |html %]", "[% (item.issues || 0) |html %]"
diff --git a/koha-tmpl/intranet-tmpl/prog/en/includes/catalogue/itemsearch_item.inc b/koha-tmpl/intranet-tmpl/prog/en/includes/catalogue/itemsearch_item.inc
new file mode 100644 (file)
index 0000000..bb5bd41
--- /dev/null
@@ -0,0 +1,23 @@
+[%- USE Branches -%]
+[% biblio = item.biblio %]
+[% biblioitem = item.biblioitem %]
+<tr>
+  <td>
+    <a href="/cgi-bin/koha/catalogue/detail.pl?biblionumber=[% biblio.biblionumber %]" title="Go to record detail page">[% biblio.title %]</a>
+    by [% biblio.author %]
+  </td>
+  <td>[% biblioitem.publicationyear %]</td>
+  <td>[% biblioitem.publishercode %]</td>
+  <td>[% biblioitem.collectiontitle %]</td>
+  <td>
+    <a href="/cgi-bin/koha/catalogue/moredetail.pl?biblionumber=[% biblio.biblionumber %]#item[% item.itemnumber %]" title="Go to item details">[% item.barcode %]</a>
+  </td>
+  <td>[% item.itemcallnumber %]</td>
+  <td>[% Branches.GetName(item.homebranch) %]</td>
+  <td>[% Branches.GetName(item.holdingbranch) %]</td>
+  <td>[% item.location %]</td>
+  <td>[% item.stocknumber %]</td>
+  <td>[% item.status %]</td>
+  <td>[% item.issues || 0 %]</td>
+  <td><a href="/cgi-bin/koha/cataloguing/additem.pl?op=edititem&biblionumber=[% item.biblionumber %]&itemnumber=[% item.itemnumber %]">Modify</a></td>
+</tr>
diff --git a/koha-tmpl/intranet-tmpl/prog/en/includes/catalogue/itemsearch_item.json.inc b/koha-tmpl/intranet-tmpl/prog/en/includes/catalogue/itemsearch_item.json.inc
new file mode 100644 (file)
index 0000000..9115ead
--- /dev/null
@@ -0,0 +1,18 @@
+[%- USE Branches -%]
+[%- biblio = item.biblio -%]
+[%- biblioitem = item.biblioitem -%]
+[
+  "<a href='/cgi-bin/koha/catalogue/detail.pl?biblionumber=[% biblio.biblionumber %]' title='Go to record detail page'>[% biblio.title |html %]</a> by [% biblio.author |html %]",
+  "[% biblioitem.publicationyear |html %]",
+  "[% biblioitem.publishercode |html %]",
+  "[% biblioitem.collectiontitle |html %]",
+  "<a href='/cgi-bin/koha/catalogue/moredetail.pl?biblionumber=[% biblio.biblionumber %]#item[% item.itemnumber %]' title='Go to item details'>[% item.barcode |html %]</a>",
+  "[% item.itemcallnumber |html %]",
+  "[% Branches.GetName(item.homebranch) |html %]",
+  "[% Branches.GetName(item.holdingbranch) |html %]",
+  "[% item.location |html %]",
+  "[% item.stocknumber |html %]",
+  "[% item.status |html %]",
+  "[% (item.issues || 0) |html %]",
+  "<a href='/cgi-bin/koha/cataloguing/additem.pl?op=edititem&biblionumber=[% item.biblionumber %]&itemnumber=[% item.itemnumber %]'>Modify</a>"
+]
diff --git a/koha-tmpl/intranet-tmpl/prog/en/includes/catalogue/itemsearch_items.inc b/koha-tmpl/intranet-tmpl/prog/en/includes/catalogue/itemsearch_items.inc
new file mode 100644 (file)
index 0000000..33b4c36
--- /dev/null
@@ -0,0 +1,49 @@
+[% names = CGI.param() %]
+[% params = [] %]
+[% FOREACH name IN names %]
+  [% IF name != 'sortby' AND name != 'sortorder' %]
+    [% params.push(name _ "=" _ CGI.param(name)) %]
+  [% END %]
+[% END %]
+[% base_url = "/cgi-bin/koha/catalogue/itemsearch.pl?" _ params.join('&') %]
+
+[% BLOCK itemsearch_header %]
+  [% sortorder = 'asc' %]
+  [% classes = [] %]
+  [% IF CGI.param('sortby') == name %]
+    [% classes.push('active') %]
+    [% classes.push('sort-' _ CGI.param('sortorder')) %]
+    [% IF CGI.param('sortorder') == 'asc' %]
+      [% sortorder = 'desc' %]
+    [% END %]
+  [% END %]
+  [% url = base_url _ '&sortby=' _ name _ '&sortorder=' _ sortorder %]
+  <th class="[% classes.join(' ') %]">
+    <a href="[% url %]" title="Sort on [% label %] ([% sortorder %])">[% text %]</a>
+  </th>
+[% END %]
+
+<table>
+  <thead>
+    <tr>
+      [% INCLUDE itemsearch_header name='title' label='title' text='Bibliographic reference' %]
+      [% INCLUDE itemsearch_header name='publicationyear' label='publication date' text='Publication date' %]
+      [% INCLUDE itemsearch_header name='publishercode' label='publisher' text='Publisher' %]
+      [% INCLUDE itemsearch_header name='collectiontitle' label='collection' text='Collection' %]
+      [% INCLUDE itemsearch_header name='barcode' label='barcode' text='Barcode' %]
+      [% INCLUDE itemsearch_header name='itemcallnumber' label='callnumber' text='Callnumber' %]
+      [% INCLUDE itemsearch_header name='homebranch' label='home branch' text='Home branch' %]
+      [% INCLUDE itemsearch_header name='holdingbranch' label='holding branch' text='Holding branch' %]
+      [% INCLUDE itemsearch_header name='location' label='location' text='Location' %]
+      [% INCLUDE itemsearch_header name='stocknumber' label='stock number' text='Stock number' %]
+      [% INCLUDE itemsearch_header name='notforloan' label='status' text='Status' %]
+      [% INCLUDE itemsearch_header name='issues' label='issues' text='Issues' %]
+      <th></th>
+    </tr>
+  </thead>
+  <tbody>
+    [% FOREACH item IN items %]
+      [% INCLUDE 'catalogue/itemsearch_item.inc' item = item %]
+    [% END %]
+  </tbody>
+</table>
index 10f794e..02bc60a 100644 (file)
@@ -77,6 +77,8 @@
     <dd>Manage rules for automatically matching MARC records during record imports.</dd>
     <dt><a href="/cgi-bin/koha/admin/oai_sets.pl">OAI sets configuration</a></dt>
     <dd>Manage OAI Sets</dd>
+    <dt><a href="/cgi-bin/koha/admin/items_search_fields.pl">Items search fields</a></dt>
+    <dd>Manage custom fields for items search</dd>
 </dl>
 
 <h3>Acquisition parameters</h3>
diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/items_search_field.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/admin/items_search_field.tt
new file mode 100644 (file)
index 0000000..6e0f373
--- /dev/null
@@ -0,0 +1,41 @@
+[% INCLUDE 'doc-head-open.inc' %]
+  <title>Koha &rsaquo; Administration &rsaquo; Items search fields</title>
+  [% INCLUDE 'doc-head-close.inc' %]
+</head>
+<body id="admin_itemssearchfields" class="admin">
+  [% INCLUDE 'header.inc' %]
+  [% INCLUDE 'cat-search.inc' %]
+  <div id="breadcrumbs">
+    <a href="/cgi-bin/koha/mainpage.pl">Home</a> &rsaquo;
+    <a href="/cgi-bin/koha/admin/admin-home.pl">Administration</a> &rsaquo;
+    <a href="/cgi-bin/koha/admin/items_search_fields.pl">Items search fields</a> &rsaquo;
+    [% field.name %]
+  </div>
+
+  <div id="doc3" class="yui-t2">
+    <div id="bd">
+      <div id="yui-main">
+        <div class="yui-b">
+          <h1>Items search field: [% field.label %]</h1>
+
+          <form action="" method="POST">
+            <fieldset class="rows">
+              <legend>Edit field</legend>
+              [% INCLUDE 'admin-items-search-field-form.inc' field=field %]
+              <div>
+                <input type="hidden" name="op" value="mod" />
+              </div>
+              <fieldset class="action">
+                <input type="submit" value="Update" />
+              </fieldset>
+            </fieldset>
+          </form>
+          <a href="/cgi-bin/koha/admin/items_search_fields.pl">Return to items search fields overview page</a>
+        </div>
+      </div>
+      <div class="yui-b">
+        [% INCLUDE 'admin-menu.inc' %]
+      </div>
+    </div>
+
+    [% INCLUDE 'intranet-bottom.inc' %]
diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/items_search_fields.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/admin/items_search_fields.tt
new file mode 100644 (file)
index 0000000..200e614
--- /dev/null
@@ -0,0 +1,95 @@
+[% INCLUDE 'doc-head-open.inc' %]
+  <title>Koha &rsaquo; Administration &rsaquo; Items search fields</title>
+  [% INCLUDE 'doc-head-close.inc' %]
+</head>
+<body id="admin_itemssearchfields" class="admin">
+  [% INCLUDE 'header.inc' %]
+  [% INCLUDE 'cat-search.inc' %]
+  <div id="breadcrumbs">
+    <a href="/cgi-bin/koha/mainpage.pl">Home</a> &rsaquo;
+    <a href="/cgi-bin/koha/admin/admin-home.pl">Administration</a> &rsaquo;
+    Items search fields
+  </div>
+
+  <div id="doc3" class="yui-t2">
+    <div id="bd">
+      <div id="yui-main">
+        <div class="yui-b">
+          [% IF field_added %]
+            <div class="dialog">
+              Field successfully added: [% field_added.label %]
+            </div>
+          [% ELSIF field_not_added %]
+            <div class="alert">
+              <p>Failed to add field. Please check if the field name doesn't already exist.</p>
+              <p>Check logs for more details.</p>
+            </div>
+          [% ELSIF field_deleted %]
+            <div class="dialog">
+              Field successfully deleted.
+            </div>
+          [% ELSIF field_not_deleted %]
+            <div class="alert">
+              <p>Failed to delete field.</p>
+              <p>Check logs for more details.</p>
+            </div>
+          [% ELSIF field_updated %]
+            <div class="dialog">
+              Field successfully updated: [% field_updated.label %]
+            </div>
+          [% ELSIF field_not_updated %]
+            <div class="alert">
+              <p>Failed to update field.</p>
+              <p>Check logs for more details.</p>
+            </div>
+          [% END %]
+          <h1>Items search fields</h1>
+          [% IF fields.size %]
+            <table>
+              <thead>
+                <tr>
+                  <th>Name</th>
+                  <th>Label</th>
+                  <th>MARC field</th>
+                  <th>MARC subfield</th>
+                  <th>Authorised values category</th>
+                  <th>Operations</th>
+                </tr>
+              </thead>
+              <tbody>
+                [% FOREACH field IN fields %]
+                  <tr>
+                    <td>[% field.name %]</td>
+                    <td>[% field.label %]</td>
+                    <td>[% field.tagfield %]</td>
+                    <td>[% field.tagsubfield %]</td>
+                    <td>[% field.authorised_values_category %]</td>
+                    <td>
+                      <a href="/cgi-bin/koha/admin/items_search_field.pl?name=[% field.name %]" title="Edit [% field.name %] field">Edit</a>
+                      <a href="/cgi-bin/koha/admin/items_search_fields.pl?op=del&name=[% field.name %]" title="Delete [% field.name %] field">Delete</a>
+                    </td>
+                  </tr>
+                [% END %]
+              </tbody>
+            </table>
+          [% END %]
+          <form action="" method="POST">
+            <fieldset class="rows">
+              <legend>Add a new field</legend>
+              [% INCLUDE 'admin-items-search-field-form.inc' field=undef %]
+              <div>
+                <input type="hidden" name="op" value="add" />
+              </div>
+              <fieldset class="action">
+                <input type="submit" value="Add this field" />
+              </fieldset>
+            </fieldset>
+          </form>
+        </div>
+      </div>
+      <div class="yui-b">
+        [% INCLUDE 'admin-menu.inc' %]
+      </div>
+    </div>
+
+    [% INCLUDE 'intranet-bottom.inc' %]
index c982eb4..b41690d 100644 (file)
@@ -30,6 +30,7 @@
 <form action="search.pl" method="get">
 <div id="advanced-search">
 <h1>Advanced search</h1>
+<a href="/cgi-bin/koha/catalogue/itemsearch.pl">Go to item search</a>
 
 [% IF ( outer_servers_loop ) %]
 <!-- DATABASES -->
diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/catalogue/itemsearch.csv.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/catalogue/itemsearch.csv.tt
new file mode 100644 (file)
index 0000000..b69f610
--- /dev/null
@@ -0,0 +1,4 @@
+Bibliographic reference, Publication Date, Publisher, Collection, Barcode, Callnumber, Home branch, Holding branch, Location, Stock number, Status, Issues
+[% FOREACH item IN results -%]
+  [%- INCLUDE 'catalogue/itemsearch_item.csv.inc' item = item -%]
+[%- END -%]
diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/catalogue/itemsearch.json.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/catalogue/itemsearch.json.tt
new file mode 100644 (file)
index 0000000..35e020f
--- /dev/null
@@ -0,0 +1,12 @@
+[%- USE CGI -%]
+{
+  "sEcho": [% CGI.param('sEcho') %],
+  "iTotalRecords": [% total_rows %],
+  "iTotalDisplayRecords": [% total_rows %],
+  "aaData": [
+  [%- FOREACH item IN results -%]
+    [%- INCLUDE 'catalogue/itemsearch_item.json.inc' item = item -%]
+    [%- UNLESS loop.last %],[% END -%]
+  [%- END -%]
+  ]
+}
diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/catalogue/itemsearch.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/catalogue/itemsearch.tt
new file mode 100644 (file)
index 0000000..d967b36
--- /dev/null
@@ -0,0 +1,425 @@
+[% USE CGI %]
+[% USE JSON.Escape %]
+
+[% BLOCK form_field_select %]
+  <div class="form-field form-field-select">
+    <label class="form-field-label" for="[% name %]">[% label %]</label>
+    <select id="[% name %]_op" name="[% name %]_op">
+      <option value="=">is</option>
+      [% IF CGI.param(name _ '_op') == '!=' %]
+        <option value="!=" selected="selected">is not</option>
+      [% ELSE %]
+        <option value="!=" >is not</option>
+      [% END %]
+    </select>
+    [% values = CGI.param(name) %]
+    <select id="[% name %]" name="[% name %]" multiple="multiple" size="[% options.size < 4 ? options.size + 1 : 4 %]">
+      [% IF (values == '') %]
+        <option value="" selected="selected">
+      [% ELSE %]
+        <option value="">
+      [% END %]
+        [% empty_option || "All" %]
+      </option>
+      [% FOREACH option IN options %]
+        [% IF values.grep(option.value).size %]
+          <option value="[% option.value %]" selected="selected">[% option.label %]</option>
+        [% ELSE %]
+          <option value="[% option.value %]">[% option.label %]</option>
+        [% END %]
+      [% END %]
+    </select>
+  </div>
+[% END %]
+
+[% BLOCK form_field_select_option %]
+  [% IF params.f == value %]
+    <option value="[% value %]" selected="selected">[% label %]</option>
+  [% ELSE %]
+    <option value="[% value %]">[% label %]</option>
+  [% END %]
+[% END %]
+
+[% BLOCK form_field_select_text %]
+  <div class="form-field form-field-select-text">
+    [% IF params.exists('c') %]
+      <select name="c" class="form-field-conjunction">
+        <option value="and">AND</option>
+        [% IF params.c == 'or' %]
+          <option value="or" selected="selected">OR</option>
+        [% ELSE %]
+          <option value="or">OR</option>
+        [% END %]
+      </select>
+    [% ELSE %]
+      <select name="c" class="form-field-conjunction" disabled="disabled">
+        <option value="and">AND</option>
+        <option value="or">OR</option>
+      </select>
+    [% END %]
+    <select name="f" class="form-field-column">
+      [% INCLUDE form_field_select_option value='barcode' label='Barcode' %]
+      [% INCLUDE form_field_select_option value='itemcallnumber' label='Callnumber' %]
+      [% INCLUDE form_field_select_option value='stocknumber' label='Stock number' %]
+      [% INCLUDE form_field_select_option value='title' label='Title' %]
+      [% INCLUDE form_field_select_option value='author' label='Author' %]
+      [% INCLUDE form_field_select_option value='publishercode' label='Publisher' %]
+      [% INCLUDE form_field_select_option value='publicationdate' label='Publication date' %]
+      [% INCLUDE form_field_select_option value='collectiontitle' label='Collection' %]
+      [% INCLUDE form_field_select_option value='isbn' label='ISBN' %]
+      [% INCLUDE form_field_select_option value='issn' label='ISSN' %]
+      [% IF items_search_fields.size %]
+        <optgroup label="Custom search fields">
+          [% FOREACH field IN items_search_fields %]
+            [% marcfield = field.tagfield %]
+            [% IF field.tagsubfield %]
+              [% marcfield = marcfield _ '$' _ field.tagsubfield %]
+            [% END %]
+            [% IF params.f == "marc:$marcfield" %]
+              <option value="marc:[% marcfield %]" data-authorised-values-category="[% field.authorised_values_category %]" selected="selected">[% field.label %] ([% marcfield %])</option>
+            [% ELSE %]
+              <option value="marc:[% marcfield %]" data-authorised-values-category="[% field.authorised_values_category %]">[% field.label %] ([% marcfield %])</option>
+            [% END %]
+          [% END %]
+        </optgroup>
+      [% END %]
+    </select>
+    <input type="text" name="q" class="form-field-value" value="[% params.q %]" />
+    <input type="hidden" name="op" value="like" />
+  </div>
+[% END %]
+
+[% BLOCK form_field_select_text_block %]
+  [% c = CGI.param('c').list %]
+  [% f = CGI.param('f').list %]
+  [% q = CGI.param('q').list %]
+  [% op = CGI.param('op').list %]
+  [% IF q.size %]
+    [% size = q.size - 1 %]
+    [% FOREACH i IN [0 .. size] %]
+      [%
+        params = {
+          f => f.$i
+          q = q.$i
+          op = op.$i
+        }
+      %]
+      [% IF i > 0 %]
+        [% j = i - 1 %]
+        [% params.c = c.$j %]
+      [% END %]
+      [% INCLUDE form_field_select_text params=params %]
+    [% END %]
+  [% ELSE %]
+    [% INCLUDE form_field_select_text %]
+  [% END %]
+[% END %]
+
+[% BLOCK form_field_radio_yes_no %]
+  <div class="form-field">
+    <label class="form-field-label" for="[% name %]">[% label %]:</label>
+    <input type="radio" name="[% name %]" id="[% name %]_indifferent" value="" checked="checked"/>
+    <label for="[% name %]_indifferent">Indifferent</label>
+    <input type="radio" name="[% name %]" id="[% name %]_yes" value="yes" />
+    <label for="[% name %]_yes">Yes</label>
+    <input type="radio" name="[% name %]" id="[% name %]_no" value="no" />
+    <label for="[% name %]_no">No</label>
+  </div>
+[% END %]
+
+[%# Page starts here %]
+
+[% INCLUDE 'doc-head-open.inc' %]
+  <title>Koha &rsaquo; Catalog &rsaquo; Advanced search</title>
+  [% INCLUDE 'doc-head-close.inc' %]
+  [% INCLUDE 'datatables.inc' %]
+  <script type="text/javascript" src="[% themelang %]/lib/jquery/plugins/jquery.dataTables.columnFilter.js"></script>
+  <link rel="stylesheet" type="text/css" href="[% themelang %]/css/datatables.css" />
+  <link rel="stylesheet" type="text/css" href="[% themelang %]/css/itemsearchform.css" />
+  <script type="text/javascript">
+    //<![CDATA[
+    var authorised_values = [% authorised_values_json %];
+
+    function loadAuthorisedValuesSelect(select) {
+      var selected = select.find('option:selected');
+      var category = selected.data('authorised-values-category');
+      var form_field_value = select.siblings('.form-field-value');
+      if (category && category in authorised_values) {
+        var values = authorised_values[category];
+        var html = '<select name="q" class="form-field-value">\n';
+        for (i in values) {
+          var value = values[i];
+          html += '<option value="' + value.authorised_value + '">' + value.lib + '</option>\n';
+        }
+        html += '</select>\n';
+        var new_form_field_value = $(html);
+        new_form_field_value.val(form_field_value.val());
+        form_field_value.replaceWith(new_form_field_value);
+      } else {
+        if (form_field_value.prop('tagName').toLowerCase() == 'select') {
+          html = '<input name="q" type="text" class="form-field-value" />';
+          var new_form_field_value = $(html);
+          form_field_value.replaceWith(new_form_field_value);
+        }
+      }
+    }
+
+    function addNewField() {
+      var form_field = $('div.form-field-select-text').last();
+      var copy = form_field.clone(true);
+      copy.find('input,select').not('[type="hidden"]').each(function() {
+        $(this).val('');
+      });
+      copy.find('.form-field-conjunction').removeAttr('disabled');
+      form_field.after(copy);
+      copy.find('select.form-field-column').change();
+    }
+
+    function submitForm($form) {
+      var tr = ''
+        + '    <tr>'
+        + '      <th>' + _("Bibliographic reference") + '</th>'
+        + '      <th>' + _("Publication date") + '</th>'
+        + '      <th>' + _("Publisher") + '</th>'
+        + '      <th>' + _("Collection") + '</th>'
+        + '      <th>' + _("Barcode") + '</th>'
+        + '      <th>' + _("Callnumber") + '</th>'
+        + '      <th>' + _("Home branch") + '</th>'
+        + '      <th>' + _("Holding branch") + '</th>'
+        + '      <th>' + _("Location") + '</th>'
+        + '      <th>' + _("Stock number") + '</th>'
+        + '      <th>' + _("Status") + '</th>'
+        + '      <th>' + _("Issues") + '</th>'
+        + '      <th></th>'
+        + '    </tr>'
+      var table = ''
+        + '<table id="results">'
+        + '  <thead>' + tr + tr + '</thead>'
+        + '  <tbody></tbody>'
+        + '</table>';
+      $('#results-wrapper').empty().html(table);
+
+      var params = [];
+      $form.find('select,input[type="text"],input[type="hidden"]').not('[disabled]').each(function () {
+        params.push({ 'name': $(this).attr('name'), 'value': $(this).val() });
+      });
+      $form.find('input[type="radio"]:checked').each(function() {
+        params.push({ 'name': $(this).attr('name'), 'value': $(this).val() });
+      });
+
+      $('#results').dataTable($.extend(true, {}, dataTablesDefaults, {
+        'bDestroy': true,
+        'bServerSide': true,
+        'sAjaxSource': '/cgi-bin/koha/catalogue/itemsearch.pl',
+        'fnServerParams': function(aoData) {
+          aoData.push( { 'name': 'format', 'value': 'json' } );
+          for (i in params) {
+            aoData.push(params[i]);
+          }
+        },
+        'sDom': '<"top pager"ilp>t<"bottom pager"ip>',
+        'aoColumns': [
+          { 'sName': 'title' },
+          { 'sName': 'publicationyear' },
+          { 'sName': 'publishercode' },
+          { 'sName': 'collectiontitle' },
+          { 'sName': 'barcode' },
+          { 'sName': 'itemcallnumber' },
+          { 'sName': 'homebranch' },
+          { 'sName': 'holdingbranch' },
+          { 'sName': 'location' },
+          { 'sName': 'stocknumber' },
+          { 'sName': 'notforloan' },
+          { 'sName': 'issues' },
+          { 'sName': 'checkbox', 'bSortable': false }
+        ]
+      })).columnFilter({
+        'sPlaceHolder': 'head:after',
+        'aoColumns': [
+          { 'type': 'text' },
+          { 'type': 'text' },
+          { 'type': 'text' },
+          { 'type': 'text' },
+          { 'type': 'text' },
+          { 'type': 'text' },
+          { 'type': 'select', 'values': [% branches.json %] },
+          { 'type': 'select', 'values': [% branches.json %] },
+          { 'type': 'select', 'values': [% locations.json %] },
+          { 'type': 'text' },
+          { 'type': 'select', 'values': [% notforloans.json %] },
+          { 'type': 'text' },
+          null
+        ]
+      });
+    }
+
+    function hideForm($form) {
+      $form.hide();
+      $('#editsearchlink').show();
+    }
+
+    $(document).ready(function () {
+      // Add the "New field" link.
+      var form_field = $('div.form-field-select-text').last()
+      var button_field_new = $('<a href="#" class="button-field-new" title="Add a new field">New field</a>');
+      button_field_new.click(function() {
+        addNewField();
+        return false;
+      });
+      form_field.after(button_field_new);
+
+      // If a field is linked to an authorised values list, display the list.
+      $('div.form-field-select-text select').change(function() {
+        loadAuthorisedValuesSelect($(this));
+      }).change();
+
+      // Prevent user to select the 'All ...' option with other options.
+      $('div.form-field-select').each(function() {
+        $(this).find('select').filter(':last').change(function() {
+          values = $(this).val();
+          if (values.length > 1) {
+            var idx = $.inArray('', values);
+            if (idx != -1) {
+              values.splice(idx, 1);
+              $(this).val(values);
+            }
+          }
+        });
+      });
+
+      $('#itemsearchform').submit(function() {
+        var format = $(this).find('input[name="format"]:checked').val();
+        if (format == 'html') {
+          submitForm($(this));
+          hideForm($(this));
+          return false;
+        }
+      });
+
+      $('#editsearchlink').click(function() {
+        $('#itemsearchform').show();
+        $(this).hide();
+        return false;
+      });
+    });
+    //]]>
+  </script>
+</head>
+<body id="catalog_itemsearch" class="catalog">
+  [% INCLUDE 'header.inc' %]
+  <div id="breadcrumbs">
+    <a href="/cgi-bin/koha/mainpage.pl">Home</a> &rsaquo; Item search
+  </div>
+
+  <div id="doc" class="yui-t7">
+    <div id="bd">
+      <h1>Item search</h1>
+      <a href="/cgi-bin/koha/catalogue/search.pl">Go to advanced search</a>
+      <form action="" method="get" id="itemsearchform">
+        <fieldset>
+          <legend>Item search</legend>
+          <fieldset>
+            [% INCLUDE form_field_select
+              name="homebranch"
+              label="Home branch"
+              options = branches
+              empty_option = "All branches"
+            %]
+            [% INCLUDE form_field_select
+              name="location"
+              label="Location"
+              options = locations
+              empty_option = "All locations"
+            %]
+          </fieldset>
+          <fieldset>
+            [% INCLUDE form_field_select
+              name="itype"
+              label="Item type"
+              options = itemtypes
+              empty_option = "All item types"
+            %]
+            [% INCLUDE form_field_select
+              name="ccode"
+              label="Collection code"
+              options = ccodes
+              empty_option = "All collection codes"
+            %]
+          </fieldset>
+          <fieldset>
+            [% INCLUDE form_field_select_text_block %]
+            <p class="hint">You can use the following joker characters: % _</p>
+          </fieldset>
+          <fieldset>
+            <div class="form-field">
+              <label class="form-field-label" for="itemcallnumber_from">From call number:</label>
+              [% value = CGI.param('itemcallnumber_from') %]
+              <input type="text" id="itemcallnumber_from" name="itemcallnumber_from" value="[% value %]" />
+              <span class="hint">(inclusive)</span>
+            </div>
+            <div class="form-field">
+              [% value = CGI.param('itemcallnumber_to') %]
+              <label class="form-field-label" for="itemcallnumber_to">To call number:</label>
+              <input type="text" id="itemcallnumber_to" name="itemcallnumber_to" value="[% value %]" />
+              <span class="hint">(inclusive)</span>
+            </div>
+            [% INCLUDE form_field_radio_yes_no name="damaged" label="Damaged" %]
+            [% INCLUDE form_field_radio_yes_no name="itemlost" label="Lost" %]
+            <div class="form-field">
+              <label class="form-field-label" for="issues">Issues count:</label>
+              <select id="issues_op" name="issues_op">
+                <option value=">">&gt;</option>
+                <option value="<">&lt;</option>
+                <option value="=">=</option>
+                <option value="!=">!=</option>
+              </select>
+              <input type="text" name="issues" />
+            </div>
+            <div class="form-field">
+              <label class="form-field-label" for="datelastborrowed">Last issue date:</label>
+              <select id="datelastborrowed_op" name="datelastborrowed_op">
+                <option value=">">After</option>
+                <option value="<">Before</option>
+                <option value="=">On</option>
+              </select>
+              <input type="text" name="datelastborrowed" />
+              <span class="hint">ISO Format (AAAA-MM-DD)</span>
+            </div>
+          </fieldset>
+          <fieldset>
+            <div class="form-field-radio">
+              <label>Output:</label>
+              <input type="radio" id="format-html" name="format" value="html" checked="checked" /> <label for="format-html">Screen</label>
+              <input type="radio" id="format-csv" name="format" value="csv" /> <label for="format-csv">CSV</label>
+            </div>
+            <div class="form-actions">
+              <input type="submit" value="Search" />
+            </div>
+          </fieldset>
+        </fieldset>
+      </form>
+
+      <p><a id="editsearchlink" href="#" style="display:none">Edit search</a></p>
+
+      <div id="results-wrapper">
+        [% IF search_done %]
+
+          [% IF total_rows > 0 %]
+            <p>Found [% total_rows %] results.</p>
+          [% ELSE %]
+            <p>No results found.</p>
+          [% END %]
+
+          [% IF results %]
+            [% INCLUDE 'catalogue/itemsearch_items.inc' items = results %]
+          [% END %]
+
+          <div id="pagination-bar">
+            [% pagination_bar %]
+          </div>
+
+        [% END %]
+      </div>
+    </div>
+
+    [% INCLUDE 'intranet-bottom.inc' %]