parent
bc58b8c9c4
commit
0a91f820f3
@ -0,0 +1,130 @@ |
|||||||
|
"""Provide integration with ROAR.""" |
||||||
|
# Bodhgaya, India (APR/19) |
||||||
|
|
||||||
|
import os.path |
||||||
|
import threading |
||||||
|
import json |
||||||
|
import time |
||||||
|
import datetime |
||||||
|
import tempfile |
||||||
|
import logging |
||||||
|
import urllib.request |
||||||
|
|
||||||
|
from flask import render_template, jsonify |
||||||
|
|
||||||
|
from vasl_templates.webapp import app |
||||||
|
|
||||||
|
_roar_scenario_index = {} |
||||||
|
_roar_scenario_index_lock = threading.Lock() |
||||||
|
|
||||||
|
_logger = logging.getLogger( "roar" ) |
||||||
|
|
||||||
|
ROAR_SCENARIO_INDEX_URL = "http://vasl-templates.org/services/roar/scenario-index.json" |
||||||
|
CACHE_TTL = 6 * 60*60 |
||||||
|
|
||||||
|
# --------------------------------------------------------------------- |
||||||
|
|
||||||
|
def init_roar( msg_store ): |
||||||
|
"""Initialize ROAR integration.""" |
||||||
|
|
||||||
|
# initialize |
||||||
|
download = True |
||||||
|
cache_fname = os.path.join( tempfile.gettempdir(), "vasl-templates.roar-scenario-index.json" ) |
||||||
|
enable_cache = not app.config.get( "DISABLE_ROAR_SCENARIO_INDEX_CACHE" ) |
||||||
|
if not enable_cache: |
||||||
|
cache_fname = None |
||||||
|
|
||||||
|
# check if we have a cached copy of the scenario index |
||||||
|
if enable_cache and os.path.isfile( cache_fname ): |
||||||
|
# yup - load it, so that we have something until we finish downloading a fresh copy |
||||||
|
_logger.info( "Loading cached ROAR scenario index: %s", cache_fname ) |
||||||
|
with open( cache_fname, "r" ) as fp: |
||||||
|
_load_roar_scenario_index( fp.read(), "cached", msg_store ) |
||||||
|
# check if we should download a fresh copy |
||||||
|
mtime = os.path.getmtime( cache_fname ) |
||||||
|
age = int( time.time() - mtime ) |
||||||
|
_logger.debug( "Cached scenario index age: %s (ttl=%s) (mtime=%s)", |
||||||
|
datetime.timedelta(seconds=age), datetime.timedelta(seconds=CACHE_TTL), |
||||||
|
time.strftime( "%Y-%m-%d %H:%M:%S", time.gmtime(mtime) ) |
||||||
|
) |
||||||
|
if age < CACHE_TTL: |
||||||
|
download = False |
||||||
|
|
||||||
|
# check if we should download the ROAR scenario index |
||||||
|
if download: |
||||||
|
if app.config.get("DISABLE_ROAR_SCENARIO_INDEX_DOWNLOAD"): |
||||||
|
_logger.warning( "Downloading the ROAR scenario index has been disabled." ) |
||||||
|
else: |
||||||
|
# yup - make it so (nb: we do it in a background thread to avoid blocking the startup process) |
||||||
|
# NOTE: This is the only place we do this, so if it fails, the program needs to be restarted to try again. |
||||||
|
# This is not great, but we can live it (e.g. we will generally be using the cached copy). |
||||||
|
threading.Thread( target = _download_roar_scenario_index, |
||||||
|
args = ( cache_fname, msg_store ) |
||||||
|
).start() |
||||||
|
|
||||||
|
def _download_roar_scenario_index( save_fname, msg_store ): |
||||||
|
"""Download the ROAR scenario index.""" |
||||||
|
|
||||||
|
# download the ROAR scenario index |
||||||
|
url = app.config.get( "ROAR_SCENARIO_INDEX_URL", "https://vasl-templates.org/services/roar/scenario-index.json" ) |
||||||
|
_logger.info( "Downloading ROAR scenario index: %s", url ) |
||||||
|
try: |
||||||
|
fp = urllib.request.urlopen( url ) |
||||||
|
data = fp.read().decode( "utf-8" ) |
||||||
|
except Exception as ex: #pylint: disable=broad-except |
||||||
|
# NOTE: We catch all exceptions, since we don't want an error here to stop us from running :-/ |
||||||
|
error_msg = "Can't download ROAR scenario index: {}".format( getattr(ex,"reason",str(ex)) ) |
||||||
|
_logger.warning( error_msg ) |
||||||
|
if msg_store: |
||||||
|
msg_store.warning( error_msg ) |
||||||
|
return |
||||||
|
if not _load_roar_scenario_index( data, "downloaded", msg_store ): |
||||||
|
# NOTE: If we fail to load the scenario index (e.g. because of invalid JSON), we exit here |
||||||
|
# and won't overwrite the cached copy of the file with the bad data. |
||||||
|
return |
||||||
|
|
||||||
|
# save a copy of the data |
||||||
|
if save_fname: |
||||||
|
_logger.debug( "Saving a copy of the ROAR scenario index: %s", save_fname ) |
||||||
|
with open( save_fname, "w" ) as fp: |
||||||
|
fp.write( data ) |
||||||
|
|
||||||
|
def _load_roar_scenario_index( data, data_type, msg_store ): |
||||||
|
"""Load the ROAR scenario index.""" |
||||||
|
|
||||||
|
# load the ROAR scenario index |
||||||
|
try: |
||||||
|
scenario_index = json.loads( data ) |
||||||
|
except Exception as ex: #pylint: disable=broad-except |
||||||
|
# NOTE: We catch all exceptions, since we don't want an error here to stop us from running :-/ |
||||||
|
error_msg = "Can't load {} ROAR scenario index: {}".format( data_type, ex ) |
||||||
|
_logger.warning( error_msg ) |
||||||
|
if msg_store: |
||||||
|
msg_store.warning( error_msg ) |
||||||
|
return False |
||||||
|
_logger.debug( "Loaded %s ROAR scenario index OK: #scenarios=%d", data_type, len(scenario_index) ) |
||||||
|
_logger.debug( "- Last updated: %s", scenario_index.get( "_lastUpdated_", "n/a" ) ) |
||||||
|
_logger.debug( "- # playings: %s", str( scenario_index.get( "_nPlayings_", "n/a" ) ) ) |
||||||
|
_logger.debug( "- Generated at: %s", scenario_index.get( "_generatedAt_", "n/a" ) ) |
||||||
|
|
||||||
|
# install the new ROAR scenario index |
||||||
|
with _roar_scenario_index_lock: |
||||||
|
global _roar_scenario_index |
||||||
|
_roar_scenario_index = scenario_index |
||||||
|
|
||||||
|
return True |
||||||
|
|
||||||
|
# --------------------------------------------------------------------- |
||||||
|
|
||||||
|
@app.route( "/roar/scenario-index" ) |
||||||
|
def get_roar_scenario_index(): |
||||||
|
"""Return the ROAR scenario index.""" |
||||||
|
with _roar_scenario_index_lock: |
||||||
|
return jsonify( _roar_scenario_index ) |
||||||
|
|
||||||
|
# --------------------------------------------------------------------- |
||||||
|
|
||||||
|
@app.route( "/roar/check" ) |
||||||
|
def check_roar(): |
||||||
|
"""Check the ROAR data (for testing porpoises only).""" |
||||||
|
return render_template( "check-roar.html" ) |
@ -0,0 +1,19 @@ |
|||||||
|
#select-roar-scenario { overflow: hidden ; } |
||||||
|
|
||||||
|
.ui-dialog.select-roar-scenario .ui-dialog-titlebar { background: #ffffcc ; border: 1px solid #e0e0cc ; } |
||||||
|
.ui-dialog.select-roar-scenario .ui-dialog-content { padding-top: 0 !important ; } |
||||||
|
.ui-dialog.select-roar-scenario .ui-dialog-buttonpane { border: none ; margin-top: 0 !important ; padding-top: 0 !important ; } |
||||||
|
|
||||||
|
#select-roar-scenario .header { height: 1.75em ; margin-top: 0.25em ; font-size: 80% ; } |
||||||
|
|
||||||
|
#select-roar-scenario .select2-selection { display: none ; } |
||||||
|
#select-roar-scenario .select2-search { padding: 0 0 5px 0 ; } |
||||||
|
#select-roar-scenario .select2-results { border: 1px solid #ccc ; } |
||||||
|
#select-roar-scenario .select2-results__options { max-height: none ; } |
||||||
|
#select-roar-scenario .select2-dropdown { border: none ; } |
||||||
|
|
||||||
|
#select-roar-scenario .select2-dropdown .scenario .scenario-id { font-size: 90% ; color: #666 ; } |
||||||
|
#select-roar-scenario .select2-dropdown .scenario .publication { display: block ; font-size: 80% ; font-style: italic ; color: #888 ; } |
||||||
|
|
||||||
|
#select-roar-scenario .select2-results__option--highlighted[aria-selected] .scenario-id { color: #eee ; } |
||||||
|
#select-roar-scenario .select2-results__option--highlighted[aria-selected] .publication { color: #eee ; } |
After Width: | Height: | Size: 4.0 KiB |
After Width: | Height: | Size: 2.8 KiB |
@ -0,0 +1,310 @@ |
|||||||
|
gRoarScenarioIndex = null ; |
||||||
|
|
||||||
|
// --------------------------------------------------------------------
|
||||||
|
|
||||||
|
function _get_roar_scenario_index( on_ready ) |
||||||
|
{ |
||||||
|
// check if we already have the ROAR scenario index
|
||||||
|
if ( gRoarScenarioIndex && Object.keys(gRoarScenarioIndex).length > 0 ) { |
||||||
|
// yup - just do it
|
||||||
|
on_ready() ; |
||||||
|
} else { |
||||||
|
// nope - download it (nb: we do this on-demand, instead of during startup,
|
||||||
|
// to give the backend time if it wants to download a fresh copy).
|
||||||
|
// NOTE: We will also get here if we downloaded the scenario index, but it's empty.
|
||||||
|
// This can happen if the cached file is not there, and the server is still downloading
|
||||||
|
// a fresh copy, in which case, we will keep retrying until we get something.
|
||||||
|
$.getJSON( gGetRoarScenarioIndexUrl, function(data) { |
||||||
|
gRoarScenarioIndex = data ; |
||||||
|
on_ready() ; |
||||||
|
} ).fail( function( xhr, status, errorMsg ) { |
||||||
|
showErrorMsg( "Can't get the ROAR scenario index:<div class='pre'>" + escapeHTML(errorMsg) + "</div>" ) ; |
||||||
|
} ) ; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// --------------------------------------------------------------------
|
||||||
|
|
||||||
|
function search_roar() |
||||||
|
{ |
||||||
|
var unknown_nats = [] ; |
||||||
|
function on_scenario_selected( roar_id ) { |
||||||
|
// update the UI for the selected ROAR scenario
|
||||||
|
set_roar_scenario( roar_id ) ; |
||||||
|
// populate the scenario name/ID
|
||||||
|
var scenario = gRoarScenarioIndex[ roar_id ] ; |
||||||
|
if ( $("input[name='SCENARIO_NAME']").val() === "" && $("input[name='SCENARIO_ID']").val() === "" ) { |
||||||
|
$("input[name='SCENARIO_NAME']").val( scenario.name ) ; |
||||||
|
$("input[name='SCENARIO_ID']").val( scenario.scenario_id ) ; |
||||||
|
} |
||||||
|
// update the player nationalities
|
||||||
|
// NOTE: The player order as returned by ROAR is undetermined (and could change from call to call),
|
||||||
|
// so what we set here might not match what's in the scenario card, but we've got a 50-50 chance of being right... :-/
|
||||||
|
update_player( scenario, 1 ) ; |
||||||
|
update_player( scenario, 2 ) ; |
||||||
|
if ( unknown_nats.length > 0 ) { |
||||||
|
var buf = [ "Unrecognized nationality in ROAR:", "<ul>" ] ; |
||||||
|
for ( var i=0 ; i < unknown_nats.length ; ++i ) |
||||||
|
buf.push( "<li>" + unknown_nats[i] ) ; |
||||||
|
buf.push( "</ul>" ) ; |
||||||
|
showWarningMsg( buf.join("") ) ; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
function update_player( scenario, player_no ) { |
||||||
|
var roar_nat = scenario.results[ player_no-1 ][0] ; |
||||||
|
var nat = convert_roar_nat( roar_nat ) ; |
||||||
|
if ( ! nat ) { |
||||||
|
unknown_nats.push( roar_nat ) ; |
||||||
|
return ; |
||||||
|
} |
||||||
|
if ( nat === get_player_nat( player_no ) ) |
||||||
|
return ; |
||||||
|
if ( ! is_player_ob_empty( player_no ) ) |
||||||
|
return ; |
||||||
|
$( "select[name='PLAYER_" + player_no + "']" ).val( nat ).trigger( "change" ) ; |
||||||
|
on_player_change( player_no ) ; |
||||||
|
} |
||||||
|
|
||||||
|
// ask the user to select a ROAR scenario
|
||||||
|
_get_roar_scenario_index( function() { |
||||||
|
do_search_roar( on_scenario_selected ) ; |
||||||
|
} ) ; |
||||||
|
} |
||||||
|
|
||||||
|
function do_search_roar( on_ok ) |
||||||
|
{ |
||||||
|
// initialize the select2
|
||||||
|
var $sel = $( "#select-roar-scenario select" ) ; |
||||||
|
$sel.select2( { |
||||||
|
width: "100%", |
||||||
|
templateResult: function( opt ) { return opt.id ? _format_entry(opt.id) : opt.text ; }, |
||||||
|
dropdownParent: $("#select-roar-scenario"), // FUDGE! need this for the searchbox to work :-/
|
||||||
|
closeOnSelect: false, |
||||||
|
} ) ; |
||||||
|
|
||||||
|
// stop the select2 droplist from closing up
|
||||||
|
$sel.on( "select2:closing", function(evt) { |
||||||
|
evt.preventDefault() ; |
||||||
|
} ) ; |
||||||
|
|
||||||
|
// let the user select a scenario
|
||||||
|
function on_resize( $dlg ) { |
||||||
|
$( ".select2-results ul" ).height( $dlg.height() - 50 ) ; |
||||||
|
} |
||||||
|
var $dlg = $("#select-roar-scenario").dialog( { |
||||||
|
title: "Search ROAR", |
||||||
|
dialogClass: "select-roar-scenario", |
||||||
|
modal: true, |
||||||
|
minWidth: 400, |
||||||
|
minHeight: 350, |
||||||
|
create: function() { |
||||||
|
// initialize the dialog
|
||||||
|
init_dialog( $(this), "OK", false ) ; |
||||||
|
// handle ENTER and double-click
|
||||||
|
function auto_select_scenario( evt ) { |
||||||
|
if ( $sel.val() ) { |
||||||
|
$( ".ui-dialog.select-roar-scenario button:contains('OK')" ).click() ; |
||||||
|
evt.preventDefault() ; |
||||||
|
} |
||||||
|
} |
||||||
|
$("#select-roar-scenario").keydown( function(evt) { |
||||||
|
if ( evt.keyCode == $.ui.keyCode.ENTER ) |
||||||
|
auto_select_scenario( evt ) ; |
||||||
|
else if ( evt.keyCode == $.ui.keyCode.ESCAPE ) |
||||||
|
$(this).dialog( "close" ) ; |
||||||
|
} ).dblclick( function(evt) { |
||||||
|
auto_select_scenario( evt ) ; |
||||||
|
} ) ; |
||||||
|
}, |
||||||
|
open: function() { |
||||||
|
// initialize
|
||||||
|
// NOTE: We do this herem instead of in the "create" handler, to handle the case
|
||||||
|
// where the scenario index was initially unavailable but the download has since completed.
|
||||||
|
_load_select2( $sel ) ; |
||||||
|
on_dialog_open( $(this) ) ; |
||||||
|
$sel.select2( "open" ) ; |
||||||
|
// update the UI
|
||||||
|
on_resize( $(this) ) ; |
||||||
|
}, |
||||||
|
resize: function() { on_resize( $(this) ) ; }, |
||||||
|
buttons: { |
||||||
|
OK: function() { |
||||||
|
// notify the caller about the selected scenario
|
||||||
|
var roar_id = $sel.select2("data")[0].id ; |
||||||
|
on_ok( roar_id ) ; |
||||||
|
$dlg.dialog( "close" ) ; |
||||||
|
}, |
||||||
|
Cancel: function() { $(this).dialog( "close" ) ; }, |
||||||
|
}, |
||||||
|
} ) ; |
||||||
|
} |
||||||
|
|
||||||
|
function _load_select2( $sel ) |
||||||
|
{ |
||||||
|
function remove_quotes( lquote, rquote ) { |
||||||
|
var len = name.length ; |
||||||
|
if ( name.substr( 0, lquote.length ) === lquote && name.substr( len-rquote.length ) === rquote ) |
||||||
|
name = name.substr( lquote.length, len-lquote.length-rquote.length ) ; |
||||||
|
if ( name.substr( 0, lquote.length ) == lquote ) |
||||||
|
name = name.substr( lquote.length ) ; |
||||||
|
return name ; |
||||||
|
} |
||||||
|
|
||||||
|
// sort the scenarios
|
||||||
|
var roar_ids=[], roar_id, scenario ; |
||||||
|
for ( roar_id in gRoarScenarioIndex ) { |
||||||
|
if ( roar_id[0] === "_" ) |
||||||
|
continue ; |
||||||
|
roar_ids.push( roar_id ) ; |
||||||
|
scenario = gRoarScenarioIndex[ roar_id ] ; |
||||||
|
var name = scenario.name ; |
||||||
|
name = remove_quotes( '"', '"' ) ; |
||||||
|
name = remove_quotes( "'", "'" ) ; |
||||||
|
name = remove_quotes( """, """ ) ; |
||||||
|
name = remove_quotes( "\u2018", "\u2019" ) ; |
||||||
|
name = remove_quotes( "\u201c", "\u201d" ) ; |
||||||
|
if ( name.substring(0,3) === "..." ) |
||||||
|
name = name.substr( 3 ) ; |
||||||
|
scenario._sort_name = name.trim().toUpperCase() ; |
||||||
|
} |
||||||
|
roar_ids.sort( function( lhs, rhs ) { |
||||||
|
lhs = gRoarScenarioIndex[ lhs ]._sort_name ; |
||||||
|
rhs = gRoarScenarioIndex[ rhs ]._sort_name ; |
||||||
|
if ( lhs < rhs ) |
||||||
|
return -1 ; |
||||||
|
else if ( lhs > rhs ) |
||||||
|
return +1 ; |
||||||
|
return 0 ; |
||||||
|
} ) ; |
||||||
|
|
||||||
|
// get the currently-active ROAR scenario
|
||||||
|
var curr_roar_id = $("input[name='ROAR_ID']").val() ; |
||||||
|
|
||||||
|
// load the select2
|
||||||
|
var buf = [] ; |
||||||
|
for ( var i=0 ; i < roar_ids.length ; ++i ) { |
||||||
|
roar_id = roar_ids[ i ] ; |
||||||
|
scenario = gRoarScenarioIndex[ roar_id ] ; |
||||||
|
// NOTE: The <option> text is what gets searched (_format_entry() generates what gets shown),
|
||||||
|
// so we include the scenario ID here, so that it also becomes searchable.
|
||||||
|
buf.push( "<option value='" + roar_id + "'" ) ; |
||||||
|
if ( roar_id === curr_roar_id ) { |
||||||
|
// FIXME! How can we scroll this into view? Calling scrollIntoView(),
|
||||||
|
// even in the "open" handler, causes weird problems.
|
||||||
|
buf.push( " selected" ) ; |
||||||
|
} |
||||||
|
buf.push( ">" ) ; |
||||||
|
buf.push( scenario.name + " " + scenario.scenario_id, |
||||||
|
"</option>" |
||||||
|
) ; |
||||||
|
} |
||||||
|
$sel.html( buf.join("") ) ; |
||||||
|
} |
||||||
|
|
||||||
|
function _format_entry( roar_id ) { |
||||||
|
// generate the HTML for a scenario
|
||||||
|
var scenario = gRoarScenarioIndex[ roar_id ] ; |
||||||
|
var buf = [ "<div class='scenario' data-roarid='", roar_id , "'>", |
||||||
|
scenario.name, |
||||||
|
" <span class='scenario-id'>[", strReplaceAll(scenario.scenario_id," "," "), "]</span>", |
||||||
|
" <span class='publication'>", scenario.publication, "</span>", |
||||||
|
"</div>" |
||||||
|
] ; |
||||||
|
return $( buf.join("") ) ; |
||||||
|
} |
||||||
|
|
||||||
|
// --------------------------------------------------------------------
|
||||||
|
|
||||||
|
function disconnect_roar() |
||||||
|
{ |
||||||
|
// disconnect from the ROAR scenario
|
||||||
|
set_roar_scenario( null ) ; |
||||||
|
} |
||||||
|
|
||||||
|
// --------------------------------------------------------------------
|
||||||
|
|
||||||
|
function go_to_roar_scenario() |
||||||
|
{ |
||||||
|
// go the currently-active ROAR scenario
|
||||||
|
var roar_id = $( "input[name='ROAR_ID']" ).val() ; |
||||||
|
var url = gRoarScenarioIndex[ roar_id ].url ; |
||||||
|
if ( gWebChannelHandler ) |
||||||
|
window.location = url ; // nb: AppWebPage will intercept this and launch a new browser window
|
||||||
|
else |
||||||
|
window.open( url ) ; |
||||||
|
} |
||||||
|
|
||||||
|
// --------------------------------------------------------------------
|
||||||
|
|
||||||
|
function set_roar_scenario( roar_id ) |
||||||
|
{ |
||||||
|
var total_playings ; |
||||||
|
function safe_score( nplayings ) { return total_playings === 0 ? 0 : nplayings / total_playings ; } |
||||||
|
function get_label( score ) { return total_playings === 0 ? "" : percentString( score ) ; } |
||||||
|
|
||||||
|
function do_set_roar_scenaro() { |
||||||
|
if ( roar_id ) { |
||||||
|
// save the ROAR ID
|
||||||
|
$( "input[name='ROAR_ID']" ).val( roar_id ) ; |
||||||
|
// update the progress bars
|
||||||
|
var scenario = gRoarScenarioIndex[ roar_id ] ; |
||||||
|
if ( ! scenario ) |
||||||
|
return ; |
||||||
|
var results = scenario.results ; |
||||||
|
if ( convert_roar_nat(results[0][0]) === get_player_nat(2) || convert_roar_nat(results[1][0]) === get_player_nat(1) ) { |
||||||
|
// FUDGE! The order of players returned by ROAR is indeterminate (and could change from call to call),
|
||||||
|
// so we try to show the results in the way that best matches what's on-screen.
|
||||||
|
results = [ results[1], results[0] ] ; |
||||||
|
} |
||||||
|
total_playings = results[0][1] + results[1][1] ; |
||||||
|
$( "#roar-info .name.player1" ).html( results[0][0] ) ; |
||||||
|
$( "#roar-info .count.player1" ).html( "(" + results[0][1] + ")" ) ; |
||||||
|
var score = 100 * safe_score( results[0][1] ) ; |
||||||
|
$( "#roar-info .progressbar.player1" ).progressbar( { value: 100-score } ) |
||||||
|
.find( ".label" ).text( get_label( score ) ) ; |
||||||
|
$( "#roar-info .name.player2" ).html( results[1][0] ) ; |
||||||
|
$( "#roar-info .count.player2" ).html( "(" + results[1][1] + ")" ) ; |
||||||
|
score = 100 * safe_score( results[1][1] ) ; |
||||||
|
$( "#roar-info .progressbar.player2" ).progressbar( { value: score } ) |
||||||
|
.find( ".label" ).text( get_label( score ) ) ; |
||||||
|
// show the ROAR scenario details
|
||||||
|
$( "#go-to-roar" ).attr( "title", scenario.name+" ["+scenario.scenario_id+"]\n" + scenario.publication ) ; |
||||||
|
// NOTE: We see the fade in if the panel is already visible and we load a scenario that has a ROAR ID,
|
||||||
|
// because we reset the scenario the scenario before loading another one, which causes the panel
|
||||||
|
// to be hidden. Fixing this is more trouble than it's worth... :-/
|
||||||
|
$( "#roar-info" ).fadeIn( 1*1000 ) ; |
||||||
|
} else { |
||||||
|
// there is no associated ROAR scenario - hide the info panel
|
||||||
|
$( "input[name='ROAR_ID']" ).val( "" ) ; |
||||||
|
$( "#roar-info" ).hide() ; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// set the ROAR scenario
|
||||||
|
_get_roar_scenario_index( do_set_roar_scenaro ) ; |
||||||
|
} |
||||||
|
|
||||||
|
// --------------------------------------------------------------------
|
||||||
|
|
||||||
|
function convert_roar_nat( roar_nat ) |
||||||
|
{ |
||||||
|
// clean up the ROAR nationality
|
||||||
|
roar_nat = roar_nat.toUpperCase() ; |
||||||
|
var pos = roar_nat.indexOf( "/" ) ; |
||||||
|
if ( pos > 0 ) |
||||||
|
roar_nat = roar_nat.substr( 0, pos ) ; // e.g. "British/Partisan" -> "British"
|
||||||
|
else { |
||||||
|
var match = roar_nat.match( /\(.*\)$/ ) ; |
||||||
|
if ( match ) |
||||||
|
roar_nat = roar_nat.substr( 0, roar_nat.length-match[0].length ).trim() ; // e.g. "Thai (Chinese)" -> "Thai"
|
||||||
|
} |
||||||
|
|
||||||
|
// try to match the ROAR nationality with one of ours
|
||||||
|
for ( var nat in gTemplatePack.nationalities ) { |
||||||
|
if ( roar_nat === gTemplatePack.nationalities[nat].display_name.toUpperCase() ) |
||||||
|
return nat ; |
||||||
|
} |
||||||
|
|
||||||
|
return null ; |
||||||
|
} |
@ -0,0 +1,120 @@ |
|||||||
|
<!doctype html> <!-- NOTE: For testing porpoises only! --> |
||||||
|
<html lang="en"> |
||||||
|
|
||||||
|
|
||||||
|
<head> |
||||||
|
<meta charset="utf-8"> |
||||||
|
<style> |
||||||
|
th, td { padding: 2px 5px ; text-align: left ; } |
||||||
|
th { background: #eee ; } |
||||||
|
</style> |
||||||
|
</head> |
||||||
|
|
||||||
|
<body> |
||||||
|
<div id="results" style="display:none;"></div> |
||||||
|
</body> |
||||||
|
|
||||||
|
<script src="{{url_for('static',filename='jquery/jquery-3.3.1.min.js')}}"></script> |
||||||
|
<script src="{{url_for('static',filename='roar.js')}}"></script> |
||||||
|
|
||||||
|
<script> |
||||||
|
gRoarScenarioIndex = null ; |
||||||
|
gTemplatePack = null ; |
||||||
|
</script> |
||||||
|
|
||||||
|
<script> |
||||||
|
$(document).ready( function () { |
||||||
|
|
||||||
|
// initialize |
||||||
|
var on_load_counter = 2 ; |
||||||
|
function on_data_loaded() { |
||||||
|
if ( --on_load_counter == 0 ) { |
||||||
|
// everything's loaded - generate the report |
||||||
|
check_roar() ; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// get the ROAR scenario index |
||||||
|
$.getJSON( "{{url_for('get_roar_scenario_index')}}", function(data) { |
||||||
|
gRoarScenarioIndex = data ; |
||||||
|
on_data_loaded() ; |
||||||
|
} ).fail( function( xhr, status, errorMsg ) { |
||||||
|
alert( "Can't get the ROAR scenario index:\n\n" + errorMsg ) ; |
||||||
|
} ) ; |
||||||
|
|
||||||
|
// get the template pack |
||||||
|
$.getJSON( "{{url_for('get_template_pack')}}", function(data) { |
||||||
|
gTemplatePack = data ; |
||||||
|
on_data_loaded() ; |
||||||
|
} ).fail( function( xhr, status, errorMsg ) { |
||||||
|
alert( "Can't get the template pack:\n\n" + errorMsg ) ; |
||||||
|
} ) ; |
||||||
|
} ) ; |
||||||
|
|
||||||
|
function check_roar() |
||||||
|
{ |
||||||
|
// initialize |
||||||
|
var buf = [] ; |
||||||
|
|
||||||
|
// generate the list of nationalities in ROAR |
||||||
|
var roar_nats = {} ; |
||||||
|
function on_nat( nat ) { |
||||||
|
if ( nat in roar_nats ) |
||||||
|
++ roar_nats[nat] ; |
||||||
|
else |
||||||
|
roar_nats[nat] = 1 ; |
||||||
|
} |
||||||
|
for ( var roar_id in gRoarScenarioIndex ) { |
||||||
|
if ( roar_id[0] == "_" ) |
||||||
|
continue ; |
||||||
|
var scenario = gRoarScenarioIndex[ roar_id ] ; |
||||||
|
on_nat( scenario.results[0][0] ) ; |
||||||
|
on_nat( scenario.results[1][0] ) ; |
||||||
|
} |
||||||
|
|
||||||
|
// sort the results |
||||||
|
var roar_nats_sorted = Object.keys( roar_nats ) ; |
||||||
|
roar_nats_sorted.sort( function( lhs, rhs ) { |
||||||
|
if ( roar_nats[lhs] < roar_nats[rhs] ) |
||||||
|
return +1 ; |
||||||
|
else if ( roar_nats[lhs] > roar_nats[rhs] ) |
||||||
|
return -1 ; |
||||||
|
else { |
||||||
|
if ( lhs < rhs ) |
||||||
|
return -1 ; |
||||||
|
else if ( lhs > rhs ) |
||||||
|
return +1 ; |
||||||
|
} |
||||||
|
return 0; |
||||||
|
} ) ; |
||||||
|
|
||||||
|
// output the results |
||||||
|
buf.push( "<table>" ) ; |
||||||
|
buf.push( "<tr>", "<th>ROAR nationality", "<th>Count", "<th><tt>vasl-templates</tt> nationality" ) ; |
||||||
|
for ( var i=0 ; i < roar_nats_sorted.length ; ++i ) { |
||||||
|
var nat = roar_nats_sorted[i] ; |
||||||
|
buf.push( "<tr>", "<td>"+nat, "<td>"+roar_nats[nat] ) ; |
||||||
|
nat = convert_roar_nat( nat ) ; |
||||||
|
if ( nat ) |
||||||
|
buf.push( "<td>" + nat ) ; |
||||||
|
} |
||||||
|
buf.push( "</table>" ) ; |
||||||
|
|
||||||
|
// check for spaces in scenario ID's |
||||||
|
buf.push( "<h2>Scenario ID's with spaces</h2>" ) ; |
||||||
|
buf.push( "<ul>" ) ; |
||||||
|
for ( roar_id in gRoarScenarioIndex ) { |
||||||
|
if ( roar_id[0] === "_" ) |
||||||
|
continue ; |
||||||
|
scenario = gRoarScenarioIndex[ roar_id ] ; |
||||||
|
if ( scenario.scenario_id.indexOf( " " ) !== -1 ) |
||||||
|
buf.push( "<li>" + roar_id + ": " + scenario.name + " [" + scenario.scenario_id + "]" ) ; |
||||||
|
} |
||||||
|
buf.push( "</ul>" ) ; |
||||||
|
|
||||||
|
$("#results").html( buf.join("") ).show() ; |
||||||
|
} |
||||||
|
|
||||||
|
</script> |
||||||
|
|
||||||
|
</html> |
@ -0,0 +1,3 @@ |
|||||||
|
<div id="select-roar-scenario" style="display:none;"> |
||||||
|
<select></select> |
||||||
|
</div> |
@ -0,0 +1,29 @@ |
|||||||
|
{ |
||||||
|
|
||||||
|
"1": { "scenario_id": "TEST 1", "name": "Fighting Withdrawal", |
||||||
|
"publication": "Beyond Valor", |
||||||
|
"results": [ [ "Finnish", 200 ], [ "Russian", 300 ] ], |
||||||
|
"url": "http://test.com/1" |
||||||
|
}, |
||||||
|
|
||||||
|
"2": { "scenario_id": "TEST 2", "name": "Whitewash 1", |
||||||
|
"results": [ [ "American", 10 ], [ "Japanese", 0 ] ], |
||||||
|
"url": "http://test.com/2" |
||||||
|
}, |
||||||
|
|
||||||
|
"3": { "scenario_id": "TEST 3", "name": "Whitewash 2", |
||||||
|
"results": [ [ "American", 0 ], [ "Japanese", 10 ] ], |
||||||
|
"url": "http://test.com/3" |
||||||
|
}, |
||||||
|
|
||||||
|
"4": { "scenario_id": "TEST 4", "name": "No playings", |
||||||
|
"results": [ [ "British", 0 ], [ "French", 0 ] ], |
||||||
|
"url": "http://test.com/4" |
||||||
|
}, |
||||||
|
|
||||||
|
"5": { "scenario_id": "TEST 5", "name": "Unknown nationality", |
||||||
|
"results": [ [ "American", 1 ], [ "Martian", 1 ] ], |
||||||
|
"url": "http://test.com/5" |
||||||
|
} |
||||||
|
|
||||||
|
} |
@ -0,0 +1,148 @@ |
|||||||
|
"""Test ROAR integration.""" |
||||||
|
|
||||||
|
import re |
||||||
|
|
||||||
|
from selenium.webdriver.support.ui import Select |
||||||
|
from selenium.webdriver.common.keys import Keys |
||||||
|
|
||||||
|
from vasl_templates.webapp.tests.utils import init_webapp, select_tab, select_menu_option, click_dialog_button, \ |
||||||
|
set_stored_msg_marker, get_stored_msg, set_template_params, add_simple_note, \ |
||||||
|
find_child, find_children, wait_for_elem |
||||||
|
|
||||||
|
# --------------------------------------------------------------------- |
||||||
|
|
||||||
|
def test_roar( webapp, webdriver ): |
||||||
|
"""Test ROAR integration.""" |
||||||
|
|
||||||
|
# initialize |
||||||
|
init_webapp( webapp, webdriver ) |
||||||
|
|
||||||
|
# check the ROAR info panel |
||||||
|
_check_roar_info( webdriver, None ) |
||||||
|
|
||||||
|
# select a ROAR scenario |
||||||
|
_select_roar_scenario( "fighting withdrawal" ) |
||||||
|
_check_roar_info( webdriver, ( |
||||||
|
( "Fighting Withdrawal", "TEST 1" ), |
||||||
|
( "Finnish", 200, "Russian", 300 ), |
||||||
|
( 40, 60 ) |
||||||
|
) ) |
||||||
|
|
||||||
|
# select some other ROAR scenarios |
||||||
|
# NOTE: The scenario name/ID are already populated, so they don't get updated with the new details. |
||||||
|
_select_roar_scenario( "whitewash 1" ) |
||||||
|
_check_roar_info( webdriver, ( |
||||||
|
( "Fighting Withdrawal", "TEST 1" ), |
||||||
|
( "American", 10, "Japanese", 0 ), |
||||||
|
( 100, 0 ) |
||||||
|
) ) |
||||||
|
_select_roar_scenario( "whitewash 2" ) |
||||||
|
_check_roar_info( webdriver, ( |
||||||
|
( "Fighting Withdrawal", "TEST 1" ), |
||||||
|
( "American", 0, "Japanese", 10 ), |
||||||
|
( 0, 100 ) |
||||||
|
) ) |
||||||
|
|
||||||
|
# unlink from the ROAR scenario |
||||||
|
btn = find_child( "#disconnect-roar" ) |
||||||
|
btn.click() |
||||||
|
_check_roar_info( webdriver, None ) |
||||||
|
|
||||||
|
# select another ROAR scenario (that has no playings) |
||||||
|
set_template_params( { "SCENARIO_NAME": "", "SCENARIO_ID": "" } ) |
||||||
|
_select_roar_scenario( "no playings" ) |
||||||
|
_check_roar_info( webdriver, ( |
||||||
|
( "No playings", "TEST 4" ), |
||||||
|
( "British", 0, "French", 0 ), |
||||||
|
None |
||||||
|
) ) |
||||||
|
|
||||||
|
# --------------------------------------------------------------------- |
||||||
|
|
||||||
|
def test_setting_players( webapp, webdriver ): |
||||||
|
"""Test setting players after selecting a ROAR scenario.""" |
||||||
|
|
||||||
|
# initialize |
||||||
|
init_webapp( webapp, webdriver ) |
||||||
|
|
||||||
|
# select a ROAR scenario |
||||||
|
_select_roar_scenario( "fighting withdrawal" ) |
||||||
|
_check_players( "finnish", "russian" ) |
||||||
|
|
||||||
|
# add something to the Player 1 OB |
||||||
|
select_tab( "ob1" ) |
||||||
|
add_simple_note( find_child("#ob_setups-sortable_1"), "a setup note", None ) |
||||||
|
|
||||||
|
# select another ROAR scenario |
||||||
|
select_tab( "scenario" ) |
||||||
|
_select_roar_scenario( "whitewash 1" ) |
||||||
|
_check_players( "finnish", "japanese" ) # nb: player 1 remains unchanged |
||||||
|
|
||||||
|
# add something to the Player 2 OB |
||||||
|
select_tab( "ob2" ) |
||||||
|
add_simple_note( find_child("#ob_setups-sortable_2"), "another setup note", None ) |
||||||
|
|
||||||
|
# select another ROAR scenario |
||||||
|
select_tab( "scenario" ) |
||||||
|
_select_roar_scenario( "no playings" ) |
||||||
|
_check_players( "finnish", "japanese" ) # nb: both players remain unchanged |
||||||
|
|
||||||
|
# reset the scenario and select a ROAR scenario with an unknown nationality |
||||||
|
select_menu_option( "new_scenario" ) |
||||||
|
click_dialog_button( "OK" ) # nb: dismiss the "discard changes?" prompt |
||||||
|
_ = set_stored_msg_marker( "_last-warning_" ) |
||||||
|
_select_roar_scenario( "unknown nationality" ) |
||||||
|
_check_players( "american", "russian" ) |
||||||
|
last_warning = get_stored_msg( "_last-warning_" ) |
||||||
|
assert re.search( r"Unrecognized nationality.+\bMartian\b", last_warning ) |
||||||
|
|
||||||
|
# --------------------------------------------------------------------- |
||||||
|
|
||||||
|
def _select_roar_scenario( scenario_name ): |
||||||
|
"""Select a ROAR scenario.""" |
||||||
|
btn = find_child( "#search-roar" ) |
||||||
|
btn.click() |
||||||
|
dlg = wait_for_elem( 2, ".ui-dialog.select-roar-scenario" ) |
||||||
|
search_field = find_child( "input", dlg ) |
||||||
|
search_field.send_keys( scenario_name ) |
||||||
|
elems = find_children( ".select2-results li", dlg ) |
||||||
|
assert len(elems) == 1 |
||||||
|
search_field.send_keys( Keys.RETURN ) |
||||||
|
|
||||||
|
def _check_roar_info( webdriver, expected ): |
||||||
|
"""Check the state of the ROAR info panel.""" |
||||||
|
|
||||||
|
# check if the panel is displayed or hidden |
||||||
|
panel = find_child( "#roar-info" ) |
||||||
|
if not expected: |
||||||
|
assert not panel.is_displayed() |
||||||
|
return |
||||||
|
assert panel.is_displayed() |
||||||
|
|
||||||
|
# check the displayed information |
||||||
|
assert find_child( ".name.player1", panel ).text == expected[1][0] |
||||||
|
assert find_child( ".count.player1", panel ).text == "({})".format( expected[1][1] ) |
||||||
|
assert find_child( ".name.player2", panel ).text == expected[1][2] |
||||||
|
assert find_child( ".count.player2", panel ).text == "({})".format( expected[1][3] ) |
||||||
|
|
||||||
|
# check the progress bars |
||||||
|
progress1 = find_child( ".progressbar.player1", panel ) |
||||||
|
progress2 = find_child( ".progressbar.player2", panel ) |
||||||
|
if expected[2]: |
||||||
|
label1 = "{}%".format( expected[2][0] ) |
||||||
|
label2 = "{}%".format( expected[2][1] ) |
||||||
|
expected1, expected2 = 100-expected[2][0], expected[2][1] |
||||||
|
else: |
||||||
|
label1 = label2 = "" |
||||||
|
expected1, expected2 = 100, 0 |
||||||
|
assert find_child( ".label", progress1 ).text == label1 |
||||||
|
assert webdriver.execute_script( "return $(arguments[0]).progressbar('value')", progress1 ) == expected1 |
||||||
|
assert find_child( ".label", progress2 ).text == label2 |
||||||
|
assert webdriver.execute_script( "return $(arguments[0]).progressbar('value')", progress2 ) == expected2 |
||||||
|
|
||||||
|
def _check_players( expected1, expected2 ): |
||||||
|
"""Check the selected players.""" |
||||||
|
sel = Select( find_child( "select[name='PLAYER_1']" ) ) |
||||||
|
assert sel.first_selected_option.get_attribute("value") == expected1 |
||||||
|
sel = Select( find_child( "select[name='PLAYER_2']" ) ) |
||||||
|
assert sel.first_selected_option.get_attribute("value") == expected2 |
Loading…
Reference in new issue