View | Details | Raw Unified | Return to bug 8233
Collapse All | Expand All

(-)a/Koha/SearchEngine.pm (+29 lines)
Line 0 Link Here
1
package Koha::SearchEngine;
2
3
use Moose;
4
use C4::Context;
5
use Koha::SearchEngine::Config;
6
7
has 'name' => (
8
    is => 'ro',
9
    default => sub {
10
        C4::Context->preference('SearchEngine');
11
    }
12
);
13
14
has config => (
15
    is => 'rw',
16
    lazy => 1,
17
    default => sub {
18
        Koha::SearchEngine::Config->new;
19
    }
20
#    lazy => 1,
21
#    builder => '_build_config',
22
);
23
24
#sub _build_config {
25
#    my ( $self ) = @_;
26
#    Koha::SearchEngine::Config->new( $self->name );
27
#);
28
29
1;
(-)a/Koha/SearchEngine/Config.pm (+12 lines)
Line 0 Link Here
1
package Koha::SearchEngine::Config;
2
3
use Moose;
4
5
use Moose::Util qw( apply_all_roles );
6
7
sub BUILD {
8
    my $self = shift;
9
    my $syspref = C4::Context->preference("SearchEngine");
10
    apply_all_roles( $self, "Koha::SearchEngine::${syspref}::Config" );
11
};
12
1;
(-)a/Koha/SearchEngine/ConfigRole.pm (+7 lines)
Line 0 Link Here
1
package Koha::SearchEngine::ConfigRole;
2
3
use Moose::Role;
4
5
requires 'indexes', 'index', 'ressource_types';
6
7
1;
(-)a/Koha/SearchEngine/FacetsBuilder.pm (+12 lines)
Line 0 Link Here
1
package Koha::SearchEngine::FacetsBuilder;
2
3
use Moose;
4
5
use Moose::Util qw( apply_all_roles );
6
7
sub BUILD {
8
    my $self = shift;
9
    my $syspref = C4::Context->preference("SearchEngine");
10
    apply_all_roles( $self, "Koha::SearchEngine::${syspref}::FacetsBuilder" );
11
};
12
1;
(-)a/Koha/SearchEngine/FacetsBuilderRole.pm (+7 lines)
Line 0 Link Here
1
package Koha::SearchEngine::FacetsBuilderRole;
2
3
use Moose::Role;
4
5
requires 'build_facets';
6
7
1;
(-)a/Koha/SearchEngine/Index.pm (+11 lines)
Line 0 Link Here
1
package Koha::SearchEngine::Index;
2
use Moose;
3
4
use Moose::Util qw( apply_all_roles );
5
6
sub BUILD {
7
    my $self = shift;
8
    my $syspref = 'Solr';
9
    apply_all_roles( $self, "Koha::SearchEngine::${syspref}::Index" );
10
};
11
1;
(-)a/Koha/SearchEngine/IndexRole.pm (+6 lines)
Line 0 Link Here
1
package Koha::SearchEngine::IndexRole;
2
use Moose::Role;
3
4
requires 'index_record';
5
6
1;
(-)a/Koha/SearchEngine/QueryBuilder.pm (+12 lines)
Line 0 Link Here
1
package Koha::SearchEngine::QueryBuilder;
2
3
use Moose;
4
5
use Moose::Util qw( apply_all_roles );
6
7
sub BUILD {
8
    my $self = shift;
9
    my $syspref = C4::Context->preference("SearchEngine");
10
    apply_all_roles( $self, "Koha::SearchEngine::${syspref}::QueryBuilder" );
11
};
12
1;
(-)a/Koha/SearchEngine/QueryBuilderRole.pm (+7 lines)
Line 0 Link Here
1
package Koha::SearchEngine::QueryBuilderRole;
2
3
use Moose::Role;
4
5
requires 'build_query';
6
7
1;
(-)a/Koha/SearchEngine/Search.pm (+12 lines)
Line 0 Link Here
1
package Koha::SearchEngine::Search;
2
use Moose;
3
use C4::Context;
4
5
use Moose::Util qw( apply_all_roles );
6
7
sub BUILD {
8
    my $self = shift;
9
    my $syspref = C4::Context->preference("SearchEngine");
10
    apply_all_roles( $self, "Koha::SearchEngine::${syspref}::Search" );
11
};
12
1;
(-)a/Koha/SearchEngine/SearchRole.pm (+7 lines)
Line 0 Link Here
1
package Koha::SearchEngine::SearchRole;
2
use Moose::Role;
3
4
requires 'search';
5
requires 'dosmth';
6
7
1;
(-)a/Koha/SearchEngine/Solr.pm (+45 lines)
Line 0 Link Here
1
package Koha::SearchEngine::Solr;
2
use Moose;
3
use Koha::SearchEngine::Config;
4
5
extends 'Koha::SearchEngine', 'Data::SearchEngine::Solr';
6
7
has '+url' => (
8
    is => 'ro',
9
    isa => 'Str',
10
#    default => sub {
11
#        C4::Context->preference('SolrAPI');
12
#    },
13
    lazy => 1,
14
    builder => '_build_url',
15
    required => 1
16
);
17
18
sub _build_url {
19
    my ( $self ) = @_;
20
    $self->config->SolrAPI;
21
}
22
23
has '+options' => (
24
    is => 'ro',
25
    isa => 'HashRef',
26
    default => sub {
27
      {
28
        wt => 'json',
29
        fl => '*,score',
30
        fq => 'recordtype:biblio',
31
        facets => 'true'
32
      }
33
    }
34
35
);
36
37
has indexes => (
38
    is => 'ro',
39
    lazy => 1,
40
    default => sub {
41
#        my $dbh => ...;
42
    },
43
);
44
45
1;
(-)a/Koha/SearchEngine/Solr/Config.pm (+115 lines)
Line 0 Link Here
1
package Koha::SearchEngine::Solr::Config;
2
3
use Modern::Perl;
4
use Moose::Role;
5
use YAML;
6
7
with 'Koha::SearchEngine::ConfigRole';
8
9
has index_config => (
10
    is => 'rw',
11
    lazy => 1,
12
    builder => '_load_index_config_file',
13
);
14
15
has solr_config => (
16
    is => 'rw',
17
    lazy => 1,
18
    builder => '_load_solr_config_file',
19
);
20
21
has index_filename => (
22
    is => 'rw',
23
    lazy => 1,
24
    default => C4::Context->config("installdir") . qq{/etc/searchengine/solr/indexes.yaml},
25
);
26
has solr_filename => (
27
    is => 'rw',
28
    lazy => 1,
29
    default => C4::Context->config("installdir") . qq{/etc/searchengine/solr/config.yaml},
30
);
31
32
sub _load_index_config_file {
33
    my ( $self, $filename ) = @_;
34
    $self->index_filename( $filename ) if defined $filename;
35
    die "The config index file (" . $self->index_filename . ") for Solr is not exist" if not -e $self->index_filename;
36
37
    return YAML::LoadFile($self->index_filename);
38
}
39
40
sub _load_solr_config_file {
41
    my ( $self ) = @_;
42
    die "The solr config index file (" . $self->solr_filename . ") for Solr is not exist" if not -e $self->solr_filename;
43
44
    return YAML::LoadFile($self->solr_filename);
45
}
46
47
sub set_config_filename {
48
    my ( $self, $filename ) = @_;
49
    $self->index_config( $self->_load_index_config_file( $filename ) );
50
}
51
52
sub SolrAPI {
53
    my ( $self ) = @_;
54
    return $self->solr_config->{SolrAPI};
55
}
56
sub indexes { # FIXME Return index list if param not an hashref (string ressource_type)
57
    my ( $self, $indexes ) = @_;
58
    return $self->write( { indexes => $indexes } ) if defined $indexes;
59
    return $self->index_config->{indexes};
60
}
61
62
sub index {
63
    my ( $self, $code ) = @_;
64
    my @index = map { ( $_->{code} eq $code ) ? $_ : () } @{$self->index_config->{indexes}};
65
    return $index[0];
66
}
67
68
sub ressource_types {
69
    my ( $self  ) = @_;
70
    my $config = $self->index_config;
71
    return $config->{ressource_types};
72
}
73
74
sub sortable_indexes {
75
    my ( $self ) = @_;
76
    my @sortable_indexes = map { $_->{sortable} ? $_ : () } @{ $self->index_config->{indexes} };
77
    return \@sortable_indexes;
78
}
79
80
sub facetable_indexes {
81
    my ( $self ) = @_;
82
    my @facetable_indexes = map { $_->{facetable} ? $_ : () } @{ $self->index_config->{indexes} };
83
    return \@facetable_indexes;
84
}
85
86
sub reload {
87
    my ( $self ) = @_;
88
    $self->index_config( $self->_load_index_config_file );
89
}
90
sub write {
91
    my ( $self, $values ) = @_;
92
    my $r;
93
    while ( my ( $k, $v ) = each %$values ) {
94
        $r->{$k} = $v;
95
    }
96
97
    if ( not grep /^ressource_type$/, keys %$values ) {
98
        $r->{ressource_types} = $self->ressource_types;
99
    }
100
101
    if ( not grep /^indexes$/, keys %$values ) {
102
        $r->{indexes} = $self->indexes;
103
    }
104
105
    eval {
106
        YAML::DumpFile( $self->index_filename, $r );
107
    };
108
    if ( $@ ) {
109
        die "Failed to dump the index config into the specified file ($@)";
110
    }
111
112
    $self->reload;
113
}
114
115
1;
(-)a/Koha/SearchEngine/Solr/FacetsBuilder.pm (+41 lines)
Line 0 Link Here
1
package Koha::SearchEngine::Solr::FacetsBuilder;
2
3
use Modern::Perl;
4
use Moose::Role;
5
6
with 'Koha::SearchEngine::FacetsBuilderRole';
7
8
sub build_facets {
9
    my ( $self, $results, $facetable_indexes, $filters ) = @_;
10
    my @facets_loop;
11
    for my $index ( @$facetable_indexes ) {
12
        my $index_name = $index->{type} . '_' . $index->{code};
13
        my $facets = $results->facets->{'str_' . $index->{code}};
14
        if ( @$facets > 1 ) {
15
            my @values;
16
            $index =~ m/^([^_]*)_(.*)$/;
17
            for ( my $i = 0 ; $i < scalar(@$facets) ; $i++ ) {
18
                my $value = $facets->[$i++];
19
                my $count = $facets->[$i];
20
                utf8::encode($value);
21
                my $lib =$value;
22
                push @values, {
23
                    'lib'     => $lib,
24
                    'value'   => $value,
25
                    'count'   => $count,
26
                    'active'  => ( $filters->{$index_name} and scalar( grep /"?\Q$value\E"?/, @{ $filters->{$index_name} } ) ) ? 1 : 0,
27
                };
28
            }
29
30
            push @facets_loop, {
31
                'indexname' => $index_name,
32
                'label'     => $index->{label},
33
                'values'    => \@values,
34
                'size'      => scalar(@values),
35
            };
36
        }
37
    }
38
    return @facets_loop;
39
}
40
41
1;
(-)a/Koha/SearchEngine/Solr/Index.pm (+100 lines)
Line 0 Link Here
1
package Koha::SearchEngine::Solr::Index;
2
use Moose::Role;
3
with 'Koha::SearchEngine::IndexRole';
4
5
use Data::SearchEngine::Solr;
6
use Data::Dump qw(dump);
7
use List::MoreUtils qw(uniq);
8
9
use Koha::SearchEngine::Solr;
10
use C4::AuthoritiesMarc;
11
use C4::Biblio;
12
13
has searchengine => (
14
    is => 'rw',
15
    isa => 'Koha::SearchEngine::Solr',
16
    default => sub { Koha::SearchEngine::Solr->new },
17
    lazy => 1
18
);
19
20
sub optimize {
21
    my ( $self ) = @_;
22
    return $self->searchengine->_solr->optimize;
23
}
24
25
sub index_record {
26
    my ($self, $recordtype, $recordids) = @_;
27
28
    my $indexes = $self->searchengine->config->indexes;
29
    my @records;
30
31
    my $recordids_str = ref($recordids) eq 'ARRAY'
32
                    ? join " ", @$recordids
33
                    : $recordids;
34
    warn "IndexRecord called with $recordtype $recordids_str";
35
36
    for my $id ( @$recordids ) {
37
        my $record;
38
39
        $record = GetAuthority( $id )  if $recordtype eq "authority";
40
        $record = GetMarcBiblio( $id ) if $recordtype eq "biblio";
41
42
        next unless ( $record );
43
44
        my $index_values = {
45
            recordid => $id,
46
            recordtype => $recordtype,
47
        };
48
49
        warn "Indexing $recordtype $id";
50
51
        for my $index ( @$indexes ) {
52
            next if $index->{ressource_type} ne $recordtype;
53
            my @values;
54
            eval {
55
                my $mappings = $index->{mappings};
56
                for my $tag_subf_code ( sort @$mappings ) {
57
                    my ( $f, $sf ) = split /\$/, $tag_subf_code;
58
                    for my $field ( $record->field( $f ) ) {
59
                        if ( $field->is_control_field ) {
60
                            push @values, $field->data;
61
                        } else {
62
                            my @sfvals = $sf eq '*'
63
                                       ? map { $_->[1] } $field->subfields
64
                                       : map { $_      } $field->subfield( $sf );
65
66
                            for ( @sfvals ) {
67
                                $_ = NormalizeDate( $_ ) if $index->{type} eq 'date';
68
                                push @values, $_ if $_;
69
                            }
70
                        }
71
                    }
72
                }
73
                @values = uniq (@values); #Removes duplicates
74
75
                $index_values->{$index->{type}."_".$index->{code}} = \@values;
76
                if ( $index->{sortable} ){
77
                    $index_values->{"srt_" . $index->{type} . "_".$index->{code}} = $values[0];
78
                }
79
                # Add index str for facets if it's not exist
80
                if ( $index->{facetable} and @values > 0 and $index->{type} ne 'str' ) {
81
                    $index_values->{"str_" . $index->{code}} = $values[0];
82
                }
83
            };
84
            if ( $@ ) {
85
                chomp $@;
86
                warn  "Error during indexation : recordid $id, index $index->{code} ( $@ )";
87
            }
88
        }
89
90
        my $solrrecord = Data::SearchEngine::Item->new(
91
            id    => "${recordtype}_$id",
92
            score => 1,
93
            values => $index_values,
94
        );
95
        push @records, $solrrecord;
96
    }
97
    $self->searchengine->add( \@records );
98
}
99
100
1;
(-)a/Koha/SearchEngine/Solr/QueryBuilder.pm (+146 lines)
Line 0 Link Here
1
package Koha::SearchEngine::Solr::QueryBuilder;
2
3
use Modern::Perl;
4
use Moose::Role;
5
6
with 'Koha::SearchEngine::QueryBuilderRole';
7
8
sub build_advanced_query {
9
    my ($class, $indexes, $operands, $operators) = @_;
10
11
    my $q = '';
12
    my $i = 0;
13
    my $index_name;
14
15
    @$operands or return "*:*"; #push @$operands, "[* TO *]";
16
17
    # Foreach operands
18
    for my $kw (@$operands){
19
        $kw =~ s/(\w*\*)/\L$1\E/g; # Lower case on words with right truncation
20
        $kw =~ s/(\s*\w*\?+\w*\s*)/\L$1\E/g; # Lower case on words contain wildcard ?
21
        $kw =~ s/([^\\]):/$1\\:/g; # escape colons if not already escaped
22
        # First element
23
        if ($i == 0){
24
            if ( (my @x = eval {@$indexes} ) == 0 ){
25
                # There is no index, then query is in first operand
26
                $q = @$operands[0];
27
                last;
28
            }
29
30
            # Catch index name if it's not 'all_fields'
31
            if ( @$indexes[$i] ne 'all_fields' ) {
32
                $index_name = @$indexes[$i];
33
            }else{
34
                $index_name = '';
35
            }
36
37
            # Generate index:operand
38
            $q .= BuildTokenString($index_name, $kw);
39
            $i = $i + 1;
40
41
            next;
42
        }
43
        # And others
44
        $index_name = @$indexes[$i] if @$indexes[$i];
45
        my $operator = defined @$operators[$i-1] ? @$operators[$i-1] : 'AND';
46
        given ( uc ( $operator ) ) {
47
            when ('OR'){
48
                $q .= BuildTokenString($index_name, $kw, 'OR');
49
            }
50
            when ('NOT'){
51
                $q .= BuildTokenString($index_name, $kw, 'NOT');
52
            }
53
            default {
54
                $q .= BuildTokenString($index_name, $kw, 'AND');
55
            }
56
        }
57
        $i = $i + 1;
58
    }
59
60
    return $q;
61
62
}
63
64
sub BuildTokenString {
65
    my ($index, $string, $operator) = @_;
66
    my $r;
67
68
    if ($index ne 'all_fields' && $index ne ''){
69
        # Operand can contains an expression in brackets
70
        if (
71
            $string =~ / /
72
                and not ( $string =~ /^\(.*\)$/ )
73
                and not $string =~ /\[.*TO.*\]/ ) {
74
            my @dqs; #double-quoted string
75
            while ( $string =~ /"(?:[^"\\]++|\\.)*+"/g ) {
76
                push @dqs, $&;
77
                $string =~ s/\ *\Q$&\E\ *//; # Remove useless space before and after
78
            }
79
80
            my @words = defined $string ? split ' ', $string : undef;
81
            my $join = join qq{ AND } , map {
82
                my $value = $_;
83
                if ( $index =~ /^date_/ ) {
84
                    #$value = C4::Search::Engine::Solr::buildDateOperand( $value ); TODO
85
                }
86
                ( $value =~ /^"/ and $value ne '""'
87
                        and $index ne "emallfields"
88
                        and $index =~ /(txt_|ste_)/ )
89
                    ? qq{em$index:$value}
90
                    : qq{$index:$value};
91
            } (@dqs, @words);
92
            $r .= qq{($join)};
93
        } else {
94
            if ( $index =~ /^date_/ ) {
95
                #$string = C4::Search::Engine::Solr::buildDateOperand( $string ); TODO
96
            }
97
98
            $r = "$index:$string";
99
        }
