diff --git a/vasl_templates/webapp/__init__.py b/vasl_templates/webapp/__init__.py index 374e115..517d93e 100644 --- a/vasl_templates/webapp/__init__.py +++ b/vasl_templates/webapp/__init__.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 diff --git a/vasl_templates/webapp/roar.py b/vasl_templates/webapp/roar.py new file mode 100644 index 0000000..e255c4a --- /dev/null +++ b/vasl_templates/webapp/roar.py @@ -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" ) diff --git a/vasl_templates/webapp/static/css/select-roar-scenario-dialog.css b/vasl_templates/webapp/static/css/select-roar-scenario-dialog.css new file mode 100644 index 0000000..37faccc --- /dev/null +++ b/vasl_templates/webapp/static/css/select-roar-scenario-dialog.css @@ -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 ; } diff --git a/vasl_templates/webapp/static/css/tabs-scenario.css b/vasl_templates/webapp/static/css/tabs-scenario.css index c24c340..0202a20 100644 --- a/vasl_templates/webapp/static/css/tabs-scenario.css +++ b/vasl_templates/webapp/static/css/tabs-scenario.css @@ -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 ; } diff --git a/vasl_templates/webapp/static/help/index.html b/vasl_templates/webapp/static/help/index.html index 316f3cc..f6e456d 100644 --- a/vasl_templates/webapp/static/help/index.html +++ b/vasl_templates/webapp/static/help/index.html @@ -136,6 +136,7 @@ The program will then not attempt to create the embedded browser, and will just
First, we enter the basic details about the scenario.
Click on one of the Snippet buttons, and the program will generate an HTML snippet and put it into your clipboard, which you can then copy into a VASL label. +
You can also click on the Search ROAR button, to look for a scenario in ROAR. The basic details for the scenario will be loaded, along with the latest results.
diff --git a/vasl_templates/webapp/static/images/cross.png b/vasl_templates/webapp/static/images/cross.png new file mode 100644 index 0000000..c1cd9a1 Binary files /dev/null and b/vasl_templates/webapp/static/images/cross.png differ diff --git a/vasl_templates/webapp/static/images/search.png b/vasl_templates/webapp/static/images/search.png new file mode 100644 index 0000000..26c38df Binary files /dev/null and b/vasl_templates/webapp/static/images/search.png differ diff --git a/vasl_templates/webapp/static/main.js b/vasl_templates/webapp/static/main.js index 5c94685..8e47ecd 100644 --- a/vasl_templates/webapp/static/main.js +++ b/vasl_templates/webapp/static/main.js @@ -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( "" ) + .click( search_roar ) ; + $("#go-to-roar").button( {} ).click( go_to_roar_scenario ) ; + $("#disconnect-roar").button( {} ) + .html( "" ) + .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() ) ; } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/vasl_templates/webapp/static/roar.js b/vasl_templates/webapp/static/roar.js new file mode 100644 index 0000000..70f0e55 --- /dev/null +++ b/vasl_templates/webapp/static/roar.js @@ -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: