Added ROAR integration.

master
Pacman Ghost 5 years ago
parent bc58b8c9c4
commit 0a91f820f3
  1. 6
      vasl_templates/webapp/__init__.py
  2. 130
      vasl_templates/webapp/roar.py
  3. 19
      vasl_templates/webapp/static/css/select-roar-scenario-dialog.css
  4. 24
      vasl_templates/webapp/static/css/tabs-scenario.css
  5. 1
      vasl_templates/webapp/static/help/index.html
  6. BIN
      vasl_templates/webapp/static/images/cross.png
  7. BIN
      vasl_templates/webapp/static/images/search.png
  8. 30
      vasl_templates/webapp/static/main.js
  9. 310
      vasl_templates/webapp/static/roar.js
  10. 7
      vasl_templates/webapp/static/snippets.js
  11. 10
      vasl_templates/webapp/static/utils.js
  12. 120
      vasl_templates/webapp/templates/check-roar.html
  13. 4
      vasl_templates/webapp/templates/index.html
  14. 3
      vasl_templates/webapp/templates/select-roar-scenario-dialog.html
  15. 24
      vasl_templates/webapp/templates/tabs-scenario.html
  16. 29
      vasl_templates/webapp/tests/fixtures/roar-scenario-index.json
  17. 14
      vasl_templates/webapp/tests/remote.py
  18. 14
      vasl_templates/webapp/tests/test_dirty_scenario_checks.py
  19. 148
      vasl_templates/webapp/tests/test_roar.py
  20. 2
      vasl_templates/webapp/tests/test_scenario_persistence.py
  21. 1
      vasl_templates/webapp/tests/test_vassal.py
  22. 24
      vasl_templates/webapp/tests/utils.py

@ -36,6 +36,11 @@ def _on_startup():
from vasl_templates.webapp.vo_notes import load_vo_notes #pylint: disable=cyclic-import
load_vo_notes()
# initialize ROAR integration
from vasl_templates.webapp.roar import init_roar #pylint: disable=cyclic-import
from vasl_templates.webapp.main import startup_msg_store #pylint: disable=cyclic-import
init_roar( startup_msg_store )
# ---------------------------------------------------------------------
def _load_config( fname, section ):
@ -95,6 +100,7 @@ import vasl_templates.webapp.snippets #pylint: disable=cyclic-import
import vasl_templates.webapp.files #pylint: disable=cyclic-import
import vasl_templates.webapp.vassal #pylint: disable=cyclic-import
import vasl_templates.webapp.vo_notes #pylint: disable=cyclic-import
import vasl_templates.webapp.roar #pylint: disable=cyclic-import
if app.config.get( "ENABLE_REMOTE_TEST_CONTROL" ):
print( "*** WARNING: Remote test control enabled! ***" )
import vasl_templates.webapp.testing #pylint: disable=cyclic-import

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

