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' ) }}" ; + diff --git a/asl_rulebook2/webapp/tests/control_tests.py b/asl_rulebook2/webapp/tests/control_tests.py index f597e70..2c087b2 100644 --- a/asl_rulebook2/webapp/tests/control_tests.py +++ b/asl_rulebook2/webapp/tests/control_tests.py @@ -6,7 +6,7 @@ from google.protobuf.empty_pb2 import Empty from asl_rulebook2.webapp.tests.proto.generated.control_tests_pb2_grpc import ControlTestsStub from asl_rulebook2.webapp.tests.proto.generated.control_tests_pb2 import \ - SetDataDirRequest + SetDataDirRequest, SetAppConfigValRequest # --------------------------------------------------------------------- @@ -35,3 +35,16 @@ class ControlTests: SetDataDirRequest( fixturesDirName = fixtures_dname ) ) return self + + def set_app_config_val( self, key, val ): + """Set an app config value.""" + if isinstance( val, str ): + req = SetAppConfigValRequest( key=key, strVal=val ) + elif isinstance( val, int ): + req = SetAppConfigValRequest( key=key, intVal=val ) + elif isinstance( val, bool ): + req = SetAppConfigValRequest( key=key, boolVal=val ) + else: + raise ValueError( "Invalid value type: {}".format( type(val).__name__ ) ) + self._stub.setAppConfigVal( req ) + return self diff --git a/asl_rulebook2/webapp/tests/control_tests_servicer.py b/asl_rulebook2/webapp/tests/control_tests_servicer.py index 4c62eb3..3d0d236 100644 --- a/asl_rulebook2/webapp/tests/control_tests_servicer.py +++ b/asl_rulebook2/webapp/tests/control_tests_servicer.py @@ -62,6 +62,18 @@ class ControlTestsServicer( BaseControlTestsServicer ): self._webapp.config.pop( "DATA_DIR", None ) return Empty() + def setAppConfigVal( self, request, context ): + """Set an app config value.""" + self._log_request( request, context ) + # set the app config setting + for val_type in ( "strVal", "intVal", "boolVal" ): + if request.HasField( val_type ): + key, val = request.key, getattr(request,val_type) + _logger.debug( "- Setting app config: %s = %s (%s)", key, str(val), type(val).__name__ ) + self._webapp.config[ key ] = val + return Empty() + raise RuntimeError( "Can't find app config key." ) + @staticmethod def _log_request( req, ctx ): #pylint: disable=unused-argument """Log a request.""" diff --git a/asl_rulebook2/webapp/tests/fixtures/asop/asop.index b/asl_rulebook2/webapp/tests/fixtures/asop/asop.index new file mode 100644 index 0000000..70f9513 --- /dev/null +++ b/asl_rulebook2/webapp/tests/fixtures/asop/asop.index @@ -0,0 +1,5 @@ +[ + +{ "_comment_": "This file is here to fool the front-end in thinking there is index content, thus showing the ASOP filter checkbox." } + +] diff --git a/asl_rulebook2/webapp/tests/fixtures/full/ASL Rulebook (Red Barricades).chapters b/asl_rulebook2/webapp/tests/fixtures/full/ASL Rulebook (Red Barricades).chapters new file mode 100644 index 0000000..5c99fb4 --- /dev/null +++ b/asl_rulebook2/webapp/tests/fixtures/full/ASL Rulebook (Red Barricades).chapters @@ -0,0 +1,13 @@ +[ + +{ "title": "Red Barricades", + "chapter_id": "O", + "page_no": 1, + "sections": [ + { "caption": "1. Debris", "ruleid": "O1" }, + { "caption": "2. Railway Embankment", "ruleid": "O2" }, + { "caption": "3. Printed Rubble", "ruleid": "O3" } + ] +} + +] diff --git a/asl_rulebook2/webapp/tests/fixtures/full/ASL Rulebook (Red Barricades).targets b/asl_rulebook2/webapp/tests/fixtures/full/ASL Rulebook (Red Barricades).targets new file mode 100644 index 0000000..5583f64 --- /dev/null +++ b/asl_rulebook2/webapp/tests/fixtures/full/ASL Rulebook (Red Barricades).targets @@ -0,0 +1,6 @@ +{ + +"O6.7": { "caption": "ENCIRCLEMENT", "page_no": 5 } + + +} diff --git a/asl_rulebook2/webapp/tests/fixtures/full/ASL Rulebook.chapters b/asl_rulebook2/webapp/tests/fixtures/full/ASL Rulebook.chapters new file mode 100644 index 0000000..c791539 --- /dev/null +++ b/asl_rulebook2/webapp/tests/fixtures/full/ASL Rulebook.chapters @@ -0,0 +1,83 @@ +[ + +{ "title": "Infantry And Basic Game Rules", + "chapter_id": "A", + "page_no": 42, + "sections": [ + { "caption": "1. Personnel Counters", "ruleid": "A1" }, + { "caption": "2. The Mapboard", "ruleid": "A2" }, + { "caption": "3. Basic Sequence Of Play", "ruleid": "A3" } +] }, + +{ "title": "Terrain", + "chapter_id": "B", + "page_no": 109, + "sections": [ + { "caption": "1. Open Ground", "ruleid": "B1" }, + { "caption": "2. Shellholes", "ruleid": "B2" }, + { "caption": "3. Roads", "ruleid": "B3" } +] }, + +{ "title": "Ordnance & Offboard Artillery (OBA)", + "chapter_id": "C", + "page_no": 158, + "sections": [ + { "caption": "1. Offboard Artillery (OBA)", "ruleid": "C1" }, + { "caption": "2. Gun Classifications", "ruleid": "C2" }, + { "caption": "3. The To Hit Process", "ruleid": "C3" } +] }, + +{ "title": "Vehicles", + "chapter_id": "D", + "page_no": 187, + "sections": [ + { "caption": "1. Vehicle Counters", "ruleid": "D1" }, + { "caption": "2. Vehicular Movement", "ruleid": "D2" }, + { "caption": "3. AFV Combat", "ruleid": "D3" } +] }, + +{ "title": "Miscellaneous", + "chapter_id": "E", + "page_no": 216, + "sections": [ + { "caption": "1. Night", "ruleid": "E1" }, + { "caption": "2. Interrogation", "ruleid": "E2" }, + { "caption": "3. Weather", "ruleid": "E3" } +] }, + +{ "title": "North Africa", + "chapter_id": "F", + "page_no": 247, + "sections": [ + { "caption": "1. Open Ground", "ruleid": "F1" }, + { "caption": "2. Scrub", "ruleid": "F2" }, + { "caption": "3. Hammada", "ruleid": "F3" } +] }, + +{ "title": "Pacific Theater", + "chapter_id": "G", + "page_no": 270, + "sections": [ + { "caption": "1. The Japanese", "ruleid": "G1" }, + { "caption": "2. Jungle", "ruleid": "G2" }, + { "caption": "3. Bamboo", "ruleid": "G3" } +] }, + +{ "title": "Deluxe ASL", + "chapter_id": "J", + "page_no": 593, + "sections": [ + { "caption": "1. Miniatures", "ruleid": "J1" }, + { "caption": "2. Converting ASL Rules To Deluxe ASL", "ruleid": "J2" } +] }, + +{ "title": "Korean War", + "chapter_id": "W", + "page_no": 647, + "sections": [ + { "caption": "1. KW Terrain", "ruleid": "W1" }, + { "caption": "2. The Americans", "ruleid": "W2" }, + { "caption": "3. The South Koreans", "ruleid": "W3" } +] } + +] diff --git a/asl_rulebook2/webapp/tests/fixtures/full/ASL Rulebook.footnotes b/asl_rulebook2/webapp/tests/fixtures/full/ASL Rulebook.footnotes new file mode 100644 index 0000000..67751c6 --- /dev/null +++ b/asl_rulebook2/webapp/tests/fixtures/full/ASL Rulebook.footnotes @@ -0,0 +1,12 @@ +{ + +"A": { + +"1": { + "captions": [ { "caption": "ERRORS", "ruleid": "A.2" } ], + "content": "To the unscrupulous, these mechanics for handling errors might be viewed as a license to steal. We do not mean to insinuate that cheating is acceptable behavior; rather, that backing up a game to accommodate a forgotten rule/unit is a drag on play. In essence, the player's knowledge of the system and methodical application of its benefits as opportunities present themselves becomes an added skill factor better reflecting the abilities of an experienced battlefield commander. Ultimately, the only protection against a cheater is not to play him." +} + +} + +} diff --git a/asl_rulebook2/webapp/tests/fixtures/full/ASL Rulebook.index b/asl_rulebook2/webapp/tests/fixtures/full/ASL Rulebook.index new file mode 100644 index 0000000..12614ec --- /dev/null +++ b/asl_rulebook2/webapp/tests/fixtures/full/ASL Rulebook.index @@ -0,0 +1,80 @@ +[ + +{ "title": "Errors", + "ruleids": [ "A.2" ] +}, + +{ "title": "CCPh", + "subtitle": "Close Combat Phase", + "content": "This rule has had an errata attached to it. Click through to see it.", + "ruleids": [ "A3.8" ], + "rulerefs": [ + { "caption": "ENEMY Attacks", "ruleids": [ "S11.5" ] }, + { "caption": "dropping SW before CC", "ruleids": [ "A4.43" ] } + ] +}, + +{ "title": "CC", + "subtitle": "Close Combat", + "ruleids": [ "A11" ], + "rulerefs": [ + { "caption": "Armor Leader", "ruleids": [ "D3.44" ] }, + { "caption": "ATMM", "ruleids": [ "C13.7" ] }, + { "caption": "Berserk", "ruleids": [ "A15.43" ] }, + { "caption": "units in Boats", "ruleids": [ "E5.6" ] }, + { "caption": "Broken Units", "ruleids": [ "A11.16" ] }, + { "caption": "BU", "ruleids": [ "D5.2" ] } + ] +}, + +{ "title": "BU", + "subtitle": "Buttoned Up", + "ruleids": [ "D5.2" ], + "rulerefs": [ + { "caption": "Amphibian", "ruleids": [ "G14.31" ] }, + { "caption": "can change BU/CE status during MPh or APh", "ruleids": [ "D5.33" ] }, + { "caption": "ENEMY Vehicles", "ruleids": [ "S9.31" ] }, + { "caption": "MA Interdiction NA", "ruleids": [ "A10.532" ] }, + { "caption": "OT AFV", "ruleids": [ "D6.61" ] }, + { "caption": "Passengers on LC", "ruleids": [ "G12.123" ] }, + { "caption": "Pinning", "ruleids": [ "A7.82" ] }, + { "caption": "Secret Record", "ruleids": [ "A12.2" ] }, + { "caption": "sN", "ruleids": [ "D13.34" ] }, + { "caption": "Sighting TC", "ruleids": [ "E7.43" ] }, + { "caption": "sM", "ruleids": [ "D13.3" ] }, + { "caption": "TH Penalty", "ruleids": [ "C5.9" ] } + ] +}, + +{ "title": "Encirclement", + "ruleids": [ "A7.7" ], + "rulerefs": [ + { "caption": "Cellars NA", "ruleids": [ "O6.7", "R4.7" ] }, + { "caption": "Japanese do not lower their morale by 1", "ruleids": [ "G1.62" ] }, + { "caption": "Mission End", "ruleids": [ "S17.43" ] }, + { "caption": "EXC Pillbox", "ruleids": [ "B30.32" ] }, + { "caption": "RePh", "ruleids": [ "O11.6041", "P8.6041", "Q9.6041", "R9.6041", "T15.6041", "T15.606" ] }, + { "caption": "Rout Effects", "ruleids": [ "A20.21" ] }, + { "caption": "Upper Level", "ruleids": [ "A7.72" ] } + ] +}, + +{ "title": "White Phosphorus", + "ruleids": [ "A24.3" ], + "see_also": [ "WP" ] +}, + +{ "title": "WP", + "subtitle": "White Phosphorus", + "ruleids": [ "A24.3" ], + "rulerefs": [ + { "caption": "vs Cave", "ruleids": [ "G11.85" ] }, + { "caption": "DM", "ruleids": [ "A10.62" ] }, + { "caption": "FFE", "ruleids": [ "C3.76" ] }, + { "caption": "Germans in ABtF", "ruleids": [ "SSR18" ] }, + { "caption": "Straying", "ruleids": [ "E1.53" ] }, + { "caption": "G.M.D.", "ruleids": [ "G18.21" ] } + ] +} + +] diff --git a/asl_rulebook2/webapp/tests/fixtures/full/ASL Rulebook.targets b/asl_rulebook2/webapp/tests/fixtures/full/ASL Rulebook.targets new file mode 100644 index 0000000..76ac239 --- /dev/null +++ b/asl_rulebook2/webapp/tests/fixtures/full/ASL Rulebook.targets @@ -0,0 +1,37 @@ +{ + +"A.2": { "caption": "ERRORS" }, +"A3.8": { "caption": "CLOSE COMBAT PHASE (CCPh)" }, +"A7.7": { "caption": "ENCIRCLEMENT" }, +"A11": { "caption": "CLOSE COMBAT (CC)" }, +"A24.3": { "caption": "WHITE PHOSPHORUS (WP)" }, +"D5.2": { "caption": "BUTTONED UP (BU)" }, + +"A1": { "caption": "PERSONNEL COUNTERS" }, +"A2": { "caption": "THE MAPBOARD" }, +"A3": { "caption": "BASIC SEQUENCE OF PLAY" }, +"B1": { "caption": "OPEN GROUND" }, +"B2": { "caption": "SHELLHOLES" }, +"B3": { "caption": "ROADS" }, +"C1": { "caption": "OFFBOARD ARTILLERY (OBA)" }, +"C2": { "caption": "GUN CLASSIFICATIONS" }, +"C3": { "caption": "THE TO HIT PROCESS" }, +"D1": { "caption": "VEHICLE COUNTERS" }, +"D2": { "caption": "VEHICULAR MOVEMENT" }, +"D3": { "caption": "AFV COMBAT" }, +"E1": { "caption": "NIGHT" }, +"E2": { "caption": "INTERROGATION" }, +"E3": { "caption": "WEATHER" }, +"F1": { "caption": "OPEN GROUND" }, +"F2": { "caption": "SCRUB" }, +"F3": { "caption": "HAMMADA" }, +"G1": { "caption": "THE JAPANESE" }, +"G2": { "caption": "JUNGLE" }, +"G3": { "caption": "BAMBOO" }, +"J1": { "caption": "MINIATURES" }, +"J2": { "caption": "CONVERTING ASL RULES TO DELUXE ASL" }, +"W1": { "caption": "KW TERRAIN" }, +"W2": { "caption": "THE AMERICANS" }, +"W3": { "caption": "THE SOUTH KOREANS" } + +} diff --git a/asl_rulebook2/webapp/tests/fixtures/full/O-background.png b/asl_rulebook2/webapp/tests/fixtures/full/O-background.png new file mode 100644 index 0000000..ebb5519 Binary files /dev/null and b/asl_rulebook2/webapp/tests/fixtures/full/O-background.png differ diff --git a/asl_rulebook2/webapp/tests/fixtures/full/O-icon.png b/asl_rulebook2/webapp/tests/fixtures/full/O-icon.png new file mode 100644 index 0000000..5fc6670 Binary files /dev/null and b/asl_rulebook2/webapp/tests/fixtures/full/O-icon.png differ diff --git a/asl_rulebook2/webapp/tests/fixtures/full/annotations.json b/asl_rulebook2/webapp/tests/fixtures/full/annotations.json new file mode 100644 index 0000000..866159a --- /dev/null +++ b/asl_rulebook2/webapp/tests/fixtures/full/annotations.json @@ -0,0 +1,7 @@ +[ + +{ "ruleid": "A24.3", + "content": "Mmmm, White Phosphorous. Is there anything it can't do...?" +} + +] diff --git a/asl_rulebook2/webapp/tests/fixtures/full/asop b/asl_rulebook2/webapp/tests/fixtures/full/asop new file mode 120000 index 0000000..29e4e03 --- /dev/null +++ b/asl_rulebook2/webapp/tests/fixtures/full/asop @@ -0,0 +1 @@ +../asop/asop/ \ No newline at end of file diff --git a/asl_rulebook2/webapp/tests/fixtures/full/errata/demo.json b/asl_rulebook2/webapp/tests/fixtures/full/errata/demo.json new file mode 100644 index 0000000..34fce34 --- /dev/null +++ b/asl_rulebook2/webapp/tests/fixtures/full/errata/demo.json @@ -0,0 +1,7 @@ +[ + +{ "ruleid": "A3.8", + "content": "The new eASLRB will, of course, be kept up-to-date, and there will be no need for errata :-), but other rulebooks might, so this is an example of how you would set them up." +} + +] diff --git a/asl_rulebook2/webapp/tests/fixtures/full/q+a/demo.json b/asl_rulebook2/webapp/tests/fixtures/full/q+a/demo.json new file mode 100644 index 0000000..4ab168b --- /dev/null +++ b/asl_rulebook2/webapp/tests/fixtures/full/q+a/demo.json @@ -0,0 +1,53 @@ +{ + +"Chapter A": [ + +{ "caption": "A4.63 & A11.8", + "ruleids": [ "A4.63", "A11.8" ], + "content": [ + { "question": "In the image (RB map), wherein Rubble is eligible for Street-Fighting Ambush like a building. would a unit in G7 be eligible for Street-Fighting Ambush against an AFV in G8?

Could a unit in G7 Dash (through G8) to F8 or H8?", + "image": "a4.63, a11.8.png", + "answers": [ + [ "No to both.", "ps" ] + ] + } + ] +}, + +{ "caption": "A7.7", + "ruleids": [ "A7.7" ], + "content": [ + { "question": "All German units combine as a Fire Group to take a 30/+3 shot at the Russian 447. Given that the 247 is one of two units in the Fire Group that potentially enable the shot to cause Encirclement, and that the 247's shot alone is not sufficient to be an Encirclement-causing shot, does the shot from this Fire Group cause Encirclement?", + "image": "a7.7.png", + "answers": [ + [ "Yes.", "ps" ] + ] + } + ] +}, + +{ "caption": "A7.7 & A23.3", + "ruleids": [ "A7.7", "A23.3" ], + "content": [ + { "question": "Does a placed DC attack count for encirclement (using the hexside it was placed through as its direction)?", + "answers": [ + [ "No.", "ps" ] + ] + } + ] +}, + +{ "caption": "A24.31", + "ruleids": [ "A24.31" ], + "content": [ + { "question": "May a MMC attempt to throw WP grenades into its own location?

Into an adjacent location solely occupied by friendly units?", + "answers": [ + [ "Yes to both; A24.31", "ps" ] + ] + } + ] +} + +] + +} diff --git a/asl_rulebook2/webapp/tests/fixtures/full/q+a/images/a7.7.png b/asl_rulebook2/webapp/tests/fixtures/full/q+a/images/a7.7.png new file mode 100644 index 0000000..61e011b Binary files /dev/null and b/asl_rulebook2/webapp/tests/fixtures/full/q+a/images/a7.7.png differ diff --git a/asl_rulebook2/webapp/tests/fixtures/full/q+a/sources.json b/asl_rulebook2/webapp/tests/fixtures/full/q+a/sources.json new file mode 100644 index 0000000..a62afda --- /dev/null +++ b/asl_rulebook2/webapp/tests/fixtures/full/q+a/sources.json @@ -0,0 +1,6 @@ +{ + +"ps": "Perry Sez", +"gs": "GameSquad" + +} diff --git a/asl_rulebook2/webapp/tests/proto/control_tests.proto b/asl_rulebook2/webapp/tests/proto/control_tests.proto index 30b196b..3b832cf 100644 --- a/asl_rulebook2/webapp/tests/proto/control_tests.proto +++ b/asl_rulebook2/webapp/tests/proto/control_tests.proto @@ -8,6 +8,15 @@ message SetDataDirRequest { string fixturesDirName = 1 ; } +message SetAppConfigValRequest { + string key = 1 ; + oneof ac_oneof { + string strVal = 2 ; + int32 intVal = 3 ; + bool boolVal = 4 ; + } +} + // -------------------------------------------------------------------- service ControlTests @@ -16,4 +25,5 @@ service ControlTests rpc endTests( google.protobuf.Empty ) returns ( google.protobuf.Empty ) ; rpc setDataDir( SetDataDirRequest ) returns ( google.protobuf.Empty ) ; + rpc setAppConfigVal( SetAppConfigValRequest ) returns ( google.protobuf.Empty ) ; } diff --git a/asl_rulebook2/webapp/tests/proto/generated/control_tests_pb2.py b/asl_rulebook2/webapp/tests/proto/generated/control_tests_pb2.py index ad05ccb..c582d10 100644 --- a/asl_rulebook2/webapp/tests/proto/generated/control_tests_pb2.py +++ b/asl_rulebook2/webapp/tests/proto/generated/control_tests_pb2.py @@ -20,7 +20,7 @@ DESCRIPTOR = _descriptor.FileDescriptor( syntax='proto3', serialized_options=None, create_key=_descriptor._internal_create_key, - serialized_pb=b'\n\x13\x63ontrol_tests.proto\x1a\x1bgoogle/protobuf/empty.proto\",\n\x11SetDataDirRequest\x12\x17\n\x0f\x66ixturesDirName\x18\x01 \x01(\t2\xc2\x01\n\x0c\x43ontrolTests\x12<\n\nstartTests\x12\x16.google.protobuf.Empty\x1a\x16.google.protobuf.Empty\x12:\n\x08\x65ndTests\x12\x16.google.protobuf.Empty\x1a\x16.google.protobuf.Empty\x12\x38\n\nsetDataDir\x12\x12.SetDataDirRequest\x1a\x16.google.protobuf.Emptyb\x06proto3' + serialized_pb=b'\n\x13\x63ontrol_tests.proto\x1a\x1bgoogle/protobuf/empty.proto\",\n\x11SetDataDirRequest\x12\x17\n\x0f\x66ixturesDirName\x18\x01 \x01(\t\"h\n\x16SetAppConfigValRequest\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\x10\n\x06strVal\x18\x02 \x01(\tH\x00\x12\x10\n\x06intVal\x18\x03 \x01(\x05H\x00\x12\x11\n\x07\x62oolVal\x18\x04 \x01(\x08H\x00\x42\n\n\x08\x61\x63_oneof2\x86\x02\n\x0c\x43ontrolTests\x12<\n\nstartTests\x12\x16.google.protobuf.Empty\x1a\x16.google.protobuf.Empty\x12:\n\x08\x65ndTests\x12\x16.google.protobuf.Empty\x1a\x16.google.protobuf.Empty\x12\x38\n\nsetDataDir\x12\x12.SetDataDirRequest\x1a\x16.google.protobuf.Empty\x12\x42\n\x0fsetAppConfigVal\x12\x17.SetAppConfigValRequest\x1a\x16.google.protobuf.Emptyb\x06proto3' , dependencies=[google_dot_protobuf_dot_empty__pb2.DESCRIPTOR,]) @@ -58,7 +58,75 @@ _SETDATADIRREQUEST = _descriptor.Descriptor( serialized_end=96, ) + +_SETAPPCONFIGVALREQUEST = _descriptor.Descriptor( + name='SetAppConfigValRequest', + full_name='SetAppConfigValRequest', + filename=None, + file=DESCRIPTOR, + containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[ + _descriptor.FieldDescriptor( + name='key', full_name='SetAppConfigValRequest.key', index=0, + number=1, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='strVal', full_name='SetAppConfigValRequest.strVal', index=1, + number=2, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='intVal', full_name='SetAppConfigValRequest.intVal', index=2, + number=3, type=5, cpp_type=1, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='boolVal', full_name='SetAppConfigValRequest.boolVal', index=3, + number=4, type=8, cpp_type=7, label=1, + has_default_value=False, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + _descriptor.OneofDescriptor( + name='ac_oneof', full_name='SetAppConfigValRequest.ac_oneof', + index=0, containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[]), + ], + serialized_start=98, + serialized_end=202, +) + +_SETAPPCONFIGVALREQUEST.oneofs_by_name['ac_oneof'].fields.append( + _SETAPPCONFIGVALREQUEST.fields_by_name['strVal']) +_SETAPPCONFIGVALREQUEST.fields_by_name['strVal'].containing_oneof = _SETAPPCONFIGVALREQUEST.oneofs_by_name['ac_oneof'] +_SETAPPCONFIGVALREQUEST.oneofs_by_name['ac_oneof'].fields.append( + _SETAPPCONFIGVALREQUEST.fields_by_name['intVal']) +_SETAPPCONFIGVALREQUEST.fields_by_name['intVal'].containing_oneof = _SETAPPCONFIGVALREQUEST.oneofs_by_name['ac_oneof'] +_SETAPPCONFIGVALREQUEST.oneofs_by_name['ac_oneof'].fields.append( + _SETAPPCONFIGVALREQUEST.fields_by_name['boolVal']) +_SETAPPCONFIGVALREQUEST.fields_by_name['boolVal'].containing_oneof = _SETAPPCONFIGVALREQUEST.oneofs_by_name['ac_oneof'] DESCRIPTOR.message_types_by_name['SetDataDirRequest'] = _SETDATADIRREQUEST +DESCRIPTOR.message_types_by_name['SetAppConfigValRequest'] = _SETAPPCONFIGVALREQUEST _sym_db.RegisterFileDescriptor(DESCRIPTOR) SetDataDirRequest = _reflection.GeneratedProtocolMessageType('SetDataDirRequest', (_message.Message,), { @@ -68,6 +136,13 @@ SetDataDirRequest = _reflection.GeneratedProtocolMessageType('SetDataDirRequest' }) _sym_db.RegisterMessage(SetDataDirRequest) +SetAppConfigValRequest = _reflection.GeneratedProtocolMessageType('SetAppConfigValRequest', (_message.Message,), { + 'DESCRIPTOR' : _SETAPPCONFIGVALREQUEST, + '__module__' : 'control_tests_pb2' + # @@protoc_insertion_point(class_scope:SetAppConfigValRequest) + }) +_sym_db.RegisterMessage(SetAppConfigValRequest) + _CONTROLTESTS = _descriptor.ServiceDescriptor( @@ -77,8 +152,8 @@ _CONTROLTESTS = _descriptor.ServiceDescriptor( index=0, serialized_options=None, create_key=_descriptor._internal_create_key, - serialized_start=99, - serialized_end=293, + serialized_start=205, + serialized_end=467, methods=[ _descriptor.MethodDescriptor( name='startTests', @@ -110,6 +185,16 @@ _CONTROLTESTS = _descriptor.ServiceDescriptor( serialized_options=None, create_key=_descriptor._internal_create_key, ), + _descriptor.MethodDescriptor( + name='setAppConfigVal', + full_name='ControlTests.setAppConfigVal', + index=3, + containing_service=None, + input_type=_SETAPPCONFIGVALREQUEST, + output_type=google_dot_protobuf_dot_empty__pb2._EMPTY, + serialized_options=None, + create_key=_descriptor._internal_create_key, + ), ]) _sym_db.RegisterServiceDescriptor(_CONTROLTESTS) diff --git a/asl_rulebook2/webapp/tests/proto/generated/control_tests_pb2_grpc.py b/asl_rulebook2/webapp/tests/proto/generated/control_tests_pb2_grpc.py index c71bb1a..affa9c1 100644 --- a/asl_rulebook2/webapp/tests/proto/generated/control_tests_pb2_grpc.py +++ b/asl_rulebook2/webapp/tests/proto/generated/control_tests_pb2_grpc.py @@ -32,6 +32,11 @@ class ControlTestsStub(object): request_serializer=control__tests__pb2.SetDataDirRequest.SerializeToString, response_deserializer=google_dot_protobuf_dot_empty__pb2.Empty.FromString, ) + self.setAppConfigVal = channel.unary_unary( + '/ControlTests/setAppConfigVal', + request_serializer=control__tests__pb2.SetAppConfigValRequest.SerializeToString, + response_deserializer=google_dot_protobuf_dot_empty__pb2.Empty.FromString, + ) class ControlTestsServicer(object): @@ -57,6 +62,12 @@ class ControlTestsServicer(object): context.set_details('Method not implemented!') raise NotImplementedError('Method not implemented!') + def setAppConfigVal(self, request, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + def add_ControlTestsServicer_to_server(servicer, server): rpc_method_handlers = { @@ -75,6 +86,11 @@ def add_ControlTestsServicer_to_server(servicer, server): request_deserializer=control__tests__pb2.SetDataDirRequest.FromString, response_serializer=google_dot_protobuf_dot_empty__pb2.Empty.SerializeToString, ), + 'setAppConfigVal': grpc.unary_unary_rpc_method_handler( + servicer.setAppConfigVal, + request_deserializer=control__tests__pb2.SetAppConfigValRequest.FromString, + response_serializer=google_dot_protobuf_dot_empty__pb2.Empty.SerializeToString, + ), } generic_handler = grpc.method_handlers_generic_handler( 'ControlTests', rpc_method_handlers) @@ -137,3 +153,20 @@ class ControlTests(object): google_dot_protobuf_dot_empty__pb2.Empty.FromString, options, channel_credentials, insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + + @staticmethod + def setAppConfigVal(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary(request, target, '/ControlTests/setAppConfigVal', + control__tests__pb2.SetAppConfigValRequest.SerializeToString, + google_dot_protobuf_dot_empty__pb2.Empty.FromString, + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) diff --git a/asl_rulebook2/webapp/tests/test_annotations.py b/asl_rulebook2/webapp/tests/test_annotations.py index eddf5b2..afe6d16 100644 --- a/asl_rulebook2/webapp/tests/test_annotations.py +++ b/asl_rulebook2/webapp/tests/test_annotations.py @@ -1,7 +1,7 @@ """ Test annotations. """ from asl_rulebook2.webapp.tests.utils import init_webapp, \ - find_child, find_children, wait_for_elem + find_child, find_children, wait_for_elem, check_sr_filters from asl_rulebook2.webapp.tests.test_search import do_search, unload_elem # --------------------------------------------------------------------- @@ -12,6 +12,7 @@ def test_full_errata( webapp, webdriver ): # initialize webapp.control_tests.set_data_dir( "annotations" ) init_webapp( webapp, webdriver ) + check_sr_filters( [ "index", "errata" ] ) # bring up the errata and check it in the search results results = do_search( "test" ) diff --git a/asl_rulebook2/webapp/tests/test_asop.py b/asl_rulebook2/webapp/tests/test_asop.py index 90d9699..6f7383f 100644 --- a/asl_rulebook2/webapp/tests/test_asop.py +++ b/asl_rulebook2/webapp/tests/test_asop.py @@ -5,7 +5,8 @@ import json from asl_rulebook2.webapp.tests.test_search import do_search from asl_rulebook2.webapp.tests.utils import init_webapp, select_tabbed_page, \ - wait_for, wait_for_elem, find_child, find_children, unload_elem, unload_sr_text, get_image_filename, has_class + wait_for, wait_for_elem, find_child, find_children, unload_elem, unload_sr_text, \ + check_sr_filters, get_image_filename, has_class # --------------------------------------------------------------------- @@ -15,6 +16,7 @@ def test_asop_nav( webdriver, webapp ): # initialize webapp.control_tests.set_data_dir( "asop" ) init_webapp( webapp, webdriver ) + check_sr_filters( [ "index", "asop-entry" ] ) # load the ASOP fname = os.path.join( os.path.dirname(__file__), "fixtures/asop/asop/index.json" ) diff --git a/asl_rulebook2/webapp/tests/test_basic.py b/asl_rulebook2/webapp/tests/test_basic.py index 5664eaa..d8be1db 100644 --- a/asl_rulebook2/webapp/tests/test_basic.py +++ b/asl_rulebook2/webapp/tests/test_basic.py @@ -1,7 +1,7 @@ """ Test basic functionality. """ from asl_rulebook2.webapp.tests.utils import init_webapp, \ - get_nav_panels, get_content_docs, select_tabbed_page, find_child + get_nav_panels, get_content_docs, check_sr_filters, select_tabbed_page, find_child # --------------------------------------------------------------------- @@ -11,6 +11,7 @@ def test_hello( webapp, webdriver ): # initialize webapp.control_tests.set_data_dir( "simple" ) init_webapp( webapp, webdriver ) + check_sr_filters( [] ) # check that the nav panel loaded correctly nav_panels = get_nav_panels() diff --git a/asl_rulebook2/webapp/tests/test_qa.py b/asl_rulebook2/webapp/tests/test_qa.py index ce87c98..232f2a9 100644 --- a/asl_rulebook2/webapp/tests/test_qa.py +++ b/asl_rulebook2/webapp/tests/test_qa.py @@ -1,7 +1,7 @@ """ Test Q+A. """ from asl_rulebook2.webapp.tests.utils import init_webapp, \ - find_child, find_children, wait_for_elem, get_image_filename, unload_elem, unload_sr_text + check_sr_filters, find_child, find_children, wait_for_elem, get_image_filename, unload_elem, unload_sr_text from asl_rulebook2.webapp.tests.test_search import do_search # --------------------------------------------------------------------- @@ -12,6 +12,7 @@ def test_full_qa_entry( webapp, webdriver ): # initialize webapp.control_tests.set_data_dir( "qa" ) init_webapp( webapp, webdriver ) + check_sr_filters( [ "index", "qa" ] ) # bring up the Q+A entry results = do_search( "full" ) diff --git a/asl_rulebook2/webapp/tests/test_search.py b/asl_rulebook2/webapp/tests/test_search.py index 4b23bf0..4e7d3b7 100644 --- a/asl_rulebook2/webapp/tests/test_search.py +++ b/asl_rulebook2/webapp/tests/test_search.py @@ -189,7 +189,7 @@ def test_see_also( webapp, webdriver ): elems[ caption ].click() expected = '"{}"'.format( caption ) if " " in caption else caption wait_for( 2, lambda: find_child( "input#query-string" ).get_attribute( "value" ) == expected ) - return _unload_search_results() + return unload_search_results() # click on the "Bar" link and check that it gets searched for results = click_see_also( "Bar" ) @@ -325,9 +325,9 @@ def do_search( query_string ): # unload the results wait_for( 2, lambda: get_seq_no() > seq_no ) - return _unload_search_results() + return unload_search_results() -def _unload_search_results(): +def unload_search_results(): """Unload the search results.""" # check if there were any search results @@ -336,8 +336,9 @@ def _unload_search_results(): return elem.text # nb: string = error message elem = find_child( "#search-results .no-results" ) if elem: - assert elem.text == "Nothing was found." - return None # nb: None = no results + if elem.text == "Nothing was found.": + return None # nb: None = no results + return elem.text def unload_ruleids( result, key, parent ): """Unload a list of ruleid's.""" @@ -397,6 +398,8 @@ def _unload_search_results(): # unload the search results results = [] for sr in find_children( "#search-results .sr"): + if not sr.is_displayed(): + continue classes = get_classes( sr ) classes.remove( "sr" ) classes = [ c for c in classes if c in ["index-sr","qa","anno","asop-entry-sr"] ] diff --git a/asl_rulebook2/webapp/tests/test_sr_filters.py b/asl_rulebook2/webapp/tests/test_sr_filters.py new file mode 100644 index 0000000..1cc0f6b --- /dev/null +++ b/asl_rulebook2/webapp/tests/test_sr_filters.py @@ -0,0 +1,127 @@ +""" Test search result filtering. """ + +from asl_rulebook2.webapp.tests.test_search import do_search, unload_search_results +from asl_rulebook2.webapp.tests.utils import init_webapp, \ + check_sr_filters, find_child + +# --------------------------------------------------------------------- + +def test_sr_filtering( webdriver, webapp ): + """Test filtering search results.""" + + # initialize + webapp.control_tests.set_data_dir( "full" ) + webapp.control_tests.set_app_config_val( "DISABLE_AUTO_SHOW_RULE_INFO", True ) + init_webapp( webapp, webdriver ) + check_sr_filters( [ "index", "qa", "errata", "asop-entry" ] ) + + def check_sr_count( nvisible, ntotal ): + """Check the search result count reported in the UI.""" + elem = find_child( "#search-box .sr-count" ) + assert elem.text == "{}/{}".format( nvisible, ntotal ) + + def do_test( query_string, sr_type, expected ): + + # make sure the checkbox for the specified search result type is enabled + sel = "#search-box input[type='checkbox'][name='show-{}-sr']".format( sr_type ) + elem = find_child( sel ) + assert elem.is_selected() + + # do the search + results = _do_search( query_string ) + assert results == expected + check_sr_count( len(expected), len(expected) ) + + # filter out the specified type of search result + find_child( sel ).click() + results = _unload_search_results() + sr_type2 = "anno" if sr_type == "errata" else sr_type + expected2 = [ e for e in expected if e["sr_type"] != sr_type2 ] + if not expected2: + expected2 = "All search results have been filtered." + assert results == expected2 + check_sr_count( len(expected2) if isinstance(expected2,list) else 0, len(expected) ) + + # refresh the page + webdriver.refresh() + elem = find_child( sel ) + assert not elem.is_selected() + + # repeat the search + results = _do_search( query_string ) + assert results == expected2 + check_sr_count( len(expected2) if isinstance(expected2,list) else 0, len(expected) ) + + # re-enable the specified type of search result + elem.click() + results = _unload_search_results() + assert results == expected + check_sr_count( len(expected), len(expected) ) + + # test filtering index search results + do_test( "bu", "index", [ + { "sr_type": "index", "title": "((BU))" }, + { "sr_type": "index", "title": "CC" }, + ] ) + + # test filtering Q+A search results + do_test( "encirclement", "qa", [ + { "sr_type": "index", "title": "((Encirclement))" }, + { "sr_type": "qa", "caption": "A7.7" }, + { "sr_type": "qa", "caption": "A7.7 & A23.3" }, + ] ) + + # test filtering errata search results + do_test( "errata", "errata", [ + { "sr_type": "index", "title": "CCPh" }, + { "sr_type": "anno", "caption": "A3.8" }, + ] ) + + # test filtering ASOP search results + do_test( "cc", "asop-entry", [ + { "sr_type": "index", "title": "((CC))" }, + { "sr_type": "index", "title": "CCPh" }, + { "sr_type": "asop-entry", "caption": "8.2: DURING LOCATION's CCPh (ASOP)" }, + ] ) + +# --------------------------------------------------------------------- + +def test_sr_count( webdriver, webapp ): + """Test the search result count reported in the UI.""" + + # initialize + webapp.control_tests.set_data_dir( "full" ) + init_webapp( webapp, webdriver ) + + # NOTE: Most of the testing is done in test_sr_filtering(), we just test error cases here. + + # check the search result when there are no search results + results = do_search( "xyz" ) + assert results is None + assert find_child( "#search-box .sr-count" ).text == "" + + # check the search result when there was an error + results = do_search( "!:simulated-error:!" ) + assert results.startswith( "Search error:" ) + assert find_child( "#search-box .sr-count" ).text == "" + +# --------------------------------------------------------------------- + +def _do_search( query_string ): + """Do a search.""" + return _strip_results( do_search( query_string ) ) + +def _unload_search_results(): + """Unload the current search results.""" + return _strip_results( unload_search_results() ) + +def _strip_results( results ): + """Strip search results down to only include what we're interested in.""" + if results is None or isinstance( results, str ): + return results + for result_no, result in enumerate( results ): + results[ result_no ] = { + key: val for key, val in result.items() + if key in ( "sr_type", "title", "caption" ) + } + return results diff --git a/asl_rulebook2/webapp/tests/utils.py b/asl_rulebook2/webapp/tests/utils.py index c84e31a..af08502 100644 --- a/asl_rulebook2/webapp/tests/utils.py +++ b/asl_rulebook2/webapp/tests/utils.py @@ -107,6 +107,20 @@ def get_curr_target(): ruleid = elem.get_attribute( "data-ruleid" ) return ( tab_id, ruleid ) +def check_sr_filters( expected ): + """Check the search result filter checkboxes.""" + sr_filters = find_child( "#search-box .sr-filters" ) + if expected: + elems = [ + c.get_attribute("name") for c in find_children( "input[type='checkbox']", sr_filters ) + if c.is_displayed() + ] + assert all( e.startswith("show-") and e.endswith("-sr") for e in elems ) + elems = [ e[5:-3] for e in elems ] + assert elems == expected + else: + assert not sr_filters.is_displayed() + # --------------------------------------------------------------------- #pylint: disable=multiple-statements,missing-function-docstring