diff --git a/application/config/search.php b/application/config/search.php index 14b6e89d8..f52d755dd 100644 --- a/application/config/search.php +++ b/application/config/search.php @@ -671,3 +671,97 @@ $config['room'] = [ LEFT JOIN public.tbl_adresse address USING (adresse_id)" ]; + +$config['cms'] = [ + 'primarykey' => 'contentsprache_id', + 'table' => 'cms', + 'prepare' => " + cms_auth (content_id) AS ( + SELECT content_id + FROM campus.tbl_content c + WHERE NOT EXISTS (SELECT 1 FROM campus.tbl_contentgruppe g WHERE g.content_id=c.content_id) + UNION + SELECT content_id + FROM public.vw_gruppen g + JOIN campus.tbl_contentgruppe c USING (gruppe_kurzbz) + WHERE uid = (TABLE auth) + ), + cms_active (content_id, template_kurzbz) AS ( + SELECT content_id, template_kurzbz + FROM cms_auth + JOIN campus.tbl_content USING (content_id) + WHERE aktiv = TRUE + ), + cms_active_redirect (content_id) AS ( + SELECT content_id + FROM cms_active + WHERE template_kurzbz = 'redirect' + ), + cms_active_redirect_linked (content_id) AS ( + SELECT content_id + FROM cms_active_redirect + JOIN campus.tbl_contentsprache USING (content_id) + WHERE LEFT((xpath('string(/content/url)', content))[1]::text, 1) <> '#' + ), + cms_active_others (content_id) AS ( + SELECT content_id + FROM cms_active + WHERE template_kurzbz IN ('contentmittitel', 'contentohnetitel', 'contentmittitel_filterwidget') + ), + cms (contentsprache_id) AS ( + SELECT contentsprache_id + FROM campus.tbl_contentsprache + WHERE content_id IN ( + SELECT content_id + FROM cms_active_redirect_linked + UNION + SELECT content_id + FROM cms_active_others + ) + AND version = campus.get_highest_content_version(content_id) + ) + ", + 'searchfields' => [ + 'content' => [ + 'alias' => ['inhalt'], + 'comparison' => "vector", + 'field' => "(setweight(to_tsvector('simple', COALESCE(titel, '')), 'A') || setweight(to_tsvector('simple', COALESCE(content, '')::text), 'B'))", + 'join' => [ + 'table' => "campus.tbl_contentsprache", + 'using' => "contentsprache_id" + ] + ], + 'content_id' => [ + 'alias' => ['id'], + 'comparison' => "equal-int", + 'field' => "content_id", + 'join' => [ + 'table' => "campus.tbl_contentsprache", + 'using' => "contentsprache_id" + ] + ], + 'lang' => [ + 'alias' => ['language', 'sprache'], + 'comparison' => "equals", + 'field' => "sprache", + 'join' => [ + 'table' => "campus.tbl_contentsprache", + 'using' => "contentsprache_id" + ] + ] + ], + 'resultfields' => [ + "contentsprache.content_id", + "content.template_kurzbz", + "contentsprache.version", + "contentsprache.sprache AS language", + "contentsprache.titel AS title", + "contentsprache.content", + "(xpath('string(/content/url)', contentsprache.content))[1] AS content_url" + ], + 'resultjoin' => " + JOIN campus.tbl_contentsprache contentsprache + USING (contentsprache_id) + JOIN campus.tbl_content content + USING (content_id)" +]; diff --git a/application/config/searchfunctions.php b/application/config/searchfunctions.php index c8244e9a3..898069aed 100644 --- a/application/config/searchfunctions.php +++ b/application/config/searchfunctions.php @@ -25,7 +25,7 @@ $config['similar'] = [ $config['vector'] = [ 'priority' => 1, - 'rank' => "ts_rank_cd({field}, to_tsquery('simple', {word}))", + 'rank' => "ts_rank({field}, to_tsquery('simple', {word}))", 'compare' => "to_tsquery('simple', {word}) @@ {field}" ]; diff --git a/application/controllers/api/frontend/v1/Language.php b/application/controllers/api/frontend/v1/Language.php new file mode 100644 index 000000000..797af4804 --- /dev/null +++ b/application/controllers/api/frontend/v1/Language.php @@ -0,0 +1,47 @@ +. + */ + +if (! defined('BASEPATH')) exit('No direct script access allowed'); + +/** + * This controller operates between (interface) the JS (GUI) and the back-end + * Provides data to the ajax get calls about languages + * This controller works with JSON calls on the HTTP GET and the output is always JSON + */ +class Language extends FHCAPI_Controller +{ + public function __construct() + { + parent::__construct([ + 'get' => self::PERM_LOGGED + ]); + + // Load models + $this->load->model('system/Sprache_model', 'SpracheModel'); + } + + public function get() + { + $this->SpracheModel->addOrder('sprache'); + + $result = $this->SpracheModel->load(); + $data = $this->getDataOrTerminateWithError($result); + + $this->terminateWithSuccess($data); + } +} diff --git a/application/libraries/SearchBarLib.php b/application/libraries/SearchBarLib.php index c3528978d..782e30843 100644 --- a/application/libraries/SearchBarLib.php +++ b/application/libraries/SearchBarLib.php @@ -168,6 +168,9 @@ class SearchBarLib FROM public.tbl_sprache WHERE sprache=" . $this->_ci->db->escape($lang) . " LIMIT 1 + ), + auth (uid) AS ( + SELECT " . $this->_ci->db->escape(getAuthUID()) . " AS uid )"; if ($sql_with) { @@ -248,6 +251,9 @@ class SearchBarLib WHERE sprache=" . $this->_ci->db->escape($lang) . " LIMIT 1 )"); + array_unshift($with, "auth (uid) AS ( + SELECT " . $this->_ci->db->escape(getAuthUID()) . " AS uid + )"); return success(" WITH " . implode(", ", $with) . " diff --git a/public/js/api/fhcapifactory.js b/public/js/api/fhcapifactory.js index 655bfa409..26ed8847b 100644 --- a/public/js/api/fhcapifactory.js +++ b/public/js/api/fhcapifactory.js @@ -23,6 +23,7 @@ import studstatus from "./studstatus.js"; import stv from "./stv.js"; import notiz from "./notiz.js"; import betriebsmittel from "./betriebsmittel.js"; +import language from "./language.js"; export default { search, @@ -32,5 +33,6 @@ export default { studstatus, stv, notiz, - betriebsmittel + betriebsmittel, + language }; diff --git a/public/js/api/language.js b/public/js/api/language.js new file mode 100644 index 000000000..6a11d193b --- /dev/null +++ b/public/js/api/language.js @@ -0,0 +1,22 @@ +/** + * Copyright (C) 2024 fhcomplete.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +export default { + getAll() { + return this.$fhcApi.get('/api/frontend/v1/language/get'); + } +}; \ No newline at end of file diff --git a/public/js/components/searchbar/result/cms.js b/public/js/components/searchbar/result/cms.js new file mode 100644 index 000000000..af2d6b0bb --- /dev/null +++ b/public/js/components/searchbar/result/cms.js @@ -0,0 +1,81 @@ +import TemplateFrame from "./template/frame.js"; + +export default { + components: { + TemplateFrame + }, + emits: [ 'actionexecuted' ], + props: { + res: Object, + actions: Object + }, + inject: [ + 'languages', + 'query' + ], + computed: { + preview() { + if (this.res.template_kurzbz != 'redirect') { + let text = this.res.content.replace(//ig, '').replace(/<[^>]+>/ig, '').replace(/^\s+|\s+$/g, ''); + + if (text.length > 1000) { + // NOTE(chris): focus on searched text! + let lower = text.toLowerCase(); + let firstOccurence = Math.min(this.query.split(' ').reduce((a, c) => { + // NOTE(chris): filter query for words that affects the content field and get the lowest index of them + if (c == 'or') + return a; + let i = c.indexOf(':'); + if (i < 0 || (i > 0 && ['content', 'inhalt'].includes(c.split(':')[0]))) { + let posInText = lower.indexOf(c); + if (posInText >= 0) + a.push(posInText); + } + return a; + }, [])); + + if (firstOccurence) { + if (firstOccurence + 997 >= text.length) { + firstOccurence = text.length - 997; + if (firstOccurence > 0) + return '...' + text.substr(firstOccurence, 997); + } else { + return '...' + text.substr(firstOccurence, 994) + '...'; + } + } + + text = text.substr(0, 997) + '...'; + } + + return text; + } + + let url = this.res.content_url; + if (url.substr(0, 16) == '../index.ci.php/') + url = this.$fhcApi.getUri(url.substr(16)); + else if (url.substr(0, 3) == '../') + url = FHC_JS_DATA_STORAGE_OBJECT.app_root.replace(/\/+$/, '') + url.substr(2); + return '' + url + ''; + }, + flag() { + if (!this.languages || !this.languages[this.res.language]) + return ""; + return "data:image/jpeg;base64," + this.languages[this.res.language].flagge; + } + }, + template: ` + + +
+
+ No Content +
+
` +}; \ No newline at end of file diff --git a/public/js/components/searchbar/searchbar.js b/public/js/components/searchbar/searchbar.js index 54049679e..8ac64e856 100644 --- a/public/js/components/searchbar/searchbar.js +++ b/public/js/components/searchbar/searchbar.js @@ -4,6 +4,7 @@ import ResultPrestudent from "./result/prestudent.js"; import ResultEmployee from "./result/employee.js"; import ResultOrganisationunit from "./result/organisationunit.js"; import ResultRoom from "./result/room.js"; +import ResultCms from "./result/cms.js"; import ResultMergedperson from "./result/mergedperson.js"; import ResultMergedstudent from "./result/mergedstudent.js"; @@ -17,10 +18,17 @@ export default { ResultEmployee, ResultOrganisationunit, ResultRoom, + ResultCms, ResultMergedperson, ResultMergedstudent }, props: [ "searchoptions", "searchfunction" ], + provide() { + return { + languages: Vue.computed(() => this.languages), + query: Vue.computed(() => this.lastQuery) + }; + }, data() { return { searchtimer: null, @@ -35,9 +43,22 @@ export default { searching: false, error: null, abortController: null, - retry: 5 + retry: 0, + languages: null, + lastQuery: '' }; }, + created() { + this.$fhcApi.factory + .language.getAll() + .then(result => { + this.languages = result.data.reduce((a, c) => { + a[c.sprache] = c; + return a; + }, {}); + }) + .catch(this.$fhcAlert.handleSystemError); + }, beforeMount() { this.updateSearchOptions(); }, @@ -82,12 +103,14 @@ export default { this.abortController = new AbortController(); this - .searchfunction(this.searchsettings, { signal: this.abortController.signal }) + .searchfunction(this.searchsettings, { timeout: 50000, signal: this.abortController.signal }) .then(response => { if (!response.data) { this.error = 'Bei der Suche ist ein Fehler aufgetreten.'; } else { let res = response.data.map(el => ({...el, ...JSON.parse(el.data)})); + this.lastQuery = response.meta.searchstring; + if (this.searchoptions.mergeResults) { let counter = 0; let mergeTypes = []; @@ -131,11 +154,11 @@ export default { this.searchresult = res; } this.searching = false; - this.retry = 5; + this.retry = 0; }) .catch(error => { if (error.code == "ERR_CANCELED") { - return this.retry = 5; + return this.retry = 0; } if (error.code == "ECONNABORTED" && this.retry) { this.retry--; @@ -144,7 +167,7 @@ export default { this.error = 'Bei der Suche ist ein Fehler aufgetreten.' + ' ' + error.message; this.searching = false; - this.retry = 5; + this.retry = 0; }); }, refreshsearch() { @@ -239,6 +262,7 @@ export default { +
Unbekannter Ergebnistyp: '{{ res.type }}'.
diff --git a/system/dbupdate_3.4/40128_search.php b/system/dbupdate_3.4/40128_search.php index 2ef8f8b4a..92e1a10fd 100644 --- a/system/dbupdate_3.4/40128_search.php +++ b/system/dbupdate_3.4/40128_search.php @@ -169,3 +169,18 @@ FROM pg_indexes WHERE indexname = 'idx_tbl_organisationseinheit_fts_bezeichnung_ else echo 'public.tbl_organisationseinheit: added index "idx_tbl_organisationseinheit_fts_bezeichnung_vector"
'; } +// Add index for titel || ' ' || content to campus.tbl_contentsprache +if (!$db->db_num_rows(@$db->db_query("SELECT 1 +FROM pg_indexes WHERE indexname = 'idx_tbl_contentsprache_fts_titel_content_vector' LIMIT 1;"))) +{ + $qry = " + CREATE INDEX idx_tbl_contentsprache_fts_titel_content_vector + ON campus.tbl_contentsprache + USING GIN ((setweight(to_tsvector('simple', COALESCE(titel, '')), 'A') || setweight(to_tsvector('simple', COALESCE(content, '')::text), 'B'))); + "; + + if (!$db->db_query($qry)) + echo 'campus.tbl_contentsprache ' . $db->db_last_error() . '
'; + else + echo 'campus.tbl_contentsprache: added index "idx_tbl_contentsprache_fts_titel_content_vector"
'; +}