@ -5,6 +5,8 @@
#panel-scenario .row { display: flex ; align-items: center ; }
#panel-scenario input { flex-grow: 1 ; }
#panel-scenario input[name='SCENARIO_ID'] { margin-left: 0.25em ; width: 80px ; flex-grow: 0 ; }
#panel-scenario #search-roar { width: 25px ; height: 22px ; margin: 0 0 0 0.25em ; padding: 0 ; margin-top:-4px; }
#panel-scenario #search-roar img { margin-top: 2px ; width: 16px ; }
#panel-scenario input[name='SCENARIO_DATE'] { width: 6em ; flex-grow: 0 ; }
#panel-scenario label[for='PLAYER_1'] { margin-top: 2px ; }
@ -22,6 +24,28 @@
#panel-scenario .select2-container[name="SCENARIO_THEATER"] .select2-selection { height: 22px !important ; margin-top: -4px ;}
#panel-scenario .select2-container[name="SCENARIO_THEATER"] .select2-selection__arrow { margin-top: -6px ; }
/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
#roar-info { position: relative ; margin-top: 0.75em ; border: 1px dotted #ccc ; border-radius: 3px ; padding: 0.5em ; background: #fcfcfc ; }
#roar-info .name { font-size: 90% ; }
#roar-info .count { font-size: 70% ; font-style: italic ; color: #666 ; }
#roar-info .progressbar { width: 4em ; height: 1em ; }
#roar-info .progressbar.player1 { background: #ffffcc ; border-top-right-radius: 0 ; border-bottom-right-radius: 0 ; border-right: 0 !important ; }
#roar-info .progressbar.player1 .ui-progressbar-value { background: #fff ; border-right: 1px solid #eee ; }
#roar-info .progressbar.player2 { background: #fff ; border-top-left-radius: 0 ; border-bottom-left-radius: 0 ; }
#roar-info .progressbar.player2 .ui-progressbar-value { background: #ffffcc ; border-right: 1px solid #eee ; }
#roar-info .progressbar { position: relative ; }
#roar-info .progressbar .label { position: absolute ; font-size: 80% ; font-style: italic ; color: #666 ; left: 35% ; }
input[name='ROAR_ID'] { flex-grow: 0 !important ; width: 3em ; }
button#go-to-roar { height: 2em ; width: 4em ; margin-right: 0.5em ; padding: 2px 5px ; background: #ffffcc ; font-weight: bold ; }
button#disconnect-roar { background: #fcfcfc ; width: 20px ; height: 19px ; padding: 0 ; border: none ;
position: absolute ; right: -2px ; top: 0 ;
}
button#disconnect-roar img { width: 12px ; }
/* -------------------------------------------------------------------- */
#panel-vc { height: 100% ; display: flex ; flex-direction: column ; }

@ -136,6 +136,7 @@ The program will then not attempt to create the embedded browser, and will just
<p> <img src="images/scenario.png" class="preview" style="width:20em;float:left;">
First, we enter the basic details about the scenario.
<p> Click on one of the <em>Snippet</em> buttons, and the program will generate an HTML snippet and put it into your clipboard, which you can then copy into a VASL label.
<p> You can also click on the <em>Search ROAR</em> button, to look for a scenario in ROAR. The basic details for the scenario will be loaded, along with the latest results.
<br clear="all">
<p> <img src="images/draggable-overlays.png" class="preview" style="width:20em;float:right;">

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

@ -103,6 +103,15 @@ $(document).ready( function () {
var navHeight = $("#tabs .ui-tabs-nav").height() ;
$("#tabs .ui-tabs-nav a").click( function() { $(this).blur() ; } ) ;
// initialize ROAR integration
$("#search-roar").button( {} )
.html( "<img src='" + gImagesBaseUrl + "/search.png'>" )
.click( search_roar ) ;
$("#go-to-roar").button( {} ).click( go_to_roar_scenario ) ;
$("#disconnect-roar").button( {} )
.html( "<img src='" + gImagesBaseUrl + "/cross.png'>" )
.click( disconnect_roar ) ;
// initialize the scenario theater
init_select2( $("select[name='SCENARIO_THEATER']"), "5em", false, null ) ;
@ -634,12 +643,7 @@ function on_player_change_with_confirm( player_no )
return ;
// check if we should confirm this operation
var is_empty = true ;
$( "#tabs-ob" + player_no + " .sortable" ).each( function() {
if ( $(this).children( "li" ).length > 0 )
is_empty = false ;
} ) ;
if ( is_empty ) {
if ( is_player_ob_empty( player_no ) ) {
// nope - just do it
on_player_change( player_no ) ;
} else {
@ -654,6 +658,17 @@ function on_player_change_with_confirm( player_no )
}
}
function is_player_ob_empty( player_no )
{
// check if the specified player's OB is empty
var is_empty = true ;
$( "#tabs-ob" + player_no + " .sortable" ).each( function() {
if ( $(this).children( "li" ).length > 0 )
is_empty = false ;
} ) ;
return is_empty ;
}
function on_player_change( player_no )
{
// update the tab label
@ -689,6 +704,9 @@ function on_player_change( player_no )
$("input[name='OB_VEHICLES_WIDTH_"+player_no+"']").val( "" ) ;
$( "#ob_ordnance-sortable_" + player_no ).sortable2( "delete-all" ) ;
$("input[name='OB_ORDNANCE_WIDTH_"+player_no+"']").val( "" ) ;
// update the ROAR info panel
set_roar_scenario( $("input[name='ROAR_ID']").val() ) ;
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

@ -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( "&quot;", "&quot;" ) ;
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," ","&nbsp;"), "]</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 ;
}

@ -1294,8 +1294,11 @@ function do_load_scenario_data( params )
warnings.push( "Invalid scenario date: " + escapeHTML( params[key] ) ) ;
}
}
else
else {
$elem.val( params[key] ) ;
if ( key === "ROAR_ID" )
set_roar_scenario( params[key] ) ;
}
if ( $elem[0].nodeName.toLowerCase() === "select" )
$elem.trigger( "change" ) ;
params_loaded[key] = true ;
@ -1387,6 +1390,8 @@ function do_load_scenario_data( params )
set_param( $(this), key ).trigger( "change" ) ;
} ) ;
}
if ( ! params.ROAR_ID )
set_roar_scenario( null ) ;
// look for unrecognized keys
var buf = [] ;

