Allow search results to be filtered.

master
Pacman Ghost 3 years ago
parent 39b760aafb
commit 8f8d03d695
  1. 14
      asl_rulebook2/webapp/search.py
  2. 19
      asl_rulebook2/webapp/startup.py
  3. 1
      asl_rulebook2/webapp/static/MainApp.js
  4. 166
      asl_rulebook2/webapp/static/SearchPane.js
  5. 4
      asl_rulebook2/webapp/static/SearchResult.js
  6. 12
      asl_rulebook2/webapp/static/UserSettings.js
  7. 14
      asl_rulebook2/webapp/static/css/SearchPane.css
  8. 163
      asl_rulebook2/webapp/static/js-cookie/js.cookie.js
  9. 2
      asl_rulebook2/webapp/templates/index.html
  10. 15
      asl_rulebook2/webapp/tests/control_tests.py
  11. 12
      asl_rulebook2/webapp/tests/control_tests_servicer.py
  12. 5
      asl_rulebook2/webapp/tests/fixtures/asop/asop.index
  13. 13
      asl_rulebook2/webapp/tests/fixtures/full/ASL Rulebook (Red Barricades).chapters
  14. 6
      asl_rulebook2/webapp/tests/fixtures/full/ASL Rulebook (Red Barricades).targets
  15. 83
      asl_rulebook2/webapp/tests/fixtures/full/ASL Rulebook.chapters
  16. 12
      asl_rulebook2/webapp/tests/fixtures/full/ASL Rulebook.footnotes
  17. 80
      asl_rulebook2/webapp/tests/fixtures/full/ASL Rulebook.index
  18. 37
      asl_rulebook2/webapp/tests/fixtures/full/ASL Rulebook.targets
  19. BIN
      asl_rulebook2/webapp/tests/fixtures/full/O-background.png
  20. BIN
      asl_rulebook2/webapp/tests/fixtures/full/O-icon.png
  21. 7
      asl_rulebook2/webapp/tests/fixtures/full/annotations.json
  22. 1
      asl_rulebook2/webapp/tests/fixtures/full/asop
  23. 7
      asl_rulebook2/webapp/tests/fixtures/full/errata/demo.json
  24. 53
      asl_rulebook2/webapp/tests/fixtures/full/q+a/demo.json
  25. BIN
      asl_rulebook2/webapp/tests/fixtures/full/q+a/images/a7.7.png
  26. 6
      asl_rulebook2/webapp/tests/fixtures/full/q+a/sources.json
  27. 10
      asl_rulebook2/webapp/tests/proto/control_tests.proto
  28. 91
      asl_rulebook2/webapp/tests/proto/generated/control_tests_pb2.py
  29. 33
      asl_rulebook2/webapp/tests/proto/generated/control_tests_pb2_grpc.py
  30. 3
      asl_rulebook2/webapp/tests/test_annotations.py
  31. 4
      asl_rulebook2/webapp/tests/test_asop.py
  32. 3
      asl_rulebook2/webapp/tests/test_basic.py
  33. 3
      asl_rulebook2/webapp/tests/test_qa.py
  34. 13
      asl_rulebook2/webapp/tests/test_search.py
  35. 127
      asl_rulebook2/webapp/tests/test_sr_filters.py
  36. 14
      asl_rulebook2/webapp/tests/utils.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:

@ -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:

@ -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 ) ;
} ) ;

