Bugzilla – Attachment 120029 Details for
Bug 27113
Elasticsearch: Autocomplete in search
Home
|
New
|
Browse
|
Search
|
[?]
|
Reports
|
Help
|
New Account
|
Log In
[x]
|
Forgot Password
Login:
[x]
[patch]
Bug 27113: ElasticSearch: Autocomplete in input search
Bug-27113-ElasticSearch-Autocomplete-in-input-sear.patch (text/plain), 45.27 KB, created by
Ivan Dziuba
on 2021-04-22 21:35:14 UTC
(
hide
)
Description:
Bug 27113: ElasticSearch: Autocomplete in input search
Filename:
MIME Type:
Creator:
Ivan Dziuba
Created:
2021-04-22 21:35:14 UTC
Size:
45.27 KB
patch
obsolete
>From b6fae204304802a9bb60af39f00c38481eee8c51 Mon Sep 17 00:00:00 2001 >From: Ivan Dziuba <ivan.dziuba@inlibro.com> >Date: Thu, 22 Apr 2021 17:30:41 -0400 >Subject: [PATCH] Bug 27113: ElasticSearch: Autocomplete in input search > >Usually the user knows only part of the title of the book or only the name of the author, etc. When he start search something Koha (ElasticSearch) predicts the rest of a word or expression which user is typing. Autocomplete predicts that thanks to index of ElasticSearch. > >TEST PLAN >Important! In this patch we need to do reindex ElasticSearch. ElasticSearch must have all information in his index. >1. Go Intranet -> Preference -> SearchEngine -> ElasticSearch >!! APPLY PATCH !! >2. Mapping is good (Intranet -> Catalog -> Search engine configuration (Elasticsearch) ). >!Recommended 'Reset Mapping' -> 'Yes' >3. In your koha-conf.xml file you must have good <index_name> for <elasticsearch> and version ES >4. Update Preference: >./installer/data/mysql/updatedatabase.pl >If that passe good you can look the lines: > - DEV atomic update: bug_27113-elasticsearch_autocomplete_input_search.perl > - Upgrade to XXX done : Bug 27113 - Autocomplete input on main page with elasticsearch >5. After that we can look two options in the preferences: >- IntranetAutocompleteElasticSearch; >- OPACAutocompleteElasticSearch; >4. For add information in the index we must run script for reindexing: >./misc/search_tools/rebuild_elasticsearch.pl -v -d >5. Waiting for the end of indexing >6. Go on Preference and find : >- IntranetAutocompleteElasticSearch; >- OPACAutocompleteElasticSearch; >Value "Show" turn on autocomplete. >7. Now we have Autocomplete for Intranet/OPAC search input (advanced search also). >--- > Koha/SearchEngine/Elasticsearch/Browse.pm | 64 +++++ > .../elasticsearch/field_config.yaml | 4 + > .../elasticsearch/index_config.yaml | 14 + > api/elasticsearch/elasticsearch.pl | 66 +++++ > ...asticsearch_autocomplete_input_search.perl | 10 + > installer/data/mysql/mandatory/sysprefs.sql | 4 +- > .../js/elasticsearch/autocomplete.js | 270 ++++++++++++++++++ > .../prog/css/elasticsearch/autocomplete.css | 28 ++ > .../prog/en/includes/doc-head-close.inc | 5 + > .../prog/en/includes/js_includes.inc | 4 + > .../modules/admin/preferences/searching.pref | 17 ++ > .../prog/en/modules/catalogue/advsearch.tt | 10 +- > .../intranet-tmpl/prog/js/staff-global.js | 6 + > .../opac-elasticsearch/opac-autocomplete.css | 29 ++ > .../bootstrap/en/includes/doc-head-close.inc | 5 + > .../bootstrap/en/includes/opac-bottom.inc | 4 + > .../bootstrap/en/modules/opac-advsearch.tt | 8 + > .../opac-elasticsearch/opac-autocomplete.js | 265 +++++++++++++++++ > opac/svc/elasticsearch/opac-elasticsearch.pl | 70 +++++ > 19 files changed, 877 insertions(+), 6 deletions(-) > create mode 100755 api/elasticsearch/elasticsearch.pl > create mode 100644 installer/data/mysql/atomicupdate/bug_27113-elasticsearch_autocomplete_input_search.perl > create mode 100644 koha-tmpl/intranet-tmpl/js/elasticsearch/autocomplete.js > create mode 100644 koha-tmpl/intranet-tmpl/prog/css/elasticsearch/autocomplete.css > create mode 100644 koha-tmpl/opac-tmpl/bootstrap/css/opac-elasticsearch/opac-autocomplete.css > create mode 100644 koha-tmpl/opac-tmpl/bootstrap/js/opac-elasticsearch/opac-autocomplete.js > create mode 100755 opac/svc/elasticsearch/opac-elasticsearch.pl > >diff --git a/Koha/SearchEngine/Elasticsearch/Browse.pm b/Koha/SearchEngine/Elasticsearch/Browse.pm >index 85070730a7..57c0a74023 100644 >--- a/Koha/SearchEngine/Elasticsearch/Browse.pm >+++ b/Koha/SearchEngine/Elasticsearch/Browse.pm >@@ -162,6 +162,70 @@ sub _build_query { > return $query; > } > >+sub autocomplete_one_idx { >+ my ($self, $cgi_q, $prefix, $analyzer, $token_counter) = @_; >+ my @source; >+ my $elasticsearch = $self->get_elasticsearch(); >+ # we can change these variables >+ my ($nb_fragments, $size_fragment, $pre_tags, $post_tags) = (1, 100, ["<strong>"], ["</strong>"]); >+ >+ my $query = $self->_build_query_autocomplete($cgi_q, $prefix, $analyzer); >+ my $res = $elasticsearch->search( >+ index => $self->index_name, >+ body => $query >+ ); >+ $res->{'val'} = $cgi_q; >+ $res->{'prefix'} = $prefix; >+ $res->{'token_counter'} = $token_counter; >+ >+ return $res; >+} >+ >+sub autocomplete_idx { >+ my ($self, $cgi_q, $prefix, $analyzer, $token_counter) = @_; >+ my %results; >+ my $idx = 0; >+ foreach my $pref ( @$prefix ) { >+ $results{$idx} = $self->autocomplete_one_idx($cgi_q, $pref, $analyzer, $token_counter); >+ $idx++; >+ } >+ $results{'val'} = $cgi_q; >+ $results{'prefix'} = join( ',', @$prefix ); >+ $results{'token_counter'} = $token_counter; >+ return \%results; >+} >+ >+sub _build_query_autocomplete { >+ my ($self, $cgi_q, $prefix, $analyzer) = @_; >+ my (@source); >+ #prefix + analyzer >+ my $prefix_analyzer = $prefix . '.' . $analyzer; >+ # we can change these variables >+ my ($nb_fragments, $size_fragment, $pre_tags, $post_tags) = (1, 100, ["<strong>"], ["</strong>"]); >+ push(@source, $prefix); >+ my $query = { >+ _source => \@source, >+ query => { >+ match => { >+ $prefix_analyzer => { >+ query => $cgi_q, >+ operator => 'and' >+ } >+ } >+ }, >+ highlight => { >+ number_of_fragments => $nb_fragments, >+ fragment_size => $size_fragment, >+ pre_tags => $pre_tags, >+ post_tags => $post_tags, >+ fields => { >+ $prefix_analyzer => {} >+ } >+ } >+ }; >+ return $query; >+} >+ > 1; > > __END__ >diff --git a/admin/searchengine/elasticsearch/field_config.yaml b/admin/searchengine/elasticsearch/field_config.yaml >index d78bdf06dd..cae35c9585 100644 >--- a/admin/searchengine/elasticsearch/field_config.yaml >+++ b/admin/searchengine/elasticsearch/field_config.yaml >@@ -47,6 +47,10 @@ search: > type: text > analyzer: analyzer_phrase > search_analyzer: analyzer_phrase >+ autocomplete: >+ type: text >+ analyzer: autocomplete >+ search_analyzer: standard > raw: > type: keyword > normalizer: nfkc_cf_normalizer >diff --git a/admin/searchengine/elasticsearch/index_config.yaml b/admin/searchengine/elasticsearch/index_config.yaml >index b8b2ac6071..ab1e5a631b 100644 >--- a/admin/searchengine/elasticsearch/index_config.yaml >+++ b/admin/searchengine/elasticsearch/index_config.yaml >@@ -2,6 +2,14 @@ > # Index configuration that defines how different analyzers work. > index: > analysis: >+ tokenizer: >+ autocomplete_tokenizer: >+ type: edge_ngram >+ min_gram: 1 >+ max_gram: 10 >+ token_chars: >+ - letter >+ - digit > analyzer: > # Phrase analyzer is used for phrases (exact phrase match) > analyzer_phrase: >@@ -10,6 +18,12 @@ index: > - icu_folding > char_filter: > - punctuation >+ autocomplete: >+ type: custom >+ filter: >+ - icu_folding >+ - lowercase >+ tokenizer: autocomplete_tokenizer > analyzer_standard: > tokenizer: icu_tokenizer > filter: >diff --git a/api/elasticsearch/elasticsearch.pl b/api/elasticsearch/elasticsearch.pl >new file mode 100755 >index 0000000000..86890dcaab >--- /dev/null >+++ b/api/elasticsearch/elasticsearch.pl >@@ -0,0 +1,66 @@ >+#!/usr/bin/perl >+ >+use strict; >+use warnings; >+use CGI qw ( -utf8 ); >+use JSON; >+use utf8; >+use Unicode::Normalize; >+use CGI::Session; >+use Koha::SearchEngine::Elasticsearch::Browse; >+ >+my $browser = Koha::SearchEngine::Elasticsearch::Browse->new( { index => 'biblios' } ); >+my $cgi = CGI->new; >+my $session = CGI::Session->new(); >+ >+$session->param(-name=>'analyzer', -value=>"autocomplete"); >+$session->param(-name=>'prefix', -value=>$cgi->param("prefix")); >+$session->param(-name=>'q', -value=>$cgi->param("q")); >+$session->param(-name=>'key', -value=>$cgi->param("key")); >+$session->param(-name=>'token_counter', -value=>$cgi->param("token_counter")); >+$session->expire('+1h'); >+ >+if ($session->param("key") == "autocomplete") { >+ my @prefix = split /,/, $session->param("prefix"); >+ #fields for autocomplete >+ my $length = scalar @prefix; >+ my $ses = NFKD( $session->param("q") ); >+ $ses =~ s/\p{NonspacingMark}//g; >+ >+ #search by many prefix fields >+ if ($length > 1){ >+ my $res = $browser->autocomplete_idx($ses, \@prefix, $session->param("analyzer"), $session->param("token_counter")); >+ print $cgi->header("application/json"); >+ print to_json($res); >+ } >+ #search by one prefix field >+ elsif ($length == 1) { >+ my $res = $browser->autocomplete_one_idx($ses, $prefix[0], $session->param("analyzer"), $session->param("token_counter")); >+ print $cgi->header("application/json"); >+ print to_json($res); >+ } >+ #no prefix 404 >+ else { >+ response404JSON(); >+ } >+} >+else { >+ response404JSON(); >+} >+ >+sub response404JSON { >+ my $json = JSON->new->utf8; >+ my $header_type = "application/json"; >+ my $header_status = "404"; >+ my $output = $json->encode({ >+ "error" => "No data", >+ "description" => "Bad request", >+ }); >+ print $cgi->header( >+ -type => $header_type, >+ -charset => "utf-8", >+ -status => $header_status >+ ); >+ print $output; >+ print "\n"; >+} >diff --git a/installer/data/mysql/atomicupdate/bug_27113-elasticsearch_autocomplete_input_search.perl b/installer/data/mysql/atomicupdate/bug_27113-elasticsearch_autocomplete_input_search.perl >new file mode 100644 >index 0000000000..c5947f6f74 >--- /dev/null >+++ b/installer/data/mysql/atomicupdate/bug_27113-elasticsearch_autocomplete_input_search.perl >@@ -0,0 +1,10 @@ >+$DBversion = 'XXX'; # will be replaced by the RM >+if( CheckVersion( $DBversion ) ) { >+ # you can use $dbh here like: >+ $dbh->do(q{INSERT IGNORE INTO systempreferences (variable,value,options,explanation,type) VALUES ('OPACAutocompleteElasticSearch', '0', NULL, NULL, 'YesNo')}); >+ >+ $dbh->do(q{INSERT IGNORE INTO systempreferences (variable,value,options,explanation,type) VALUES ('IntranetAutocompleteElasticSearch', '0', NULL, NULL, 'YesNo')}); >+ >+ # Always end with this (adjust the bug info) >+ NewVersion( $DBversion, 27113, "Autocomplete with elasticsearch"); >+} >diff --git a/installer/data/mysql/mandatory/sysprefs.sql b/installer/data/mysql/mandatory/sysprefs.sql >index ef7d4561fb..ddfe3d692c 100644 >--- a/installer/data/mysql/mandatory/sysprefs.sql >+++ b/installer/data/mysql/mandatory/sysprefs.sql >@@ -733,5 +733,7 @@ INSERT INTO systempreferences ( `variable`, `value`, `options`, `explanation`, ` > ('XSLTListsDisplay','default','','Enable XSLT stylesheet control over lists pages display on intranet','Free'), > ('XSLTResultsDisplay','default','','Enable XSL stylesheet control over results page display on intranet','Free'), > ('z3950AuthorAuthFields','701,702,700',NULL,'Define the MARC biblio fields for Personal Name Authorities to fill biblio.author','free'), >-('z3950NormalizeAuthor','0','','If ON, Personal Name Authorities will replace authors in biblio.author','YesNo') >+('z3950NormalizeAuthor','0','','If ON, Personal Name Authorities will replace authors in biblio.author','YesNo'), >+('OPACAutocompleteElasticSearch','0',NULL,NULL,'YesNo'), >+('IntranetAutocompleteElasticSearch','0',NULL,NULL,'YesNo') > ; >diff --git a/koha-tmpl/intranet-tmpl/js/elasticsearch/autocomplete.js b/koha-tmpl/intranet-tmpl/js/elasticsearch/autocomplete.js >new file mode 100644 >index 0000000000..0f689a7b38 >--- /dev/null >+++ b/koha-tmpl/intranet-tmpl/js/elasticsearch/autocomplete.js >@@ -0,0 +1,270 @@ >+/* OPAC JS file OPACAutocompleteElasticSearch */ >+/* prefix for search ES */ >+var es_prefix = { >+ 'au': 'author', >+ 'pb': 'publisher', >+ 'se': 'title-series', >+ 'su': 'subject', >+ 'ti': 'title-cover', >+ /* for all */ >+ '': ['title-cover', 'author', 'subject', 'title-series', 'publisher'], >+ 'kw': ['title-cover', 'author', 'subject', 'title-series', 'publisher'] >+}; >+ >+/* stop class for elements name=["q"] */ >+var stop_class_input = ["form-field-value"]; >+ >+var url_request = '/cgi-bin/koha/api/elasticsearch/elasticsearch.pl?q='; >+/* query for elasticsearch encode*/ >+var query_url_encode = { >+ '\'':'%5C%27', /* \\' for decode */ >+ '+': '' >+}; >+/* query for elasticsearch decode*/ >+var query_url_decode = { >+ '\'':"\\'", >+ '+': '' >+}; >+/* count of lines for autocomplete */ >+var nb_autocomplete = 10; >+/* key API */ >+var key = 'autocomplete'; >+ >+function AutocompleteInitIntranet(){ >+ /* vars for class position absolute autocomplete */ >+ var left = "0px"; >+ var right = "0px"; >+ var top = ""; >+ /* get all input name q for search */ >+ var input_q = document.getElementsByName("q"); >+ for (var nb = 0; nb < input_q.length; nb++){ >+ /* addEventListener for every 'input' */ >+ if (!stop_class_input.includes(input_q[nb].className)){ >+ autocomplete(input_q[nb], nb, left, right, top); >+ } >+ }; >+}; >+ >+function autocomplete(inp, nb, left, right) { >+ var select_idx = document.getElementsByName("idx"); >+ /* autocomplete off for input */ >+ inp.setAttribute("autocomplete", "off"); >+ /* get parent of input */ >+ var parent_inp = $(inp).parent(); >+ /* get element after input */ >+ var next_elem_inp = inp.nextElementSibling; >+ /* create new div with position relative for class .autocomplete with absolute */ >+ var div_relative = document.createElement('div'); >+ $(div_relative).addClass( "autocomplete" ); >+ div_relative.append(inp); >+ /* input doesn't have an elem after, add it to parent */ >+ if (next_elem_inp === null){ >+ parent_inp.append( div_relative ); >+ } else { // input has an elem after, add elem after it >+ next_elem_inp.before(div_relative); >+ }; >+ var currentFocus; >+ /*execute a function when someone writes in the text field:*/ >+ var token_counter = 0; >+ inp.addEventListener("input", function(e) { >+ var a, val = this.value; >+ /* var for async compare */ >+ var tmp_input = this.value.replace(/[+']/g, function(matched){ >+ return query_url_decode[matched]; >+ }); >+ token_counter++; >+ currentFocus = -1; >+ if (document.getElementsByClassName("autocomplete-items").length !== 0){ >+ a = document.getElementsByClassName("autocomplete-items")[0]; >+ } else { >+ /*create a DIV element that will contain the items (values):*/ >+ a = document.createElement("DIV"); >+ a.setAttribute("id", this.id + "autocomplete-list"); >+ a.setAttribute("class", "autocomplete-items"); >+ /*append the DIV element as a child of the autocomplete container:*/ >+ this.parentNode.appendChild(a); >+ /*append position absolute left/right:*/ >+ $(".autocomplete-items").css("left",left); >+ $(".autocomplete-items").css("right",right); >+ }; >+ /* get es_prefix key for builder */ >+ var chose_prefix = (select_idx == null || select_idx.length == 0) ? '' : GetValueIdx(select_idx, nb); >+ chose_prefix = chose_prefix.replace(/([^,])[,-]([^,].*)?$/, '$1'); >+ if (chose_prefix !== null){ >+ var prefix = es_prefix[chose_prefix].toString(); >+ val = val.replace(/[+']/g, function(matched){ >+ return query_url_encode[matched]; >+ }); >+ if (tmp_input == '' || tmp_input == null){ >+ closeAllLists(); >+ token_counter = 0; >+ } else { >+ $.ajax({ >+ type: 'GET', >+ url: url_request + val + '&key=' + key + '&prefix=' + prefix + '&token_counter=' + token_counter, >+ success: function (data) { >+ //console.log(data); >+ if (data.length !== 0){ >+ var myset; //Set for Autocomplete unique >+ /* autocomplete for all prefix */ >+ if (chose_prefix === 'kw' || chose_prefix === ''){ >+ myset = GetSetAutocompleteAllIdx(data, prefix, key); >+ } else { // autocomplete for one prefix >+ myset = GetSetAutocompleteOneIdx(data, prefix, key); >+ }; >+ /* append set to autocomplete */ >+ if ( tmp_input + prefix == data['val'] + data['prefix'] && token_counter === parseInt(data['token_counter'], 10)){ >+ a.innerHTML = ""; >+ for (let item of myset){ >+ a.appendChild(CreateDivItemAutocomplete(item, val)); >+ }; >+ }; >+ } else { >+ closeAllLists(this); >+ }; >+ }, >+ error: function (data) { >+ console.log(data); >+ }, >+ }); >+ } >+ >+ }; >+ }); >+ /* get value for tag with name idx */ >+ function GetValueIdx(elem, nb){ >+ switch (elem[0].tagName){ >+ case 'INPUT': >+ return elem[0].value; >+ case 'SELECT': >+ return select_idx[nb].options[select_idx[nb].selectedIndex].value; >+ default: >+ return null; >+ }; >+ }; >+ /* get autocomplete for only one prefix title/author/etc... */ >+ function GetSetAutocompleteOneIdx(data, prefix, key){ >+ let myset = new Set(); >+ let tmp_data = data['hits']['hits']; >+ for (let i = 0; i < tmp_data.length; i++) { >+ for (let j = 0; j < tmp_data[i]['highlight'][prefix + '.' + key].length; j++){ >+ /* div with data for autocomplete */ >+ let tmp = tmp_data[i]['highlight'][prefix + '.' + key][j]; >+ tmp = tmp.replace(/^\[/g, ''); >+ tmp = tmp.replace(/\]+$/g, ''); >+ myset.add(tmp.replace(/^[ &\/\\#,+)$~%.'":*?>{}!;]+|[ &\/\\#,+($~%.'":*?<{}!;]+$/g, '')); >+ if (myset.size >= nb_autocomplete) break; >+ }; >+ if (myset.size >= nb_autocomplete) break; >+ }; >+ return myset; >+ }; >+ /* get autocomplete for all prefix */ >+ function GetSetAutocompleteAllIdx(data, prefix, key){ >+ let myset = new Set(); >+ var pref = prefix.split(","); >+ for (k = 0; k < Object.keys(data).length; k++){ //Object.keys(data).length >+ if (data[k] != '' && data[k] != null){ >+ let tmp_data = data[k]['hits']['hits']; >+ for (i = 0; i < tmp_data.length; i++) { >+ for (j = 0; j < tmp_data[i]['highlight'][pref[k] + '.' + key].length; j++){ >+ /* div with data for autocomplete */ >+ let tmp = tmp_data[i]['highlight'][pref[k] + '.' + key][j] >+ myset.add(tmp.replace(/[ &#,+()$~%.'":*?<{}!/;]+$/g, '')); >+ if (myset.size >= nb_autocomplete) break; >+ }; >+ if (myset.size >= nb_autocomplete) break; >+ }; >+ if (myset.size >= nb_autocomplete) break; >+ } >+ } >+ return myset; >+ }; >+ >+ /*execute a function presses a key on the keyboard:*/ >+ inp.addEventListener("keydown", function(e) { >+ var x = document.getElementById(this.id + "autocomplete-list"); >+ if (x) x = x.getElementsByTagName("div"); >+ if (e.keyCode == 40) { //DOWN >+ /*If the arrow DOWN key is pressed, >+ increase the currentFocus variable:*/ >+ currentFocus++; >+ /*and and make the current item more visible:*/ >+ addActive(x); >+ } else if (e.keyCode == 38) { //up >+ /*If the arrow UP key is pressed, >+ decrease the currentFocus variable:*/ >+ currentFocus--; >+ /*and and make the current item more visible:*/ >+ addActive(x); >+ e.preventDefault(); >+ } else if (e.keyCode == 13) { >+ /*If the ENTER key is pressed, prevent the form from being submitted,*/ >+ //e.preventDefault(); >+ if (currentFocus > -1) { >+ /*and simulate a click on the "active" item:*/ >+ if (x) x[currentFocus].click(); >+ } >+ } >+ /* press Esc clear all autocomplete */ >+ else if (e.keyCode == 27) { >+ closeAllLists(); >+ } >+ /* press Esc clear all autocomplete */ >+ else if (e.keyCode == 8) { >+ closeAllLists(); >+ }; >+ }); >+ function addActive(x) { >+ /*a function to classify an item as "active":*/ >+ if (!x) return false; >+ /*start by removing the "active" class on all items:*/ >+ removeActive(x); >+ if (currentFocus >= x.length) currentFocus = 0; >+ if (currentFocus < 0) currentFocus = (x.length - 1); >+ /*add class "autocomplete-active":*/ >+ x[currentFocus].classList.add("autocomplete-active"); >+ inp.value = (x[currentFocus].textContent.replace(/<\/?[^>]+(>|$)/g, "")).trim(); >+ }; >+ function removeActive(x) { >+ /*a function to remove the "active" class from all autocomplete items:*/ >+ for (var i = 0; i < x.length; i++) { >+ x[i].classList.remove("autocomplete-active"); >+ }; >+ }; >+ >+ function closeAllLists(elem) { >+ /*close all autocomplete lists in the document with class autocomplete-items */ >+ var x = document.getElementsByClassName("autocomplete-items"); >+ for (var i = 0; i < x.length; i++) { >+ x[i].parentNode.removeChild(x[i]) >+ }; >+ }; >+ >+ /* div for one item autocomplete */ >+ function CreateDivItemAutocomplete (elem){ >+ var b = document.createElement("DIV"); >+ // add element "; >+ b.innerHTML += elem; >+ /*insert a input field that will hold the current array item's value:*/ >+ b.innerHTML += "<input type='hidden' value=''>"; >+ /*execute a function when someone clicks on the item value (DIV element):*/ >+ b.addEventListener("click", function(e) { >+ /* insert the value for the autocomplete text field: */ >+ inp.value = this.getElementsByTagName("input")[0].value; >+ /* normalyzer hightlight without tags */ >+ //inp.value = (inp.value.replace(/<\/?[^>]+(>|$)/g, "")).trim(); >+ inp.value = this.innerText; >+ /* Submit form click mouse in div */ >+ this.closest("form").submit(); >+ }); >+ return b; >+ }; >+ >+ /*execute a function when someone clicks in the document:*/ >+ document.addEventListener("click", function (e) { >+ closeAllLists(e.target); >+ }); >+}; >+ >+AutocompleteInitIntranet(); >diff --git a/koha-tmpl/intranet-tmpl/prog/css/elasticsearch/autocomplete.css b/koha-tmpl/intranet-tmpl/prog/css/elasticsearch/autocomplete.css >new file mode 100644 >index 0000000000..40dfefb0d6 >--- /dev/null >+++ b/koha-tmpl/intranet-tmpl/prog/css/elasticsearch/autocomplete.css >@@ -0,0 +1,28 @@ >+.autocomplete { >+ /*the container must be positioned relative:*/ >+ position: relative; >+ display: inline-block; >+} >+.autocomplete-items { >+ position: absolute; >+ border: 1px solid #d4d4d4; >+ border-bottom: none; >+ border-top: none; >+ z-index: 99; >+ /*position the autocomplete items to be the same width as the container:*/ >+ } >+ .autocomplete-items div { >+ padding: 10px; >+ cursor: pointer; >+ background-color: #fff; >+ border-bottom: 1px solid #d4d4d4; >+ } >+ .autocomplete-items div:hover { >+ /*when hovering an item:*/ >+ background-color: #e9e9e9; >+ } >+ .autocomplete-active { >+ /*when navigating through the items using the arrow keys:*/ >+ background-color: #cedfb1 !important; >+ color: #ffffff; >+ } >\ No newline at end of file >diff --git a/koha-tmpl/intranet-tmpl/prog/en/includes/doc-head-close.inc b/koha-tmpl/intranet-tmpl/prog/en/includes/doc-head-close.inc >index c2833e3009..a2a5c5d258 100644 >--- a/koha-tmpl/intranet-tmpl/prog/en/includes/doc-head-close.inc >+++ b/koha-tmpl/intranet-tmpl/prog/en/includes/doc-head-close.inc >@@ -21,6 +21,11 @@ > [% Asset.css("css/print.css", { media = "print" }) | $raw %] > [% INCLUDE intranetstylesheet.inc %] > [% IF ( bidi ) %][% Asset.css("css/right-to-left.css") | $raw %][% END %] >+<!-- Intranet inc CSS IntranetAutocompleteElasticSearch --> >+[% IF ( Koha.Preference('IntranetAutocompleteElasticSearch') ) %] >+ [% SET Optylesheet = 'elasticsearch/autocomplete.css' %] >+ <link rel="stylesheet" type="text/css" href="[% interface | url %]/[% theme | url %]/css/[% Optylesheet | url %]" /> >+[% END %] > > <script> > var Koha = {}; >diff --git a/koha-tmpl/intranet-tmpl/prog/en/includes/js_includes.inc b/koha-tmpl/intranet-tmpl/prog/en/includes/js_includes.inc >index b832eec518..371c2d9664 100644 >--- a/koha-tmpl/intranet-tmpl/prog/en/includes/js_includes.inc >+++ b/koha-tmpl/intranet-tmpl/prog/en/includes/js_includes.inc >@@ -144,4 +144,8 @@ > }); > </script> > [% END %] >+<!-- Intranet inc JS IntranetAutocompleteElasticSearch --> >+[% IF ( Koha.Preference('IntranetAutocompleteElasticSearch') ) %] >+ [% Asset.js("js/elasticsearch/autocomplete.js") %] >+[% END %] > <!-- / js_includes.inc --> >diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/searching.pref b/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/searching.pref >index 04a77f8085..fb195edc5c 100644 >--- a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/searching.pref >+++ b/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/searching.pref >@@ -276,3 +276,20 @@ Searching: > - LIBRIS base URL > - pref: LibrisURL > - "Please only change this if you are sure it needs changing." >+ - >+ - pref: OPACAutocompleteElasticSearch >+ type: boolean >+ default: 0 >+ choices: >+ 1: Show >+ 0: "Don't show" >+ - looking terms based on a provided text by using an ElasticSearch for OPAC. >+ - >+ - pref: IntranetAutocompleteElasticSearch >+ type: boolean >+ default: 0 >+ choices: >+ 1: Show >+ 0: "Don't show" >+ - looking terms based on a provided text by using an ElasticSearch for Intranet. >+ - >diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/catalogue/advsearch.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/catalogue/advsearch.tt >index 6660d2ca8f..e91f2599c2 100644 >--- a/koha-tmpl/intranet-tmpl/prog/en/modules/catalogue/advsearch.tt >+++ b/koha-tmpl/intranet-tmpl/prog/en/modules/catalogue/advsearch.tt >@@ -106,11 +106,7 @@ > <fieldset id="searchterms"> > <legend>Search for </legend> > [% FOREACH search_box IN search_boxes_loop %] >- [% IF ( search_boxes_label ) %] >- <div class="search-term-row" style="text-indent: 4.5em;"> >- [% ELSE %] >- <div class="search-term-row"> >- [% END %] >+ [% IF ( search_boxes_label ) %]<div style="text-indent: 4.5em;">[% ELSE %]<div>[% END %] > [% IF ( expanded_options ) %] > [% IF ( search_box.boolean ) %] > <select name="op"> >@@ -337,6 +333,10 @@ > var dad = line.parentNode; > dad.appendChild(line.cloneNode(true)); > line.removeChild(ButtonPlus); >+ /* Intranet JS IntranetAutocompleteElasticSearch */ >+ [% IF ( Koha.Preference('IntranetAutocompleteElasticSearch') ) %] >+ AutocompleteInitIntranet(); >+ [% END %] > } > var Sticky; > $(document).ready(function() { >diff --git a/koha-tmpl/intranet-tmpl/prog/js/staff-global.js b/koha-tmpl/intranet-tmpl/prog/js/staff-global.js >index 5a7f7fff74..bde2b2dd82 100644 >--- a/koha-tmpl/intranet-tmpl/prog/js/staff-global.js >+++ b/koha-tmpl/intranet-tmpl/prog/js/staff-global.js >@@ -82,6 +82,12 @@ $.fn.selectTabByID = function (tabID) { > $(".keep_text").on("click",function(){ > var field_index = $(this).parent().index(); > keep_text( field_index ); >+ /* IntranetAutocompleteElasticSearch Tab */ >+ var tab = this.hash.substr(1, this.hash.length-1); >+ /* Koha.Preference('IntranetAutocompleteElasticSearch') == Show */ >+ if (typeof AutocompleteInitIntranet !== "undefined" && tab === 'catalog_search' ){ >+ AutocompleteInitIntranet(); >+ } > }); > > $(".toggle_element").on("click",function(e){ >diff --git a/koha-tmpl/opac-tmpl/bootstrap/css/opac-elasticsearch/opac-autocomplete.css b/koha-tmpl/opac-tmpl/bootstrap/css/opac-elasticsearch/opac-autocomplete.css >new file mode 100644 >index 0000000000..2b8a566568 >--- /dev/null >+++ b/koha-tmpl/opac-tmpl/bootstrap/css/opac-elasticsearch/opac-autocomplete.css >@@ -0,0 +1,29 @@ >+/* CSS file OPACAutocompleteElasticSearch */ >+.autocomplete { >+ /*the container must be positioned relative:*/ >+ position: relative; >+ display: inline-block; >+ width: 100%; >+} >+.autocomplete-items { >+ position: absolute; >+ border: 1px solid #d4d4d4; >+ border-bottom: none; >+ border-top: none; >+ z-index: 99; >+ } >+ .autocomplete-items div { >+ padding: 10px; >+ cursor: pointer; >+ background-color: #fff; >+ border-bottom: 1px solid #d4d4d4; >+ } >+ .autocomplete-items div:hover { >+ /*when hovering an item:*/ >+ background-color: #e9e9e9; >+ } >+ .autocomplete-active { >+ /*when navigating through the items using the arrow keys:*/ >+ background-color: #cedfb1 !important; >+ color: #ffffff; >+ } >\ No newline at end of file >diff --git a/koha-tmpl/opac-tmpl/bootstrap/en/includes/doc-head-close.inc b/koha-tmpl/opac-tmpl/bootstrap/en/includes/doc-head-close.inc >index e0963ca5d3..c09b8a6d25 100644 >--- a/koha-tmpl/opac-tmpl/bootstrap/en/includes/doc-head-close.inc >+++ b/koha-tmpl/opac-tmpl/bootstrap/en/includes/doc-head-close.inc >@@ -23,6 +23,11 @@ > [% SET opaclayoutstylesheet = 'opac.css' %] > [% END %] > [% END %] >+<!-- OPAC inc CSS OPACAutocompleteElasticSearch --> >+[% IF ( Koha.Preference('OPACAutocompleteElasticSearch') ) %] >+ [% SET Optylesheet = 'opac-elasticsearch/opac-autocomplete.css' %] >+ <link rel="stylesheet" type="text/css" href="[% interface | url %]/[% theme | url %]/css/[% Optylesheet | url %]" /> >+[% END %] > [% IF (opaclayoutstylesheet.match('^https?:|^\/')) %] > <link rel="stylesheet" type="text/css" href="[% opaclayoutstylesheet | url %]" /> > [% ELSE %] >diff --git a/koha-tmpl/opac-tmpl/bootstrap/en/includes/opac-bottom.inc b/koha-tmpl/opac-tmpl/bootstrap/en/includes/opac-bottom.inc >index c103f0ec4c..29cc43ac08 100644 >--- a/koha-tmpl/opac-tmpl/bootstrap/en/includes/opac-bottom.inc >+++ b/koha-tmpl/opac-tmpl/bootstrap/en/includes/opac-bottom.inc >@@ -318,6 +318,10 @@ $(document).ready(function() { > }); > </script> > [% PROCESS jsinclude %] >+<!-- OPAC *.inc JS OPACAutocompleteElasticSearch --> >+[% IF ( Koha.Preference('OPACAutocompleteElasticSearch') ) %] >+ [% Asset.js("js/opac-elasticsearch/opac-autocomplete.js") %] >+[% END %] > [% IF ( Koha.Preference('OPACUserJS') ) %] > <script> > [% Koha.Preference('OPACUserJS') | $raw %] >diff --git a/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-advsearch.tt b/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-advsearch.tt >index 3db62831e8..69ba425585 100644 >--- a/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-advsearch.tt >+++ b/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-advsearch.tt >@@ -527,6 +527,10 @@ $(document).ready(function() { > var newLine = thisLine.clone(); > newLine.find('input').val(''); > thisLine.after(newLine); >+ /* OPAC JS OPACAutocompleteElasticSearch */ >+ [% IF ( Koha.Preference('OPACAutocompleteElasticSearch') ) %] >+ AutocompleteInitOpac(); >+ [% END %] > }); > > $(document).on("click", '.ButtonLess', function(e) { >@@ -535,6 +539,10 @@ $(document).ready(function() { > $('.ButtonLess').hide(); > } > $(this).parent().parent().remove(); >+ /* OPAC JS OPACAutocompleteElasticSearch */ >+ [% IF ( Koha.Preference('OPACAutocompleteElasticSearch') ) %] >+ AutocompleteInitOpac(); >+ [% END %] > }); > > </script> >diff --git a/koha-tmpl/opac-tmpl/bootstrap/js/opac-elasticsearch/opac-autocomplete.js b/koha-tmpl/opac-tmpl/bootstrap/js/opac-elasticsearch/opac-autocomplete.js >new file mode 100644 >index 0000000000..eaa8ece900 >--- /dev/null >+++ b/koha-tmpl/opac-tmpl/bootstrap/js/opac-elasticsearch/opac-autocomplete.js >@@ -0,0 +1,265 @@ >+/* OPAC JS file OPACAutocompleteElasticSearch */ >+/* prefix for search ES */ >+var es_prefix = { >+ 'au': 'author', >+ 'pb': 'publisher', >+ 'se': 'title-series', >+ 'su': 'subject', >+ 'ti': 'title-cover', >+ /* for all */ >+ '': ['title-cover', 'author', 'subject', 'title-series', 'publisher'], >+ 'kw': ['title-cover', 'author', 'subject', 'title-series', 'publisher'] >+}; >+ >+var url_request = '/cgi-bin/koha/svc/elasticsearch/opac-elasticsearch.pl?q='; >+/* query for elasticsearch encode*/ >+var query_url_encode = { >+ '\'':'%5C%27', /* \\' for decode */ >+ '+': '' >+}; >+/* query for elasticsearch decode*/ >+var query_url_decode = { >+ '\'':"\\'", >+ '+': '' >+}; >+/* count of lines for autocomplete */ >+var nb_autocomplete = 10; >+/* key API */ >+var key = 'autocomplete'; >+ >+function AutocompleteInitOpac(){ >+ /* vars for class position absolute autocomplete */ >+ var left = "0px"; >+ var right = "0px"; >+ var top = ""; >+ /* get all input name q for search */ >+ var input_q = document.getElementsByName("q"); >+ for (var nb = 0; nb < input_q.length; nb++){ >+ /* addEventListener for every 'input' */ >+ autocomplete(input_q[nb], nb, left, right, top); >+ }; >+}; >+ >+function autocomplete(inp, nb, left, right) { >+ var select_idx = document.getElementsByName("idx"); >+ /* autocomplete off for input */ >+ inp.setAttribute("autocomplete", "off"); >+ /* get parent of input */ >+ var parent_inp = $(inp).parent(); >+ /* get element after input */ >+ var next_elem_inp = inp.nextElementSibling; >+ /* create new div with position relative for class .autocomplete with absolute */ >+ var div_relative = document.createElement('div'); >+ $(div_relative).addClass( "autocomplete" ); >+ div_relative.append(inp); >+ /* input doesn't have an elem after, add it to parent */ >+ if (next_elem_inp === null){ >+ parent_inp.append( div_relative ); >+ } else { // input has an elem after, add elem after it >+ next_elem_inp.before(div_relative); >+ }; >+ var currentFocus; >+ /*execute a function when someone writes in the text field:*/ >+ var token_counter = 0; >+ inp.addEventListener("input", function(e) { >+ var a, val = this.value; >+ /* var for async compare */ >+ var tmp_input = this.value.replace(/[+']/g, function(matched){ >+ return query_url_decode[matched]; >+ }); >+ token_counter++; >+ currentFocus = -1; >+ if (document.getElementsByClassName("autocomplete-items").length !== 0){ >+ a = document.getElementsByClassName("autocomplete-items")[0]; >+ } else { >+ /*create a DIV element that will contain the items (values):*/ >+ a = document.createElement("DIV"); >+ a.setAttribute("id", this.id + "autocomplete-list"); >+ a.setAttribute("class", "autocomplete-items"); >+ /*append the DIV element as a child of the autocomplete container:*/ >+ this.parentNode.appendChild(a); >+ /*append position absolute left/right:*/ >+ $(".autocomplete-items").css("left",left); >+ $(".autocomplete-items").css("right",right); >+ }; >+ /* get es_prefix key for builder */ >+ var chose_prefix = (select_idx == null || select_idx.length == 0) ? '' : GetValueIdx(select_idx, nb); >+ chose_prefix = chose_prefix.replace(/([^,])[,-]([^,].*)?$/, '$1'); >+ if (chose_prefix !== null){ >+ var prefix = es_prefix[chose_prefix].toString(); >+ val = val.replace(/[+']/g, function(matched){ >+ return query_url_encode[matched]; >+ }); >+ if (tmp_input == '' || tmp_input == null){ >+ closeAllLists(); >+ token_counter = 0; >+ } else { >+ $.ajax({ >+ type: 'GET', >+ url: url_request + val + '&key=' + key + '&prefix=' + prefix + '&token_counter=' + token_counter, >+ success: function (data) { >+ //console.log(data); >+ if (data.length !== 0){ >+ var myset; //Set for Autocomplete unique >+ /* autocomplete for all prefix */ >+ if (chose_prefix === 'kw' || chose_prefix === ''){ >+ myset = GetSetAutocompleteAllIdx(data, prefix, key); >+ } else { // autocomplete for one prefix >+ myset = GetSetAutocompleteOneIdx(data, prefix, key); >+ }; >+ /* append set to autocomplete */ >+ if ( tmp_input + prefix == data['val'] + data['prefix'] && token_counter === parseInt(data['token_counter'], 10)){ >+ a.innerHTML = ""; >+ for (let item of myset){ >+ a.appendChild(CreateDivItemAutocomplete(item, val)); >+ }; >+ }; >+ } else { >+ closeAllLists(this); >+ }; >+ }, >+ error: function (data) { >+ console.log(data); >+ }, >+ }); >+ } >+ >+ }; >+ }); >+ /* get value for tag with name idx */ >+ function GetValueIdx(elem, nb){ >+ switch (elem[0].tagName){ >+ case 'INPUT': >+ return elem[0].value; >+ case 'SELECT': >+ return select_idx[nb].options[select_idx[nb].selectedIndex].value; >+ default: >+ return null; >+ }; >+ }; >+ /* get autocomplete for only one prefix title/author/etc... */ >+ function GetSetAutocompleteOneIdx(data, prefix, key){ >+ let myset = new Set(); >+ let tmp_data = data['hits']['hits']; >+ for (let i = 0; i < tmp_data.length; i++) { >+ for (let j = 0; j < tmp_data[i]['highlight'][prefix + '.' + key].length; j++){ >+ /* div with data for autocomplete */ >+ let tmp = tmp_data[i]['highlight'][prefix + '.' + key][j]; >+ tmp = tmp.replace(/^\[/g, ''); >+ tmp = tmp.replace(/\]+$/g, ''); >+ myset.add(tmp.replace(/^[ &\/\\#,+)$~%.'":*?>{}!;]+|[ &\/\\#,+($~%.'":*?<{}!;]+$/g, '')); >+ if (myset.size >= nb_autocomplete) break; >+ }; >+ if (myset.size >= nb_autocomplete) break; >+ }; >+ return myset; >+ }; >+ /* get autocomplete for all prefix */ >+ function GetSetAutocompleteAllIdx(data, prefix, key){ >+ let myset = new Set(); >+ var pref = prefix.split(","); >+ for (k = 0; k < Object.keys(data).length; k++){ //Object.keys(data).length >+ if (data[k] != '' && data[k] != null){ >+ let tmp_data = data[k]['hits']['hits']; >+ for (i = 0; i < tmp_data.length; i++) { >+ for (j = 0; j < tmp_data[i]['highlight'][pref[k] + '.' + key].length; j++){ >+ /* div with data for autocomplete */ >+ let tmp = tmp_data[i]['highlight'][pref[k] + '.' + key][j] >+ myset.add(tmp.replace(/[ &#,+()$~%.'":*?<{}!/;]+$/g, '')); >+ if (myset.size >= nb_autocomplete) break; >+ }; >+ if (myset.size >= nb_autocomplete) break; >+ }; >+ if (myset.size >= nb_autocomplete) break; >+ } >+ } >+ return myset; >+ }; >+ >+ /*execute a function presses a key on the keyboard:*/ >+ inp.addEventListener("keydown", function(e) { >+ var x = document.getElementById(this.id + "autocomplete-list"); >+ if (x) x = x.getElementsByTagName("div"); >+ if (e.keyCode == 40) { //DOWN >+ /*If the arrow DOWN key is pressed, >+ increase the currentFocus variable:*/ >+ currentFocus++; >+ /*and and make the current item more visible:*/ >+ addActive(x); >+ } else if (e.keyCode == 38) { //up >+ /*If the arrow UP key is pressed, >+ decrease the currentFocus variable:*/ >+ currentFocus--; >+ /*and and make the current item more visible:*/ >+ addActive(x); >+ e.preventDefault(); >+ } else if (e.keyCode == 13) { >+ /*If the ENTER key is pressed, prevent the form from being submitted,*/ >+ //e.preventDefault(); >+ if (currentFocus > -1) { >+ /*and simulate a click on the "active" item:*/ >+ if (x) x[currentFocus].click(); >+ } >+ } >+ /* press Esc clear all autocomplete */ >+ else if (e.keyCode == 27) { >+ closeAllLists(); >+ } >+ /* press Esc clear all autocomplete */ >+ else if (e.keyCode == 8) { >+ closeAllLists(); >+ }; >+ }); >+ function addActive(x) { >+ /*a function to classify an item as "active":*/ >+ if (!x) return false; >+ /*start by removing the "active" class on all items:*/ >+ removeActive(x); >+ if (currentFocus >= x.length) currentFocus = 0; >+ if (currentFocus < 0) currentFocus = (x.length - 1); >+ /*add class "autocomplete-active":*/ >+ x[currentFocus].classList.add("autocomplete-active"); >+ inp.value = (x[currentFocus].textContent.replace(/<\/?[^>]+(>|$)/g, "")).trim(); >+ }; >+ function removeActive(x) { >+ /*a function to remove the "active" class from all autocomplete items:*/ >+ for (var i = 0; i < x.length; i++) { >+ x[i].classList.remove("autocomplete-active"); >+ }; >+ }; >+ >+ function closeAllLists(elem) { >+ /*close all autocomplete lists in the document with class autocomplete-items */ >+ var x = document.getElementsByClassName("autocomplete-items"); >+ for (var i = 0; i < x.length; i++) { >+ x[i].parentNode.removeChild(x[i]) >+ }; >+ }; >+ >+ /* div for one item autocomplete */ >+ function CreateDivItemAutocomplete (elem){ >+ var b = document.createElement("DIV"); >+ // add element "; >+ b.innerHTML += elem; >+ /*insert a input field that will hold the current array item's value:*/ >+ b.innerHTML += "<input type='hidden' value=''>"; >+ /*execute a function when someone clicks on the item value (DIV element):*/ >+ b.addEventListener("click", function(e) { >+ /* insert the value for the autocomplete text field: */ >+ inp.value = this.getElementsByTagName("input")[0].value; >+ /* normalyzer hightlight without tags */ >+ //inp.value = (inp.value.replace(/<\/?[^>]+(>|$)/g, "")).trim(); >+ inp.value = this.innerText; >+ /* Submit form click mouse in div */ >+ this.closest("form").submit(); >+ }); >+ return b; >+ }; >+ >+ /*execute a function when someone clicks in the document:*/ >+ document.addEventListener("click", function (e) { >+ closeAllLists(e.target); >+ }); >+}; >+ >+AutocompleteInitOpac(); >diff --git a/opac/svc/elasticsearch/opac-elasticsearch.pl b/opac/svc/elasticsearch/opac-elasticsearch.pl >new file mode 100755 >index 0000000000..e9dfa0a81f >--- /dev/null >+++ b/opac/svc/elasticsearch/opac-elasticsearch.pl >@@ -0,0 +1,70 @@ >+#!/usr/bin/perl >+ >+use strict; >+use warnings; >+use CGI qw ( -utf8 ); >+use JSON; >+use utf8; >+use Unicode::Normalize; >+use CGI::Session; >+use Koha::SearchEngine::Elasticsearch::Browse; >+ >+my $browser = Koha::SearchEngine::Elasticsearch::Browse->new( { index => 'biblios' } ); >+my $cgi = CGI->new; >+my $session = CGI::Session->new(); >+ >+$session->param(-name=>'analyzer', -value=>"autocomplete"); >+$session->param(-name=>'prefix', -value=>$cgi->param("prefix")); >+$session->param(-name=>'q', -value=>$cgi->param("q")); >+$session->param(-name=>'key', -value=>$cgi->param("key")); >+$session->param(-name=>'token_counter', -value=>$cgi->param("token_counter")); >+$session->expire('+1h'); >+ >+my $iskey = 0; >+ >+if ($session->param("key") == "autocomplete") { >+ $iskey = 1; >+ my @prefix = split /,/, $session->param("prefix"); >+ #fields for autocomplete >+ my $length = scalar @prefix; >+ my $ses = NFKD( $session->param("q") ); >+ $ses =~ s/\p{NonspacingMark}//g; >+ >+ #search by many prefix fields >+ if ($length > 1){ >+ my $res = $browser->autocomplete_idx($ses, \@prefix, $session->param("analyzer"), $session->param("token_counter")); >+ print $cgi->header("application/json"); >+ print to_json($res); >+ } >+ #search by one prefix field >+ elsif ($length == 1) { >+ my $res = $browser->autocomplete_one_idx($ses, $prefix[0], $session->param("analyzer"), $session->param("token_counter")); >+ print $cgi->header("application/json"); >+ print to_json($res); >+ } >+ #no prefix 404 >+ else { >+ response404JSON(); >+ } >+} >+ >+if ($iskey == 0) { >+ response404JSON(); >+} >+ >+sub response404JSON { >+ my $json = JSON->new->utf8; >+ my $header_type = "application/json"; >+ my $header_status = "404"; >+ my $output = $json->encode({ >+ "error" => "No data", >+ "description" => "Bad request", >+ }); >+ print $cgi->header( >+ -type => $header_type, >+ -charset => "utf-8", >+ -status => $header_status >+ ); >+ print $output; >+ print "\n"; >+} >-- >2.17.1
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 27113
:
114168
|
114169
|
114945
|
115117
|
116293
|
119979
|
120028
|
120029
|
120084
|
120088
|
120109
|
120140
|
120141
|
120806
|
121497
|
121498
|
121555
|
121556
|
121557
|
121558
|
131207
|
131208
|
131209
|
131210
|
131211
|
132360
|
132361
|
132362
|
132363
|
132364
|
132808
|
132945
|
132946
|
133042
|
133045
|
133407
|
133409
|
133410
|
136312
|
136456
|
136457
|
136464
|
136465
|
136653
|
136657
|
136658
|
136659
|
136660
|
137436
|
137437
|
137438
|
137439
|
137780
|
137781
|
137782
|
137783
|
139624
|
141466
|
145167
|
146015
|
159769
|
159770
|
159771
|
159772
|
159773
|
159774
|
159775
|
160134
|
160135
|
160136
|
160137
|
160138
|
160139
|
160140
|
160141
|
160142
|
161076
|
161077
|
163826
|
165895
|
165896
|
165897
|
165898
|
165899
|
165900
|
165901
|
165902
|
165903
|
165904
|
165913
|
165922
|
177537
|
177538
|
177539
|
177540
|
177541
|
177542
|
177543
|
177544
|
177545
|
177546
|
177547
|
177548
|
177657
|
178822
|
178823
|
178824
|
178825
|
178826
|
178827
|
178828
|
178829
|
178830
|
178831
|
178832
|
178833