@ -365,6 +365,16 @@ function pluralString( n, str1, str2 )
return (n == 1) ? str1 : str2 ;
}
function percentString( val )
{
val = Math.round( val ) ;
if ( val < 0 )
val = 0 ;
else if ( val > 100 )
val = 100 ;
return val + "%" ;
}
function strReplaceAll( val, searchFor, replaceWith )
{
// str.replace() only replaces a single instance!?!? :wtf:

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

@ -22,6 +22,7 @@
<link rel="stylesheet" type="text/css" href="{{url_for('static',filename='css/ask-dialog.css')}}" />
<link rel="stylesheet" type="text/css" href="{{url_for('static',filename='css/select-vo-dialog.css')}}" />
<link rel="stylesheet" type="text/css" href="{{url_for('static',filename='css/edit-vo-dialog.css')}}" />
<link rel="stylesheet" type="text/css" href="{{url_for('static',filename='css/select-roar-scenario-dialog.css')}}" />
<link rel="stylesheet" type="text/css" href="{{url_for('static',filename='css/vassal.css')}}" />
<link rel="stylesheet" type="text/css" href="{{url_for('static',filename='css/snippets.css')}}" />
<link rel="stylesheet" type="text/css" href="{{url_for('static',filename='css/user-settings-dialog.css')}}" />
@ -63,6 +64,7 @@
{%include "select-vo-dialog.html"%}
{%include "select-vo-image-dialog.html"%}
{%include "edit-vo-dialog.html"%}
{%include "select-roar-scenario-dialog.html"%}
{%include "vassal.html"%}
{%include "snippets.html"%}
@ -98,6 +100,7 @@ gOrdnanceListingsUrl = "{{url_for('get_ordnance_listings',merge_common=1)}}" ;
gVehicleNotesUrl = "{{url_for('get_vehicle_notes')}}" ;
gOrdnanceNotesUrl = "{{url_for('get_ordnance_notes')}}" ;
gGetVaslPieceInfoUrl = "{{url_for('get_vasl_piece_info')}}" ;
gGetRoarScenarioIndexUrl = "{{url_for('get_roar_scenario_index')}}" ;
gUpdateVsavUrl = "{{url_for('update_vsav')}}" ;
gMakeSnippetImageUrl = "{{url_for('make_snippet_image')}}" ;
gHelpUrl = "{{url_for('show_help')}}" ;
@ -110,6 +113,7 @@ gHelpUrl = "{{url_for('show_help')}}" ;
<script src="{{url_for('static',filename='vo.js')}}"></script>
<script src="{{url_for('static',filename='vo2.js')}}"></script>
<script src="{{url_for('static',filename='vassal.js')}}"></script>
<script src="{{url_for('static',filename='roar.js')}}"></script>
<script src="{{url_for('static',filename='sortable.js')}}"></script>
<script src="{{url_for('static',filename='user_settings.js')}}"></script>
<script src="{{url_for('static',filename='utils.js')}}"></script>

@ -0,0 +1,3 @@
<div id="select-roar-scenario" style="display:none;">
<select></select>
</div>

@ -4,12 +4,13 @@
<fieldset name="scenario" class="tl" style="display:none;"> <legend>S<u>c</u>enario</legend>
<div id="panel-scenario">
<div class='row'>
<div class="row">
<label for="SCENARIO_NAME">Name:</label>
<input name="SCENARIO_NAME" type="text" class="param">
<input name="SCENARIO_ID" type="text" class="param" title="Scenario ID">
<button id="search-roar" title="Search ROAR"></button>
</div>
<div class='row'>
<div class="row">
<label for="SCENARIO_LOCATION">Location:</label>
<input name="SCENARIO_LOCATION" type="text" class="param">
<select name="SCENARIO_THEATER" class="param" title="Scenario theater">
@ -19,7 +20,7 @@
<option value="other">other</option>
</select>
</div>
<div class='row'>
<div class="row">
<label for="SCENARIO_DATE">Date:</label>
<input name="SCENARIO_DATE" type="text" size="10" class="param">
<span class="spacer"></span>
@ -28,19 +29,19 @@
<button class="generate" data-id="scenario">Snippet</button>
</span>
</div>
<div class='row' style="margin-top:0.5em;">
<div class="row" style="margin-top:0.5em;">
<label></label>
<span style='width:9.45em'></span>
<label class="header" for="ELR">ELR</label>
<label class="header" for="SAN">SAN</label>
</div>
<div class='row'>
<div class="row">
<label for="PLAYER_1"><u>P</u>layer 1:</label>
<select name="PLAYER_1" class="param"></select>
<select name="PLAYER_1_ELR" class="param"></select>
<select name="PLAYER_1_SAN" class="param"></select>
</div>
<div class='row'>
<div class="row">
<label for="PLAYER_2">Player 2:</label>
<select name="PLAYER_2" class="param"></select>
<select name="PLAYER_2_ELR" class="param"></select>
@ -50,6 +51,17 @@
<button class="generate" data-id="players">Snippet</button>
</span>
</div>
<div id="roar-info" class="row" style="display:none;">
<button id="go-to-roar">ROAR</button>
<input name="ROAR_ID" type="text" size="2" class="param" style="display:none;"> &nbsp;
<span class="name player1"></span>&nbsp;<span class="count player1"></span>
&nbsp;
<div class="progressbar player1"><div class="label"></div></div>
<div class="progressbar player2"><div class="label"></div></div>
&nbsp;
<span class="name player2"></span>&nbsp;<span class="count player2"></span>
<button id="disconnect-roar" title="Unlink from this ROAR scenario."></button>
</div>
</div>
</fieldset>

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

@ -22,6 +22,7 @@ from vasl_templates.webapp.config.constants import DATA_DIR
from vasl_templates.webapp.vasl_mod import set_vasl_mod
from vasl_templates.webapp import main as webapp_main
from vasl_templates.webapp import snippets as webapp_snippets
from vasl_templates.webapp import roar as webapp_roar
from vasl_templates.webapp import vasl_mod as vasl_mod_module
_logger = logging.getLogger( "control_tests" )
@ -287,3 +288,16 @@ class ControlTests:
_logger.info( "Reseting the default template pack." )
globvars.template_pack = None
return self
def _set_roar_scenario_index( self, fname=None ):
"""Set the ROAR scenario index file."""
if fname:
dname = os.path.join( os.path.split(__file__)[0], "fixtures" )
fname = os.path.join( dname, fname )
_logger.info( "Setting the ROAR scenario index file: %s", fname )
with open( fname, "r" ) as fp:
with webapp_roar._roar_scenario_index_lock: #pylint: disable=protected-access
webapp_roar._roar_scenario_index = json.load( fp ) #pylint: disable=protected-access
else:
assert False
return self

@ -57,9 +57,12 @@ def test_dirty_scenario_checks( webapp, webdriver ):
] if e )
if target.tag_name in ("input","textarea"):
prev_val = target.get_attribute( "value" )
target.clear()
new_val = "01/01/2000" if param == "SCENARIO_DATE" else "changed value"
target.send_keys( new_val )
if target.is_displayed():
target.clear()
target.send_keys( new_val )
else:
webdriver.execute_script( "arguments[0].value = arguments[1]", target, new_val )
return target, prev_val, new_val
elif target.tag_name == "select":
sel = Select( target )
@ -90,8 +93,11 @@ def test_dirty_scenario_checks( webapp, webdriver ):
elif param in VEHICLE_ORDNANCE:
drag_sortable_entry_to_trash( state, 0 )
elif state[0].tag_name in ("input","textarea"):
state[0].clear()
state[0].send_keys( state[1] )
if state[0].is_displayed():
state[0].clear()
state[0].send_keys( state[1] )
else:
webdriver.execute_script( "arguments[0].value = arguments[1]", state[0], state[1] )
elif state[0].tag_name == "select":
select_droplist_val( Select(state[0]), state[1] )
else:

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

