diff --git a/asl_rulebook2/webapp/search.py b/asl_rulebook2/webapp/search.py
index 90a37bd..670642e 100644
--- a/asl_rulebook2/webapp/search.py
+++ b/asl_rulebook2/webapp/search.py
@@ -120,7 +120,7 @@ def _do_search( args ):
row[col_no] = remove_bad_hilites( row[col_no] )
if row[1] == "index":
result = _unload_index_sr( row )
- elif row[1] == "q+a":
+ elif row[1] == "qa":
result = _unload_qa_sr( row )
elif row[1] == "errata":
result = _unload_anno_sr( row, "errata" )
@@ -186,7 +186,7 @@ def _unload_index_sr( row ):
def _unload_qa_sr( row ):
"""Unload a Q+A search result from the database."""
- qa_entry = _fts_index["q+a"][ row[0] ] # nb: our copy of the Q+A entry (must remain unchanged)
+ qa_entry = _fts_index["qa"][ row[0] ] # nb: our copy of the Q+A entry (must remain unchanged)
result = copy.deepcopy( qa_entry ) # nb: the Q+A entry we will return to the caller (will be changed)
# replace the content in the Q+A entry we will return to the caller with the values
# from the search index (which will have search term highlighting)
@@ -412,7 +412,7 @@ def init_search( content_sets, qa, errata, user_anno, asop, asop_content, startu
# initialize
global _fts_index
- _fts_index = { "index": {}, "q+a": {}, "errata": {}, "user-anno": {}, "asop-entry": {} }
+ _fts_index = { "index": {}, "qa": {}, "errata": {}, "user-anno": {}, "asop-entry": {} }
# initialize the database
global _sqlite_path
@@ -488,7 +488,7 @@ def _init_qa( curs, qa, logger ):
"""Add the Q+A to the search index."""
logger.info( "- Adding the Q+A." )
nrows = 0
- sr_type = "q+a"
+ sr_type = "qa"
for qa_entries in qa.values():
for qa_entry in qa_entries:
buf = []
@@ -615,7 +615,7 @@ def load_search_config( startup_msgs, logger ):
# load the search replacements
def load_search_replacements( fname, ftype ):
- if not os.path.isfile( fname ):
+ if fname is None or not os.path.isfile( fname ):
return
logger.info( "Loading %s search replacements: %s", ftype, fname )
try:
@@ -637,7 +637,7 @@ def load_search_config( startup_msgs, logger ):
# load the search aliases
def load_search_aliases( fname, ftype ):
- if not os.path.isfile( fname ):
+ if fname is None or not os.path.isfile( fname ):
return
logger.info( "Loading %s search aliases: %s", ftype, fname )
try:
@@ -660,7 +660,7 @@ def load_search_config( startup_msgs, logger ):
# load the search synonyms
def load_search_synonyms( fname, ftype ):
- if not os.path.isfile( fname ):
+ if fname is None or not os.path.isfile( fname ):
return
logger.info( "Loading %s search synonyms: %s", ftype, fname )
try:
diff --git a/asl_rulebook2/webapp/startup.py b/asl_rulebook2/webapp/startup.py
index 664aec9..c8252f2 100644
--- a/asl_rulebook2/webapp/startup.py
+++ b/asl_rulebook2/webapp/startup.py
@@ -15,6 +15,8 @@ from asl_rulebook2.webapp.utils import parse_int
_logger = logging.getLogger( "startup" )
_startup_msgs = None
+_capabilities = None
+
# ---------------------------------------------------------------------
def init_webapp():
@@ -25,15 +27,26 @@ def init_webapp():
"""
# initialize
- global _startup_msgs
+ global _startup_msgs, _capabilities
_startup_msgs = StartupMsgs()
+ _capabilities = {}
# initialize the webapp
content_sets = load_content_sets( _startup_msgs, _logger )
+ if content_sets:
+ _capabilities[ "content-sets" ] = True
qa = init_qa( _startup_msgs, _logger )
+ if qa:
+ _capabilities[ "qa" ] = True
errata = init_errata( _startup_msgs, _logger )
+ if errata:
+ _capabilities[ "errata" ] = True
user_anno = init_annotations( _startup_msgs, _logger )
+ if user_anno:
+ _capabilities[ "user-anno" ] = True
asop, asop_content = init_asop( _startup_msgs, _logger )
+ if asop:
+ _capabilities[ "asop" ] = True
init_search(
content_sets, qa, errata, user_anno, asop, asop_content,
_startup_msgs, _logger
@@ -44,7 +57,9 @@ def init_webapp():
@app.route( "/app-config" )
def get_app_config():
"""Return the app config."""
- result = {}
+ result = {
+ "capabilities": _capabilities,
+ }
for key in [ "INITIAL_QUERY_STRING", "DISABLE_AUTO_SHOW_RULE_INFO" ]:
val = app.config.get( key )
if val is not None:
diff --git a/asl_rulebook2/webapp/static/MainApp.js b/asl_rulebook2/webapp/static/MainApp.js
index 29218d0..2ba35af 100644
--- a/asl_rulebook2/webapp/static/MainApp.js
+++ b/asl_rulebook2/webapp/static/MainApp.js
@@ -77,6 +77,7 @@ gMainApp.component( "main-app", {
// get the app config
return getJSON( gGetAppConfigUrl ).then( (resp) => { //eslint-disable-line no-undef
gAppConfig = resp ;
+ gEventBus.emit( "app-config-loaded" ) ;
} ).catch( (errorMsg) => {
showErrorMsg( "Couldn't get the app config.", errorMsg ) ;
} ) ;
diff --git a/asl_rulebook2/webapp/static/SearchPane.js b/asl_rulebook2/webapp/static/SearchPane.js
index 3a17060..5ea2420 100644
--- a/asl_rulebook2/webapp/static/SearchPane.js
+++ b/asl_rulebook2/webapp/static/SearchPane.js
@@ -1,5 +1,6 @@
import { gMainApp, gAppConfig, gEventBus } from "./MainApp.js" ;
import { postURL, findTargets, getPrimaryTarget, fixupSearchHilites, hideFootnotes } from "./utils.js" ;
+import { gUserSettings, saveUserSettings } from "./UserSettings.js" ;
// --------------------------------------------------------------------
@@ -15,25 +16,76 @@ gMainApp.component( "search-box", {
data: function() { return {
queryString: "",
+ srCount: null, srCountInfo: null, showSrCount: false,
} ; },
template: `
`,
created() {
- // check if we should start off with a query (for debugging porpoises)
+
+ gEventBus.on( "app-config-loaded", () => {
+ // initialize the search result filter checkboxes
+ let nVisible = 0 ;
+ $( this.$el ).find( ".sr-filters input[type='checkbox']" ).each( function() {
+ // check if the next checkbox will have any effect (e.g. if no Q+A have been configured,
+ // then there's no point in showing the checkbox to filter Q+A search results)
+ let name = $(this).attr( "name" ) ;
+ let match = name.match( /^show-(.+)-sr$/ ) ;
+ let key = match[1] ;
+ let caps_key = { "index": "content-sets", "asop-entry": "asop" }[ key ] || key ;
+ if ( gAppConfig.capabilities[ caps_key ] ) {
+ // yup - load the last-saved state
+ key = "HIDE_" + key.toUpperCase().replace("-","_") + "_SR" ;
+ $(this).prop( "checked", ! gUserSettings[key] ) ;
+ nVisible += 1 ;
+ } else {
+ // nope - just hide it
+ $(this).hide() ;
+ $(this).siblings( "label[for='" + name + "']" ).hide() ;
+ }
+ } ) ;
+ if ( nVisible <= 1 ) {
+ // there's only 1 checkbox - turn it on and leave everything hidden
+ $( this.$el ).find( "input[type='checkbox']" ).prop( "checked", true ) ;
+ } else {
+ // there are multiple checkboxes - show them to the user
+ $( this.$refs.srFilters ).show() ;
+ }
+ } ) ;
+
gEventBus.on( "app-loaded", () => {
+ // check if we should start off with a query (for debugging porpoises)
if ( gAppConfig.INITIAL_QUERY_STRING )
gEventBus.emit( "search-for", gAppConfig.INITIAL_QUERY_STRING ) ;
} ) ;
+
+ gEventBus.on( "search-done", (showSrCount) => {
+ // a search has been completed - update the search result count
+ this.showSrCount = showSrCount ;
+ if ( showSrCount )
+ this.updateSrCount() ;
+ } ) ;
+
gEventBus.on( "search-for", (queryString) => {
// search for the specified query string
this.queryString = queryString ;
this.$refs.submit.click() ;
} ) ;
+
},
mounted: function() {
@@ -43,10 +95,50 @@ gMainApp.component( "search-box", {
},
methods: {
+
+ onClickSrFilter( evt ) {
+ // a search result filter checkbox was clicked - update the user settings
+ let match = evt.target.getAttribute( "name" ).match( /^show-(.+)-sr$/ ) ;
+ let srType = match[1] ;
+ let state = evt.target.checked ;
+ gUserSettings[ "HIDE_" + srType.toUpperCase().replace("-","_") + "_SR" ] = ! state ;
+ saveUserSettings() ;
+ // hide/show the corresponding search results
+ let $elem = $( "#search-results .sr[data-srtype='" + srType + "']" ) ;
+ $elem.css( "display", state ? "block" : "none" ) ;
+ this.updateSrCount() ;
+ },
+
+ updateSrCount() {
+ // show the number of hidden/visible search results
+ if ( ! this.showSrCount )
+ return ;
+ let nVisible=0, nTotal=0 ;
+ $( "#search-results .sr" ).each( function() {
+ nTotal += 1 ;
+ if ( $(this).css( "display" ) != "none" )
+ nVisible += 1 ;
+ } ) ;
+ if ( nVisible == 0 && nTotal == 0 ) {
+ this.srCount = null ;
+ this.srCountInfo = null ;
+ } else {
+ this.srCount = nVisible + "/" + nTotal ;
+ if ( nVisible == 0 && nTotal == 1 )
+ this.srCountInfo = "Not showing the 1 search result" ;
+ else if ( nVisible == 1 && nTotal == 1 )
+ this.srCountInfo = "Showing the 1 search result" ;
+ else
+ this.srCountInfo = "Showing " + nVisible + " of " + nTotal + " search results" ;
+ }
+ gEventBus.emit( "sr-filtered", nVisible, nTotal ) ;
+ },
+
onKeyUp( evt ) {
if ( evt.keyCode == 13 )
this.$refs.submit.click() ;
- }
+ },
+
},
} ) ;
@@ -58,23 +150,51 @@ gMainApp.component( "search-results", {
data() { return {
searchResults: null,
errorMsg: null,
+ noResultsMsg: null,
} ; },
template: `
Search error:
{{errorMsg}}
-
Nothing was found.
-
-
-
-
-
-
-
???:{{sr.sr_type}}
+
+
{{noResultsMsg}}
+
+
+
+
+
+
+
???:{{sr.sr_type}}
+
`,
mounted() {
+
+ // handle requests to do a search
gEventBus.on( "search", this.onSearch ) ;
+
+ // update after search result filtering has been changed
+ gEventBus.on( "sr-filtered", (nVisible, nTotal) => {
+ if ( nTotal == 0 )
+ this.noResultsMsg = "Nothing was found." ;
+ else if ( nVisible == 0 )
+ this.noResultsMsg = "All search results have been filtered." ;
+ else
+ this.noResultsMsg = null ;
+ } ) ;
+
},
methods: {
@@ -83,9 +203,10 @@ gMainApp.component( "search-results", {
// initialize
this.errorMsg = null ;
+ this.noResultsMsg = null ;
hideFootnotes() ;
- function onSearchDone() {
- Vue.nextTick( () => { gEventBus.emit( "search-done" ) ; } ) ;
+ function onSearchDone( showSrCount ) {
+ Vue.nextTick( () => { gEventBus.emit( "search-done", showSrCount ) ; } ) ;
}
// check if the query string is just a ruleid
@@ -94,14 +215,14 @@ gMainApp.component( "search-results", {
// yup - just show it directly (first one, if multiple)
this.searchResults = null ;
gEventBus.emit( "show-target", targets[0].cdoc_id, targets[0].ruleid ) ;
- onSearchDone() ;
+ onSearchDone( false ) ;
return ;
}
// submit the search request
const onError = (errorMsg) => {
this.errorMsg = errorMsg ;
- onSearchDone() ;
+ onSearchDone( true ) ;
} ;
postURL( gSearchUrl, //eslint-disable-line no-undef
{ queryString: queryString }
@@ -123,7 +244,7 @@ gMainApp.component( "search-results", {
gEventBus.emit( "show-target", target.cdoc_id, target.ruleid ) ;
}
// flag that the search was completed
- onSearchDone() ;
+ onSearchDone( true ) ;
} ).catch( (errorMsg) => {
onError( errorMsg ) ;
} ) ;
@@ -136,7 +257,7 @@ gMainApp.component( "search-results", {
if ( sr[key] !== undefined )
sr[key] = fixupSearchHilites( sr[key] ) ;
} ) ;
- } else if ( sr.sr_type == "q+a" ) {
+ } else if ( sr.sr_type == "qa" ) {
if ( ! sr.content )
return ;
sr.content.forEach( (content) => {
@@ -156,6 +277,15 @@ gMainApp.component( "search-results", {
console.log( "INTERNAL ERROR: Unknown search result type:", sr.sr_type ) ;
}
},
+
+ showSr( srType ) {
+ // figure out if a search result should start off hidden or visible
+ let name = "show-" + srType + "-sr" ;
+ let $elem = $( "#search-box input[type='checkbox'][name='" + name + "']" ) ;
+ let state = $elem.prop( "checked" ) ;
+ return state ;
+ },
+
},
} ) ;
diff --git a/asl_rulebook2/webapp/static/SearchResult.js b/asl_rulebook2/webapp/static/SearchResult.js
index 50a9a3d..731d548 100644
--- a/asl_rulebook2/webapp/static/SearchResult.js
+++ b/asl_rulebook2/webapp/static/SearchResult.js
@@ -13,7 +13,7 @@ gMainApp.component( "index-sr", {
} ; },
template: `
-
+
@@ -147,7 +147,7 @@ gMainApp.component( "asop-entry-sr", {
props: [ "sr" ],
template: `
-
+
`,
diff --git a/asl_rulebook2/webapp/static/UserSettings.js b/asl_rulebook2/webapp/static/UserSettings.js
new file mode 100644
index 0000000..25b0dfd
--- /dev/null
+++ b/asl_rulebook2/webapp/static/UserSettings.js
@@ -0,0 +1,12 @@
+export let gUserSettings = Cookies.getJSON( "user-settings" ) || {} ; //eslint-disable-line no-undef
+
+// --------------------------------------------------------------------
+
+export function saveUserSettings()
+{
+ // save the user settings
+ Cookies.set( //eslint-disable-line no-undef
+ "user-settings", gUserSettings,
+ { SameSite: "strict", expires: 999 }
+ ) ;
+}
diff --git a/asl_rulebook2/webapp/static/css/SearchPane.css b/asl_rulebook2/webapp/static/css/SearchPane.css
index ab22f6f..f1ff90a 100644
--- a/asl_rulebook2/webapp/static/css/SearchPane.css
+++ b/asl_rulebook2/webapp/static/css/SearchPane.css
@@ -1,9 +1,17 @@
+#nav .tabbed-page[data-tabid='search'] .wrapper {
+ display: flex ; flex-direction: column ;
+ overflow-y: none ;
+}
+
/* search box */
-#search-box { display: flex ; padding: 1px ; margin-bottom: 8px ; }
+#search-box { display: flex ; flex-direction: column ; padding: 1px ; margin-bottom: 8px ; }
+#search-box .row { display: flex ; }
#search-box input#query-string { margin-right: 5px ; flex-grow: 1 ; }
+#search-box .sr-filters { margin-top: 4px ; font-size: 80% ; }
+#search-box .sr-filters input[type="checkbox"] { margin: 0 2px 0 6px ; }
+#search-box .sr-count { flex-grow: 1 ; text-align: right ; font-style: italic ; color: #888 ; }
/* search results */
-#nav .tabbed-page[data-tabid='search'] .wrapper { overflow-y: none ; }
-#search-results { height: calc(100% - 35px) ; overflow-y: auto ; }
+#search-results { height: 100% ; overflow-y: auto ; }
#search-results .no-results { font-style: italic ; color: #666 ; }
#search-results .error .pre { font-family: monospace ; margin: 0.25em 0 0 0.5em ; }
diff --git a/asl_rulebook2/webapp/static/js-cookie/js.cookie.js b/asl_rulebook2/webapp/static/js-cookie/js.cookie.js
new file mode 100644
index 0000000..6d0965a
--- /dev/null
+++ b/asl_rulebook2/webapp/static/js-cookie/js.cookie.js
@@ -0,0 +1,163 @@
+/*!
+ * JavaScript Cookie v2.2.0
+ * https://github.com/js-cookie/js-cookie
+ *
+ * Copyright 2006, 2015 Klaus Hartl & Fagner Brack
+ * Released under the MIT license
+ */
+;(function (factory) {
+ var registeredInModuleLoader;
+ if (typeof define === 'function' && define.amd) {
+ define(factory);
+ registeredInModuleLoader = true;
+ }
+ if (typeof exports === 'object') {
+ module.exports = factory();
+ registeredInModuleLoader = true;
+ }
+ if (!registeredInModuleLoader) {
+ var OldCookies = window.Cookies;
+ var api = window.Cookies = factory();
+ api.noConflict = function () {
+ window.Cookies = OldCookies;
+ return api;
+ };
+ }
+}(function () {
+ function extend () {
+ var i = 0;
+ var result = {};
+ for (; i < arguments.length; i++) {
+ var attributes = arguments[ i ];
+ for (var key in attributes) {
+ result[key] = attributes[key];
+ }
+ }
+ return result;
+ }
+
+ function decode (s) {
+ return s.replace(/(%[0-9A-Z]{2})+/g, decodeURIComponent);
+ }
+
+ function init (converter) {
+ function api() {}
+
+ function set (key, value, attributes) {
+ if (typeof document === 'undefined') {
+ return;
+ }
+
+ attributes = extend({
+ path: '/'
+ }, api.defaults, attributes);
+
+ if (typeof attributes.expires === 'number') {
+ attributes.expires = new Date(new Date() * 1 + attributes.expires * 864e+5);
+ }
+
+ // We're using "expires" because "max-age" is not supported by IE
+ attributes.expires = attributes.expires ? attributes.expires.toUTCString() : '';
+
+ try {
+ var result = JSON.stringify(value);
+ if (/^[\{\[]/.test(result)) {
+ value = result;
+ }
+ } catch (e) {}
+
+ value = converter.write ?
+ converter.write(value, key) :
+ encodeURIComponent(String(value))
+ .replace(/%(23|24|26|2B|3A|3C|3E|3D|2F|3F|40|5B|5D|5E|60|7B|7D|7C)/g, decodeURIComponent);
+
+ key = encodeURIComponent(String(key))
+ .replace(/%(23|24|26|2B|5E|60|7C)/g, decodeURIComponent)
+ .replace(/[\(\)]/g, escape);
+
+ var stringifiedAttributes = '';
+ for (var attributeName in attributes) {
+ if (!attributes[attributeName]) {
+ continue;
+ }
+ stringifiedAttributes += '; ' + attributeName;
+ if (attributes[attributeName] === true) {
+ continue;
+ }
+
+ // Considers RFC 6265 section 5.2:
+ // ...
+ // 3. If the remaining unparsed-attributes contains a %x3B (";")
+ // character:
+ // Consume the characters of the unparsed-attributes up to,
+ // not including, the first %x3B (";") character.
+ // ...
+ stringifiedAttributes += '=' + attributes[attributeName].split(';')[0];
+ }
+
+ return (document.cookie = key + '=' + value + stringifiedAttributes);
+ }
+
+ function get (key, json) {
+ if (typeof document === 'undefined') {
+ return;
+ }
+
+ var jar = {};
+ // To prevent the for loop in the first place assign an empty array
+ // in case there are no cookies at all.
+ var cookies = document.cookie ? document.cookie.split('; ') : [];
+ var i = 0;
+
+ for (; i < cookies.length; i++) {
+ var parts = cookies[i].split('=');
+ var cookie = parts.slice(1).join('=');
+
+ if (!json && cookie.charAt(0) === '"') {
+ cookie = cookie.slice(1, -1);
+ }
+
+ try {
+ var name = decode(parts[0]);
+ cookie = (converter.read || converter)(cookie, name) ||
+ decode(cookie);
+
+ if (json) {
+ try {
+ cookie = JSON.parse(cookie);
+ } catch (e) {}
+ }
+
+ jar[name] = cookie;
+
+ if (key === name) {
+ break;
+ }
+ } catch (e) {}
+ }
+
+ return key ? jar[key] : jar;
+ }
+
+ api.set = set;
+ api.get = function (key) {
+ return get(key, false /* read as raw */);
+ };
+ api.getJSON = function (key) {
+ return get(key, true /* read as json */);
+ };
+ api.remove = function (key, attributes) {
+ set(key, '', extend(attributes, {
+ expires: -1
+ }));
+ };
+
+ api.defaults = {};
+
+ api.withConverter = init;
+
+ return api;
+ }
+
+ return init(function () {});
+}));
diff --git a/asl_rulebook2/webapp/templates/index.html b/asl_rulebook2/webapp/templates/index.html
index 9647cb1..856237a 100644
--- a/asl_rulebook2/webapp/templates/index.html
+++ b/asl_rulebook2/webapp/templates/index.html
@@ -48,6 +48,7 @@
+
@@ -75,6 +76,7 @@ gGetFootnotesUrl = "{{ url_for( 'get_footnotes' ) }}" ;
+