100
    }else{
101
        $r = $string;
102
    }
103
104
    return " $operator $r" if $operator;
105
    return $r;
106
}
107
108
sub build_query {
109
    my ($class, $query) = @_;
110
111
    return "*:*" if not defined $query;
112
113
    # Particular *:* query
114
    if ($query  eq '*:*'){
115
        return $query;
116
    }
117
118
    $query =~ s/(\w*\*)/\L$1\E/g; # Lower case on words with right truncation
119
    $query =~ s/(\s*\w*\?+\w*\s*)/\L$1\E/g; # Lower case on words contain wildcard ?
120
121
    my @quotes; # Process colons in quotes
122
    while ( $query =~ /'(?:[^'\\]++|\\.)*+'/g ) {
123
        push @quotes, $&;
124
    }
125
126
    for ( @quotes ) {
127
        my $replacement = $_;
128
        $replacement =~ s/[^\\]\K:/\\:/g;
129
        $query =~ s/$_/$replacement/;
130
    }
131
132
    $query =~ s/ : / \\: /g; # escape colons if " : "
133
134
    my $new_query = $query;#C4::Search::Query::splitToken($query); TODO
135
136
    $new_query =~ s/all_fields://g;
137
138
    # Upper case for operators
139
    $new_query =~ s/ or / OR /g;
140
    $new_query =~ s/ and / AND /g;
141
    $new_query =~ s/ not / NOT /g;
142
143
    return $new_query;