@ -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: `
<div>
<input type="text" id="query-string" @keyup=onKeyUp v-model.trim="queryString" ref="queryString" autofocus >
<button @click="$emit('search',this.queryString)" ref="submit"> Go </button>
<div class="row">
<input type="text" id="query-string" @keyup=onKeyUp v-model.trim="queryString" ref="queryString" autofocus >
<button @click="$emit('search',this.queryString)" ref="submit"> Go </button>
</div>
<div class="row sr-filters" style="display:none;" ref="srFilters" >
<b> Show: </b>
<input type="checkbox" name="show-index-sr" @click=onClickSrFilter > <label for="show-index-sr"> Index </label>
<input type="checkbox" name="show-qa-sr" @click=onClickSrFilter > <label for="show-qa-sr"> Q+A </label>
<input type="checkbox" name="show-errata-sr" @click=onClickSrFilter > <label for="show-errata-sr"> Errata </label>
<input type="checkbox" name="show-asop-entry-sr" @click=onClickSrFilter > <label for="show-asop-entry-sr"> ASOP </label>
<span v-if=showSrCount :title=srCountInfo class="sr-count"> {{srCount}} </span>
</div>
</div>`,
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: `<div>
<div v-if=errorMsg class="error"> Search error: <div class="pre"> {{errorMsg}} </div> </div>
<div v-else-if="searchResults != null && searchResults.length == 0" class="no-results"> Nothing was found. </div>
<div v-else v-for="sr in searchResults" :key=sr >
<index-sr v-if="sr.sr_type == 'index'" :sr=sr />
<qa-entry v-else-if="sr.sr_type == 'q+a'" :qaEntry=sr class="sr rule-info" />
<annotation v-else-if="sr.sr_type == 'errata'" :anno=sr class="sr rule-info" />
<annotation v-else-if="sr.sr_type == 'user-anno'" :anno=sr class="sr rule-info" />
<asop-entry-sr v-else-if="sr.sr_type == 'asop-entry'" :sr=sr class="sr" />
<div v-else> ???:{{sr.sr_type}} </div>
<div v-else>
<div v-if=noResultsMsg class="no-results"> {{noResultsMsg}} </div>
<div v-for="sr in searchResults" :key=sr >
<index-sr v-if="sr.sr_type == 'index'" :sr=sr data-srtype="index"
:style="{ display: showSr(sr.sr_type) ? 'block' : 'none' }"
class="sr"
/>
<qa-entry v-else-if="sr.sr_type == 'qa'" :qaEntry=sr data-srtype="qa"
:style="{ display: showSr(sr.sr_type) ? 'block' : 'none' }"
class="sr"
/>
<annotation v-else-if="sr.sr_type == 'errata'" :anno=sr data-srtype="errata"
:style="{ display: showSr(sr.sr_type) ? 'block' : 'none' }"
class="sr"
/>
<annotation v-else-if="sr.sr_type == 'user-anno'" :anno=sr class="sr rule-info" />
<asop-entry-sr v-else-if="sr.sr_type == 'asop-entry'" :sr=sr data-srtype="asop-entry"
:style="{ display: showSr(sr.sr_type) ? 'block' : 'none' }"
class="sr"
/>
<div v-else> ???:{{sr.sr_type}} </div>
</div>
</div>
</div>`,
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 ;
},
},
} ) ;

@ -13,7 +13,7 @@ gMainApp.component( "index-sr", {
} ; },
template: `
<div class="sr index-sr" >
<div class="index-sr" >
<div v-if="sr.title || sr.subtitle" :style="{background: cssBackground}" class="title" >
<a v-if=iconUrl href="#" @click=onClickIcon >
<img :src=iconUrl class="icon" />
@ -147,7 +147,7 @@ gMainApp.component( "asop-entry-sr", {
props: [ "sr" ],
template: `
<div class="sr asop-entry-sr asop" >
<div class="asop-entry-sr asop" >
<div v-html="sr.caption+' (ASOP)'" @click=onClickCaption class="caption" title="Go to the ASOP" />
<div class="content" v-html=sr.content />
</div>`,

@ -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 }
) ;
}

@ -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 ; }

@ -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 () {});
}));

@ -48,6 +48,7 @@
<script src="{{ url_for( 'static',
filename = 'tinyemitter/tinyemitter' + WEB_DEBUG_MIN + '.js'
) }}"></script>
<script src="{{ url_for( 'static', filename='js-cookie/js.cookie.js' ) }}"></script>
<script src="{{ url_for( 'static', filename='growl/jquery.growl.js' ) }}"></script>
<script src="{{ url_for( 'static', filename='imageZoom/jquery.imageZoom.min.js' ) }}"></script>
@ -75,6 +76,7 @@ gGetFootnotesUrl = "{{ url_for( 'get_footnotes' ) }}" ;
<script type="module" src="{{ url_for( 'static', filename='ASOP.js' ) }}"></script>
<script type="module" src="{{ url_for( 'static', filename='TabbedPages.js' ) }}"></script>
<script type="module" src="{{ url_for( 'static', filename='Accordian.js' ) }}"></script>
<script type="module" src="{{ url_for( 'static', filename='UserSettings.js' ) }}"></script>
<script type="module" src="{{ url_for( 'static', filename='utils.js' ) }}"></script>
</html>

@ -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

@ -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."""

@ -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." }
]

@ -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" }
]
}
]

@ -0,0 +1,6 @@
{
"O6.7": { "caption": "ENCIRCLEMENT", "page_no": 5 }
}

@ -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" }
] }
]

@ -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."
}
}
}

@ -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" ] }
]
}
]

@ -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" }
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

@ -0,0 +1,7 @@
[
{ "ruleid": "A24.3",
"content": "Mmmm, White Phosphorous. Is there anything it can't do...?"
}
]

@ -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."
}
]

@ -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? <p> 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? <p> Into an adjacent location solely occupied by friendly units?",
"answers": [
[ "Yes to both; A24.31", "ps" ]
]
}
]
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 120 KiB

@ -0,0 +1,6 @@
{
"ps": "Perry Sez",
"gs": "GameSquad"
}

@ -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 ) ;
}

@ -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)

@ -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)

@ -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" )

@ -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" )

@ -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()

@ -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" )

@ -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"] ]

@ -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

@ -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

Loading…
Cancel
Save