Bugzilla – Attachment 10334 Details for
Bug 8233
New search engine layer - introduce solr without breaking anything else
Home
|
New
|
Browse
|
Search
|
[?]
|
Reports
|
Help
|
New Account
|
Log In
[x]
|
Forgot Password
Login:
[x]
[patch]
Bug 8233 : SearchEngine: Add a Koha::SearchEngine module
Bug-8233--SearchEngine-Add-a-KohaSearchEngine-modu.patch (text/plain), 95.77 KB, created by
Chris Cormack
on 2012-06-17 17:48:18 UTC
(
hide
)
Description:
Bug 8233 : SearchEngine: Add a Koha::SearchEngine module
Filename:
MIME Type:
Creator:
Chris Cormack
Created:
2012-06-17 17:48:18 UTC
Size:
95.77 KB
patch
obsolete
>From 7223692370abb7d5cdc0af1ef845252fe30fe3f4 Mon Sep 17 00:00:00 2001 >From: Jonathan Druart <jonathan.druart@biblibre.com> >Date: Wed, 13 Jun 2012 11:48:22 +0200 >Subject: [PATCH] Bug 8233 : SearchEngine: Add a Koha::SearchEngine module > >First draft introducing solr into Koha :-) > >List of files : > $ tree t/searchengine/ > t/searchengine > |-- 000_conn > | `-- conn.t > |-- 001_search > | `-- search_base.t > |-- 002_index > | `-- index_base.t > |-- 003_query > | `-- buildquery.t > |-- 004_config > | `-- load_config.t > `-- indexes.yaml > just do `prove -r t/searchengine/**/*.t` > > t/lib > |-- Mocks > | `-- Context.pm > `-- Mocks.pm > provide a mock to SearchEngine syspref (set_zebra and set_solr). > > $ tree Koha/SearchEngine > Koha/SearchEngine > |-- Config.pm > |-- ConfigRole.pm > |-- FacetsBuilder.pm > |-- FacetsBuilderRole.pm > |-- Index.pm > |-- IndexRole.pm > |-- QueryBuilder.pm > |-- QueryBuilderRole.pm > |-- Search.pm > |-- SearchRole.pm > |-- Solr > | |-- Config.pm > | |-- FacetsBuilder.pm > | |-- Index.pm > | |-- QueryBuilder.pm > | `-- Search.pm > |-- Solr.pm > |-- Zebra > | |-- QueryBuilder.pm > | `-- Search.pm > `-- Zebra.pm > >How to install and configure Solr ? > See the wiki page: http://wiki.koha-community.org/wiki/SearchEngine_Layer_RFC > >http://bugs.koha-community.org/show_bug.cgi?id=8233 >Signed-off-by: Chris Cormack <chris@bigballofwax.co.nz> >--- > Koha/SearchEngine.pm | 29 ++ > Koha/SearchEngine/Config.pm | 12 + > Koha/SearchEngine/ConfigRole.pm | 7 + > Koha/SearchEngine/FacetsBuilder.pm | 12 + > Koha/SearchEngine/FacetsBuilderRole.pm | 7 + > Koha/SearchEngine/Index.pm | 11 + > Koha/SearchEngine/IndexRole.pm | 6 + > Koha/SearchEngine/QueryBuilder.pm | 12 + > Koha/SearchEngine/QueryBuilderRole.pm | 7 + > Koha/SearchEngine/Search.pm | 12 + > Koha/SearchEngine/SearchRole.pm | 7 + > Koha/SearchEngine/Solr.pm | 45 +++ > Koha/SearchEngine/Solr/Config.pm | 115 ++++++ > Koha/SearchEngine/Solr/FacetsBuilder.pm | 41 +++ > Koha/SearchEngine/Solr/Index.pm | 100 +++++ > Koha/SearchEngine/Solr/QueryBuilder.pm | 146 ++++++++ > Koha/SearchEngine/Solr/Search.pm | 108 ++++++ > Koha/SearchEngine/Zebra.pm | 14 + > Koha/SearchEngine/Zebra/QueryBuilder.pm | 14 + > Koha/SearchEngine/Zebra/Search.pm | 40 ++ > admin/searchengine/solr/indexes.pl | 103 ++++++ > etc/searchengine/solr/config.yaml | 2 + > etc/searchengine/solr/indexes.yaml | 45 +++ > etc/searchengine/solr/indexes.yaml.bak | 33 ++ > etc/solr/indexes.yaml | 33 ++ > installer/data/mysql/updatedatabase.pl | 7 + > .../intranet-tmpl/prog/en/css/staff-global.css | 4 + > .../lib/jquery/plugins/jquery.textarea-expander.js | 95 +++++ > .../prog/en/lib/jquery/plugins/tablednd.js | 382 ++++++++++++++++++++ > .../prog/en/modules/admin/admin-home.tt | 2 + > .../prog/en/modules/admin/preferences/admin.pref | 8 + > .../en/modules/admin/searchengine/solr/indexes.tt | 190 ++++++++++ > .../opac-tmpl/prog/en/includes/search/facets.inc | 56 +++ > .../prog/en/includes/search/page-numbers.inc | 17 + > .../prog/en/includes/search/resort_form.inc | 23 ++ > .../opac-tmpl/prog/en/modules/search/results.tt | 108 ++++++ > misc/migration_tools/rebuild_solr.pl | 179 +++++++++ > opac/opac-search.pl | 16 +- > opac/search.pl | 172 +++++++++ > t/lib/Mocks.pm | 25 ++ > t/lib/Mocks/Context.pm | 13 + > t/searchengine/000_conn/conn.t | 23 ++ > t/searchengine/001_search/search_base.t | 12 + > t/searchengine/002_index/index_base.t | 15 + > t/searchengine/003_query/buildquery.t | 45 +++ > t/searchengine/004_config/load_config.t | 54 +++ > t/searchengine/indexes.yaml | 33 ++ > 47 files changed, 2438 insertions(+), 2 deletions(-) > create mode 100644 Koha/SearchEngine.pm > create mode 100644 Koha/SearchEngine/Config.pm > create mode 100644 Koha/SearchEngine/ConfigRole.pm > create mode 100644 Koha/SearchEngine/FacetsBuilder.pm > create mode 100644 Koha/SearchEngine/FacetsBuilderRole.pm > create mode 100644 Koha/SearchEngine/Index.pm > create mode 100644 Koha/SearchEngine/IndexRole.pm > create mode 100644 Koha/SearchEngine/QueryBuilder.pm > create mode 100644 Koha/SearchEngine/QueryBuilderRole.pm > create mode 100644 Koha/SearchEngine/Search.pm > create mode 100644 Koha/SearchEngine/SearchRole.pm > create mode 100644 Koha/SearchEngine/Solr.pm > create mode 100644 Koha/SearchEngine/Solr/Config.pm > create mode 100644 Koha/SearchEngine/Solr/FacetsBuilder.pm > create mode 100644 Koha/SearchEngine/Solr/Index.pm > create mode 100644 Koha/SearchEngine/Solr/QueryBuilder.pm > create mode 100644 Koha/SearchEngine/Solr/Search.pm > create mode 100644 Koha/SearchEngine/Zebra.pm > create mode 100644 Koha/SearchEngine/Zebra/QueryBuilder.pm > create mode 100644 Koha/SearchEngine/Zebra/Search.pm > create mode 100755 admin/searchengine/solr/indexes.pl > create mode 100644 etc/searchengine/solr/config.yaml > create mode 100644 etc/searchengine/solr/indexes.yaml > create mode 100644 etc/searchengine/solr/indexes.yaml.bak > create mode 100644 etc/solr/indexes.yaml > create mode 100644 koha-tmpl/intranet-tmpl/prog/en/lib/jquery/plugins/jquery.textarea-expander.js > create mode 100644 koha-tmpl/intranet-tmpl/prog/en/lib/jquery/plugins/tablednd.js > create mode 100644 koha-tmpl/intranet-tmpl/prog/en/modules/admin/searchengine/solr/indexes.tt > create mode 100644 koha-tmpl/opac-tmpl/prog/en/includes/search/facets.inc > create mode 100644 koha-tmpl/opac-tmpl/prog/en/includes/search/page-numbers.inc > create mode 100644 koha-tmpl/opac-tmpl/prog/en/includes/search/resort_form.inc > create mode 100644 koha-tmpl/opac-tmpl/prog/en/modules/search/results.tt > create mode 100755 misc/migration_tools/rebuild_solr.pl > create mode 100755 opac/search.pl > create mode 100644 t/lib/Mocks.pm > create mode 100644 t/lib/Mocks/Context.pm > create mode 100644 t/searchengine/000_conn/conn.t > create mode 100755 t/searchengine/001_search/search_base.t > create mode 100755 t/searchengine/002_index/index_base.t > create mode 100644 t/searchengine/003_query/buildquery.t > create mode 100644 t/searchengine/004_config/load_config.t > create mode 100644 t/searchengine/indexes.yaml > >diff --git a/Koha/SearchEngine.pm b/Koha/SearchEngine.pm >new file mode 100644 >index 0000000..407156f >--- /dev/null >+++ b/Koha/SearchEngine.pm >@@ -0,0 +1,29 @@ >+package Koha::SearchEngine; >+ >+use Moose; >+use C4::Context; >+use Koha::SearchEngine::Config; >+ >+has 'name' => ( >+ is => 'ro', >+ default => sub { >+ C4::Context->preference('SearchEngine'); >+ } >+); >+ >+has config => ( >+ is => 'rw', >+ lazy => 1, >+ default => sub { >+ Koha::SearchEngine::Config->new; >+ } >+# lazy => 1, >+# builder => '_build_config', >+); >+ >+#sub _build_config { >+# my ( $self ) = @_; >+# Koha::SearchEngine::Config->new( $self->name ); >+#); >+ >+1; >diff --git a/Koha/SearchEngine/Config.pm b/Koha/SearchEngine/Config.pm >new file mode 100644 >index 0000000..089bac6 >--- /dev/null >+++ b/Koha/SearchEngine/Config.pm >@@ -0,0 +1,12 @@ >+package Koha::SearchEngine::Config; >+ >+use Moose; >+ >+use Moose::Util qw( apply_all_roles ); >+ >+sub BUILD { >+ my $self = shift; >+ my $syspref = C4::Context->preference("SearchEngine"); >+ apply_all_roles( $self, "Koha::SearchEngine::${syspref}::Config" ); >+}; >+1; >diff --git a/Koha/SearchEngine/ConfigRole.pm b/Koha/SearchEngine/ConfigRole.pm >new file mode 100644 >index 0000000..9ea224f >--- /dev/null >+++ b/Koha/SearchEngine/ConfigRole.pm >@@ -0,0 +1,7 @@ >+package Koha::SearchEngine::ConfigRole; >+ >+use Moose::Role; >+ >+requires 'indexes', 'index', 'ressource_types'; >+ >+1; >diff --git a/Koha/SearchEngine/FacetsBuilder.pm b/Koha/SearchEngine/FacetsBuilder.pm >new file mode 100644 >index 0000000..2c974b1 >--- /dev/null >+++ b/Koha/SearchEngine/FacetsBuilder.pm >@@ -0,0 +1,12 @@ >+package Koha::SearchEngine::FacetsBuilder; >+ >+use Moose; >+ >+use Moose::Util qw( apply_all_roles ); >+ >+sub BUILD { >+ my $self = shift; >+ my $syspref = C4::Context->preference("SearchEngine"); >+ apply_all_roles( $self, "Koha::SearchEngine::${syspref}::FacetsBuilder" ); >+}; >+1; >diff --git a/Koha/SearchEngine/FacetsBuilderRole.pm b/Koha/SearchEngine/FacetsBuilderRole.pm >new file mode 100644 >index 0000000..f0886e0 >--- /dev/null >+++ b/Koha/SearchEngine/FacetsBuilderRole.pm >@@ -0,0 +1,7 @@ >+package Koha::SearchEngine::FacetsBuilderRole; >+ >+use Moose::Role; >+ >+requires 'build_facets'; >+ >+1; >diff --git a/Koha/SearchEngine/Index.pm b/Koha/SearchEngine/Index.pm >new file mode 100644 >index 0000000..2807fbf >--- /dev/null >+++ b/Koha/SearchEngine/Index.pm >@@ -0,0 +1,11 @@ >+package Koha::SearchEngine::Index; >+use Moose; >+ >+use Moose::Util qw( apply_all_roles ); >+ >+sub BUILD { >+ my $self = shift; >+ my $syspref = 'Solr'; >+ apply_all_roles( $self, "Koha::SearchEngine::${syspref}::Index" ); >+}; >+1; >diff --git a/Koha/SearchEngine/IndexRole.pm b/Koha/SearchEngine/IndexRole.pm >new file mode 100644 >index 0000000..a52f852 >--- /dev/null >+++ b/Koha/SearchEngine/IndexRole.pm >@@ -0,0 +1,6 @@ >+package Koha::SearchEngine::IndexRole; >+use Moose::Role; >+ >+requires 'index_record'; >+ >+1; >diff --git a/Koha/SearchEngine/QueryBuilder.pm b/Koha/SearchEngine/QueryBuilder.pm >new file mode 100644 >index 0000000..7c2f793 >--- /dev/null >+++ b/Koha/SearchEngine/QueryBuilder.pm >@@ -0,0 +1,12 @@ >+package Koha::SearchEngine::QueryBuilder; >+ >+use Moose; >+ >+use Moose::Util qw( apply_all_roles ); >+ >+sub BUILD { >+ my $self = shift; >+ my $syspref = C4::Context->preference("SearchEngine"); >+ apply_all_roles( $self, "Koha::SearchEngine::${syspref}::QueryBuilder" ); >+}; >+1; >diff --git a/Koha/SearchEngine/QueryBuilderRole.pm b/Koha/SearchEngine/QueryBuilderRole.pm >new file mode 100644 >index 0000000..b768035 >--- /dev/null >+++ b/Koha/SearchEngine/QueryBuilderRole.pm >@@ -0,0 +1,7 @@ >+package Koha::SearchEngine::QueryBuilderRole; >+ >+use Moose::Role; >+ >+requires 'build_query'; >+ >+1; >diff --git a/Koha/SearchEngine/Search.pm b/Koha/SearchEngine/Search.pm >new file mode 100644 >index 0000000..8d9b469 >--- /dev/null >+++ b/Koha/SearchEngine/Search.pm >@@ -0,0 +1,12 @@ >+package Koha::SearchEngine::Search; >+use Moose; >+use C4::Context; >+ >+use Moose::Util qw( apply_all_roles ); >+ >+sub BUILD { >+ my $self = shift; >+ my $syspref = C4::Context->preference("SearchEngine"); >+ apply_all_roles( $self, "Koha::SearchEngine::${syspref}::Search" ); >+}; >+1; >diff --git a/Koha/SearchEngine/SearchRole.pm b/Koha/SearchEngine/SearchRole.pm >new file mode 100644 >index 0000000..40a5c5b >--- /dev/null >+++ b/Koha/SearchEngine/SearchRole.pm >@@ -0,0 +1,7 @@ >+package Koha::SearchEngine::SearchRole; >+use Moose::Role; >+ >+requires 'search'; >+requires 'dosmth'; >+ >+1; >diff --git a/Koha/SearchEngine/Solr.pm b/Koha/SearchEngine/Solr.pm >new file mode 100644 >index 0000000..3d7041d >--- /dev/null >+++ b/Koha/SearchEngine/Solr.pm >@@ -0,0 +1,45 @@ >+package Koha::SearchEngine::Solr; >+use Moose; >+use Koha::SearchEngine::Config; >+ >+extends 'Koha::SearchEngine', 'Data::SearchEngine::Solr'; >+ >+has '+url' => ( >+ is => 'ro', >+ isa => 'Str', >+# default => sub { >+# C4::Context->preference('SolrAPI'); >+# }, >+ lazy => 1, >+ builder => '_build_url', >+ required => 1 >+); >+ >+sub _build_url { >+ my ( $self ) = @_; >+ $self->config->SolrAPI; >+} >+ >+has '+options' => ( >+ is => 'ro', >+ isa => 'HashRef', >+ default => sub { >+ { >+ wt => 'json', >+ fl => '*,score', >+ fq => 'recordtype:biblio', >+ facets => 'true' >+ } >+ } >+ >+); >+ >+has indexes => ( >+ is => 'ro', >+ lazy => 1, >+ default => sub { >+# my $dbh => ...; >+ }, >+); >+ >+1; >diff --git a/Koha/SearchEngine/Solr/Config.pm b/Koha/SearchEngine/Solr/Config.pm >new file mode 100644 >index 0000000..0162082 >--- /dev/null >+++ b/Koha/SearchEngine/Solr/Config.pm >@@ -0,0 +1,115 @@ >+package Koha::SearchEngine::Solr::Config; >+ >+use Modern::Perl; >+use Moose::Role; >+use YAML; >+ >+with 'Koha::SearchEngine::ConfigRole'; >+ >+has index_config => ( >+ is => 'rw', >+ lazy => 1, >+ builder => '_load_index_config_file', >+); >+ >+has solr_config => ( >+ is => 'rw', >+ lazy => 1, >+ builder => '_load_solr_config_file', >+); >+ >+has index_filename => ( >+ is => 'rw', >+ lazy => 1, >+ default => C4::Context->config("installdir") . qq{/etc/searchengine/solr/indexes.yaml}, >+); >+has solr_filename => ( >+ is => 'rw', >+ lazy => 1, >+ default => C4::Context->config("installdir") . qq{/etc/searchengine/solr/config.yaml}, >+); >+ >+sub _load_index_config_file { >+ my ( $self, $filename ) = @_; >+ $self->index_filename( $filename ) if defined $filename; >+ die "The config index file (" . $self->index_filename . ") for Solr is not exist" if not -e $self->index_filename; >+ >+ return YAML::LoadFile($self->index_filename); >+} >+ >+sub _load_solr_config_file { >+ my ( $self ) = @_; >+ die "The solr config index file (" . $self->solr_filename . ") for Solr is not exist" if not -e $self->solr_filename; >+ >+ return YAML::LoadFile($self->solr_filename); >+} >+ >+sub set_config_filename { >+ my ( $self, $filename ) = @_; >+ $self->index_config( $self->_load_index_config_file( $filename ) ); >+} >+ >+sub SolrAPI { >+ my ( $self ) = @_; >+ return $self->solr_config->{SolrAPI}; >+} >+sub indexes { # FIXME Return index list if param not an hashref (string ressource_type) >+ my ( $self, $indexes ) = @_; >+ return $self->write( { indexes => $indexes } ) if defined $indexes; >+ return $self->index_config->{indexes}; >+} >+ >+sub index { >+ my ( $self, $code ) = @_; >+ my @index = map { ( $_->{code} eq $code ) ? $_ : () } @{$self->index_config->{indexes}}; >+ return $index[0]; >+} >+ >+sub ressource_types { >+ my ( $self ) = @_; >+ my $config = $self->index_config; >+ return $config->{ressource_types}; >+} >+ >+sub sortable_indexes { >+ my ( $self ) = @_; >+ my @sortable_indexes = map { $_->{sortable} ? $_ : () } @{ $self->index_config->{indexes} }; >+ return \@sortable_indexes; >+} >+ >+sub facetable_indexes { >+ my ( $self ) = @_; >+ my @facetable_indexes = map { $_->{facetable} ? $_ : () } @{ $self->index_config->{indexes} }; >+ return \@facetable_indexes; >+} >+ >+sub reload { >+ my ( $self ) = @_; >+ $self->index_config( $self->_load_index_config_file ); >+} >+sub write { >+ my ( $self, $values ) = @_; >+ my $r; >+ while ( my ( $k, $v ) = each %$values ) { >+ $r->{$k} = $v; >+ } >+ >+ if ( not grep /^ressource_type$/, keys %$values ) { >+ $r->{ressource_types} = $self->ressource_types; >+ } >+ >+ if ( not grep /^indexes$/, keys %$values ) { >+ $r->{indexes} = $self->indexes; >+ } >+ >+ eval { >+ YAML::DumpFile( $self->index_filename, $r ); >+ }; >+ if ( $@ ) { >+ die "Failed to dump the index config into the specified file ($@)"; >+ } >+ >+ $self->reload; >+} >+ >+1; >diff --git a/Koha/SearchEngine/Solr/FacetsBuilder.pm b/Koha/SearchEngine/Solr/FacetsBuilder.pm >new file mode 100644 >index 0000000..e8478a0 >--- /dev/null >+++ b/Koha/SearchEngine/Solr/FacetsBuilder.pm >@@ -0,0 +1,41 @@ >+package Koha::SearchEngine::Solr::FacetsBuilder; >+ >+use Modern::Perl; >+use Moose::Role; >+ >+with 'Koha::SearchEngine::FacetsBuilderRole'; >+ >+sub build_facets { >+ my ( $self, $results, $facetable_indexes, $filters ) = @_; >+ my @facets_loop; >+ for my $index ( @$facetable_indexes ) { >+ my $index_name = $index->{type} . '_' . $index->{code}; >+ my $facets = $results->facets->{'str_' . $index->{code}}; >+ if ( @$facets > 1 ) { >+ my @values; >+ $index =~ m/^([^_]*)_(.*)$/; >+ for ( my $i = 0 ; $i < scalar(@$facets) ; $i++ ) { >+ my $value = $facets->[$i++]; >+ my $count = $facets->[$i]; >+ utf8::encode($value); >+ my $lib =$value; >+ push @values, { >+ 'lib' => $lib, >+ 'value' => $value, >+ 'count' => $count, >+ 'active' => ( $filters->{$index_name} and scalar( grep /"?\Q$value\E"?/, @{ $filters->{$index_name} } ) ) ? 1 : 0, >+ }; >+ } >+ >+ push @facets_loop, { >+ 'indexname' => $index_name, >+ 'label' => $index->{label}, >+ 'values' => \@values, >+ 'size' => scalar(@values), >+ }; >+ } >+ } >+ return @facets_loop; >+} >+ >+1; >diff --git a/Koha/SearchEngine/Solr/Index.pm b/Koha/SearchEngine/Solr/Index.pm >new file mode 100644 >index 0000000..fd7c3d7 >--- /dev/null >+++ b/Koha/SearchEngine/Solr/Index.pm >@@ -0,0 +1,100 @@ >+package Koha::SearchEngine::Solr::Index; >+use Moose::Role; >+with 'Koha::SearchEngine::IndexRole'; >+ >+use Data::SearchEngine::Solr; >+use Data::Dump qw(dump); >+use List::MoreUtils qw(uniq); >+ >+use Koha::SearchEngine::Solr; >+use C4::AuthoritiesMarc; >+use C4::Biblio; >+ >+has searchengine => ( >+ is => 'rw', >+ isa => 'Koha::SearchEngine::Solr', >+ default => sub { Koha::SearchEngine::Solr->new }, >+ lazy => 1 >+); >+ >+sub optimize { >+ my ( $self ) = @_; >+ return $self->searchengine->_solr->optimize; >+} >+ >+sub index_record { >+ my ($self, $recordtype, $recordids) = @_; >+ >+ my $indexes = $self->searchengine->config->indexes; >+ my @records; >+ >+ my $recordids_str = ref($recordids) eq 'ARRAY' >+ ? join " ", @$recordids >+ : $recordids; >+ warn "IndexRecord called with $recordtype $recordids_str"; >+ >+ for my $id ( @$recordids ) { >+ my $record; >+ >+ $record = GetAuthority( $id ) if $recordtype eq "authority"; >+ $record = GetMarcBiblio( $id ) if $recordtype eq "biblio"; >+ >+ next unless ( $record ); >+ >+ my $index_values = { >+ recordid => $id, >+ recordtype => $recordtype, >+ }; >+ >+ warn "Indexing $recordtype $id"; >+ >+ for my $index ( @$indexes ) { >+ next if $index->{ressource_type} ne $recordtype; >+ my @values; >+ eval { >+ my $mappings = $index->{mappings}; >+ for my $tag_subf_code ( sort @$mappings ) { >+ my ( $f, $sf ) = split /\$/, $tag_subf_code; >+ for my $field ( $record->field( $f ) ) { >+ if ( $field->is_control_field ) { >+ push @values, $field->data; >+ } else { >+ my @sfvals = $sf eq '*' >+ ? map { $_->[1] } $field->subfields >+ : map { $_ } $field->subfield( $sf ); >+ >+ for ( @sfvals ) { >+ $_ = NormalizeDate( $_ ) if $index->{type} eq 'date'; >+ push @values, $_ if $_; >+ } >+ } >+ } >+ } >+ @values = uniq (@values); #Removes duplicates >+ >+ $index_values->{$index->{type}."_".$index->{code}} = \@values; >+ if ( $index->{sortable} ){ >+ $index_values->{"srt_" . $index->{type} . "_".$index->{code}} = $values[0]; >+ } >+ # Add index str for facets if it's not exist >+ if ( $index->{facetable} and @values > 0 and $index->{type} ne 'str' ) { >+ $index_values->{"str_" . $index->{code}} = $values[0]; >+ } >+ }; >+ if ( $@ ) { >+ chomp $@; >+ warn "Error during indexation : recordid $id, index $index->{code} ( $@ )"; >+ } >+ } >+ >+ my $solrrecord = Data::SearchEngine::Item->new( >+ id => "${recordtype}_$id", >+ score => 1, >+ values => $index_values, >+ ); >+ push @records, $solrrecord; >+ } >+ $self->searchengine->add( \@records ); >+} >+ >+1; >diff --git a/Koha/SearchEngine/Solr/QueryBuilder.pm b/Koha/SearchEngine/Solr/QueryBuilder.pm >new file mode 100644 >index 0000000..ba653b3 >--- /dev/null >+++ b/Koha/SearchEngine/Solr/QueryBuilder.pm >@@ -0,0 +1,146 @@ >+package Koha::SearchEngine::Solr::QueryBuilder; >+ >+use Modern::Perl; >+use Moose::Role; >+ >+with 'Koha::SearchEngine::QueryBuilderRole'; >+ >+sub build_advanced_query { >+ my ($class, $indexes, $operands, $operators) = @_; >+ >+ my $q = ''; >+ my $i = 0; >+ my $index_name; >+ >+ @$operands or return "*:*"; #push @$operands, "[* TO *]"; >+ >+ # Foreach operands >+ for my $kw (@$operands){ >+ $kw =~ s/(\w*\*)/\L$1\E/g; # Lower case on words with right truncation >+ $kw =~ s/(\s*\w*\?+\w*\s*)/\L$1\E/g; # Lower case on words contain wildcard ? >+ $kw =~ s/([^\\]):/$1\\:/g; # escape colons if not already escaped >+ # First element >+ if ($i == 0){ >+ if ( (my @x = eval {@$indexes} ) == 0 ){ >+ # There is no index, then query is in first operand >+ $q = @$operands[0]; >+ last; >+ } >+ >+ # Catch index name if it's not 'all_fields' >+ if ( @$indexes[$i] ne 'all_fields' ) { >+ $index_name = @$indexes[$i]; >+ }else{ >+ $index_name = ''; >+ } >+ >+ # Generate index:operand >+ $q .= BuildTokenString($index_name, $kw); >+ $i = $i + 1; >+ >+ next; >+ } >+ # And others >+ $index_name = @$indexes[$i] if @$indexes[$i]; >+ my $operator = defined @$operators[$i-1] ? @$operators[$i-1] : 'AND'; >+ given ( uc ( $operator ) ) { >+ when ('OR'){ >+ $q .= BuildTokenString($index_name, $kw, 'OR'); >+ } >+ when ('NOT'){ >+ $q .= BuildTokenString($index_name, $kw, 'NOT'); >+ } >+ default { >+ $q .= BuildTokenString($index_name, $kw, 'AND'); >+ } >+ } >+ $i = $i + 1; >+ } >+ >+ return $q; >+ >+} >+ >+sub BuildTokenString { >+ my ($index, $string, $operator) = @_; >+ my $r; >+ >+ if ($index ne 'all_fields' && $index ne ''){ >+ # Operand can contains an expression in brackets >+ if ( >+ $string =~ / / >+ and not ( $string =~ /^\(.*\)$/ ) >+ and not $string =~ /\[.*TO.*\]/ ) { >+ my @dqs; #double-quoted string >+ while ( $string =~ /"(?:[^"\\]++|\\.)*+"/g ) { >+ push @dqs, $&; >+ $string =~ s/\ *\Q$&\E\ *//; # Remove useless space before and after >+ } >+ >+ my @words = defined $string ? split ' ', $string : undef; >+ my $join = join qq{ AND } , map { >+ my $value = $_; >+ if ( $index =~ /^date_/ ) { >+ #$value = C4::Search::Engine::Solr::buildDateOperand( $value ); TODO >+ } >+ ( $value =~ /^"/ and $value ne '""' >+ and $index ne "emallfields" >+ and $index =~ /(txt_|ste_)/ ) >+ ? qq{em$index:$value} >+ : qq{$index:$value}; >+ } (@dqs, @words); >+ $r .= qq{($join)}; >+ } else { >+ if ( $index =~ /^date_/ ) { >+ #$string = C4::Search::Engine::Solr::buildDateOperand( $string ); TODO >+ } >+ >+ $r = "$index:$string"; >+ } >+ }else{ >+ $r = $string; >+ } >+ >+ return " $operator $r" if $operator; >+ return $r; >+} >+ >+sub build_query { >+ my ($class, $query) = @_; >+ >+ return "*:*" if not defined $query; >+ >+ # Particular *:* query >+ if ($query eq '*:*'){ >+ return $query; >+ } >+ >+ $query =~ s/(\w*\*)/\L$1\E/g; # Lower case on words with right truncation >+ $query =~ s/(\s*\w*\?+\w*\s*)/\L$1\E/g; # Lower case on words contain wildcard ? >+ >+ my @quotes; # Process colons in quotes >+ while ( $query =~ /'(?:[^'\\]++|\\.)*+'/g ) { >+ push @quotes, $&; >+ } >+ >+ for ( @quotes ) { >+ my $replacement = $_; >+ $replacement =~ s/[^\\]\K:/\\:/g; >+ $query =~ s/$_/$replacement/; >+ } >+ >+ $query =~ s/ : / \\: /g; # escape colons if " : " >+ >+ my $new_query = $query;#C4::Search::Query::splitToken($query); TODO >+ >+ $new_query =~ s/all_fields://g; >+ >+ # Upper case for operators >+ $new_query =~ s/ or / OR /g; >+ $new_query =~ s/ and / AND /g; >+ $new_query =~ s/ not / NOT /g; >+ >+ return $new_query; >+} >+ >+1; >diff --git a/Koha/SearchEngine/Solr/Search.pm b/Koha/SearchEngine/Solr/Search.pm >new file mode 100644 >index 0000000..5c626ba >--- /dev/null >+++ b/Koha/SearchEngine/Solr/Search.pm >@@ -0,0 +1,108 @@ >+package Koha::SearchEngine::Solr::Search; >+use Moose::Role; >+with 'Koha::SearchEngine::SearchRole'; >+ >+use Data::Dump qw(dump); >+use XML::Simple; >+ >+use Data::SearchEngine::Solr; >+use Data::Pagination; >+use Data::SearchEngine::Query; >+use Koha::SearchEngine::Solr; >+ >+has searchengine => ( >+ is => 'rw', >+ isa => 'Koha::SearchEngine::Solr', >+ default => sub { Koha::SearchEngine::Solr->new }, >+ lazy => 1 >+); >+ >+sub search { >+ my ( $self, $q, $filters, $params ) = @_; >+ >+ $q ||= '*:*'; >+ $filters ||= {}; >+ my $page = defined $params->{page} ? $params->{page} : 1; >+ my $count = defined $params->{count} ? $params->{count} : 999999999; >+ my $sort = defined $params->{sort} ? $params->{sort} : 'score desc'; >+ my $facets = defined $params->{facets} ? $params->{facets} : 0; >+ >+ # Construct fl from $params->{fl} >+ # If "recordid" or "id" not exist, we push them >+ my $fl = join ",", >+ defined $params->{fl} >+ ? ( >+ @{$params->{fl}}, >+ grep ( /^recordid$/, @{$params->{fl}} ) ? () : "recordid", >+ grep ( /^id$/, @{$params->{fl}} ) ? () : "id" >+ ) >+ : ( "recordid", "id" ); >+ >+ my $recordtype = ref($filters->{recordtype}) eq 'ARRAY' >+ ? $filters->{recordtype}[0] >+ : $filters->{recordtype} >+ if defined $filters && defined $filters->{recordtype}; >+ >+ if ( $facets ) { >+ $self->searchengine->options->{"facet"} = 'true'; >+ $self->searchengine->options->{"facet.mincount"} = 1; >+ $self->searchengine->options->{"facet.limit"} = 10; # TODO create a new systempreference C4::Context->preference("numFacetsDisplay") >+ my @facetable_indexes = map { 'str_' . $_->{code} } @{$self->searchengine->config->facetable_indexes}; >+ $self->searchengine->options->{"facet.field"} = \@facetable_indexes; >+ } >+ $self->searchengine->options->{sort} = $sort; >+ $self->searchengine->options->{fl} = $fl; >+ >+ # Construct filters >+ $self->searchengine->options->{fq} = [ >+ map { >+ my $idx = $_; >+ ref($filters->{$idx}) eq 'ARRAY' >+ ? >+ '(' >+ . join( ' AND ', >+ map { >+ my $filter_str = $_; >+ utf8::decode($filter_str); >+ my $quotes_existed = ( $filter_str =~ m/^".*"$/ ); >+ $filter_str =~ s/^"(.*)"$/$1/; #remove quote around value if exist >+ $filter_str =~ s/[^\\]\K"/\\"/g; >+ $filter_str = qq{"$filter_str"} # Add quote around value if not exist >+ if not $filter_str =~ /^".*"$/ >+ and $quotes_existed; >+ qq{$idx:$filter_str}; >+ } @{ $filters->{$idx} } ) >+ . ')' >+ : "$idx:$filters->{$idx}"; >+ } keys %$filters >+ ]; >+ >+ my $sq = Data::SearchEngine::Query->new( >+ page => $page, >+ count => $count, >+ query => $q, >+ ); >+ >+ # Get results >+ my $results = eval { $self->searchengine->search( $sq ) }; >+ >+ # Get error if exists >+ if ( $@ ) { >+ my $err = $@; >+ >+ $err =~ s#^[^\n]*\n##; # Delete first line >+ if ( $err =~ "400 URL must be absolute" ) { >+ $err = "Your system preference 'SolrAPI' is not set correctly"; >+ } >+ elsif ( not $err =~ 'Connection refused' ) { >+ my $document = XMLin( $err ); >+ $err = "$$document{body}{h2} : $$document{body}{pre}"; >+ } >+ $results->{error} = $err; >+ } >+ return $results; >+} >+ >+sub dosmth {'bou' } >+ >+1; >diff --git a/Koha/SearchEngine/Zebra.pm b/Koha/SearchEngine/Zebra.pm >new file mode 100644 >index 0000000..9071071 >--- /dev/null >+++ b/Koha/SearchEngine/Zebra.pm >@@ -0,0 +1,14 @@ >+package Koha::SearchEngine::Zebra; >+use Moose; >+ >+extends 'Data::SearchEngine::Zebra'; >+ >+# the configuration file is retrieved from KOHA_CONF by default, provide it from there² >+has '+conf_file' => ( >+ is => 'ro', >+ isa => 'Str', >+ default => $ENV{KOHA_CONF}, >+ required => 1 >+); >+ >+1; >diff --git a/Koha/SearchEngine/Zebra/QueryBuilder.pm b/Koha/SearchEngine/Zebra/QueryBuilder.pm >new file mode 100644 >index 0000000..50792a0 >--- /dev/null >+++ b/Koha/SearchEngine/Zebra/QueryBuilder.pm >@@ -0,0 +1,14 @@ >+package Koha::SearchEngine::Zebra::QueryBuilder; >+ >+use Modern::Perl; >+use Moose::Role; >+use C4::Search; >+ >+with 'Koha::SearchEngine::QueryBuilderRole'; >+ >+sub build_query { >+ shift; >+ C4::Search::buildQuery @_; >+} >+ >+1; >diff --git a/Koha/SearchEngine/Zebra/Search.pm b/Koha/SearchEngine/Zebra/Search.pm >new file mode 100644 >index 0000000..0531ab9 >--- /dev/null >+++ b/Koha/SearchEngine/Zebra/Search.pm >@@ -0,0 +1,40 @@ >+package Koha::SearchEngine::Zebra::Search; >+use Moose::Role; >+with 'Koha::SearchEngine::SearchRole'; >+ >+use Data::SearchEngine::Zebra; >+use Data::SearchEngine::Query; >+use Koha::SearchEngine::Zebra; >+use Data::Dump qw(dump); >+ >+has searchengine => ( >+ is => 'rw', >+ isa => 'Koha::SearchEngine::Zebra', >+ default => sub { Koha::SearchEngine::Zebra->new }, >+ lazy => 1 >+); >+ >+sub search { >+ my ($self,$query_string) = @_; >+ >+ my $query = Data::SearchEngine::Query->new( >+ count => 10, >+ page => 1, >+ query => $query_string, >+ ); >+ >+ warn "search for $query_string"; >+ >+ my $results = $self->searchengine->search($query); >+ >+ foreach my $item (@{ $results->items }) { >+ my $title = $item->get_value('ste_title'); >+ #utf8::encode($title); >+ print "$title\n"; >+ warn dump $title; >+ } >+} >+ >+sub dosmth {'bou' } >+ >+1; >diff --git a/admin/searchengine/solr/indexes.pl b/admin/searchengine/solr/indexes.pl >new file mode 100755 >index 0000000..39a5276 >--- /dev/null >+++ b/admin/searchengine/solr/indexes.pl >@@ -0,0 +1,103 @@ >+#!/usr/bin/perl >+ >+# Copyright 2012 BibLibre SARL >+# >+# 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 2 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::Koha; >+use C4::Output; >+use C4::Auth; >+use Koha::SearchEngine; >+ >+my $input = new CGI; >+my ( $template, $borrowernumber, $cookie ) = get_template_and_user( >+ { >+ template_name => 'admin/searchengine/solr/indexes.tt', >+ query => $input, >+ type => 'intranet', >+# authnotrequired => 0, >+# flagsrequired => { reserveforothers => "place_holds" }, #TODO >+ } >+); >+ >+my $ressource_type = $input->param('ressource_type') || 'biblio'; >+my $se = Koha::SearchEngine->new; >+my $se_config = $se->config; >+ >+my $indexes; >+if ( $input->param('op') and $input->param('op') eq 'edit' ) { >+ my @code = $input->param('code'); >+ my @label = $input->param('label'); >+ my @type = $input->param('type'); >+ my @sortable = $input->param('sortable'); >+ my @facetable = $input->param('facetable'); >+ my @mandatory = $input->param('mandatory'); >+ my @ressource_type = $input->param('ressource_type'); >+ my @mappings = $input->param('mappings'); >+ my @indexes; >+ my @errors; >+ for ( 0 .. @code-1 ) { >+ my $icode = $code[$_]; >+ my @current_mappings = split /\r\n/, $mappings[$_]; >+ if ( not @current_mappings ) { >+ @current_mappings = split /\n/, $mappings[$_]; >+ } >+ if ( not @current_mappings ) { >+ push @errors, { type => 'no_mapping', value => $icode}; >+ } >+ >+ push @indexes, { >+ code => $icode, >+ label => $label[$_], >+ type => $type[$_], >+ sortable => scalar(grep(/^$icode$/, @sortable)), >+ facetable => scalar(grep(/^$icode$/, @facetable)), >+ mandatory => $mandatory[$_] eq '1' ? '1' : '0', >+ ressource_type => $ressource_type[$_], >+ mappings => \@current_mappings, >+ }; >+ for my $m ( @current_mappings ) { >+ push @errors, {type => 'malformed_mapping', value => $m} >+ if not $m =~ /^\d(\d|\*|\.){2}\$.$/; >+ } >+ } >+ $indexes = \@indexes if @errors; >+ $template->param( errors => \@errors ); >+ >+ $se_config->indexes(\@indexes) if not @errors; >+} >+ >+my $ressource_types = $se_config->ressource_types; >+$indexes //= $se_config->indexes; >+ >+my $indexloop; >+for my $rt ( @$ressource_types ) { >+ my @indexes = map { >+ $_->{ressource_type} eq $rt ? $_ : (); >+ } @$indexes; >+ push @$indexloop, { >+ ressource_type => $rt, >+ indexes => \@indexes, >+ } >+} >+ >+$template->param( >+ indexloop => $indexloop, >+); >+ >+output_html_with_http_headers $input, $cookie, $template->output; >diff --git a/etc/searchengine/solr/config.yaml b/etc/searchengine/solr/config.yaml >new file mode 100644 >index 0000000..b30692e >--- /dev/null >+++ b/etc/searchengine/solr/config.yaml >@@ -0,0 +1,2 @@ >+SolrAPI: 'http://localhost:8983/solr/solr' >+ >diff --git a/etc/searchengine/solr/indexes.yaml b/etc/searchengine/solr/indexes.yaml >new file mode 100644 >index 0000000..ca598f8 >--- /dev/null >+++ b/etc/searchengine/solr/indexes.yaml >@@ -0,0 +1,45 @@ >+--- >+indexes: >+ - code: title >+ facetable: 1 >+ label: Title >+ mandatory: 1 >+ mappings: >+ - 200$a >+ - 210$a >+ - 4..$t >+ ressource_type: biblio >+ sortable: 1 >+ type: ste >+ - code: author >+ facetable: 1 >+ label: Author >+ mandatory: 0 >+ mappings: >+ - 700$* >+ - 710$* >+ ressource_type: biblio >+ sortable: 1 >+ type: str >+ - code: subject >+ facetable: 0 >+ label: Subject >+ mandatory: 0 >+ mappings: >+ - 600$a >+ - 601$a >+ ressource_type: biblio >+ sortable: 0 >+ type: str >+ - code: biblionumber >+ facetable: 0 >+ label: Biblionumber >+ mandatory: 1 >+ mappings: >+ - 001$@ >+ ressource_type: biblio >+ sortable: 0 >+ type: int >+ressource_types: >+ - biblio >+ - authority >diff --git a/etc/searchengine/solr/indexes.yaml.bak b/etc/searchengine/solr/indexes.yaml.bak >new file mode 100644 >index 0000000..b369160 >--- /dev/null >+++ b/etc/searchengine/solr/indexes.yaml.bak >@@ -0,0 +1,33 @@ >+ressource_types: >+ - biblio >+ - authority >+ >+indexes: >+ - code: title >+ label: Title >+ type: ste >+ ressource_type: biblio >+ sortable: 1 >+ mandatory: 1 >+ mappings: >+ - 200$a >+ - 210$a >+ - 4..$t >+ - code: author >+ label: Author >+ type: str >+ ressource_type: biblio >+ sortable: 1 >+ mandatory: 0 >+ mappings: >+ - 700$* >+ - 710$* >+ - code: subject >+ label: Subject >+ type: str >+ ressource_type: biblio >+ sortable: 0 >+ mandatory: 0 >+ mappings: >+ - 600$a >+ - 601$a >diff --git a/etc/solr/indexes.yaml b/etc/solr/indexes.yaml >new file mode 100644 >index 0000000..b369160 >--- /dev/null >+++ b/etc/solr/indexes.yaml >@@ -0,0 +1,33 @@ >+ressource_types: >+ - biblio >+ - authority >+ >+indexes: >+ - code: title >+ label: Title >+ type: ste >+ ressource_type: biblio >+ sortable: 1 >+ mandatory: 1 >+ mappings: >+ - 200$a >+ - 210$a >+ - 4..$t >+ - code: author >+ label: Author >+ type: str >+ ressource_type: biblio >+ sortable: 1 >+ mandatory: 0 >+ mappings: >+ - 700$* >+ - 710$* >+ - code: subject >+ label: Subject >+ type: str >+ ressource_type: biblio >+ sortable: 0 >+ mandatory: 0 >+ mappings: >+ - 600$a >+ - 601$a >diff --git a/installer/data/mysql/updatedatabase.pl b/installer/data/mysql/updatedatabase.pl >index 9b02cb9..a7163d9 100755 >--- a/installer/data/mysql/updatedatabase.pl >+++ b/installer/data/mysql/updatedatabase.pl >@@ -5369,6 +5369,13 @@ if ( C4::Context->preference("Version") < TransformToNum($DBversion) ) { > SetVersion($DBversion); > } > >+$DBversion = "3.07.00.XXX"; >+if ( C4::Context->preference("Version") < TransformToNum($DBversion) ) { >+ $dbh->do("INSERT IGNORE INTO systempreferences (variable,value,options,explanation,type) VALUES('SearchEngine','Zebra','Solr|Zebra','Search Engine','Choice')"); >+ print "Upgrade to $DBversion done (Add system preference SearchEngine )\n"; >+ SetVersion($DBversion); >+} >+ > > =head1 FUNCTIONS > >diff --git a/koha-tmpl/intranet-tmpl/prog/en/css/staff-global.css b/koha-tmpl/intranet-tmpl/prog/en/css/staff-global.css >index 555005b..2c2f874 100644 >--- a/koha-tmpl/intranet-tmpl/prog/en/css/staff-global.css >+++ b/koha-tmpl/intranet-tmpl/prog/en/css/staff-global.css >@@ -285,6 +285,10 @@ td { > vertical-align : top; > } > >+table.indexes td { >+ vertical-align : middle; >+} >+ > td.borderless { > border-collapse : separate; > border : 0 none; >diff --git a/koha-tmpl/intranet-tmpl/prog/en/lib/jquery/plugins/jquery.textarea-expander.js b/koha-tmpl/intranet-tmpl/prog/en/lib/jquery/plugins/jquery.textarea-expander.js >new file mode 100644 >index 0000000..ee24000 >--- /dev/null >+++ b/koha-tmpl/intranet-tmpl/prog/en/lib/jquery/plugins/jquery.textarea-expander.js >@@ -0,0 +1,95 @@ >+/** >+ * TextAreaExpander plugin for jQuery >+ * v1.0 >+ * Expands or contracts a textarea height depending on the >+ * quatity of content entered by the user in the box. >+ * >+ * By Craig Buckler, Optimalworks.net >+ * >+ * As featured on SitePoint.com: >+ * http://www.sitepoint.com/blogs/2009/07/29/build-auto-expanding-textarea-1/ >+ * >+ * Please use as you wish at your own risk. >+ */ >+ >+/** >+ * Usage: >+ * >+ * From JavaScript, use: >+ * $(<node>).TextAreaExpander(<minHeight>, <maxHeight>); >+ * where: >+ * <node> is the DOM node selector, e.g. "textarea" >+ * <minHeight> is the minimum textarea height in pixels (optional) >+ * <maxHeight> is the maximum textarea height in pixels (optional) >+ * >+ * Alternatively, in you HTML: >+ * Assign a class of "expand" to any <textarea> tag. >+ * e.g. <textarea name="textarea1" rows="3" cols="40" class="expand"></textarea> >+ * >+ * Or assign a class of "expandMIN-MAX" to set the <textarea> minimum and maximum height. >+ * e.g. <textarea name="textarea1" rows="3" cols="40" class="expand50-200"></textarea> >+ * The textarea will use an appropriate height between 50 and 200 pixels. >+ */ >+ >+(function($) { >+ >+ // jQuery plugin definition >+ $.fn.TextAreaExpander = function(minHeight, maxHeight) { >+ >+ var hCheck = !($.browser.msie || $.browser.opera); >+ >+ // resize a textarea >+ function ResizeTextarea(e) { >+ >+ // event or initialize element? >+ e = e.target || e; >+ >+ // find content length and box width >+ var vlen = e.value.length, ewidth = e.offsetWidth; >+ if (vlen != e.valLength || ewidth != e.boxWidth) { >+ >+ if (hCheck && (vlen < e.valLength || ewidth != e.boxWidth)) e.style.height = "0px"; >+ var h = Math.max(e.expandMin, Math.min(e.scrollHeight, e.expandMax)); >+ >+ e.style.overflow = (e.scrollHeight > h ? "auto" : "hidden"); >+ e.style.height = h + "px"; >+ >+ e.valLength = vlen; >+ e.boxWidth = ewidth; >+ } >+ >+ return true; >+ }; >+ >+ // initialize >+ this.each(function() { >+ >+ // is a textarea? >+ if (this.nodeName.toLowerCase() != "textarea") return; >+ >+ // set height restrictions >+ var p = this.className.match(/expand(\d+)\-*(\d+)*/i); >+ this.expandMin = minHeight || (p ? parseInt('0'+p[1], 10) : 0); >+ this.expandMax = maxHeight || (p ? parseInt('0'+p[2], 10) : 99999); >+ >+ // initial resize >+ ResizeTextarea(this); >+ >+ // zero vertical padding and add events >+ if (!this.Initialized) { >+ this.Initialized = true; >+ $(this).css("padding-top", 0).css("padding-bottom", 0); >+ $(this).bind("keyup", ResizeTextarea).bind("focus", ResizeTextarea); >+ } >+ }); >+ >+ return this; >+ }; >+ >+})(jQuery); >+ >+ >+// initialize all expanding textareas >+jQuery(document).ready(function() { >+ jQuery("textarea[class*=expand]").TextAreaExpander(); >+}); >diff --git a/koha-tmpl/intranet-tmpl/prog/en/lib/jquery/plugins/tablednd.js b/koha-tmpl/intranet-tmpl/prog/en/lib/jquery/plugins/tablednd.js >new file mode 100644 >index 0000000..4f4bbff >--- /dev/null >+++ b/koha-tmpl/intranet-tmpl/prog/en/lib/jquery/plugins/tablednd.js >@@ -0,0 +1,382 @@ >+/** >+ * TableDnD plug-in for JQuery, allows you to drag and drop table rows >+ * You can set up various options to control how the system will work >+ * Copyright (c) Denis Howlett <denish@isocra.com> >+ * Licensed like jQuery, see http://docs.jquery.com/License. >+ * >+ * Configuration options: >+ * >+ * onDragStyle >+ * This is the style that is assigned to the row during drag. There are limitations to the styles that can be >+ * associated with a row (such as you can't assign a border--well you can, but it won't be >+ * displayed). (So instead consider using onDragClass.) The CSS style to apply is specified as >+ * a map (as used in the jQuery css(...) function). >+ * onDropStyle >+ * This is the style that is assigned to the row when it is dropped. As for onDragStyle, there are limitations >+ * to what you can do. Also this replaces the original style, so again consider using onDragClass which >+ * is simply added and then removed on drop. >+ * onDragClass >+ * This class is added for the duration of the drag and then removed when the row is dropped. It is more >+ * flexible than using onDragStyle since it can be inherited by the row cells and other content. The default >+ * is class is tDnD_whileDrag. So to use the default, simply customise this CSS class in your >+ * stylesheet. >+ * onDrop >+ * Pass a function that will be called when the row is dropped. The function takes 2 parameters: the table >+ * and the row that was dropped. You can work out the new order of the rows by using >+ * table.rows. >+ * onDragStart >+ * Pass a function that will be called when the user starts dragging. The function takes 2 parameters: the >+ * table and the row which the user has started to drag. >+ * onAllowDrop >+ * Pass a function that will be called as a row is over another row. If the function returns true, allow >+ * dropping on that row, otherwise not. The function takes 2 parameters: the dragged row and the row under >+ * the cursor. It returns a boolean: true allows the drop, false doesn't allow it. >+ * scrollAmount >+ * This is the number of pixels to scroll if the user moves the mouse cursor to the top or bottom of the >+ * window. The page should automatically scroll up or down as appropriate (tested in IE6, IE7, Safari, FF2, >+ * FF3 beta >+ * dragHandle >+ * This is the name of a class that you assign to one or more cells in each row that is draggable. If you >+ * specify this class, then you are responsible for setting cursor: move in the CSS and only these cells >+ * will have the drag behaviour. If you do not specify a dragHandle, then you get the old behaviour where >+ * the whole row is draggable. >+ * >+ * Other ways to control behaviour: >+ * >+ * Add class="nodrop" to any rows for which you don't want to allow dropping, and class="nodrag" to any rows >+ * that you don't want to be draggable. >+ * >+ * Inside the onDrop method you can also call $.tableDnD.serialize() this returns a string of the form >+ * <tableID>[]=<rowID1>&<tableID>[]=<rowID2> so that you can send this back to the server. The table must have >+ * an ID as must all the rows. >+ * >+ * Other methods: >+ * >+ * $("...").tableDnDUpdate() >+ * Will update all the matching tables, that is it will reapply the mousedown method to the rows (or handle cells). >+ * This is useful if you have updated the table rows using Ajax and you want to make the table draggable again. >+ * The table maintains the original configuration (so you don't have to specify it again). >+ * >+ * $("...").tableDnDSerialize() >+ * Will serialize and return the serialized string as above, but for each of the matching tables--so it can be >+ * called from anywhere and isn't dependent on the currentTable being set up correctly before calling >+ * >+ * Known problems: >+ * - Auto-scoll has some problems with IE7 (it scrolls even when it shouldn't), work-around: set scrollAmount to 0 >+ * >+ * Version 0.2: 2008-02-20 First public version >+ * Version 0.3: 2008-02-07 Added onDragStart option >+ * Made the scroll amount configurable (default is 5 as before) >+ * Version 0.4: 2008-03-15 Changed the noDrag/noDrop attributes to nodrag/nodrop classes >+ * Added onAllowDrop to control dropping >+ * Fixed a bug which meant that you couldn't set the scroll amount in both directions >+ * Added serialize method >+ * Version 0.5: 2008-05-16 Changed so that if you specify a dragHandle class it doesn't make the whole row >+ * draggable >+ * Improved the serialize method to use a default (and settable) regular expression. >+ * Added tableDnDupate() and tableDnDSerialize() to be called when you are outside the table >+ */ >+jQuery.tableDnD = { >+ /** Keep hold of the current table being dragged */ >+ currentTable : null, >+ /** Keep hold of the current drag object if any */ >+ dragObject: null, >+ /** The current mouse offset */ >+ mouseOffset: null, >+ /** Remember the old value of Y so that we don't do too much processing */ >+ oldY: 0, >+ >+ /** Actually build the structure */ >+ build: function(options) { >+ // Set up the defaults if any >+ >+ this.each(function() { >+ // This is bound to each matching table, set up the defaults and override with user options >+ this.tableDnDConfig = jQuery.extend({ >+ onDragStyle: null, >+ onDropStyle: null, >+ // Add in the default class for whileDragging >+ onDragClass: "tDnD_whileDrag", >+ onDrop: null, >+ onDragStart: null, >+ scrollAmount: 5, >+ serializeRegexp: /[^\-]*$/, // The regular expression to use to trim row IDs >+ serializeParamName: null, // If you want to specify another parameter name instead of the table ID >+ dragHandle: null // If you give the name of a class here, then only Cells with this class will be draggable >+ }, options || {}); >+ // Now make the rows draggable >+ jQuery.tableDnD.makeDraggable(this); >+ }); >+ >+ // Now we need to capture the mouse up and mouse move event >+ // We can use bind so that we don't interfere with other event handlers >+ jQuery(document) >+ .bind('mousemove', jQuery.tableDnD.mousemove) >+ .bind('mouseup', jQuery.tableDnD.mouseup); >+ >+ // Don't break the chain >+ return this; >+ }, >+ >+ /** This function makes all the rows on the table draggable apart from those marked as "NoDrag" */ >+ makeDraggable: function(table) { >+ var config = table.tableDnDConfig; >+ if (table.tableDnDConfig.dragHandle) { >+ // We only need to add the event to the specified cells >+ var cells = jQuery("td."+table.tableDnDConfig.dragHandle, table); >+ cells.each(function() { >+ // The cell is bound to "this" >+ jQuery(this).mousedown(function(ev) { >+ jQuery.tableDnD.dragObject = this.parentNode; >+ jQuery.tableDnD.currentTable = table; >+ jQuery.tableDnD.mouseOffset = jQuery.tableDnD.getMouseOffset(this, ev); >+ if (config.onDragStart) { >+ // Call the onDrop method if there is one >+ config.onDragStart(table, this); >+ } >+ return false; >+ }); >+ }) >+ } else { >+ // For backwards compatibility, we add the event to the whole row >+ var rows = jQuery("tr", table); // get all the rows as a wrapped set >+ rows.each(function() { >+ // Iterate through each row, the row is bound to "this" >+ var row = jQuery(this); >+ if (! row.hasClass("nodrag")) { >+ row.mousedown(function(ev) { >+ if (ev.target.tagName == "TD") { >+ jQuery.tableDnD.dragObject = this; >+ jQuery.tableDnD.currentTable = table; >+ jQuery.tableDnD.mouseOffset = jQuery.tableDnD.getMouseOffset(this, ev); >+ if (config.onDragStart) { >+ // Call the onDrop method if there is one >+ config.onDragStart(table, this); >+ } >+ return false; >+ } >+ }).css("cursor", "move"); // Store the tableDnD object >+ } >+ }); >+ } >+ }, >+ >+ updateTables: function() { >+ this.each(function() { >+ // this is now bound to each matching table >+ if (this.tableDnDConfig) { >+ jQuery.tableDnD.makeDraggable(this); >+ } >+ }) >+ }, >+ >+ /** Get the mouse coordinates from the event (allowing for browser differences) */ >+ mouseCoords: function(ev){ >+ if(ev.pageX || ev.pageY){ >+ return {x:ev.pageX, y:ev.pageY}; >+ } >+ return { >+ x:ev.clientX + document.body.scrollLeft - document.body.clientLeft, >+ y:ev.clientY + document.body.scrollTop - document.body.clientTop >+ }; >+ }, >+ >+ /** Given a target element and a mouse event, get the mouse offset from that element. >+ To do this we need the element's position and the mouse position */ >+ getMouseOffset: function(target, ev) { >+ ev = ev || window.event; >+ >+ var docPos = this.getPosition(target); >+ var mousePos = this.mouseCoords(ev); >+ return {x:mousePos.x - docPos.x, y:mousePos.y - docPos.y}; >+ }, >+ >+ /** Get the position of an element by going up the DOM tree and adding up all the offsets */ >+ getPosition: function(e){ >+ var left = 0; >+ var top = 0; >+ /** Safari fix -- thanks to Luis Chato for this! */ >+ if (e.offsetHeight == 0) { >+ /** Safari 2 doesn't correctly grab the offsetTop of a table row >+ this is detailed here: >+ http://jacob.peargrove.com/blog/2006/technical/table-row-offsettop-bug-in-safari/ >+ the solution is likewise noted there, grab the offset of a table cell in the row - the firstChild. >+ note that firefox will return a text node as a first child, so designing a more thorough >+ solution may need to take that into account, for now this seems to work in firefox, safari, ie */ >+ e = e.firstChild; // a table cell >+ } >+ >+ while (e.offsetParent){ >+ left += e.offsetLeft; >+ top += e.offsetTop; >+ e = e.offsetParent; >+ } >+ >+ left += e.offsetLeft; >+ top += e.offsetTop; >+ >+ return {x:left, y:top}; >+ }, >+ >+ mousemove: function(ev) { >+ if (jQuery.tableDnD.dragObject == null) { >+ return; >+ } >+ >+ var dragObj = jQuery(jQuery.tableDnD.dragObject); >+ var config = jQuery.tableDnD.currentTable.tableDnDConfig; >+ var mousePos = jQuery.tableDnD.mouseCoords(ev); >+ var y = mousePos.y - jQuery.tableDnD.mouseOffset.y; >+ //auto scroll the window >+ var yOffset = window.pageYOffset; >+ if (document.all) { >+ // Windows version >+ //yOffset=document.body.scrollTop; >+ if (typeof document.compatMode != 'undefined' && >+ document.compatMode != 'BackCompat') { >+ yOffset = document.documentElement.scrollTop; >+ } >+ else if (typeof document.body != 'undefined') { >+ yOffset=document.body.scrollTop; >+ } >+ >+ } >+ >+ if (mousePos.y-yOffset < config.scrollAmount) { >+ window.scrollBy(0, -config.scrollAmount); >+ } else { >+ var windowHeight = window.innerHeight ? window.innerHeight >+ : document.documentElement.clientHeight ? document.documentElement.clientHeight : document.body.clientHeight; >+ if (windowHeight-(mousePos.y-yOffset) < config.scrollAmount) { >+ window.scrollBy(0, config.scrollAmount); >+ } >+ } >+ >+ >+ if (y != jQuery.tableDnD.oldY) { >+ // work out if we're going up or down... >+ var movingDown = y > jQuery.tableDnD.oldY; >+ // update the old value >+ jQuery.tableDnD.oldY = y; >+ // update the style to show we're dragging >+ if (config.onDragClass) { >+ dragObj.addClass(config.onDragClass); >+ } else { >+ dragObj.css(config.onDragStyle); >+ } >+ // If we're over a row then move the dragged row to there so that the user sees the >+ // effect dynamically >+ var currentRow = jQuery.tableDnD.findDropTargetRow(dragObj, y); >+ if (currentRow) { >+ // TODO worry about what happens when there are multiple TBODIES >+ if (movingDown && jQuery.tableDnD.dragObject != currentRow) { >+ jQuery.tableDnD.dragObject.parentNode.insertBefore(jQuery.tableDnD.dragObject, currentRow.nextSibling); >+ } else if (! movingDown && jQuery.tableDnD.dragObject != currentRow) { >+ jQuery.tableDnD.dragObject.parentNode.insertBefore(jQuery.tableDnD.dragObject, currentRow); >+ } >+ } >+ } >+ >+ return false; >+ }, >+ >+ /** We're only worried about the y position really, because we can only move rows up and down */ >+ findDropTargetRow: function(draggedRow, y) { >+ var rows = jQuery.tableDnD.currentTable.rows; >+ for (var i=0; i<rows.length; i++) { >+ var row = rows[i]; >+ var rowY = this.getPosition(row).y; >+ var rowHeight = parseInt(row.offsetHeight)/2; >+ if (row.offsetHeight == 0) { >+ rowY = this.getPosition(row.firstChild).y; >+ rowHeight = parseInt(row.firstChild.offsetHeight)/2; >+ } >+ // Because we always have to insert before, we need to offset the height a bit >+ if ((y > rowY - rowHeight) && (y < (rowY + rowHeight))) { >+ // that's the row we're over >+ // If it's the same as the current row, ignore it >+ if (row == draggedRow) {return null;} >+ var config = jQuery.tableDnD.currentTable.tableDnDConfig; >+ if (config.onAllowDrop) { >+ if (config.onAllowDrop(draggedRow, row)) { >+ return row; >+ } else { >+ return null; >+ } >+ } else { >+ // If a row has nodrop class, then don't allow dropping (inspired by John Tarr and Famic) >+ var nodrop = jQuery(row).hasClass("nodrop"); >+ if (! nodrop) { >+ return row; >+ } else { >+ return null; >+ } >+ } >+ return row; >+ } >+ } >+ return null; >+ }, >+ >+ mouseup: function(e) { >+ if (jQuery.tableDnD.currentTable && jQuery.tableDnD.dragObject) { >+ var droppedRow = jQuery.tableDnD.dragObject; >+ var config = jQuery.tableDnD.currentTable.tableDnDConfig; >+ // If we have a dragObject, then we need to release it, >+ // The row will already have been moved to the right place so we just reset stuff >+ if (config.onDragClass) { >+ jQuery(droppedRow).removeClass(config.onDragClass); >+ } else { >+ jQuery(droppedRow).css(config.onDropStyle); >+ } >+ jQuery.tableDnD.dragObject = null; >+ if (config.onDrop) { >+ // Call the onDrop method if there is one >+ config.onDrop(jQuery.tableDnD.currentTable, droppedRow); >+ } >+ jQuery.tableDnD.currentTable = null; // let go of the table too >+ } >+ }, >+ >+ serialize: function() { >+ if (jQuery.tableDnD.currentTable) { >+ return jQuery.tableDnD.serializeTable(jQuery.tableDnD.currentTable); >+ } else { >+ return "Error: No Table id set, you need to set an id on your table and every row"; >+ } >+ }, >+ >+ serializeTable: function(table) { >+ var result = ""; >+ var tableId = table.id; >+ var rows = table.rows; >+ for (var i=0; i<rows.length; i++) { >+ if (result.length > 0) result += "&"; >+ var rowId = rows[i].id; >+ if (rowId && rowId && table.tableDnDConfig && table.tableDnDConfig.serializeRegexp) { >+ rowId = rowId.match(table.tableDnDConfig.serializeRegexp)[0]; >+ } >+ >+ result += tableId + '[]=' + rowId; >+ } >+ return result; >+ }, >+ >+ serializeTables: function() { >+ var result = ""; >+ this.each(function() { >+ // this is now bound to each matching table >+ result += jQuery.tableDnD.serializeTable(this); >+ }); >+ return result; >+ } >+ >+} >+ >+jQuery.fn.extend( >+ { >+ tableDnD : jQuery.tableDnD.build, >+ tableDnDUpdate : jQuery.tableDnD.updateTables, >+ tableDnDSerialize: jQuery.tableDnD.serializeTables >+ } >+); >diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/admin-home.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/admin/admin-home.tt >index ef46ede..6e19132 100644 >--- a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/admin-home.tt >+++ b/koha-tmpl/intranet-tmpl/prog/en/modules/admin/admin-home.tt >@@ -76,6 +76,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/searchengine/solr/indexes.pl">Search engine configuration</a></dt> >+ <dd>Manage indexes, facets, and their mappings to MARC fields and subfields.</dd> > </dl> > > <h3>Acquisition parameters</h3> >diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/admin.pref b/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/admin.pref >index 08eb954..4e907e1 100644 >--- a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/admin.pref >+++ b/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/admin.pref >@@ -104,3 +104,11 @@ Administration: > Common Name: the Common Name > emailAddress: the emailAddress > - field for SSL client certificate authentication >+ Search Engine: >+ - >+ - pref: SearchEngine >+ default: Zebra >+ choices: >+ Solr: Solr >+ Zebra: Zebra >+ - is the search engine used. >diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/searchengine/solr/indexes.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/admin/searchengine/solr/indexes.tt >new file mode 100644 >index 0000000..2ccbe5a >--- /dev/null >+++ b/koha-tmpl/intranet-tmpl/prog/en/modules/admin/searchengine/solr/indexes.tt >@@ -0,0 +1,190 @@ >+[% INCLUDE 'doc-head-open.inc' %] >+<title>Koha › Administration › Solr config</title> >+[% INCLUDE 'doc-head-close.inc' %] >+<script type="text/javascript" src="[% themelang %]/lib/jquery/plugins/tablednd.js"></script> >+<script type="text/javascript" src="[% themelang %]/lib/jquery/plugins/jquery.textarea-expander.js"></script> >+<script type="text/javascript" language="javascript"> >+ function clean_line( line ) { >+ $(line).find('input[type="text"]').val(""); >+ $(line).find('input[type="checkbox"]').attr("checked", false); >+ $(line).find('textarea').val(""); >+ $(line).find('select').find('option:first').attr("selected", "selected"); >+ } >+ >+ function clone_line( line ) { >+ var new_line = $(line).clone(); >+ $(new_line).removeClass("nodrag nodrop"); >+ $(new_line).find('td:last-child>a').removeClass("add").addClass("delete").html(_("Delete")); >+ $(new_line).find('[data-id]').each( function() { >+ $(this).attr({ name: $(this).attr('data-id') }).removeAttr('data-id'); >+ } ); >+ $(new_line).find("select").each( function() { >+ var attr = $(this).attr('name'); >+ var val = $(line).find('[data-id="' + attr + '"]').val(); >+ $(this).find('option[value="' + val + '"]').attr("selected", "selected"); >+ } ); >+ return new_line; >+ } >+ >+ $(document).ready(function() { >+ $('.delete').click(function() { >+ $(this).parents('tr').remove(); >+ }); >+ >+ $(".indexes").tableDnD( { >+ onDragClass: "dragClass", >+ } ); >+ $("textarea").TextAreaExpander(); >+ $('.add').click(function() { >+ var table = $(this).closest('table'); >+ var ressource_type = $(table).attr('data-ressource_type'); >+ var code = $(table).find('input[data-id="code"]').val(); >+ var label = $(table).find('input[data-id="label"]').val(); >+ if ( code.length > 0 && label.length > 0 ) { >+ var line = $(this).closest("tr"); >+ var mappings = $(line).find('textarea').val(); >+ var new_line = clone_line( line ); >+ $(new_line).find('textarea').val(mappings); >+ $(new_line).find("input:checkbox").val(code); >+ new_line.appendTo($('table[data-ressource_type="'+ressource_type+'"]>tbody')); >+ $('.delete').click(function() { >+ $(this).parents('tr').remove(); >+ }); >+ clean_line(line); >+ >+ $(table).tableDnD( { >+ onDragClass: "dragClass", >+ } ); >+ } >+ }); >+ }); >+</script> >+</head> >+<body id="admin_searchengine_indexes" class="admin"> >+[% INCLUDE 'header.inc' %] >+[% INCLUDE 'cat-search.inc' %] >+ >+<div id="breadcrumbs"><a href="/cgi-bin/koha/mainpage.pl">Home</a> › <a href="/cgi-bin/koha/admin/admin-home.pl">Administration</a> › Search engine configuration</div> >+ >+<div id="doc3" class="yui-t1"> >+ >+ <div id="bd"> >+ <div id="yui-main"> >+ <div class="yui-b"> >+ <h1>Search engine configuration</h1> >+ <div class="warning"> >+ Warning: Any modification in these configurations will need a total reindexation to be fully taken into account ! >+ </div> >+ [% IF ( errors ) %] >+ <div class="error"> >+ Errors occurred, Modifications does not apply. Please check following values: >+ <ul> >+ [% FOREACH e IN errors %] >+ <li> >+ [% IF ( e.type == "malformed_mapping" ) %] >+ The value "[% e.value %]" is not supported for mappings >+ [% ELSIF ( e.type == "no_mapping" ) %] >+ There is no mapping for the index [% e.value %] >+ [% END %] >+ </li> >+ [% END %] >+ </ul> >+ </div> >+ [% END %] >+ >+ <form method="post"> >+ <input type="hidden" name="op" value="edit" /> >+ [% FOREACH rt IN indexloop %] >+ <h2>[% rt.ressource_type %]</h2> >+ [% IF ( rt.ressource_type == 'authority' ) %] >+ This part is not yet implemented >+ [% END %] >+ <table id="pouet" class="indexes" data-ressource_type="[% rt.ressource_type %]"> >+ <thead> >+ <tr class="nodrag nodrop"> >+ <th>Code</th> >+ <th>Label</th> >+ <th>Type</th> >+ <th>Sortable</th> >+ <th>Facetable</th> >+ <th>Mapping</th> >+ <th></th> >+ </tr> >+ </thead> >+ <tbody> >+ [% FOREACH index IN rt.indexes %] >+ <tr> >+ <td> >+ [% IF ( index.mandatory ) %] >+ <input name="code" type="text" maxlength="25" value="[% index.code %]" disabled="disabled" /> >+ <input name="code" type="hidden" maxlength="25" value="[% index.code %]" /> >+ [% ELSE %] >+ <input name="code" type="text" maxlength="25" value="[% index.code %]" /> >+ [% END %] >+ <input name="mandatory" type="hidden" maxlength="1" value="[% index.mandatory %]" /> >+ <input name="ressource_type" type="hidden" value="[% index.ressource_type %]" /> >+ </td> >+ <td><input name="label" type="text" maxlength="25" value="[% index.label %]" /></td> >+ <td> >+ [% IF ( index.mandatory ) %] >+ <input type="hidden" name="type" value="[% index.type %]" /> >+ [% END %] >+ <select name="type"[% IF ( index.mandatory ) %] disabled="disabled"[% END %]> >+ <option [% IF ( index.type == 'str' ) %] selected="selected"[% END %] value="str">String</option> >+ <option [% IF ( index.type == 'ste' ) %] selected="selected"[% END %] value="ste">Simple Text</option> >+ <option [% IF ( index.type == 'txt' ) %] selected="selected"[% END %] value="txt">Text</option> >+ <option [% IF ( index.type == 'int' ) %] selected="selected"[% END %] value="int">Integer</option> >+ <option [% IF ( index.type == 'date') %] selected="selected"[% END %] value="date">Date</option> >+ </select> >+ </td> >+ <td> >+ <input name="sortable" type="checkbox" [% IF ( index.sortable ) %]checked="checked"[% END %] value="[% index.code %]" /> >+ </td> >+ <td> >+ <input name="facetable" type="checkbox" [% IF ( index.facetable ) %]checked="checked"[% END %] value="[% index.code %]" /> >+ </td> >+ <td> >+ <textarea name="mappings" class="contentEditable">[% FOREACH m IN index.mappings %][% m %] >+[% END %]</textarea> <!-- Don't indent this line --> >+ </td> >+ <td>[% UNLESS ( index.mandatory ) %]<a class="delete">Delete</a>[% END %]</td> >+ </tr> >+ [% END %] >+ </tbody> >+ <tfoot> >+ <tr class="nodrag nodrop"> >+ <td> >+ <input data-id="code" type="text" maxlength="25" /> >+ <input data-id="ressource_type" type="hidden" value="[% rt.ressource_type %]" /> >+ <input data-id="mandatory" type="hidden" value="0" /> >+ </td> >+ <td><input data-id="label" type="text" maxlength="25" /></td> >+ <td> >+ <select data-id="type"> >+ <option value="str">String</option> >+ <option value="ste">Simple Text</option> >+ <option value="txt">Text</option> >+ <option value="int">Integer</option> >+ <option value="date">Date</option> >+ </select> >+ </td> >+ <td><input data-id="sortable" type="checkbox" /></td> >+ <td><input data-id="facetable" type="checkbox" /></td> >+ <td> >+ <textarea data-id="mappings" class="contentEditable"></textarea> >+ </td> >+ <td><a class="add">Add</a></td> >+ </tr> >+ </tfoot> >+ </table> >+ [% END %] >+ <p><input type="submit" value="Save" /></p> >+ </form> >+</div> >+ >+</div> >+<div class="yui-b"> >+[% INCLUDE 'admin-menu.inc' %] >+</div> >+</div> >+[% INCLUDE 'intranet-bottom.inc' %] >diff --git a/koha-tmpl/opac-tmpl/prog/en/includes/search/facets.inc b/koha-tmpl/opac-tmpl/prog/en/includes/search/facets.inc >new file mode 100644 >index 0000000..93c8b0f >--- /dev/null >+++ b/koha-tmpl/opac-tmpl/prog/en/includes/search/facets.inc >@@ -0,0 +1,56 @@ >+[% IF facets_loop %] >+ <div id="search-facets"> >+ <h4>Refine your search</h4> >+ <ul> >+ [% FOR facets IN facets_loop %] >+ <li> >+ [% facets.label %] >+ <ul class="facets_list"> >+ [% FOR value IN facets.values %] >+ <li> >+ [% IF ( value.active ) %] >+ [% value.lib %] ([% value.count %]) >+ [% SET url = "/cgi-bin/koha/opac-search.pl?" %] >+ [% SET first = 1 %] >+ [% FOR p IN follower_params %] >+ [% IF p.var != 'filters' %] >+ [% UNLESS first %] >+ [% SET url = url _ '&' %] >+ [% END %] >+ [% SET first = 0 %] >+ [% SET url = url _ p.var _ '=' _ p.val %] >+ [% END %] >+ [% END %] >+ [% FOR f IN filters %] >+ [% UNLESS f.var == facets.indexname && f.val == value.value %] >+ [% SET url = url _ '&filters=' _ f.var _ ':"' _ f.val _ '"' %] >+ [% END %] >+ [% END %] >+ [<a href="[% url |url %]">x</a>] >+ [% ELSE %] >+ [% SET url = "/cgi-bin/koha/opac-search.pl?" %] >+ [% SET first = 1 %] >+ [% FOR p IN follower_params %] >+ [% IF p.var != 'filters' %] >+ [% UNLESS first %] >+ [% SET url = url _ '&' %] >+ [% END %] >+ [% SET first = 0 %] >+ [% SET url = url _ p.var _ '=' _ p.val %] >+ [% END %] >+ [% END %] >+ [% FOR f IN filters %] >+ [% SET url = url _ '&filters=' _ f.var _ ':"' _ f.val _ '"' %] >+ [% END %] >+ [% SET url = url _ '&filters=' _ facets.indexname _ ':"' _ value.value _ '"' %] >+ >+ <a href="[% url |url %]">[% value.lib %]</a> ([% value.count %]) >+ [% END %] >+ </li> >+ [% END %] >+ </ul> >+ </li> >+ [% END %] >+ </ul> >+</div> >+[% END %] >diff --git a/koha-tmpl/opac-tmpl/prog/en/includes/search/page-numbers.inc b/koha-tmpl/opac-tmpl/prog/en/includes/search/page-numbers.inc >new file mode 100644 >index 0000000..2de511f >--- /dev/null >+++ b/koha-tmpl/opac-tmpl/prog/en/includes/search/page-numbers.inc >@@ -0,0 +1,17 @@ >+[% IF ( PAGE_NUMBERS ) %] >+ <div class="pages"> >+ [% IF ( previous_page ) %] >+ <a class="nav" href="?[% FOREACH fp IN follower_params %][% fp.var |url %]=[% fp.val |url %]&[% END %]page=[% previous_page |url %]"><< Previous</a> >+ [% END %] >+ [% FOREACH PAGE_NUMBER IN PAGE_NUMBERS %] >+ [% IF ( PAGE_NUMBER.current ) %] >+ <span class="current">[% PAGE_NUMBER.page %]</span> >+ [% ELSE %] >+ <a class="nav" href="?[% FOREACH fp IN follower_params %][% fp.var |url %]=[% fp.val |url %]&[% END %]page=[% PAGE_NUMBER.page |url %]">[% PAGE_NUMBER.page %]</a> >+ [% END %] >+ [% END %] >+ [% IF ( next_page ) %] >+ <a class="nav" href="?[% FOREACH fp IN follower_params %][% fp.var |url %]=[% fp.val |url %]&[% END %]page=[% next_page |url %]">Next >></a> >+ [% END %] >+ </div> >+[% END %] >diff --git a/koha-tmpl/opac-tmpl/prog/en/includes/search/resort_form.inc b/koha-tmpl/opac-tmpl/prog/en/includes/search/resort_form.inc >new file mode 100644 >index 0000000..88c9f66 >--- /dev/null >+++ b/koha-tmpl/opac-tmpl/prog/en/includes/search/resort_form.inc >@@ -0,0 +1,23 @@ >+[% IF sort_by == "score asc" %] >+ <option value="score asc" selected="selected">Relevance asc</option> >+[% ELSE %] >+ <option value="score asc">Relevance asc</option> >+[% END %] >+[% IF sort_by == "score desc" %] >+ <option value="score desc" selected="selected">Relevance desc</option> >+[% ELSE %] >+ <option value="score desc">Relevance desc</option> >+[% END %] >+ >+[% FOREACH ind IN sortable_indexes %] >+ [% IF sort_by == "$ind.code asc" %] >+ <option value="[% ind.code %] asc" selected="selected">[% ind.label %] asc</option> >+ [% ELSE %] >+ <option value="[% ind.code %] asc">[% ind.label %] asc</option> >+ [% END %] >+ [% IF sort_by == "$ind.code desc" %] >+ <option value="[% ind.code %] desc" selected="selected">[% ind.label %] desc</option> >+ [% ELSE %] >+ <option value="[% ind.code %] desc">[% ind.label %] desc</option> >+ [% END %] >+[% END %] >diff --git a/koha-tmpl/opac-tmpl/prog/en/modules/search/results.tt b/koha-tmpl/opac-tmpl/prog/en/modules/search/results.tt >new file mode 100644 >index 0000000..ab2ea12 >--- /dev/null >+++ b/koha-tmpl/opac-tmpl/prog/en/modules/search/results.tt >@@ -0,0 +1,108 @@ >+[% INCLUDE 'doc-head-open.inc' %] >+[% IF ( LibraryNameTitle ) %][% LibraryNameTitle %][% ELSE %]Koha online[% END %] catalog › >+[% IF ( searchdesc ) %] >+ Results of search [% IF ( query_desc ) %]for '[% query_desc | html%]'[% END %][% IF ( limit_desc ) %] with limit(s): '[% limit_desc | html %]'[% END %] >+[% ELSE %] >+ You did not specify any search criteria. >+[% END %] >+[% INCLUDE 'doc-head-close.inc' %] >+<link rel="alternate" type="application/rss+xml" title="[% LibraryName |html %] Search RSS Feed" href="[% OPACBaseurl %]/cgi-bin/koha/opac-search.pl?[% query_cgi |html %][% limit_cgi |html %]&count=[% countrss |html %]&sort_by=acqdate_dsc&format=rss2" /> >+<script type="text/javascript" src="/opac-tmpl/prog/en/lib/jquery/jquery.js"></script> >+<link rel="stylesheet" type="text/css" href="/opac-tmpl/prog/en/css/jquery.rating.css" /> >+ >+<script type="text/javascript" src="[% themelang %]/lib/jquery/plugins/jquery.checkboxes.min.js"></script> >+ >+<script type="text/javascript"> >+ $(document).ready(function() { >+ $('#bookbag_form').find("input").hide(); >+ $('#sort_by').change(function() { >+ $('#bookbag_form').submit(); >+ }); >+ } ); >+</script> >+</head> >+ >+<body id="results"> >+ <div id="doc3" class="yui-t1"> >+ <div id="bd"> >+ >+[% INCLUDE 'masthead.inc' %] >+ >+ >+ <div id="yui-main"> >+ <div class="yui-b"> >+ <div id="userresults" class="container"> >+ >+[% IF ( query_error ) %] >+<div class="dialog alert"> >+ <h4>Error:</h4> >+ [% query_error %] >+</div> >+[% END %] >+ >+<!-- Search Results Table --> >+[% IF ( total ) %] >+ <div class="num_of_results"> >+ We have [% total %] results for your search >+ </div> >+ <div class="searchresults"> >+ <form action="/cgi-bin/koha/opac-search.pl" method="get" name="bookbag_form" id="bookbag_form"> >+ <!-- TABLE RESULTS START --> >+ <table> >+ <thead> >+ <tr> >+ <th colspan="5" class="resultscontrol"> >+ <div class="resort"> >+ <form method="get" id="sortbyform"> >+ [% FOREACH param IN follower_params %] >+ [% UNLESS param.var == 'sort_by' %] >+ <input type="hidden" name='[% param.var |html %]' value='[% param.val %]' /> >+ [% END %] >+ [% END %] >+ <label for="sort_by">Sort By: </label> >+ <select id="sort_by" name="sort_by"> >+ [% INCLUDE 'search/resort_form.inc' %] >+ </select> >+ <input type="submit" value="Go" /> >+ </form> >+ </div> >+ <div class="cartlist"> >+ <!-- checkall, clearall are now needed for placehold --> >+ <span class="checkall"></span> >+ <span class="clearall"></span> >+ </div> >+ </th> >+ </tr> >+ </thead> >+ <!-- Actual Search Results --> >+ <tbody> >+ [% FOREACH SEARCH_RESULT IN SEARCH_RESULTS %] >+ <tr> >+ <td> >+ <input type="checkbox" id="bib[% SEARCH_RESULT.biblionumber %]" name="biblionumber" value="[% SEARCH_RESULT.biblionumber %]" /> <label for="bib[% SEARCH_RESULT.biblionumber %]"></label> >+ </td> >+ <td> >+ <a class="title" href="/cgi-bin/koha/opac-detail.pl?biblionumber=[% SEARCH_RESULT.biblionumber |url %]" title="View details for this title">[% SEARCH_RESULT.title |html %]</a> >+ by <a href="/cgi-bin/koha/opac-search.pl?q=author:[% SEARCH_RESULT.author |url %]" title="Search for works by this author" class="author">[% SEARCH_RESULT.author %]</a> >+ </td> >+ </tr> >+ [% END %] >+ </tbody> >+ </table> >+ </form> >+ </div> >+ [% INCLUDE 'search/page-numbers.inc' %] >+[% END %] >+</div> >+</div> >+</div> >+ >+<div class="yui-b"> >+ <div class="container"> >+ [% INCLUDE 'search/facets.inc' %] >+ </div> >+</div> >+ >+</div> >+ >+[% INCLUDE 'opac-bottom.inc' %] >diff --git a/misc/migration_tools/rebuild_solr.pl b/misc/migration_tools/rebuild_solr.pl >new file mode 100755 >index 0000000..b72b10e >--- /dev/null >+++ b/misc/migration_tools/rebuild_solr.pl >@@ -0,0 +1,179 @@ >+#!/usr/bin/perl >+ >+# Copyright 2012 BibLibre SARL >+# >+# 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 2 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 Data::Dumper; >+use Getopt::Long; >+use LWP::Simple; >+use XML::Simple; >+ >+use C4::Context; >+use C4::Search; >+use Koha::SearchEngine::Index; >+ >+$|=1; # flushes output >+ >+if ( C4::Context->preference("SearchEngine") ne 'Solr' ) { >+ warn "System preference 'SearchEngine' not equal 'Solr'."; >+ warn "We can not indexing"; >+ exit(1); >+} >+ >+#Setup >+ >+my ( $reset, $number, $recordtype, $biblionumbers, $optimize, $info, $want_help ); >+GetOptions( >+ 'r' => \$reset, >+ 'n:s' => \$number, >+ 't:s' => \$recordtype, >+ 'w:s' => \$biblionumbers, >+ 'o' => \$optimize, >+ 'i' => \$info, >+ 'h|help' => \$want_help, >+); >+my $debug = C4::Context->preference("DebugLevel"); >+my $index_service = Koha::SearchEngine::Index->new; >+my $solrurl = $index_service->searchengine->config->SolrAPI; >+ >+my $ping = &ping_command; >+if (!defined $ping) { >+ print "SolrAPI = $solrurl\n"; >+ print "Solr is Down\n"; >+ exit(1); >+} >+ >+#Script >+ >+&print_help if ($want_help); >+&print_info if ($info); >+if ($reset){ >+ if ($recordtype){ >+ &reset_index("recordtype:".$recordtype); >+ } else { >+ &reset_index("*:*"); >+ } >+} >+ >+if (defined $biblionumbers){ >+ if (not defined $recordtype) { print "You must specify a recordtype\n"; exit 1;} >+ &index_biblio($_) for split ',', $biblionumbers; >+} elsif (defined $recordtype) { >+ &index_data; >+ &optimise_index; >+} >+ >+if ($optimize) { >+ &optimise_index; >+} >+ >+#Functions >+ >+sub index_biblio { >+ my ($biblionumber) = @_; >+ $index_service->index_record($recordtype, [ $biblionumber ] ); >+} >+ >+sub index_data { >+ my $dbh = C4::Context->dbh; >+ $dbh->do('SET NAMES UTF8;'); >+ >+ my $query; >+ if ( $recordtype eq 'biblio' ) { >+ $query = "SELECT biblionumber FROM biblio ORDER BY biblionumber"; >+ } elsif ( $recordtype eq 'authority' ) { >+ $query = "SELECT authid FROM auth_header ORDER BY authid"; >+ } >+ $query .= " LIMIT $number" if $number; >+ >+ my $sth = $dbh->prepare( $query ); >+ $sth->execute(); >+ >+ $index_service->index_record($recordtype, [ map { $_->[0] } @{ $sth->fetchall_arrayref } ] ); >+ >+ $sth->finish; >+} >+ >+sub reset_index { >+ &reset_command; >+ &commit_command; >+ $debug eq '2' && &count_all_docs eq 0 && warn "Index cleaned!" >+} >+ >+sub commit_command { >+ my $commiturl = "/update?stream.body=%3Ccommit/%3E"; >+ my $urlreturns = get $solrurl.$commiturl; >+} >+ >+sub ping_command { >+ my $pingurl = "/admin/ping"; >+ my $urlreturns = get $solrurl.$pingurl; >+} >+ >+sub reset_command { >+ my ($query) = @_; >+ my $deleteurl = "/update?stream.body=%3Cdelete%3E%3Cquery%3E".$query."%3C/query%3E%3C/delete%3E"; >+ my $urlreturns = get $solrurl.$deleteurl; >+} >+ >+sub optimise_index { >+ $index_service->optimize; >+} >+ >+sub count_all_docs { >+ my $queryurl = "/select/?q=*:*"; >+ my $urlreturns = get $solrurl.$queryurl; >+ my $xmlsimple = XML::Simple->new(); >+ my $data = $xmlsimple->XMLin($urlreturns); >+ return $data->{result}->{numFound}; >+} >+ >+sub print_info { >+ my $count = &count_all_docs; >+ print <<_USAGE_; >+SolrAPI = $solrurl >+How many indexed documents = $count; >+_USAGE_ >+} >+ >+sub print_help { >+ print <<_USAGE_; >+$0: reindex biblios and/or authorities in Solr. >+ >+Use this batch job to reindex all biblio or authority records in your Koha database. This job is useful only if you are using Solr search engine. >+ >+Parameters: >+ -t biblio index bibliographic records >+ >+ -t authority index authority records >+ >+ -r clear Solr index before adding records to index - use this option carefully! >+ >+ -n 100 index 100 first records >+ >+ -n "100,2" index 2 records after 100th (101 and 102) >+ >+ -w 101 index biblio with biblionumber equals 101 >+ >+ -o launch optimize command at the end of indexing >+ >+ -i gives solr install information: SolrAPI value and count all documents indexed >+ >+ --help or -h show this message. >+_USAGE_ >+} >diff --git a/opac/opac-search.pl b/opac/opac-search.pl >index 5f3ad86..6976c8c 100755 >--- a/opac/opac-search.pl >+++ b/opac/opac-search.pl >@@ -21,14 +21,26 @@ > > # Script to perform searching > # Mostly copied from search.pl, see POD there >-use strict; # always use >-use warnings; >+use Modern::Perl; > > ## STEP 1. Load things that are used in both search page and > # results page and decide which template to load, operations > # to perform, etc. > ## load Koha modules > use C4::Context; >+ >+my $searchengine = C4::Context->preference("SearchEngine"); >+given ( $searchengine ) { >+ when ( /^Solr$/ ) { >+ warn "We use Solr"; >+ require 'opac/search.pl'; >+ exit; >+ } >+ when ( /^Zebra$/ ) { >+ >+ } >+} >+ > use C4::Output; > use C4::Auth qw(:DEFAULT get_session); > use C4::Languages qw(getAllLanguages); >diff --git a/opac/search.pl b/opac/search.pl >new file mode 100755 >index 0000000..1a8aa41 >--- /dev/null >+++ b/opac/search.pl >@@ -0,0 +1,172 @@ >+#!/usr/bin/perl >+ >+# Copyright 2012 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 2 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 C4::Context; >+use CGI; >+use C4::Auth; >+use C4::Koha; >+use C4::Output; >+use Koha::SearchEngine::Search; >+use Koha::SearchEngine::QueryBuilder; >+use Koha::SearchEngine::FacetsBuilder; >+ >+my $cgi = new CGI; >+ >+my $template_name; >+my $template_type = "basic"; >+if ( $cgi->param("idx") or $cgi->param("q") ) { >+ $template_name = 'search/results.tt'; >+} else { >+ $template_name = 'search/advsearch.tt'; >+ $template_type = 'advsearch'; >+} >+ >+# load the template >+my ( $template, $borrowernumber, $cookie ) = get_template_and_user( >+ { template_name => $template_name, >+ query => $cgi, >+ type => "opac", >+ authnotrequired => 1, >+ } >+); >+ >+my $format = $cgi->param("format") || 'html'; >+ >+ >+ >+ >+# load the Type stuff >+my $itemtypes = GetItemTypes; >+ >+my $page = $cgi->param("page") || 1; >+my $count = $cgi->param('count') || C4::Context->preference('OPACnumSearchResults') || 20; >+$count = 5; >+my $q = $cgi->param("q"); >+my $builder = Koha::SearchEngine::QueryBuilder->new; >+$q = $builder->build_query( $q ); >+my $search_service = Koha::SearchEngine::Search->new; >+ >+# load the sorting stuff >+my $sort_by = $cgi->param('sort_by') >+ || C4::Context->preference('OPACdefaultSortField') . ' ' . C4::Context->preference('OPACdefaultSortOrder'); >+ >+my $search_engine_config = Koha::SearchEngine->new->config; >+my $sortable_indexes = $search_engine_config->sortable_indexes; >+my ( $sort_indexname, $sort_order ); >+( $sort_indexname, $sort_order ) = ($1, $2) if ( $sort_by =~ m/^(.*) (asc|desc)$/ ); >+my $sort_by_indexname = eval { >+ [ >+ map { >+ $_->{code} eq $sort_indexname >+ ? 'srt_' . $_->{type} . '_' . $_->{code} . ' ' . $sort_order >+ : () >+ } @$sortable_indexes >+ ]->[0] >+}; >+ >+# This array is used to build facets GUI >+my %filters; >+my @tplfilters; >+for my $filter ( $cgi->param('filters') ) { >+ next if not $filter; >+ my ($k, @v) = $filter =~ /(?: \\. | [^:] )+/xg; >+ my $v = join ':', @v; >+ push @{$filters{$k}}, $v; >+ $v =~ s/^"(.*)"$/$1/; # Remove quotes around >+ push @tplfilters, { >+ 'var' => $k, >+ 'val' => $v, >+ }; >+} >+push @{$filters{recordtype}}, 'biblio'; >+ >+my $results = $search_service->search( >+ $q, >+ \%filters, >+ { >+ page => $page, >+ count => $count, >+ sort => $sort_by_indexname, >+ facets => 1, >+ fl => ["ste_title", "str_author", 'int_biblionumber'], >+ } >+); >+ >+if ($results->{error}){ >+ $template->param(query_error => $results->{error}); >+ output_with_http_headers $cgi, $cookie, $template->output, 'html'; >+ exit; >+} >+ >+ >+# populate results with records >+my @r; >+for my $searchresult ( @{ $results->items } ) { >+ my $biblionumber = $searchresult->{values}->{recordid}; >+ >+ my $nr; >+ while ( my ($k, $v) = each %{$searchresult->{values}} ) { >+ my $nk = $k; >+ $nk =~ s/^[^_]*_(.*)$/$1/; >+ $nr->{$nk} = ref $v ? shift @{$v} : $v; >+ } >+ push( @r, $nr ); >+} >+ >+# build facets >+my $facets_builder = Koha::SearchEngine::FacetsBuilder->new; >+my @facets_loop = $facets_builder->build_facets( $results, $search_engine_config->facetable_indexes, \%filters ); >+ >+my $total = $results->{pager}->{total_entries}; >+my $pager = Data::Pagination->new( >+ $total, >+ $count, >+ 20, >+ $page, >+); >+ >+# params we want to pass for all actions require another query (pagination, sort, facets) >+my @follower_params = map { { >+ var => 'filters', >+ val => $_->{var}.':"'.$_->{val}.'"' >+} } @tplfilters; >+push @follower_params, { var => 'q', val => $q}; >+push @follower_params, { var => 'sort_by', val => $sort_by}; >+ >+# Pager template params >+$template->param( >+ previous_page => $pager->{'prev_page'}, >+ next_page => $pager->{'next_page'}, >+ PAGE_NUMBERS => [ map { { page => $_, current => $_ == $page } } @{ $pager->{'numbers_of_set'} } ], >+ current_page => $page, >+ follower_params => \@follower_params, >+ total => $total, >+ SEARCH_RESULTS => \@r, >+ query => $q, >+ count => $count, >+ sort_by => $sort_by, >+ sortable_indexes => $sortable_indexes, >+ facets_loop => \@facets_loop, >+ filters => \@tplfilters, >+); >+ >+my $content_type = ( $format eq 'rss' or $format eq 'atom' ) ? $format : 'html'; >+output_with_http_headers $cgi, $cookie, $template->output, $content_type; >diff --git a/t/lib/Mocks.pm b/t/lib/Mocks.pm >new file mode 100644 >index 0000000..af82186 >--- /dev/null >+++ b/t/lib/Mocks.pm >@@ -0,0 +1,25 @@ >+package t::lib::Mocks; >+ >+use Modern::Perl; >+use Test::MockModule; >+use t::lib::Mocks::Context; >+ >+our (@ISA,@EXPORT,@EXPORT_OK); >+BEGIN { >+ require Exporter; >+ @ISA = qw(Exporter); >+ push @EXPORT, qw( >+ &set_solr >+ &set_zebra >+ ); >+} >+ >+my $context = new Test::MockModule('C4::Context'); >+sub set_solr { >+ $context->mock('preference', sub { &t::lib::Mocks::Context::MockPreference( @_, "Solr", $context ) }); >+} >+sub set_zebra { >+ $context->mock('preference', sub { &t::lib::Mocks::Context::MockPreference( @_, "Zebra", $context ) }); >+} >+ >+ >diff --git a/t/lib/Mocks/Context.pm b/t/lib/Mocks/Context.pm >new file mode 100644 >index 0000000..185209a >--- /dev/null >+++ b/t/lib/Mocks/Context.pm >@@ -0,0 +1,13 @@ >+package t::lib::Mocks::Context; >+use t::lib::Mocks::Context; >+use C4::Context; >+ >+sub MockPreference { >+ my ( $self, $syspref, $value, $mock_object ) = @_; >+ return $value if $syspref eq 'SearchEngine'; >+ $mock_object->unmock("preference"); >+ my $r = C4::Context->preference($syspref); >+ $mock_object->mock('preference', sub { &t::lib::Mocks::Context::MockPreference( @_, $value, $mock_object ) }); >+ return $r; >+} >+1; >diff --git a/t/searchengine/000_conn/conn.t b/t/searchengine/000_conn/conn.t >new file mode 100644 >index 0000000..8510269 >--- /dev/null >+++ b/t/searchengine/000_conn/conn.t >@@ -0,0 +1,23 @@ >+use Modern::Perl; >+use Test::More; >+use Koha::SearchEngine::Solr; >+use Koha::SearchEngine::Zebra; >+use Koha::SearchEngine::Search; >+use t::lib::Mocks; >+ >+my $se_index = Koha::SearchEngine::Solr->new; >+ok($se_index->isa('Data::SearchEngine::Solr'), 'Solr is a Solr data searchengine'); >+ >+$se_index = Koha::SearchEngine::Zebra->new; >+ok($se_index->isa('Data::SearchEngine::Zebra'), 'Zebra search engine'); >+ >+set_solr(); >+$se_index = Koha::SearchEngine::Search->new; >+ok($se_index->searchengine->isa('Data::SearchEngine::Solr'), 'Solr search engine'); >+ >+set_zebra(); >+$se_index = Koha::SearchEngine::Search->new; >+ok($se_index->searchengine->isa('Data::SearchEngine::Zebra'), 'Zebra search engine'); >+ >+ >+done_testing; >diff --git a/t/searchengine/001_search/search_base.t b/t/searchengine/001_search/search_base.t >new file mode 100755 >index 0000000..8351b17 >--- /dev/null >+++ b/t/searchengine/001_search/search_base.t >@@ -0,0 +1,12 @@ >+use Test::More; >+ >+use t::lib::Mocks; >+ >+set_solr; >+use Koha::SearchEngine::Search; >+my $search_service = Koha::SearchEngine::Search->new; >+isnt (scalar $search_service->search("fort"), 0, 'test search') ; >+ >+#$search_service->search($query_service->build_query(@,@,@)); >+ >+done_testing; >diff --git a/t/searchengine/002_index/index_base.t b/t/searchengine/002_index/index_base.t >new file mode 100755 >index 0000000..7f86bb8 >--- /dev/null >+++ b/t/searchengine/002_index/index_base.t >@@ -0,0 +1,15 @@ >+use Test::More; >+use FindBin qw($Bin); >+ >+use t::lib::::Mocks; >+ >+use Koha::SearchEngine::Index; >+ >+set_solr; >+my $index_service = Koha::SearchEngine::Index->new; >+system( qq{/bin/cp $FindBin::Bin/../indexes.yaml /tmp/indexes.yaml} ); >+$index_service->searchengine->config->set_config_filename( "/tmp/indexes.yaml" ); >+is ($index_service->index_record("biblio", [2]), 1, 'test search') ; >+is ($index_service->optimize, 1, 'test search') ; >+ >+done_testing; >diff --git a/t/searchengine/003_query/buildquery.t b/t/searchengine/003_query/buildquery.t >new file mode 100644 >index 0000000..e9d609f >--- /dev/null >+++ b/t/searchengine/003_query/buildquery.t >@@ -0,0 +1,45 @@ >+use Modern::Perl; >+use Test::More; >+use C4::Context; >+ >+use Koha::SearchEngine; >+use Koha::SearchEngine::QueryBuilder; >+use t::lib::Mocks; >+ >+my $titleindex = "title"; >+my $authorindex = "author"; >+#my $eanindex = "str_ean"; >+#my $pubdateindex = "date_pubdate"; >+ >+my ($operands, $indexes, $operators); >+ >+ >+# === Solr part === >+@$operands = ('cup', 'rowling'); >+@$indexes = ('ti', 'au'); >+@$operators = ('AND'); >+ >+set_solr; >+my $qs = Koha::SearchEngine::QueryBuilder->new; >+ >+my $se = Koha::SearchEngine->new; >+is( $se->name, "Solr", "Test searchengine name eq Solr" ); >+ >+my $gotsolr = $qs->build_advanced_query($indexes, $operands, $operators); >+my $expectedsolr = "ti:cup AND au:rowling"; >+is($gotsolr, $expectedsolr, "Test build_query Solr"); >+ >+ >+# === Zebra part === >+set_zebra; >+$se = Koha::SearchEngine->new; >+is( $se->name, "Zebra", "Test searchengine name eq Zebra" ); >+$qs = Koha::SearchEngine::QueryBuilder->new; >+my ( $builterror, $builtquery, $simple_query, $query_cgi, $query_desc, $limit, $limit_cgi, $limit_desc, $stopwords_removed, $query_type ) = $qs->build_query($operators, $operands, $indexes); >+my $gotzebra = $builtquery; >+my $expectedzebra = qq{ti,wrdl= cup AND au,wrdl= rowling }; >+is($gotzebra, $expectedzebra, "Test Zebra indexes in 'normal' search"); >+# @and @attr 1=title @attr 4=6 "des mots de mon titre" @attr 1=author Jean en PQF >+ >+ >+done_testing; >diff --git a/t/searchengine/004_config/load_config.t b/t/searchengine/004_config/load_config.t >new file mode 100644 >index 0000000..4e3665d >--- /dev/null >+++ b/t/searchengine/004_config/load_config.t >@@ -0,0 +1,54 @@ >+use Modern::Perl; >+use Test::More; >+use FindBin qw($Bin); >+ >+use C4::Context; >+use Koha::SearchEngine; >+use t::lib::Mocks; >+ >+set_solr; >+ >+my $se = Koha::SearchEngine->new; >+is( $se->name, "Solr", "Test searchengine name eq Solr" ); >+ >+my $config = $se->config; >+$config->set_config_filename( "$FindBin::Bin/../indexes.yaml" ); >+my $ressource_types = $config->ressource_types; >+is ( grep ( /^biblio$/, @$ressource_types ), 1, "Ressource type biblio must to be defined" ); >+is ( grep ( /^authority$/, @$ressource_types ), 1, "Ressource type authority must to be defined" ); >+ >+my $indexes = $config->indexes; >+is ( scalar(@$indexes), 3, "There are 3 indexes configured" ); >+ >+my $index1 = @$indexes[0]; >+is ( $index1->{code}, 'title', "My index first have code=title"); >+is ( $index1->{label}, 'Title', "My index first have label=Title"); >+is ( $index1->{type}, 'ste', "My index first have type=ste"); >+is ( $index1->{ressource_type}, 'biblio', "My index first have ressource_type=biblio"); >+is ( $index1->{sortable}, '1', "My index first have sortable=1"); >+is ( $index1->{mandatory}, '1', "My index first have mandatory=1"); >+eq_array ( $index1->{mappings}, ["200\$a", "4..\$t"], "My first index have mappings=[200\$a,4..\$t]"); >+ >+system( qq{/bin/cp $FindBin::Bin/../indexes.yaml /tmp/indexes.yaml} ); >+$config->set_config_filename( "/tmp/indexes.yaml" ); >+$indexes = $config->indexes; >+my $new_index = { >+ code => 'isbn', >+ label => 'ISBN', >+ type => 'str', >+ ressource_type => 'biblio', >+ sortable => 0, >+ mandatory => 0 >+}; >+push @$indexes, $new_index; >+$config->indexes( $indexes ); >+ >+$indexes = $config->indexes; >+ >+my $isbn_index = $config->index( 'isbn' ); >+is( $isbn_index->{code}, 'isbn', 'Index isbn has been written' ); >+ >+my $sortable_indexes = $config->sortable_indexes; >+is ( @$sortable_indexes, 2, "There are 2 sortable indexes" ); >+ >+done_testing; >diff --git a/t/searchengine/indexes.yaml b/t/searchengine/indexes.yaml >new file mode 100644 >index 0000000..b369160 >--- /dev/null >+++ b/t/searchengine/indexes.yaml >@@ -0,0 +1,33 @@ >+ressource_types: >+ - biblio >+ - authority >+ >+indexes: >+ - code: title >+ label: Title >+ type: ste >+ ressource_type: biblio >+ sortable: 1 >+ mandatory: 1 >+ mappings: >+ - 200$a >+ - 210$a >+ - 4..$t >+ - code: author >+ label: Author >+ type: str >+ ressource_type: biblio >+ sortable: 1 >+ mandatory: 0 >+ mappings: >+ - 700$* >+ - 710$* >+ - code: subject >+ label: Subject >+ type: str >+ ressource_type: biblio >+ sortable: 0 >+ mandatory: 0 >+ mappings: >+ - 600$a >+ - 601$a >-- >1.7.10
You cannot view the attachment while viewing its details because your browser does not support IFRAMEs.
View the attachment on a separate page
.
View Attachment As Diff
View Attachment As Raw
Actions:
View
|
Diff
|
Splinter Review
Attachments on
bug 8233
:
10257
|
10334
|
10335
|
10434
|
10459
|
10464
|
10465
|
10466
|
10467
|
10480
|
11126
|
11128
|
12290
|
12408