144
}
145
146
1;
(-)a/Koha/SearchEngine/Solr/Search.pm (+108 lines)
Line 0 Link Here
1
package Koha::SearchEngine::Solr::Search;
2
use Moose::Role;
3
with 'Koha::SearchEngine::SearchRole';
4
5
use Data::Dump qw(dump);
6
use XML::Simple;
7
8
use Data::SearchEngine::Solr;
9
use Data::Pagination;
10
use Data::SearchEngine::Query;
11
use Koha::SearchEngine::Solr;
12
13
has searchengine => (
14
    is => 'rw',
15
    isa => 'Koha::SearchEngine::Solr',
16
    default => sub { Koha::SearchEngine::Solr->new },
17
    lazy => 1
18
);
19
20
sub search {
21
    my ( $self, $q, $filters, $params ) = @_;
22
23
    $q         ||= '*:*';
24
    $filters   ||= {};
25
    my $page   = defined $params->{page}   ? $params->{page}   : 1;
26
    my $count  = defined $params->{count}  ? $params->{count}  : 999999999;
27
    my $sort   = defined $params->{sort}   ? $params->{sort}   : 'score desc';
28
    my $facets = defined $params->{facets} ? $params->{facets} : 0;
29
30
    # Construct fl from $params->{fl}
31
    # If "recordid" or "id" not exist, we push them
32
    my $fl = join ",",
33
        defined $params->{fl}
34
            ? (
35
                @{$params->{fl}},
36
                grep ( /^recordid$/, @{$params->{fl}} ) ? () : "recordid",
37
                grep ( /^id$/, @{$params->{fl}} ) ? () : "id"
38
              )
39
            : ( "recordid", "id" );
40
41
    my $recordtype = ref($filters->{recordtype}) eq 'ARRAY'
42
                    ? $filters->{recordtype}[0]
43
                    : $filters->{recordtype}
44
                if defined $filters && defined $filters->{recordtype};
45
46
    if ( $facets ) {
47
        $self->searchengine->options->{"facet"}          = 'true';
48
        $self->searchengine->options->{"facet.mincount"} = 1;
49
        $self->searchengine->options->{"facet.limit"}    = 10; # TODO create a new systempreference C4::Context->preference("numFacetsDisplay")
50
        my @facetable_indexes = map { 'str_' . $_->{code} } @{$self->searchengine->config->facetable_indexes};
51
        $self->searchengine->options->{"facet.field"}    = \@facetable_indexes;
52
    }
53
    $self->searchengine->options->{sort} = $sort;
54
    $self->searchengine->options->{fl} = $fl;
55
56
    # Construct filters
57
    $self->searchengine->options->{fq} = [
58
        map {
59
            my $idx = $_;
60
            ref($filters->{$idx}) eq 'ARRAY'
61
                ?
62
                    '('
63
                    . join( ' AND ',
64
                        map {
65
                            my $filter_str = $_;
66
                            utf8::decode($filter_str);
67
                            my $quotes_existed = ( $filter_str =~ m/^".*"$/ );
68
                            $filter_str =~ s/^"(.*)"$/$1/; #remove quote around value if exist
69
                            $filter_str =~ s/[^\\]\K"/\\"/g;
70
                            $filter_str = qq{"$filter_str"} # Add quote around value if not exist
71
                                if not $filter_str =~ /^".*"$/
72
                                    and $quotes_existed;
73
                            qq{$idx:$filter_str};
74
                        } @{ $filters->{$idx} } )
75
                    . ')'
76
                : "$idx:$filters->{$idx}";
77
        } keys %$filters
78
    ];
79
80
    my $sq = Data::SearchEngine::Query->new(
81
        page  => $page,
82
        count => $count,
83
        query => $q,
84
    );
85
86
    # Get results
87
    my $results = eval { $self->searchengine->search( $sq ) };
88
89
    # Get error if exists
90
    if ( $@ ) {
91
        my $err = $@;
92
93
        $err =~ s#^[^\n]*\n##; # Delete first line
94
        if ( $err =~ "400 URL must be absolute" ) {
95
            $err = "Your system preference 'SolrAPI' is not set correctly";
96
        }
97
        elsif ( not $err =~ 'Connection refused' ) {
98
            my $document = XMLin( $err );
99
            $err = "$$document{body}{h2} : $$document{body}{pre}";
100
        }
101
        $results->{error} = $err;
102
    }
103
    return $results;
104
}
105
106
sub dosmth {'bou' }
107
108
1;
(-)a/Koha/SearchEngine/Zebra.pm (+14 lines)
Line 0 Link Here
1
package Koha::SearchEngine::Zebra;
2
use Moose;
3
4
extends 'Data::SearchEngine::Zebra';
5
6
# the configuration file is retrieved from KOHA_CONF by default, provide it from there²
7
has '+conf_file' => (
8
    is => 'ro',
9
    isa => 'Str',
10
    default =>  $ENV{KOHA_CONF},
11
    required => 1
12
);
13
14
1;
(-)a/Koha/SearchEngine/Zebra/QueryBuilder.pm (+14 lines)
Line 0 Link Here
1
package Koha::SearchEngine::Zebra::QueryBuilder;
2
3
use Modern::Perl;
4
use Moose::Role;
5
use C4::Search;
6
7
with 'Koha::SearchEngine::QueryBuilderRole';
8
9
sub build_query {
10
    shift;
11
    C4::Search::buildQuery @_;
12
}
13
14
1;
(-)a/Koha/SearchEngine/Zebra/Search.pm (+40 lines)
Line 0 Link Here
1
package Koha::SearchEngine::Zebra::Search;
2
use Moose::Role;
3
with 'Koha::SearchEngine::SearchRole';
4
5
use Data::SearchEngine::Zebra;
6
use Data::SearchEngine::Query;
7
use Koha::SearchEngine::Zebra;
8
use Data::Dump qw(dump);
9
10
has searchengine => (
11
    is => 'rw',
12
    isa => 'Koha::SearchEngine::Zebra',
13
    default => sub { Koha::SearchEngine::Zebra->new },
14
    lazy => 1
15
);
16
17
sub search {
18
    my ($self,$query_string) = @_;
19
20
     my $query = Data::SearchEngine::Query->new(
21
       count => 10,
22
       page => 1,
23
       query => $query_string,
24
     );
25
26
    warn "search for $query_string";
27
28
    my $results = $self->searchengine->search($query);
29
30
    foreach my $item (@{ $results->items }) {
31
        my $title = $item->get_value('ste_title');
32
        #utf8::encode($title);
33
        print "$title\n";
34
                warn dump $title;
35
    }
36
}
37
38
sub dosmth {'bou' }
39
40
1;
(-)a/admin/searchengine/solr/indexes.pl (+103 lines)
Line 0 Link Here
1
#!/usr/bin/perl
2
3
# Copyright 2012 BibLibre SARL
4
#
5
# This file is part of Koha.
6
#
7
# Koha is free software; you can redistribute it and/or modify it under the
8
# terms of the GNU General Public License as published by the Free Software
9
# Foundation; either version 2 of the License, or (at your option) any later
10
# version.
11
#
12
# Koha is distributed in the hope that it will be useful, but WITHOUT ANY
13
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
14
# A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
15
#
16
# You should have received a copy of the GNU General Public License along
17
# with Koha; if not, write to the Free Software Foundation, Inc.,
18
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
19
20
use Modern::Perl;
21
use CGI;
22
use C4::Koha;
23
use C4::Output;
24
use C4::Auth;
25
use Koha::SearchEngine;
26
27
my $input = new CGI;
28
my ( $template, $borrowernumber, $cookie ) = get_template_and_user(
29
    {
30
        template_name   => 'admin/searchengine/solr/indexes.tt',
31
        query           => $input,
32
        type            => 'intranet',
33
#        authnotrequired => 0,
34
#        flagsrequired   => { reserveforothers => "place_holds" }, #TODO
35
    }
36
);
37
38
my $ressource_type = $input->param('ressource_type') || 'biblio';
39
my $se = Koha::SearchEngine->new;
40
my $se_config = $se->config;
41
42
my $indexes;
43
if ( $input->param('op') and $input->param('op') eq 'edit' ) {
44
    my @code            = $input->param('code');
45
    my @label           = $input->param('label');
46
    my @type            = $input->param('type');
47
    my @sortable        = $input->param('sortable');
48
    my @facetable       = $input->param('facetable');
49
    my @mandatory       = $input->param('mandatory');
50
    my @ressource_type  = $input->param('ressource_type');
51
    my @mappings        = $input->param('mappings');
52
    my @indexes;
53
    my @errors;
54
    for ( 0 .. @code-1 ) {
55
        my $icode = $code[$_];
56
        my @current_mappings = split /\r\n/, $mappings[$_];
57
        if ( not @current_mappings ) {
58
            @current_mappings = split /\n/, $mappings[$_];
59
        }
60
        if ( not @current_mappings ) {
61
            push @errors, { type => 'no_mapping', value => $icode};
62
        }
63
64
        push @indexes, {
65
            code           => $icode,
66
            label          => $label[$_],
67
            type           => $type[$_],
68
            sortable       => scalar(grep(/^$icode$/, @sortable)),
69
            facetable      => scalar(grep(/^$icode$/, @facetable)),
70
            mandatory      => $mandatory[$_] eq '1' ? '1' : '0',
71
            ressource_type => $ressource_type[$_],
72
            mappings       => \@current_mappings,
73
        };
74
        for my $m ( @current_mappings ) {
75
            push @errors, {type => 'malformed_mapping', value => $m}
76
                if not $m =~ /^\d(\d|\*|\.){2}\$.$/;
77
        }
78
    }
79
    $indexes = \@indexes if @errors;
80
    $template->param( errors => \@errors );
81
82
    $se_config->indexes(\@indexes) if not @errors;
83
}
84
85
my $ressource_types = $se_config->ressource_types;
86
$indexes //= $se_config->indexes;
87
88
my $indexloop;
89
for my $rt ( @$ressource_types ) {
90
    my @indexes = map {
91
        $_->{ressource_type} eq $rt ? $_ : ();
92
    } @$indexes;
93
    push @$indexloop, {
94
        ressource_type => $rt,
95
        indexes => \@indexes,
96
    }
97
}
98
99
$template->param(
100
    indexloop       => $indexloop,
101
);
102
103
output_html_with_http_headers $input, $cookie, $template->output;
(-)a/etc/searchengine/solr/config.yaml (+2 lines)
Line 0 Link Here
1
SolrAPI: 'http://localhost:8983/solr/solr'
2
(-)a/etc/searchengine/solr/indexes.yaml (+45 lines)
Line 0 Link Here
1
---
2
indexes:
3
  - code: title
4
    facetable: 1
5
    label: Title
6
    mandatory: 1
7
    mappings:
8
      - 200$a
9
      - 210$a
10
      - 4..$t
11
    ressource_type: biblio
12
    sortable: 1
13
    type: ste
14
  - code: author
15
    facetable: 1
16
    label: Author
17
    mandatory: 0
18
    mappings:
19
      - 700$*
20
      - 710$*
21
    ressource_type: biblio
22
    sortable: 1
23
    type: str
24
  - code: subject
25
    facetable: 0
26
    label: Subject
27
    mandatory: 0
28
    mappings:
29
      - 600$a
30
      - 601$a
31
    ressource_type: biblio
32
    sortable: 0
33
    type: str
34
  - code: biblionumber
35
    facetable: 0
36
    label: Biblionumber
37
    mandatory: 1
38
    mappings:
39
      - 001$@
40
    ressource_type: biblio
41
    sortable: 0
42
    type: int
43
ressource_types:
44
  - biblio
45
  - authority
(-)a/etc/searchengine/solr/indexes.yaml.bak (+33 lines)
Line 0 Link Here
1
ressource_types:
2
    - biblio
3
    - authority
4
5
indexes:
6
    - code: title
7
      label: Title
8
      type: ste
9
      ressource_type: biblio
10
      sortable: 1
11
      mandatory: 1
12
      mappings:
13
          - 200$a
14
          - 210$a
15
          - 4..$t
16
    - code: author
17
      label: Author
18
      type: str
19
      ressource_type: biblio
20
      sortable: 1
21
      mandatory: 0
22
      mappings:
23
          - 700$*
24
          - 710$*
25
    - code: subject
26
      label: Subject
27
      type: str
28
      ressource_type: biblio
29
      sortable: 0
30
      mandatory: 0
31
      mappings:
32
          - 600$a
33
          - 601$a
(-)a/etc/solr/indexes.yaml (+33 lines)
Line 0 Link Here
1
ressource_types:
2
    - biblio
3
    - authority
4
5
indexes:
6
    - code: title
7
      label: Title
8
      type: ste
9
      ressource_type: biblio
10
      sortable: 1
11
      mandatory: 1
12
      mappings:
13
          - 200$a
14
          - 210$a
15
          - 4..$t
16
    - code: author
17
      label: Author
18
      type: str
19
      ressource_type: biblio
20
      sortable: 1
21
      mandatory: 0
22
      mappings:
23
          - 700$*
24
          - 710$*
25
    - code: subject
26
      label: Subject
27
      type: str
28
      ressource_type: biblio
29
      sortable: 0
30
      mandatory: 0
31
      mappings:
32
          - 600$a
33
          - 601$a