@ -18,6 +18,7 @@ ALL_SCENARIO_PARAMS = {
"SCENARIO_NAME", "SCENARIO_ID",
"SCENARIO_LOCATION", "SCENARIO_THEATER",
"SCENARIO_DATE", "SCENARIO_WIDTH",
"ROAR_ID",
"PLAYER_1", "PLAYER_1_ELR", "PLAYER_1_SAN",
"PLAYER_2", "PLAYER_2_ELR", "PLAYER_2_SAN",
"VICTORY_CONDITIONS", "VICTORY_CONDITIONS_WIDTH",
@ -70,6 +71,7 @@ def test_scenario_persistence( webapp, webdriver ): #pylint: disable=too-many-st
"SCENARIO_THEATER": "PTO",
"SCENARIO_DATE": "12/31/1945",
"SCENARIO_WIDTH": "101",
"ROAR_ID": "",
"PLAYER_1": "russian", "PLAYER_1_ELR": "1", "PLAYER_1_SAN": "2",
"PLAYER_2": "german", "PLAYER_2_ELR": "3", "PLAYER_2_SAN": "4",
"VICTORY_CONDITIONS": "just do it!", "VICTORY_CONDITIONS_WIDTH": "102",

@ -52,6 +52,7 @@ def test_full_update( webapp, webdriver ):
"SCENARIO_THEATER": "PTO",
"SCENARIO_DATE": "12/31/1945",
"SCENARIO_WIDTH": "101",
"ROAR_ID": "",
"PLAYER_1": "russian", "PLAYER_1_ELR": "5", "PLAYER_1_SAN": "4",
"PLAYER_2": "german", "PLAYER_2_ELR": "3", "PLAYER_2_SAN": "2",
"VICTORY_CONDITIONS": "Just do it!", "VICTORY_CONDITIONS_WIDTH": "102",

@ -65,7 +65,8 @@ def init_webapp( webapp, webdriver, **options ):
.set_vasl_mod( vmod=None, extns_dtype=None ) \
.set_vassal_engine( vengine=None ) \
.set_vo_notes_dir( dtype=None ) \
.set_user_files_dir( dtype=None )
.set_user_files_dir( dtype=None ) \
.set_roar_scenario_index( fname="roar-scenario-index.json" )
if "reset" in options:
options.pop( "reset" )( control_tests )
@ -260,13 +261,20 @@ def set_template_params( params ): #pylint: disable=too-many-branches
if elem.tag_name == "select":
select_droplist_val( Select(elem), val )
else:
elem.clear()
if val:
elem.send_keys( val )
if key == "SCENARIO_DATE":
elem.send_keys( Keys.TAB ) # nb: force the calendar popup to close :-/
wait_for( 5, lambda: find_child("#ui-datepicker-div").value_of_css_property("display") == "none" )
time.sleep( 0.25 )
if elem.is_displayed():
elem.clear()
if val:
elem.send_keys( val )
if key == "SCENARIO_DATE":
elem.send_keys( Keys.TAB ) # nb: force the calendar popup to close :-/
wait_for( 5,
lambda: find_child( "#ui-datepicker-div" ).value_of_css_property( "display" ) == "none"
)
time.sleep( 0.25 )
else:
# FUDGE! Selenium can't interact with hidden elements, so we do it like this.
# However, we don't do this for everything since it doesn't always triggers events.
_webdriver.execute_script( "arguments[0].value = arguments[1]", elem, val )
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

Loading…
Cancel
Save