(-)a/installer/data/mysql/updatedatabase.pl (+7 lines)
Lines 5369-5374 if ( C4::Context->preference("Version") < TransformToNum($DBversion) ) { Link Here
5369
    SetVersion($DBversion);
5369
    SetVersion($DBversion);
5370
}
5370
}
5371
5371
5372
$DBversion = "3.07.00.XXX";
5373
if ( C4::Context->preference("Version") < TransformToNum($DBversion) ) {
5374
    $dbh->do("INSERT IGNORE INTO systempreferences (variable,value,options,explanation,type) VALUES('SearchEngine','Zebra','Solr|Zebra','Search Engine','Choice')");
5375
    print "Upgrade to $DBversion done (Add system preference SearchEngine )\n";
5376
    SetVersion($DBversion);
5377
}
5378
5372
5379
5373
=head1 FUNCTIONS
5380
=head1 FUNCTIONS
5374
5381
(-)a/koha-tmpl/intranet-tmpl/prog/en/css/staff-global.css (+4 lines)
Lines 285-290 td { Link Here
285
	vertical-align : top;
285
	vertical-align : top;
286
}
286
}
287
287
288
table.indexes td {
289
    vertical-align : middle;
290
}
291
288
td.borderless {
292
td.borderless {
289
    border-collapse : separate;
293
    border-collapse : separate;
290
    border : 0 none;
294
    border : 0 none;
(-)a/koha-tmpl/intranet-tmpl/prog/en/lib/jquery/plugins/jquery.textarea-expander.js (+95 lines)
Line 0 Link Here
1
/**
2
 * TextAreaExpander plugin for jQuery
3
 * v1.0
4
 * Expands or contracts a textarea height depending on the
5
 * quatity of content entered by the user in the box.
6
 *
7
 * By Craig Buckler, Optimalworks.net
8
 *
9
 * As featured on SitePoint.com:
10
 * http://www.sitepoint.com/blogs/2009/07/29/build-auto-expanding-textarea-1/
11
 *
12
 * Please use as you wish at your own risk.
13
 */
14
15
/**
16
 * Usage:
17
 *
18
 * From JavaScript, use:
19
 *     $(<node>).TextAreaExpander(<minHeight>, <maxHeight>);
20
 *     where:
21
 *       <node> is the DOM node selector, e.g. "textarea"
22
 *       <minHeight> is the minimum textarea height in pixels (optional)
23
 *       <maxHeight> is the maximum textarea height in pixels (optional)
24
 *
25
 * Alternatively, in you HTML:
26
 *     Assign a class of "expand" to any <textarea> tag.
27
 *     e.g. <textarea name="textarea1" rows="3" cols="40" class="expand"></textarea>
28
 *
29
 *     Or assign a class of "expandMIN-MAX" to set the <textarea> minimum and maximum height.
30
 *     e.g. <textarea name="textarea1" rows="3" cols="40" class="expand50-200"></textarea>
31
 *     The textarea will use an appropriate height between 50 and 200 pixels.
32
 */
33
34
(function($) {
35
36
	// jQuery plugin definition
37
	$.fn.TextAreaExpander = function(minHeight, maxHeight) {
38
39
		var hCheck = !($.browser.msie || $.browser.opera);
40
41
		// resize a textarea
42
		function ResizeTextarea(e) {
43
44
			// event or initialize element?
45
			e = e.target || e;
46
47
			// find content length and box width
48
			var vlen = e.value.length, ewidth = e.offsetWidth;
49
			if (vlen != e.valLength || ewidth != e.boxWidth) {
50
51
				if (hCheck && (vlen < e.valLength || ewidth != e.boxWidth)) e.style.height = "0px";
52
				var h = Math.max(e.expandMin, Math.min(e.scrollHeight, e.expandMax));
53
54
				e.style.overflow = (e.scrollHeight > h ? "auto" : "hidden");
55
				e.style.height = h + "px";
56
57
				e.valLength = vlen;
58
				e.boxWidth = ewidth;
59
			}
60
61
			return true;
62
		};
63
64
		// initialize
65
		this.each(function() {
66
67
			// is a textarea?
68
			if (this.nodeName.toLowerCase() != "textarea") return;
69
70
			// set height restrictions
71
			var p = this.className.match(/expand(\d+)\-*(\d+)*/i);
72
			this.expandMin = minHeight || (p ? parseInt('0'+p[1], 10) : 0);
73
			this.expandMax = maxHeight || (p ? parseInt('0'+p[2], 10) : 99999);
74
75
			// initial resize
76
			ResizeTextarea(this);
77
78
			// zero vertical padding and add events
79
			if (!this.Initialized) {
80
				this.Initialized = true;
81
				$(this).css("padding-top", 0).css("padding-bottom", 0);
82
				$(this).bind("keyup", ResizeTextarea).bind("focus", ResizeTextarea);
83
			}
84
		});
85
86
		return this;
87
	};
88
89
})(jQuery);
90
91
92
// initialize all expanding textareas
93
jQuery(document).ready(function() {
94
	jQuery("textarea[class*=expand]").TextAreaExpander();
95
});
(-)a/koha-tmpl/intranet-tmpl/prog/en/lib/jquery/plugins/tablednd.js (+382 lines)
Line 0 Link Here
1
/**
2
 * TableDnD plug-in for JQuery, allows you to drag and drop table rows
3
 * You can set up various options to control how the system will work
4
 * Copyright (c) Denis Howlett <denish@isocra.com>
5
 * Licensed like jQuery, see http://docs.jquery.com/License.
6
 *
7
 * Configuration options:
8
 * 
9
 * onDragStyle
10
 *     This is the style that is assigned to the row during drag. There are limitations to the styles that can be
11
 *     associated with a row (such as you can't assign a border--well you can, but it won't be
12
 *     displayed). (So instead consider using onDragClass.) The CSS style to apply is specified as
13
 *     a map (as used in the jQuery css(...) function).
14
 * onDropStyle
15
 *     This is the style that is assigned to the row when it is dropped. As for onDragStyle, there are limitations
16
 *     to what you can do. Also this replaces the original style, so again consider using onDragClass which
17
 *     is simply added and then removed on drop.
18
 * onDragClass
19
 *     This class is added for the duration of the drag and then removed when the row is dropped. It is more
20
 *     flexible than using onDragStyle since it can be inherited by the row cells and other content. The default
21
 *     is class is tDnD_whileDrag. So to use the default, simply customise this CSS class in your
22
 *     stylesheet.
23
 * onDrop
24
 *     Pass a function that will be called when the row is dropped. The function takes 2 parameters: the table
25
 *     and the row that was dropped. You can work out the new order of the rows by using
26
 *     table.rows.
27
 * onDragStart
28
 *     Pass a function that will be called when the user starts dragging. The function takes 2 parameters: the
29
 *     table and the row which the user has started to drag.
30
 * onAllowDrop
31
 *     Pass a function that will be called as a row is over another row. If the function returns true, allow 
32
 *     dropping on that row, otherwise not. The function takes 2 parameters: the dragged row and the row under
33
 *     the cursor. It returns a boolean: true allows the drop, false doesn't allow it.
34
 * scrollAmount
35
 *     This is the number of pixels to scroll if the user moves the mouse cursor to the top or bottom of the
36
 *     window. The page should automatically scroll up or down as appropriate (tested in IE6, IE7, Safari, FF2,
37
 *     FF3 beta
38
 * dragHandle
39
 *     This is the name of a class that you assign to one or more cells in each row that is draggable. If you
40
 *     specify this class, then you are responsible for setting cursor: move in the CSS and only these cells
41
 *     will have the drag behaviour. If you do not specify a dragHandle, then you get the old behaviour where
42
 *     the whole row is draggable.
43
 * 
44
 * Other ways to control behaviour:
45
 *
46
 * Add class="nodrop" to any rows for which you don't want to allow dropping, and class="nodrag" to any rows
47
 * that you don't want to be draggable.
48
 *
49
 * Inside the onDrop method you can also call $.tableDnD.serialize() this returns a string of the form
50
 * <tableID>[]=<rowID1>&<tableID>[]=<rowID2> so that you can send this back to the server. The table must have
51
 * an ID as must all the rows.
52
 *
53
 * Other methods:
54
 *
55
 * $("...").tableDnDUpdate() 
56
 * Will update all the matching tables, that is it will reapply the mousedown method to the rows (or handle cells).
57
 * This is useful if you have updated the table rows using Ajax and you want to make the table draggable again.
58
 * The table maintains the original configuration (so you don't have to specify it again).
59
 *
60
 * $("...").tableDnDSerialize()
61
 * Will serialize and return the serialized string as above, but for each of the matching tables--so it can be
62
 * called from anywhere and isn't dependent on the currentTable being set up correctly before calling
63
 *
64
 * Known problems:
65
 * - Auto-scoll has some problems with IE7  (it scrolls even when it shouldn't), work-around: set scrollAmount to 0
66
 * 
67
 * Version 0.2: 2008-02-20 First public version
68
 * Version 0.3: 2008-02-07 Added onDragStart option
69
 *                         Made the scroll amount configurable (default is 5 as before)
70
 * Version 0.4: 2008-03-15 Changed the noDrag/noDrop attributes to nodrag/nodrop classes
71
 *                         Added onAllowDrop to control dropping
72
 *                         Fixed a bug which meant that you couldn't set the scroll amount in both directions
73
 *                         Added serialize method
74
 * Version 0.5: 2008-05-16 Changed so that if you specify a dragHandle class it doesn't make the whole row
75
 *                         draggable
76
 *                         Improved the serialize method to use a default (and settable) regular expression.
77
 *                         Added tableDnDupate() and tableDnDSerialize() to be called when you are outside the table
78
 */
79
jQuery.tableDnD = {
80
    /** Keep hold of the current table being dragged */
81
    currentTable : null,
82
    /** Keep hold of the current drag object if any */
83
    dragObject: null,
84
    /** The current mouse offset */
85
    mouseOffset: null,
86
    /** Remember the old value of Y so that we don't do too much processing */
87
    oldY: 0,
88
89
    /** Actually build the structure */
90
    build: function(options) {
91
        // Set up the defaults if any
92
93
        this.each(function() {
94
            // This is bound to each matching table, set up the defaults and override with user options
95
            this.tableDnDConfig = jQuery.extend({
96
                onDragStyle: null,
97
                onDropStyle: null,
98
				// Add in the default class for whileDragging
99
				onDragClass: "tDnD_whileDrag",
100
                onDrop: null,
101
                onDragStart: null,
102
                scrollAmount: 5,
103
				serializeRegexp: /[^\-]*$/, // The regular expression to use to trim row IDs
104
				serializeParamName: null, // If you want to specify another parameter name instead of the table ID
105
                dragHandle: null // If you give the name of a class here, then only Cells with this class will be draggable
106
            }, options || {});
107
            // Now make the rows draggable
108
            jQuery.tableDnD.makeDraggable(this);
109
        });
110
111
        // Now we need to capture the mouse up and mouse move event
112
        // We can use bind so that we don't interfere with other event handlers
113
        jQuery(document)
114
            .bind('mousemove', jQuery.tableDnD.mousemove)
115
            .bind('mouseup', jQuery.tableDnD.mouseup);
116
117
        // Don't break the chain
118
        return this;
119
    },
120
121
    /** This function makes all the rows on the table draggable apart from those marked as "NoDrag" */
122
    makeDraggable: function(table) {
123
        var config = table.tableDnDConfig;
124
		if (table.tableDnDConfig.dragHandle) {
125
			// We only need to add the event to the specified cells
126
			var cells = jQuery("td."+table.tableDnDConfig.dragHandle, table);
127
			cells.each(function() {
128
				// The cell is bound to "this"
129
                jQuery(this).mousedown(function(ev) {
130
                    jQuery.tableDnD.dragObject = this.parentNode;
131
                    jQuery.tableDnD.currentTable = table;
132
                    jQuery.tableDnD.mouseOffset = jQuery.tableDnD.getMouseOffset(this, ev);
133
                    if (config.onDragStart) {
134
                        // Call the onDrop method if there is one
135
                        config.onDragStart(table, this);
136
                    }
137
                    return false;
138
                });
139
			})
140
		} else {
141
			// For backwards compatibility, we add the event to the whole row
142
	        var rows = jQuery("tr", table); // get all the rows as a wrapped set
143
	        rows.each(function() {
144
				// Iterate through each row, the row is bound to "this"
145
				var row = jQuery(this);
146
				if (! row.hasClass("nodrag")) {
147
	                row.mousedown(function(ev) {
148
	                    if (ev.target.tagName == "TD") {
149
	                        jQuery.tableDnD.dragObject = this;
150
	                        jQuery.tableDnD.currentTable = table;
151
	                        jQuery.tableDnD.mouseOffset = jQuery.tableDnD.getMouseOffset(this, ev);
152
	                        if (config.onDragStart) {
153
	                            // Call the onDrop method if there is one
154
	                            config.onDragStart(table, this);
155
	                        }
156
	                        return false;
157
	                    }
158
	                }).css("cursor", "move"); // Store the tableDnD object
159
				}
160
			});
161
		}
162
	},
163
164
	updateTables: function() {
165
		this.each(function() {
166
			// this is now bound to each matching table
167
			if (this.tableDnDConfig) {
168
				jQuery.tableDnD.makeDraggable(this);
169
			}
170
		})
171
	},
172
173
    /** Get the mouse coordinates from the event (allowing for browser differences) */
174
    mouseCoords: function(ev){
175
        if(ev.pageX || ev.pageY){
176
            return {x:ev.pageX, y:ev.pageY};
177
        }
178
        return {
179
            x:ev.clientX + document.body.scrollLeft - document.body.clientLeft,
180
            y:ev.clientY + document.body.scrollTop  - document.body.clientTop
181
        };
182
    },
183
184
    /** Given a target element and a mouse event, get the mouse offset from that element.
185
        To do this we need the element's position and the mouse position */
186
    getMouseOffset: function(target, ev) {
187
        ev = ev || window.event;
188
189
        var docPos    = this.getPosition(target);
190
        var mousePos  = this.mouseCoords(ev);
191
        return {x:mousePos.x - docPos.x, y:mousePos.y - docPos.y};
192
    },
193
194
    /** Get the position of an element by going up the DOM tree and adding up all the offsets */
195
    getPosition: function(e){
196
        var left = 0;
197
        var top  = 0;
198
        /** Safari fix -- thanks to Luis Chato for this! */
199
        if (e.offsetHeight == 0) {
200
            /** Safari 2 doesn't correctly grab the offsetTop of a table row
201
            this is detailed here:
202
            http://jacob.peargrove.com/blog/2006/technical/table-row-offsettop-bug-in-safari/
203
            the solution is likewise noted there, grab the offset of a table cell in the row - the firstChild.
204
            note that firefox will return a text node as a first child, so designing a more thorough
205
            solution may need to take that into account, for now this seems to work in firefox, safari, ie */
206
            e = e.firstChild; // a table cell
207
        }
208
209
        while (e.offsetParent){
210
            left += e.offsetLeft;
211
            top  += e.offsetTop;
212
            e     = e.offsetParent;
213
        }
214
215
        left += e.offsetLeft;
216
        top  += e.offsetTop;
217
218
        return {x:left, y:top};
219
    },
220
221
    mousemove: function(ev) {
222
        if (jQuery.tableDnD.dragObject == null) {
223
            return;
224
        }
225
226
        var dragObj = jQuery(jQuery.tableDnD.dragObject);
227
        var config = jQuery.tableDnD.currentTable.tableDnDConfig;
228
        var mousePos = jQuery.tableDnD.mouseCoords(ev);
229
        var y = mousePos.y - jQuery.tableDnD.mouseOffset.y;
230
        //auto scroll the window
231
	    var yOffset = window.pageYOffset;
232
	 	if (document.all) {
233
	        // Windows version
234
	        //yOffset=document.body.scrollTop;
235
	        if (typeof document.compatMode != 'undefined' &&
236
	             document.compatMode != 'BackCompat') {
237
	           yOffset = document.documentElement.scrollTop;
238
	        }
239
	        else if (typeof document.body != 'undefined') {
240
	           yOffset=document.body.scrollTop;
241
	        }
242
243
	    }
244
		    
245
		if (mousePos.y-yOffset < config.scrollAmount) {
246
	    	window.scrollBy(0, -config.scrollAmount);
247
	    } else {
248
            var windowHeight = window.innerHeight ? window.innerHeight
249
                    : document.documentElement.clientHeight ? document.documentElement.clientHeight : document.body.clientHeight;
250
            if (windowHeight-(mousePos.y-yOffset) < config.scrollAmount) {
251
                window.scrollBy(0, config.scrollAmount);
252
            }
253
        }
254
255
256
        if (y != jQuery.tableDnD.oldY) {
257
            // work out if we're going up or down...
258
            var movingDown = y > jQuery.tableDnD.oldY;
259
            // update the old value
260
            jQuery.tableDnD.oldY = y;
261
            // update the style to show we're dragging
262
			if (config.onDragClass) {
263
				dragObj.addClass(config.onDragClass);
264
			} else {
265
	            dragObj.css(config.onDragStyle);
266
			}
267
            // If we're over a row then move the dragged row to there so that the user sees the
268
            // effect dynamically
269
            var currentRow = jQuery.tableDnD.findDropTargetRow(dragObj, y);
270
            if (currentRow) {
271
                // TODO worry about what happens when there are multiple TBODIES
272
                if (movingDown && jQuery.tableDnD.dragObject != currentRow) {
273
                    jQuery.tableDnD.dragObject.parentNode.insertBefore(jQuery.tableDnD.dragObject, currentRow.nextSibling);
274
                } else if (! movingDown && jQuery.tableDnD.dragObject != currentRow) {
275
                    jQuery.tableDnD.dragObject.parentNode.insertBefore(jQuery.tableDnD.dragObject, currentRow);
276
                }
277
            }
278
        }
279
280
        return false;
281
    },
282
283
    /** We're only worried about the y position really, because we can only move rows up and down */
284
    findDropTargetRow: function(draggedRow, y) {
285
        var rows = jQuery.tableDnD.currentTable.rows;
286
        for (var i=0; i<rows.length; i++) {
287
            var row = rows[i];
288
            var rowY    = this.getPosition(row).y;
289
            var rowHeight = parseInt(row.offsetHeight)/2;
290
            if (row.offsetHeight == 0) {
291
                rowY = this.getPosition(row.firstChild).y;
292
                rowHeight = parseInt(row.firstChild.offsetHeight)/2;
293
            }
294
            // Because we always have to insert before, we need to offset the height a bit
295
            if ((y > rowY - rowHeight) && (y < (rowY + rowHeight))) {
296
                // that's the row we're over
297
				// If it's the same as the current row, ignore it
298
				if (row == draggedRow) {return null;}
299
                var config = jQuery.tableDnD.currentTable.tableDnDConfig;
300
                if (config.onAllowDrop) {
301
                    if (config.onAllowDrop(draggedRow, row)) {
302
                        return row;
303
                    } else {
304
                        return null;
305
                    }
306
                } else {
307
					// If a row has nodrop class, then don't allow dropping (inspired by John Tarr and Famic)
308
                    var nodrop = jQuery(row).hasClass("nodrop");
309
                    if (! nodrop) {
310
                        return row;
311
                    } else {
312
                        return null;
313
                    }
314
                }
315
                return row;
316
            }
317
        }
318
        return null;
319
    },
320
321
    mouseup: function(e) {
322
        if (jQuery.tableDnD.currentTable && jQuery.tableDnD.dragObject) {
323
            var droppedRow = jQuery.tableDnD.dragObject;
324
            var config = jQuery.tableDnD.currentTable.tableDnDConfig;
325
            // If we have a dragObject, then we need to release it,
326
            // The row will already have been moved to the right place so we just reset stuff
327
			if (config.onDragClass) {
328
	            jQuery(droppedRow).removeClass(config.onDragClass);
329
			} else {
330
	            jQuery(droppedRow).css(config.onDropStyle);
331
			}
332
            jQuery.tableDnD.dragObject   = null;
333
            if (config.onDrop) {
334
                // Call the onDrop method if there is one
335
                config.onDrop(jQuery.tableDnD.currentTable, droppedRow);
336
            }
337
            jQuery.tableDnD.currentTable = null; // let go of the table too
338
        }
339
    },
340
341
    serialize: function() {
342
        if (jQuery.tableDnD.currentTable) {
343
            return jQuery.tableDnD.serializeTable(jQuery.tableDnD.currentTable);
344
        } else {
345
            return "Error: No Table id set, you need to set an id on your table and every row";
346
        }
347
    },
348
349
	serializeTable: function(table) {
350
        var result = "";
351
        var tableId = table.id;
352
        var rows = table.rows;
353
        for (var i=0; i<rows.length; i++) {
354
            if (result.length > 0) result += "&";
355
            var rowId = rows[i].id;
356
            if (rowId && rowId && table.tableDnDConfig && table.tableDnDConfig.serializeRegexp) {
357
                rowId = rowId.match(table.tableDnDConfig.serializeRegexp)[0];
358
            }
359
360
            result += tableId + '[]=' + rowId;
361
        }
362
        return result;
363
	},
364
365
	serializeTables: function() {
366
        var result = "";
367
        this.each(function() {
368
			// this is now bound to each matching table
369
			result += jQuery.tableDnD.serializeTable(this);
370
		});
371
        return result;
372
    }
373
374
}
375
376
jQuery.fn.extend(
377
	{
378
		tableDnD : jQuery.tableDnD.build,
379
		tableDnDUpdate : jQuery.tableDnD.updateTables,
380
		tableDnDSerialize: jQuery.tableDnD.serializeTables
381
	}
382
);
(-)a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/admin-home.tt (+2 lines)
Lines 76-81 Link Here
76
    <dd>Manage rules for automatically matching MARC records during record imports.</dd>
76
    <dd>Manage rules for automatically matching MARC records during record imports.</dd>
77
    <dt><a href="/cgi-bin/koha/admin/oai_sets.pl">OAI sets configuration</a></dt>
77
    <dt><a href="/cgi-bin/koha/admin/oai_sets.pl">OAI sets configuration</a></dt>
78
    <dd>Manage OAI Sets</dd>
78
    <dd>Manage OAI Sets</dd>
79
    <dt><a href="/cgi-bin/koha/admin/searchengine/solr/indexes.pl">Search engine configuration</a></dt>
80
    <dd>Manage indexes, facets, and their mappings to MARC fields and subfields.</dd>
79
</dl>
81
</dl>
80
82
81
<h3>Acquisition parameters</h3>
83
<h3>Acquisition parameters</h3>
(-)a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/admin.pref (+8 lines)
Lines 104-106 Administration: Link Here
104
                  Common Name: the Common Name
104
                  Common Name: the Common Name
105
                  emailAddress: the emailAddress
105
                  emailAddress: the emailAddress
106
            - field for SSL client certificate authentication
106
            - field for SSL client certificate authentication
107
    Search Engine:
108
        -
109
            - pref: SearchEngine
110
              default: Zebra
111
              choices:
112
                Solr: Solr
113
                Zebra: Zebra
114
            - is the search engine used.
(-)a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/searchengine/solr/indexes.tt (+190 lines)
Line 0 Link Here
1
[% INCLUDE 'doc-head-open.inc' %]
2
<title>Koha &rsaquo; Administration &rsaquo; Solr config</title>
3
[% INCLUDE 'doc-head-close.inc' %]
4
<script type="text/javascript" src="[% themelang %]/lib/jquery/plugins/tablednd.js"></script>
5
<script type="text/javascript" src="[% themelang %]/lib/jquery/plugins/jquery.textarea-expander.js"></script>
6
<script type="text/javascript" language="javascript">
7
    function clean_line( line ) {
8
        $(line).find('input[type="text"]').val("");
9
        $(line).find('input[type="checkbox"]').attr("checked", false);
10
        $(line).find('textarea').val("");
11
        $(line).find('select').find('option:first').attr("selected", "selected");
12
    }
13
14
    function clone_line( line ) {
15
        var new_line = $(line).clone();
16
        $(new_line).removeClass("nodrag nodrop");
17
        $(new_line).find('td:last-child>a').removeClass("add").addClass("delete").html(_("Delete"));
18
        $(new_line).find('[data-id]').each( function() {
19
            $(this).attr({ name: $(this).attr('data-id') }).removeAttr('data-id');
20
        } );
21
        $(new_line).find("select").each( function() {
22
            var attr = $(this).attr('name');
23
            var val = $(line).find('[data-id="' + attr + '"]').val();
24
            $(this).find('option[value="' + val + '"]').attr("selected", "selected");
25
        } );
26
        return new_line;
27
    }
28
29
    $(document).ready(function() {
30
        $('.delete').click(function() {
31
            $(this).parents('tr').remove();
32
        });
33
34
        $(".indexes").tableDnD( {
35
            onDragClass: "dragClass",
36
        } );
37
        $("textarea").TextAreaExpander();
38
        $('.add').click(function() {
39
            var table = $(this).closest('table');
40
            var ressource_type  = $(table).attr('data-ressource_type');
41
            var code            = $(table).find('input[data-id="code"]').val();
42
            var label           = $(table).find('input[data-id="label"]').val();
43
            if ( code.length > 0 && label.length > 0 ) {
44
                var line = $(this).closest("tr");
45
                var mappings = $(line).find('textarea').val();
46
                var new_line = clone_line( line );
47
                $(new_line).find('textarea').val(mappings);
48
                $(new_line).find("input:checkbox").val(code);
49
                new_line.appendTo($('table[data-ressource_type="'+ressource_type+'"]>tbody'));
50
                $('.delete').click(function() {
51
                    $(this).parents('tr').remove();
52
                });
53
                clean_line(line);
54
55
                $(table).tableDnD( {
56
                    onDragClass: "dragClass",
57
                } );
58
            }
59
        });
60
    });
61
</script>
62
</head>
63
<body id="admin_searchengine_indexes" class="admin">
64
[% INCLUDE 'header.inc' %]
65
[% INCLUDE 'cat-search.inc' %]
66
67
<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; Search engine configuration</div>
68
69
<div id="doc3" class="yui-t1">
70
71
  <div id="bd">
72
    <div id="yui-main">
73
    <div class="yui-b">
74
    <h1>Search engine configuration</h1>
75
    <div class="warning">
76
        Warning: Any modification in these configurations will need a total reindexation to be fully taken into account !
77
    </div>
78
    [% IF ( errors ) %]
79
        <div class="error">
80
        Errors occurred, Modifications does not apply. Please check following values:
81
          <ul>
82
            [% FOREACH e IN errors %]
83
                <li>
84
                    [% IF ( e.type == "malformed_mapping" ) %]
85
                        The value "[% e.value %]" is not supported for mappings
86
                    [% ELSIF ( e.type == "no_mapping" ) %]
87
                        There is no mapping for the index [% e.value %]
88
                    [% END %]
89
                </li>
90
            [% END %]
91
          </ul>
92
        </div>
93
    [% END %]
94
95
    <form method="post">
96
      <input type="hidden" name="op" value="edit" />
97
      [% FOREACH rt IN indexloop %]
98
        <h2>[% rt.ressource_type %]</h2>
99
        [% IF ( rt.ressource_type == 'authority' ) %]
100
            This part is not yet implemented
101
        [% END %]
102
        <table id="pouet" class="indexes" data-ressource_type="[% rt.ressource_type %]">
103
          <thead>
104
            <tr class="nodrag nodrop">
105
              <th>Code</th>
106
              <th>Label</th>
107
              <th>Type</th>
108
              <th>Sortable</th>
109
              <th>Facetable</th>
110
              <th>Mapping</th>
111
              <th></th>
112
            </tr>
113
          </thead>
114
          <tbody>
115
            [% FOREACH index IN rt.indexes %]
116
              <tr>
117
                <td>
118
                  [% IF ( index.mandatory ) %]
119
                    <input name="code" type="text" maxlength="25" value="[% index.code %]" disabled="disabled" />
120
                    <input name="code" type="hidden" maxlength="25" value="[% index.code %]" />
121
                  [% ELSE %]
122
                    <input name="code" type="text" maxlength="25" value="[% index.code %]" />
123
                  [% END %]
124
                  <input name="mandatory" type="hidden" maxlength="1" value="[% index.mandatory %]" />
125
                  <input name="ressource_type" type="hidden" value="[% index.ressource_type %]" />
126
                </td>
127
                <td><input name="label" type="text" maxlength="25" value="[% index.label %]" /></td>
128
                <td>
129
                  [% IF ( index.mandatory ) %]
130
                    <input type="hidden" name="type" value="[% index.type %]" />
131
                  [% END %]
132
                  <select name="type"[% IF ( index.mandatory ) %] disabled="disabled"[% END %]>
133
                    <option [% IF ( index.type == 'str' ) %] selected="selected"[% END %] value="str">String</option>
134
                    <option [% IF ( index.type == 'ste' ) %] selected="selected"[% END %] value="ste">Simple Text</option>
135
                    <option [% IF ( index.type == 'txt' ) %] selected="selected"[% END %] value="txt">Text</option>
136
                    <option [% IF ( index.type == 'int' ) %] selected="selected"[% END %] value="int">Integer</option>
137
                    <option [% IF ( index.type == 'date') %] selected="selected"[% END %] value="date">Date</option>
138
                  </select>
139
                </td>
140
                <td>
141
                  <input name="sortable" type="checkbox" [% IF ( index.sortable ) %]checked="checked"[% END %] value="[% index.code %]" />
142
                </td>
143
                <td>
144
                  <input name="facetable" type="checkbox" [% IF ( index.facetable ) %]checked="checked"[% END %] value="[% index.code %]" />
145
                </td>
146
                <td>
147
                    <textarea name="mappings" class="contentEditable">[% FOREACH m IN index.mappings %][% m %]
148
[% END %]</textarea> <!-- Don't indent this line -->
149
                </td>
150
                <td>[% UNLESS ( index.mandatory ) %]<a class="delete">Delete</a>[% END %]</td>
151
              </tr>
152
              [% END %]
153
          </tbody>
154
          <tfoot>
155
            <tr class="nodrag nodrop">
156
              <td>
157
                <input data-id="code" type="text" maxlength="25" />
158
                <input data-id="ressource_type" type="hidden" value="[% rt.ressource_type %]" />
159
                <input data-id="mandatory" type="hidden" value="0" />
160
              </td>
161
              <td><input data-id="label" type="text" maxlength="25" /></td>
162
              <td>
163
                <select data-id="type">
164
                  <option value="str">String</option>
165
                  <option value="ste">Simple Text</option>
166
                  <option value="txt">Text</option>
167
                  <option value="int">Integer</option>
168
                  <option value="date">Date</option>
169
                </select>
170
              </td>
171
              <td><input data-id="sortable" type="checkbox" /></td>
172
              <td><input data-id="facetable" type="checkbox" /></td>
173
              <td>
174
                <textarea data-id="mappings" class="contentEditable"></textarea>
175
              </td>
176
              <td><a class="add">Add</a></td>
177
            </tr>
178
          </tfoot>
179
        </table>
180
      [% END %]
181
      <p><input type="submit" value="Save" /></p>
182
    </form>
183
</div>
184
185
</div>
186
<div class="yui-b">
187
[% INCLUDE 'admin-menu.inc' %]
188
</div>
189
</div>
190
[% INCLUDE 'intranet-bottom.inc' %]
(-)a/koha-tmpl/opac-tmpl/prog/en/includes/search/facets.inc (+56 lines)
Line 0 Link Here
1
[% IF facets_loop %]
2
  <div id="search-facets">
3
    <h4>Refine your search</h4>
4
    <ul>
5
      [% FOR facets IN facets_loop %]
6
        <li>
7
          [% facets.label %]
8
          <ul class="facets_list">
9
            [% FOR value IN facets.values %]
10
              <li>
11
                [% IF ( value.active ) %]
12
                  [% value.lib %] ([% value.count %])
13
                  [% SET url = "/cgi-bin/koha/opac-search.pl?" %]
14
                  [% SET first = 1 %]
15
                  [% FOR p IN follower_params %]
16
                    [% IF p.var != 'filters' %]
17
                      [% UNLESS first %]
18
                        [% SET url = url _ '&' %]
19
                      [% END %]
20
                      [% SET first = 0 %]
21
                      [% SET url = url _ p.var _ '=' _ p.val %]
22
                    [% END %]
23
                  [% END %]
24
                  [% FOR f IN filters %]
25
                    [% UNLESS f.var == facets.indexname && f.val == value.value %]
26
                      [% SET url = url _ '&filters=' _ f.var _ ':&quot;' _ f.val _ '&quot;' %]
27
                    [% END %]
28
                  [% END %]
29
                  [<a href="[% url |url %]">x</a>]
30
                [% ELSE %]
31
                  [% SET url = "/cgi-bin/koha/opac-search.pl?" %]
32
                  [% SET first = 1 %]
33
                  [% FOR p IN follower_params %]
34
                    [% IF p.var != 'filters' %]
35
                      [% UNLESS first %]
36
                        [% SET url = url _ '&' %]
37
                      [% END %]
38
                      [% SET first = 0 %]
39
                      [% SET url = url _ p.var _ '=' _ p.val %]
40
                    [% END %]
41
                  [% END %]
42
                  [% FOR f IN filters %]
43
                    [% SET url = url _ '&filters=' _ f.var _ ':&quot;' _ f.val _ '&quot;' %]
44
                  [% END %]
45
                  [% SET url = url _ '&filters=' _ facets.indexname _ ':&quot;' _ value.value _ '&quot;' %]
46
47
                  <a href="[% url |url %]">[% value.lib %]</a> ([% value.count %])
48
                [% END %]
49
              </li>
50
            [% END %]
51
          </ul>
52
        </li>
53
      [% END %]
54
    </ul>
55
</div>
56
[% END %]
(-)a/koha-tmpl/opac-tmpl/prog/en/includes/search/page-numbers.inc (+17 lines)
Line 0 Link Here
1
[% IF ( PAGE_NUMBERS ) %]
2
  <div class="pages">
3
    [% IF ( previous_page ) %]
4
      <a class="nav" href="?[% FOREACH fp IN follower_params %][% fp.var |url %]=[% fp.val |url %]&amp;[% END %]page=[% previous_page |url %]">&lt;&lt; Previous</a>
5
    [% END %]
6
    [% FOREACH PAGE_NUMBER IN PAGE_NUMBERS %]
7
      [% IF ( PAGE_NUMBER.current ) %]
8
        <span class="current">[% PAGE_NUMBER.page %]</span>
9
      [% ELSE %]
10
        <a class="nav" href="?[% FOREACH fp IN follower_params %][% fp.var |url %]=[% fp.val |url %]&amp;[% END %]page=[% PAGE_NUMBER.page |url %]">[% PAGE_NUMBER.page %]</a>
11
      [% END %]
12
    [% END %]
13
    [% IF ( next_page ) %]
14
      <a class="nav" href="?[% FOREACH fp IN follower_params %][% fp.var |url %]=[% fp.val |url %]&amp;[% END %]page=[% next_page |url %]">Next &gt;&gt;</a>
15
    [% END %]
16
  </div>
17
[% END %]
(-)a/koha-tmpl/opac-tmpl/prog/en/includes/search/resort_form.inc (+23 lines)
Line 0 Link Here
1
[% IF sort_by == "score asc" %]
2
  <option value="score asc" selected="selected">Relevance asc</option>
3
[% ELSE %]
4
  <option value="score asc">Relevance asc</option>
5
[% END %]
6
[% IF sort_by == "score desc" %]
7
  <option value="score desc" selected="selected">Relevance desc</option>
8
[% ELSE %]
9
  <option value="score desc">Relevance desc</option>
10
[% END %]
11
12
[% FOREACH ind IN sortable_indexes %]
13
  [% IF sort_by == "$ind.code asc" %]
14
    <option value="[% ind.code %] asc" selected="selected">[% ind.label %] asc</option>
15
  [% ELSE %]
16
    <option value="[% ind.code %] asc">[% ind.label %] asc</option>
17
  [% END %]
18
  [% IF sort_by == "$ind.code desc" %]
19
    <option value="[% ind.code %] desc" selected="selected">[% ind.label %] desc</option>
20
  [% ELSE %]
21
    <option value="[% ind.code %] desc">[% ind.label %] desc</option>
22
  [% END %]
23
[% END %]
(-)a/koha-tmpl/opac-tmpl/prog/en/modules/search/results.tt (+108 lines)
Line 0 Link Here
1
[% INCLUDE 'doc-head-open.inc' %]
2
[% IF ( LibraryNameTitle ) %][% LibraryNameTitle %][% ELSE %]Koha online[% END %] catalog &rsaquo;
3
[% IF ( searchdesc ) %]
4
    Results of search [% IF ( query_desc ) %]for '[% query_desc | html%]'[% END %][% IF ( limit_desc ) %]&nbsp;with limit(s):&nbsp;'[% limit_desc | html %]'[% END %]
5
[% ELSE %]
6
    You did not specify any search criteria.
7
[% END %]
8
[% INCLUDE 'doc-head-close.inc' %]
9
<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 %]&amp;count=[% countrss |html %]&amp;sort_by=acqdate_dsc&amp;format=rss2" />
10
<script type="text/javascript" src="/opac-tmpl/prog/en/lib/jquery/jquery.js"></script>
11
<link rel="stylesheet" type="text/css" href="/opac-tmpl/prog/en/css/jquery.rating.css" />
12
13
<script type="text/javascript" src="[% themelang %]/lib/jquery/plugins/jquery.checkboxes.min.js"></script>
14
15
<script type="text/javascript">
16
  $(document).ready(function() {
17
    $('#bookbag_form').find("input").hide();
18
    $('#sort_by').change(function() {
19
        $('#bookbag_form').submit();
20
    });
21
  } );
22
</script>
23
</head>
24
25
<body id="results">
26
  <div id="doc3" class="yui-t1">
27
    <div id="bd">
28
29
[% INCLUDE 'masthead.inc' %]
30
31
32
    <div id="yui-main">
33
    <div class="yui-b">
34
    <div id="userresults" class="container">
35
36
[% IF ( query_error ) %]
37
<div class="dialog alert">
38
    <h4>Error:</h4>
39
    [% query_error %]
40
</div>
41
[% END %]
42
43
<!-- Search Results Table -->
44
[% IF ( total ) %]
45
  <div class="num_of_results">
46
    We have [% total %] results for your search
47
  </div>
48
  <div class="searchresults">
49
    <form action="/cgi-bin/koha/opac-search.pl" method="get" name="bookbag_form" id="bookbag_form">
50
      <!-- TABLE RESULTS START -->
51
      <table>
52
        <thead>
53
          <tr>
54
            <th colspan="5" class="resultscontrol">
55
              <div class="resort">
56
                <form method="get" id="sortbyform">
57
                  [% FOREACH param IN follower_params %]
58
                    [% UNLESS param.var == 'sort_by' %]
59
                      <input type="hidden" name='[% param.var |html %]' value='[% param.val %]' />
60
                    [% END %]
61
                  [% END %]
62
                  <label for="sort_by">Sort By: </label>
63
                  <select id="sort_by" name="sort_by">
64
                    [% INCLUDE 'search/resort_form.inc' %]
65
                  </select>
66
                  <input type="submit" value="Go" />
67
                </form>
68
              </div>
69
              <div class="cartlist">
70
                <!-- checkall, clearall are now needed for placehold -->
71
                <span class="checkall"></span>
72
                <span class="clearall"></span>
73
              </div>
74
            </th>
75
          </tr>
76
        </thead>
77
        <!-- Actual Search Results -->
78
        <tbody>
79
          [% FOREACH SEARCH_RESULT IN SEARCH_RESULTS %]
80
            <tr>
81
              <td>
82
                <input type="checkbox" id="bib[% SEARCH_RESULT.biblionumber %]" name="biblionumber" value="[% SEARCH_RESULT.biblionumber %]" /> <label for="bib[% SEARCH_RESULT.biblionumber %]"></label>
83
              </td>
84
              <td>
85
                <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>
86
                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>
87
              </td>
88
            </tr>
89
          [% END %]
90
        </tbody>
91
      </table>
92
    </form>
93
  </div>
94
  [% INCLUDE 'search/page-numbers.inc' %]
95
[% END %]
96
</div>
97
</div>
98
</div>
99
100
<div class="yui-b">
101
  <div class="container">
102
    [% INCLUDE 'search/facets.inc' %]
103
  </div>
104
</div>
105
106
</div>
107
108
[% INCLUDE 'opac-bottom.inc' %]
(-)a/misc/migration_tools/rebuild_solr.pl (+179 lines)
Line 0 Link Here
1
#!/usr/bin/perl
2
3
# Copyright 2012 BibLibre SARL
4
#
5
# This file is part of Koha.
6
#
7
# Koha is free software; you can redistribute it and/or modify it under the
8
# terms of the GNU General Public License as published by the Free Software
9
# Foundation; either version 2 of the License, or (at your option) any later
10
# version.
11
#
12
# Koha is distributed in the hope that it will be useful, but WITHOUT ANY
13
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
14
# A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
15
#
16
# You should have received a copy of the GNU General Public License along
17
# with Koha; if not, write to the Free Software Foundation, Inc.,
18
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
19
20
use Modern::Perl;
21
use Data::Dumper;
22
use Getopt::Long;
23
use LWP::Simple;
24
use XML::Simple;
25
26
use C4::Context;
27
use C4::Search;
28
use Koha::SearchEngine::Index;
29
30
$|=1; # flushes output
31
32
if ( C4::Context->preference("SearchEngine") ne 'Solr' ) {
33
    warn "System preference 'SearchEngine' not equal 'Solr'.";
34
    warn "We can not indexing";
35
    exit(1);
36
}
37
38
#Setup
39
40
my ( $reset, $number, $recordtype, $biblionumbers, $optimize, $info, $want_help );
41
GetOptions(
42
    'r'   => \$reset,
43
    'n:s' => \$number,
44
    't:s' => \$recordtype,
45
    'w:s' => \$biblionumbers,
46
    'o'   => \$optimize,
47
    'i'   => \$info,
48
    'h|help' => \$want_help,
49
);
50
my $debug = C4::Context->preference("DebugLevel");
51
my $index_service = Koha::SearchEngine::Index->new;
52
my $solrurl = $index_service->searchengine->config->SolrAPI;
53
54
my $ping = &ping_command;
55
if (!defined $ping) {
56
    print "SolrAPI = $solrurl\n";
57
    print "Solr is Down\n";
58
    exit(1);
59
}
60
61
#Script
62
63
&print_help if ($want_help);
64
&print_info if ($info);
65
if ($reset){
66
  if ($recordtype){
67
      &reset_index("recordtype:".$recordtype);
68
  } else {
69
      &reset_index("*:*");
70
  }
71
}
72
73
if (defined $biblionumbers){
74
    if (not defined $recordtype) { print "You must specify a recordtype\n"; exit 1;}
75
    &index_biblio($_) for split ',', $biblionumbers;
76
} elsif  (defined $recordtype) {
77
    &index_data;
78
    &optimise_index;
79
}
80
81
if ($optimize) {
82
    &optimise_index;
83
}
84
85
#Functions
86
87
sub index_biblio {
88
    my ($biblionumber) = @_;
89
    $index_service->index_record($recordtype, [ $biblionumber ] );
90
}
91
92
sub index_data {
93
    my $dbh = C4::Context->dbh;
94
        $dbh->do('SET NAMES UTF8;');
95
96
    my $query;
97
    if ( $recordtype eq 'biblio' ) {
98
      $query = "SELECT biblionumber FROM biblio ORDER BY biblionumber";
99
    } elsif ( $recordtype eq 'authority' ) {
100
      $query = "SELECT authid FROM auth_header ORDER BY authid";
101
    }
102
    $query .= " LIMIT $number" if $number;
103
104
    my $sth = $dbh->prepare( $query );
105
    $sth->execute();
106
107
    $index_service->index_record($recordtype, [ map { $_->[0] } @{ $sth->fetchall_arrayref } ] );
108
109
    $sth->finish;
110
}
111
112
sub reset_index {
113
    &reset_command;
114
    &commit_command;
115
    $debug eq '2' && &count_all_docs eq 0 && warn  "Index cleaned!"
116
}
117
118
sub commit_command {
119
    my $commiturl = "/update?stream.body=%3Ccommit/%3E";
120
    my $urlreturns = get $solrurl.$commiturl;
121
}
122
123
sub ping_command {
124
    my $pingurl = "/admin/ping";
125
    my $urlreturns = get $solrurl.$pingurl;
126
}
127
128
sub reset_command {
129
    my ($query) = @_;
130
    my $deleteurl = "/update?stream.body=%3Cdelete%3E%3Cquery%3E".$query."%3C/query%3E%3C/delete%3E";
131
    my $urlreturns = get $solrurl.$deleteurl;
132
}
133
134
sub optimise_index {
135
    $index_service->optimize;
136
}
137
138
sub count_all_docs {
139
    my $queryurl = "/select/?q=*:*";
140
    my $urlreturns = get $solrurl.$queryurl;
141
    my $xmlsimple = XML::Simple->new();
142
    my $data = $xmlsimple->XMLin($urlreturns);
143
    return $data->{result}->{numFound};
144
}
145
146
sub print_info {
147
    my $count = &count_all_docs;
148
    print <<_USAGE_;
149
SolrAPI = $solrurl
150
How many indexed documents = $count;
151
_USAGE_
152
}
153
154
sub print_help {
155
    print <<_USAGE_;
156
$0: reindex biblios and/or authorities in Solr.
157
158
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.
159
160
Parameters:
161
    -t biblio               index bibliographic records
162
163
    -t authority            index authority records
164
165
    -r                      clear Solr index before adding records to index - use this option carefully!
166
167
    -n 100                  index 100 first records
168
169
    -n "100,2"              index 2 records after 100th (101 and 102)
170
171
    -w 101                  index biblio with biblionumber equals 101
172
173
    -o                      launch optimize command at the end of indexing
174
175
    -i                      gives solr install information: SolrAPI value and count all documents indexed
176
177
    --help or -h            show this message.
178
_USAGE_
179
}
(-)a/opac/opac-search.pl (-2 / +14 lines)
Lines 21-34 Link Here
21
21
22
# Script to perform searching
22
# Script to perform searching
23
# Mostly copied from search.pl, see POD there
23
# Mostly copied from search.pl, see POD there
24
use strict;            # always use
24
use Modern::Perl;
25
use warnings;
26
25
27
## STEP 1. Load things that are used in both search page and
26
## STEP 1. Load things that are used in both search page and
28
# results page and decide which template to load, operations 
27
# results page and decide which template to load, operations 
29
# to perform, etc.
28
# to perform, etc.
30
## load Koha modules
29
## load Koha modules
31
use C4::Context;
30
use C4::Context;
31
32
my $searchengine = C4::Context->preference("SearchEngine");
33
given ( $searchengine ) {
34
    when ( /^Solr$/ ) {
35
        warn "We use Solr";
36
        require 'opac/search.pl';
37
        exit;
38
    }
39
    when ( /^Zebra$/ ) {
40
41
    }
42
}
43
32
use C4::Output;
44
use C4::Output;
33
use C4::Auth qw(:DEFAULT get_session);
45
use C4::Auth qw(:DEFAULT get_session);
34
use C4::Languages qw(getAllLanguages);
46
use C4::Languages qw(getAllLanguages);
(-)a/opac/search.pl (+172 lines)
Line 0 Link Here
1
#!/usr/bin/perl
2
3
# Copyright 2012 BibLibre
4
#
5
# This file is part of Koha.
6
#
7
# Koha is free software; you can redistribute it and/or modify it under the
8
# terms of the GNU General Public License as published by the Free Software
9
# Foundation; either version 2 of the License, or (at your option) any later
10
# version.
11
#
12
# Koha is distributed in the hope that it will be useful, but WITHOUT ANY
13
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
14
# A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
15
#
16
# You should have received a copy of the GNU General Public License along
17
# with Koha; if not, write to the Free Software Foundation, Inc.,
18
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
19
20
use Modern::Perl;
21
22
use C4::Context;
23
use CGI;
24
use C4::Auth;
25
use C4::Koha;
26
use C4::Output;
27
use Koha::SearchEngine::Search;
28
use Koha::SearchEngine::QueryBuilder;
29
use Koha::SearchEngine::FacetsBuilder;
30
31
my $cgi = new CGI;
32
33
my $template_name;
34
my $template_type = "basic";
35
if ( $cgi->param("idx") or $cgi->param("q") ) {
36
    $template_name = 'search/results.tt';
37
} else {
38
    $template_name = 'search/advsearch.tt';
39
    $template_type = 'advsearch';
40
}
41
42
# load the template
43
my ( $template, $borrowernumber, $cookie ) = get_template_and_user(
44
    {   template_name   => $template_name,
45
        query           => $cgi,
46
        type            => "opac",
47
        authnotrequired => 1,
48
    }
49
);
50
51
my $format = $cgi->param("format") || 'html';
52
53
54
55
56
# load the Type stuff
57
my $itemtypes = GetItemTypes;
58
59
my $page = $cgi->param("page") || 1;
60
my $count = $cgi->param('count') || C4::Context->preference('OPACnumSearchResults') || 20;
61
$count = 5;
62
my $q = $cgi->param("q");
63
my $builder = Koha::SearchEngine::QueryBuilder->new;
64
$q = $builder->build_query( $q );
65
my $search_service = Koha::SearchEngine::Search->new;
66
67
# load the sorting stuff
68
my $sort_by = $cgi->param('sort_by')
69
        || C4::Context->preference('OPACdefaultSortField') . ' ' . C4::Context->preference('OPACdefaultSortOrder');
70
71
my $search_engine_config = Koha::SearchEngine->new->config;
72
my $sortable_indexes = $search_engine_config->sortable_indexes;
73
my ( $sort_indexname, $sort_order );
74
( $sort_indexname, $sort_order ) = ($1, $2) if ( $sort_by =~ m/^(.*) (asc|desc)$/ );
75
my $sort_by_indexname = eval {
76
    [
77
        map {
78
            $_->{code} eq $sort_indexname
79
                ? 'srt_' . $_->{type} . '_' . $_->{code} . ' ' . $sort_order
80
                : ()
81
        } @$sortable_indexes
82
    ]->[0]
83
};
84
85
# This array is used to build facets GUI
86
my %filters;
87
my @tplfilters;
88
for my $filter ( $cgi->param('filters') ) {
89
    next if not $filter;
90
    my ($k, @v) = $filter =~ /(?: \\. | [^:] )+/xg;
91
    my $v = join ':', @v;
92
    push @{$filters{$k}}, $v;
93
    $v =~ s/^"(.*)"$/$1/; # Remove quotes around
94
    push @tplfilters, {
95
        'var' => $k,
96
        'val' => $v,
97
    };
98
}
99
push @{$filters{recordtype}}, 'biblio';
100
101
my $results = $search_service->search(
102
    $q,
103
    \%filters,
104
    {
105
        page => $page,
106
        count => $count,
107
        sort => $sort_by_indexname,
108
        facets => 1,
109
        fl => ["ste_title", "str_author", 'int_biblionumber'],
110
    }
111
);
112
113
if ($results->{error}){
114
    $template->param(query_error => $results->{error});
115
    output_with_http_headers $cgi, $cookie, $template->output, 'html';
116
    exit;
117
}
118
119
120
# populate results with records
121
my @r;
122
for my $searchresult ( @{ $results->items } ) {
123
    my $biblionumber = $searchresult->{values}->{recordid};
124
125
    my $nr;
126
    while ( my ($k, $v) = each %{$searchresult->{values}} ) {
127
        my $nk = $k;
128
        $nk =~ s/^[^_]*_(.*)$/$1/;
129
        $nr->{$nk} = ref $v ? shift @{$v} : $v;
130
    }
131
    push( @r, $nr );
132
}
133
134
# build facets
135
my $facets_builder = Koha::SearchEngine::FacetsBuilder->new;
136
my @facets_loop = $facets_builder->build_facets( $results, $search_engine_config->facetable_indexes, \%filters );
137
138
my $total = $results->{pager}->{total_entries};
139
my $pager = Data::Pagination->new(
140
    $total,
141
    $count,
142
    20,
143
    $page,
144
);
145
146
# params we want to pass for all actions require another query (pagination, sort, facets)
147
my @follower_params = map { {
148
    var => 'filters',
149
    val => $_->{var}.':"'.$_->{val}.'"'
150
} } @tplfilters;
151
push @follower_params, { var => 'q', val => $q};
152
push @follower_params, { var => 'sort_by', val => $sort_by};
153
154
# Pager template params
155
$template->param(
156
    previous_page    => $pager->{'prev_page'},
157
    next_page        => $pager->{'next_page'},
158
    PAGE_NUMBERS     => [ map { { page => $_, current => $_ == $page } } @{ $pager->{'numbers_of_set'} } ],
159
    current_page     => $page,
160
    follower_params  => \@follower_params,
161
    total            => $total,
162
    SEARCH_RESULTS   => \@r,
163
    query            => $q,
164
    count            => $count,
165
    sort_by          => $sort_by,
166
    sortable_indexes => $sortable_indexes,
167
    facets_loop      => \@facets_loop,
168
    filters          => \@tplfilters,
169
);
170
171
my $content_type = ( $format eq 'rss' or $format eq 'atom' ) ? $format : 'html';
172
output_with_http_headers $cgi, $cookie, $template->output, $content_type;
(-)a/t/lib/Mocks.pm (+25 lines)
Line 0 Link Here
1
package t::lib::Mocks;
2
3
use Modern::Perl;
4
use Test::MockModule;
5
use t::lib::Mocks::Context;
6
7
our (@ISA,@EXPORT,@EXPORT_OK);
8
BEGIN {
9
    require Exporter;
10
    @ISA = qw(Exporter);
11
    push @EXPORT, qw(
12
        &set_solr
13
        &set_zebra
14
    );
15
}
16
17
my $context = new Test::MockModule('C4::Context');
18
sub set_solr {
19
    $context->mock('preference', sub { &t::lib::Mocks::Context::MockPreference( @_, "Solr", $context ) });
20
}
21
sub set_zebra {
22
    $context->mock('preference', sub { &t::lib::Mocks::Context::MockPreference( @_, "Zebra", $context ) });
23
}
24
25
(-)a/t/lib/Mocks/Context.pm (+13 lines)
Line 0 Link Here
1
package t::lib::Mocks::Context;
2
use t::lib::Mocks::Context;
3
use C4::Context;
4
5
sub MockPreference {
6
    my ( $self, $syspref, $value, $mock_object ) = @_;
7
    return $value if $syspref eq 'SearchEngine';
8
    $mock_object->unmock("preference");
9
    my $r = C4::Context->preference($syspref);
10
    $mock_object->mock('preference', sub { &t::lib::Mocks::Context::MockPreference( @_, $value, $mock_object ) });
11
    return $r;
12
}
13
1;
(-)a/t/searchengine/000_conn/conn.t (+23 lines)
Line 0 Link Here
1
use Modern::Perl;
2
use Test::More;
3
use Koha::SearchEngine::Solr;
4
use Koha::SearchEngine::Zebra;
5
use Koha::SearchEngine::Search;
6
use t::lib::Mocks;
7
8
my $se_index = Koha::SearchEngine::Solr->new;
9
ok($se_index->isa('Data::SearchEngine::Solr'), 'Solr is a Solr data searchengine');
10
11
$se_index = Koha::SearchEngine::Zebra->new;
12
ok($se_index->isa('Data::SearchEngine::Zebra'), 'Zebra search engine');
13
14
set_solr();
15
$se_index = Koha::SearchEngine::Search->new;
16
ok($se_index->searchengine->isa('Data::SearchEngine::Solr'), 'Solr search engine');
17
18
set_zebra();
19
$se_index = Koha::SearchEngine::Search->new;
20
ok($se_index->searchengine->isa('Data::SearchEngine::Zebra'), 'Zebra search engine');
21
22
23
done_testing;
(-)a/t/searchengine/001_search/search_base.t (+12 lines)
Line 0 Link Here
1
use Test::More;
2
3
use t::lib::Mocks;
4
5
set_solr;
6
use Koha::SearchEngine::Search;
7
my $search_service = Koha::SearchEngine::Search->new;
8
isnt (scalar $search_service->search("fort"), 0, 'test search') ;
9
10
#$search_service->search($query_service->build_query(@,@,@));
11
12
done_testing;
(-)a/t/searchengine/002_index/index_base.t (+15 lines)
Line 0 Link Here
1
use Test::More;
2
use FindBin qw($Bin);
3
4
use t::lib::::Mocks;
5
6
use Koha::SearchEngine::Index;
7
8
set_solr;
9
my $index_service = Koha::SearchEngine::Index->new;
10
system( qq{/bin/cp $FindBin::Bin/../indexes.yaml /tmp/indexes.yaml} );
11
$index_service->searchengine->config->set_config_filename( "/tmp/indexes.yaml" );
12
is ($index_service->index_record("biblio", [2]), 1, 'test search') ;
13
is ($index_service->optimize, 1, 'test search') ;
14
15
done_testing;
(-)a/t/searchengine/003_query/buildquery.t (+45 lines)
Line 0 Link Here
1
use Modern::Perl;
2
use Test::More;
3
use C4::Context;
4
5
use Koha::SearchEngine;
6
use Koha::SearchEngine::QueryBuilder;
7
use t::lib::Mocks;
8
9
my $titleindex = "title";
10
my $authorindex = "author";
11
#my $eanindex = "str_ean";
12
#my $pubdateindex = "date_pubdate";
13
14
my ($operands, $indexes, $operators);
15
16
17
# === Solr part ===
18
@$operands = ('cup', 'rowling');
19
@$indexes = ('ti', 'au'); 
20
@$operators = ('AND'); 
21
22
set_solr;
23
my $qs = Koha::SearchEngine::QueryBuilder->new;
24
25
my $se = Koha::SearchEngine->new;
26
is( $se->name, "Solr", "Test searchengine name eq Solr" );
27
28
my $gotsolr = $qs->build_advanced_query($indexes, $operands, $operators);
29
my $expectedsolr = "ti:cup AND au:rowling";
30
is($gotsolr, $expectedsolr, "Test build_query Solr");
31
32
33
# === Zebra part ===
34
set_zebra;
35
$se = Koha::SearchEngine->new;
36
is( $se->name, "Zebra", "Test searchengine name eq Zebra" );
37
$qs = Koha::SearchEngine::QueryBuilder->new;
38
my ( $builterror, $builtquery, $simple_query, $query_cgi, $query_desc, $limit, $limit_cgi, $limit_desc, $stopwords_removed, $query_type ) = $qs->build_query($operators, $operands, $indexes);
39
my $gotzebra = $builtquery;
40
my $expectedzebra = qq{ti,wrdl= cup AND au,wrdl= rowling };
41
is($gotzebra, $expectedzebra, "Test Zebra indexes in 'normal' search");
42
# @and @attr 1=title @attr 4=6 "des mots de mon titre" @attr 1=author Jean en PQF
43
44
45
done_testing;
(-)a/t/searchengine/004_config/load_config.t (+54 lines)
Line 0 Link Here
1
use Modern::Perl;
2
use Test::More;
3
use FindBin qw($Bin);
4
5
use C4::Context;
6
use Koha::SearchEngine;
7
use t::lib::Mocks;
8
9
set_solr;
10
11
my $se = Koha::SearchEngine->new;
12
is( $se->name, "Solr", "Test searchengine name eq Solr" );
13
14
my $config = $se->config;
15
$config->set_config_filename( "$FindBin::Bin/../indexes.yaml" );
16
my $ressource_types = $config->ressource_types;
17
is ( grep ( /^biblio$/, @$ressource_types ), 1, "Ressource type biblio must to be defined" );
18
is ( grep ( /^authority$/, @$ressource_types ), 1, "Ressource type authority must to be defined" );
19
20
my $indexes = $config->indexes;
21
is ( scalar(@$indexes), 3, "There are 3 indexes configured" );
22
23
my $index1 = @$indexes[0];
24
is ( $index1->{code}, 'title', "My index first have code=title");
25
is ( $index1->{label}, 'Title', "My index first have label=Title");
26
is ( $index1->{type}, 'ste', "My index first have type=ste");
27
is ( $index1->{ressource_type}, 'biblio', "My index first have ressource_type=biblio");
28
is ( $index1->{sortable}, '1', "My index first have sortable=1");
29
is ( $index1->{mandatory}, '1', "My index first have mandatory=1");
30
eq_array ( $index1->{mappings}, ["200\$a", "4..\$t"], "My first index have mappings=[200\$a,4..\$t]");
31
32
system( qq{/bin/cp $FindBin::Bin/../indexes.yaml /tmp/indexes.yaml} );
33
$config->set_config_filename( "/tmp/indexes.yaml" );
34
$indexes = $config->indexes;
35
my $new_index = {
36
    code => 'isbn',
37
    label => 'ISBN',
38
    type => 'str',
39
    ressource_type => 'biblio',
40
    sortable => 0,
41
    mandatory => 0
42
};
43
push @$indexes, $new_index;
44
$config->indexes( $indexes );
45
46
$indexes = $config->indexes;
47
48
my $isbn_index = $config->index( 'isbn' );
49
is( $isbn_index->{code}, 'isbn', 'Index isbn has been written' );
50
51
my $sortable_indexes = $config->sortable_indexes;
52
is ( @$sortable_indexes, 2, "There are 2 sortable indexes" );
53
54
done_testing;
(-)a/t/searchengine/indexes.yaml (-1 / +33 lines)
Line 0 Link Here
0
- 
1
ressource_types:
2
    - biblio
3
    - authority
4
5
indexes:
6
    - code: title
7
      label: Title
8
      type: ste
9
      ressource_type: biblio
10
      sortable: 1
11
      mandatory: 1
12
      mappings:
13
          - 200$a
14
          - 210$a
15
          - 4..$t
16
    - code: author
17
      label: Author
18
      type: str
19
      ressource_type: biblio
20
      sortable: 1
21
      mandatory: 0
22
      mappings:
23
          - 700$*
24
          - 710$*
25
    - code: subject
26
      label: Subject
27
      type: str
28
      ressource_type: biblio
29
      sortable: 0
30
      mandatory: 0
31
      mappings:
32
          - 600$a
33
          - 601$a

Return to bug 8233