@ -0,0 +1,62 @@ |
||||
#!/usr/bin/env python3 |
||||
""" Check how scenarios at the ASL Scenario Archive are connected to those at ROAR. """ |
||||
|
||||
import sys |
||||
import json |
||||
|
||||
from vasl_templates.webapp.scenarios import _match_roar_scenario, \ |
||||
_asa_scenarios, _build_asa_scenario_index, _roar_scenarios, _build_roar_scenario_index |
||||
|
||||
# --------------------------------------------------------------------- |
||||
|
||||
def asa_string( s ): |
||||
"""Return an ASL Scenario Archive scenario as a string.""" |
||||
return "[{}] {} ({})".format( |
||||
s["scenario_id"], s.get("title"), s.get("sc_id") |
||||
) |
||||
|
||||
def roar_string( s ): |
||||
"""Return ROAR scenario as a string.""" |
||||
return "[{}] {} ({})".format( |
||||
s["roar_id"], s.get("name"), s.get("scenario_id") |
||||
) |
||||
|
||||
# --------------------------------------------------------------------- |
||||
|
||||
# load the ASL Scenario Archive scenarios |
||||
fname = sys.argv[1] |
||||
asa_data = json.load( open( fname, "r" ) ) |
||||
_build_asa_scenario_index( _asa_scenarios, asa_data, None ) |
||||
|
||||
# load the ROAR scenarios |
||||
fname = sys.argv[2] |
||||
roar_data = json.load( open( fname, "r" ) ) |
||||
_build_roar_scenario_index( _roar_scenarios, roar_data, None ) |
||||
|
||||
# try to connect each ASA scenario to ROAR |
||||
exact_matches, multiple_matches, unmatched = [], [], [] |
||||
for scenario in asa_data["scenarios"]: |
||||
matches = _match_roar_scenario( scenario ) |
||||
if not matches: |
||||
unmatched.append( scenario ) |
||||
elif len(matches) == 1: |
||||
exact_matches.append( scenario ) |
||||
else: |
||||
multiple_matches.append( [ scenario, matches ] ) |
||||
|
||||
# output the results |
||||
print( "ASL Scenario Archive scenarios: {}".format( len(asa_data["scenarios"]) ) ) |
||||
print() |
||||
print( "Exact matches: {}".format( len(exact_matches) ) ) |
||||
print() |
||||
print( "Multiple matches: {}".format( len(multiple_matches) ) ) |
||||
if multiple_matches: |
||||
for scenario,matches in multiple_matches: |
||||
print( " {}:".format( asa_string(scenario) ) ) |
||||
for match in matches: |
||||
print( " - {}".format( roar_string( match ) ) ) |
||||
print() |
||||
print( "Unmatched: {}".format( len(unmatched) ) ) |
||||
if unmatched: |
||||
for scenario in unmatched: |
||||
print( " {}".format( asa_string(scenario) ) ) |
@ -0,0 +1,46 @@ |
||||
{ |
||||
|
||||
"_comment_": "This section maps theaters from those at the ASL Scenario Archive to ours.", |
||||
"theater-mappings": { |
||||
"MTO": "ETO", |
||||
"Normandy": "ETO", |
||||
"KW": "Korea" |
||||
}, |
||||
|
||||
"_comment_": "This section maps player nationalities from those at the ASL Scenario Archive (must be lower-case) to our nationality ID's.", |
||||
"nat-mappings": { |
||||
"australian": "anzac", |
||||
"belgians": "belgian", |
||||
"canada": "british~canadian", |
||||
"canadian": "british~canadian", |
||||
"china cmd": "chinese~gmd", |
||||
"commonwealth": "anzac", |
||||
"filipinos": "filipino", |
||||
"finland": "finnish", |
||||
"free french": "free-french", |
||||
"germany": "german", |
||||
"gurkha": "british", |
||||
"gurkhas": "british", |
||||
"ina": "indonesian", |
||||
"japan": "japanese", |
||||
"kpa": "kfw-kpa", |
||||
"nkpa": "kfw-kpa", |
||||
"north korea": "kfw-kpa", |
||||
"philippine": "filipino", |
||||
"poland": "polish", |
||||
"republic of korea": "kfw-rok", |
||||
"rok": "kfw-rok", |
||||
"rumanian": "romanian", |
||||
"russia": "russian", |
||||
"russians": "russian", |
||||
"slovak": "slovakian", |
||||
"siamese": "thai", |
||||
"soviet": "russian", |
||||
"ss": "german", |
||||
"u.s.": "american", |
||||
"usmc": "american", |
||||
"yugoslav": "yugoslavian", |
||||
"vichy": "french" |
||||
} |
||||
|
||||
} |
@ -0,0 +1,155 @@ |
||||
""" Manage downloading files. |
||||
|
||||
This module manages downloading files on a schedule e.g. the ASL Scenario Archive and ROAR scenario indexes. |
||||
""" |
||||
|
||||
import os |
||||
import threading |
||||
import json |
||||
import urllib.request |
||||
import time |
||||
import datetime |
||||
import tempfile |
||||
import logging |
||||
|
||||
from vasl_templates.webapp import app |
||||
from vasl_templates.webapp.utils import parse_int |
||||
|
||||
_registry = set() |
||||
_logger = logging.getLogger( "downloads" ) |
||||
|
||||
# --------------------------------------------------------------------- |
||||
|
||||
class DownloadedFile: |
||||
"""Manage a downloaded file.""" |
||||
|
||||
def __init__( self, key, ttl, fname, url, on_data, extra_args=None ): |
||||
|
||||
# initialize |
||||
self.key = key |
||||
self.ttl = ttl |
||||
self.fname = fname |
||||
self.url = url |
||||
self.on_data = on_data |
||||
self.error_msg = None |
||||
|
||||
# initialize |
||||
self._lock = threading.Lock() |
||||
self._data = None |
||||
|
||||
# install any extra member variables |
||||
if extra_args: |
||||
for k,v in extra_args.items(): |
||||
setattr( self, k, v ) |
||||
|
||||
# register this instance |
||||
_registry.add( self ) |
||||
|
||||
# check if we have a cached copy of the file |
||||
self._cache_fname = os.path.join( tempfile.gettempdir(), "vasl-templates."+fname ) |
||||
if os.path.isfile( self._cache_fname ): |
||||
# yup - load it |
||||
_logger.info( "Using cached %s file: %s", key, self._cache_fname ) |
||||
self._set_data( self._cache_fname ) |
||||
else: |
||||
# nope - start with an empty data set |
||||
_logger.debug( "No cached %s file: %s", key, self._cache_fname ) |
||||
|
||||
def _set_data( self, data ): |
||||
"""Install a new data set.""" |
||||
with self: |
||||
try: |
||||
# install the new data |
||||
if len(data) < 1024 and os.path.isfile( data ): |
||||
with open( data, "r", encoding="utf-8" ) as fp: |
||||
data = fp.read() |
||||
self._data = json.loads( data ) |
||||
# notify the owner |
||||
if self.on_data: |
||||
self.on_data( self, self._data, _logger ) |
||||
except Exception as ex: #pylint: disable=broad-except |
||||
# NOTE: It would be nice to report this to the user in the UI, but because downloading |
||||
# happens in a background thread, the web page will probably have already finished rendering, |
||||
# and without the ability to push notifications, it's too late to tell the user. |
||||
_logger.error( "Can't install %s data: %s", self.key, ex ) |
||||
|
||||
def __enter__( self ): |
||||
"""Gain access to the underlying data. |
||||
|
||||
Since the file is downloaded in a background thread, access to the underlying data |
||||
must be protected by a lock. |
||||
""" |
||||
self._lock.acquire() |
||||
return self._data |
||||
|
||||
def __exit__( self, exc_type, exc_val, exc_tb ): |
||||
"""Relinquish access to the underlying data.""" |
||||
self._lock.release() |
||||
|
||||
@staticmethod |
||||
def download_files(): |
||||
"""Download fresh copies of each file.""" |
||||
#pylint: disable=protected-access |
||||
|
||||
# process each DownloadedFile |
||||
for df in _registry: |
||||
|
||||
# check if we should simulate slow downloads |
||||
delay = parse_int( app.config.get( "DOWNLOADED_FILES_DELAY" ) ) |
||||
if delay: |
||||
_logger.debug( "Simulating a slow download for the %s file: delay=%s", df.key, delay ) |
||||
time.sleep( delay ) |
||||
|
||||
# get the download URL |
||||
url = app.config.get( "{}_DOWNLOAD_URL".format( df.key.upper() ), df.url ) |
||||
if os.path.isfile( url ): |
||||
# read the data directly from a file (for debugging porpoises) |
||||
_logger.info( "Loading the %s data directly from a file: %s", df.key, url ) |
||||
df._set_data( url ) |
||||
continue |
||||
|
||||
# check if we have a cached copy of the file |
||||
ttl = parse_int( app.config.get( "{}_DOWNLOAD_CACHE_TTL".format( df.key ), df.ttl ), 24 ) |
||||
if ttl <= 0: |
||||
_logger.info( "Download of the %s file has been disabled.", df.key ) |
||||
continue |
||||
ttl *= 60*60 |
||||
if os.path.isfile( df._cache_fname ): |
||||
# yup - check how long ago it was downloaded |
||||
mtime = os.path.getmtime( df._cache_fname ) |
||||
age = int( time.time() - mtime ) |
||||
_logger.debug( "Checking the cached %s file: age=%s, ttl=%s (mtime=%s)", |
||||
df.key, |
||||
datetime.timedelta( seconds=age ), |
||||
datetime.timedelta( seconds=ttl ), |
||||
time.strftime( "%Y-%m-%d %H:%M:%S", time.localtime(mtime) ) |
||||
) |
||||
if age < ttl: |
||||
continue |
||||
|
||||
# download the file |
||||
_logger.info( "Downloading the %s file: %s", df.key, url ) |
||||
try: |
||||
fp = urllib.request.urlopen( url ) |
||||
data = fp.read().decode( "utf-8" ) |
||||
except Exception as ex: #pylint: disable=broad-except |
||||
msg = str( getattr(ex,"reason",None) or ex ) |
||||
_logger.error( "Can't download the %s file: %s", df.key, msg ) |
||||
df.error_msg = msg |
||||
continue |
||||
_logger.info( "Downloaded the %s file OK: %d bytes", df.key, len(data) ) |
||||
|
||||
# install the new data |
||||
df._set_data( data ) |
||||
# NOTE: We only need to worry about thread-safety because a fresh copy of the file is downloaded |
||||
# while the old one is in use, but because downloads are only done once at startup, once we get here, |
||||
# we could delete the lock and allow unfettered access to the underlying data (since it's all |
||||
# going to be read-only). |
||||
# For simplicty, we leave the lock in place. It will slow things down a bit, since we will be |
||||
# serializing access to the data (unnecessarily, because it's all read-only) but none of the code |
||||
# is performance-critical and we can probably live it. |
||||
|
||||
# save a cached copy of the data |
||||
_logger.debug( "Saving a cached copy of the %s file: %s", df.key, df._cache_fname ) |
||||
with open( df._cache_fname, "w", encoding="utf-8" ) as fp: |
||||
fp.write( data ) |
@ -1,134 +0,0 @@ |
||||
"""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 = "https://vasl-templates.org/services/roar/scenario-index.json" |
||||
CACHE_TTL = 6 * 60*60 |
||||
|
||||
# --------------------------------------------------------------------- |
||||
|
||||
def init_roar(): |
||||
"""Initialize ROAR integration.""" |
||||
|
||||
# initialize |
||||
from vasl_templates.webapp.main import startup_msg_store |
||||
msg_store = startup_msg_store |
||||
|
||||
# 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.localtime(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.critical( "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", ROAR_SCENARIO_INDEX_URL ) |
||||
_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 :-/ |
||||
msg = "Can't download the ROAR scenario index: {}".format( getattr(ex,"reason",str(ex)) ) |
||||
_logger.warning( "%s", msg ) |
||||
if msg_store: |
||||
msg_store.warning( 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 :-/ |
||||
msg = "Can't load the {} ROAR scenario index: {}".format( data_type, ex ) |
||||
_logger.error( "%s", msg ) |
||||
if msg_store: |
||||
msg_store.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,405 @@ |
||||
"""Provide access to the scenarios.""" |
||||
|
||||
# NOTE: Disable "DownloadedFile has no 'index' member" warnings. |
||||
#pylint: disable=no-member |
||||
|
||||
import re |
||||
|
||||
from flask import request, render_template, jsonify, abort |
||||
|
||||
from vasl_templates.webapp import app |
||||
from vasl_templates.webapp.downloads import DownloadedFile |
||||
from vasl_templates.webapp.utils import get_month_name, make_formatted_day_of_month, friendly_fractions, parse_int |
||||
|
||||
# --------------------------------------------------------------------- |
||||
|
||||
def _build_asa_scenario_index( df, new_data, logger ): |
||||
"""Build the ASL Scenario Archive index.""" |
||||
df.index = { |
||||
scenario["scenario_id"]: scenario |
||||
for scenario in new_data["scenarios"] |
||||
} |
||||
if logger: |
||||
logger.debug( "Loaded the ASL Secenario Archive index: #scenarios=%d", len(df.index) ) |
||||
logger.debug( "- Generated at: %s", new_data.get( "_generatedAt_", "n/a" ) ) |
||||
|
||||
_asa_scenarios = DownloadedFile( "ASA", 1*24, |
||||
"asl-scenario-archive.json", |
||||
"https://vasl-templates.org/services/asl-scenario-archive/scenario-index.json", |
||||
_build_asa_scenario_index, |
||||
extra_args = { "index": None } |
||||
) |
||||
|
||||
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
||||
|
||||
def _build_roar_scenario_index( df, new_data, logger ): |
||||
"""Build the ROAR scenario index.""" |
||||
df.index, df.title_matching, df.id_matching = {}, {}, {} |
||||
for roar_id,scenario in new_data.items(): |
||||
if roar_id.startswith( "_" ): |
||||
continue |
||||
scenario[ "roar_id" ] = roar_id |
||||
df.index[ roar_id ] = scenario |
||||
_update_roar_matching_index( df.title_matching, scenario.get("name"), roar_id ) |
||||
_update_roar_matching_index( df.id_matching, scenario.get("scenario_id"), roar_id ) |
||||
if logger: |
||||
logger.debug( "Loaded the ROAR scenario index: #scenarios=%d", len(df.index) ) |
||||
logger.debug( "- Generated at: %s", new_data.get( "_generatedAt_", "n/a" ) ) |
||||
logger.debug( "- Last updated: %s", new_data.get( "_lastUpdated_", "n/a" ) ) |
||||
logger.debug( "- # playings: %s", str( new_data.get( "_nPlayings_", "n/a" ) ) ) |
||||
|
||||
def _update_roar_matching_index( index, val, roar_id ): |
||||
"""Update the index that will be used for matching ROAR scenarios.""" |
||||
if not val: |
||||
return |
||||
key = _make_roar_matching_key( val ) |
||||
if key not in index: |
||||
index[ key ] = set() |
||||
index[ key ].add( roar_id ) |
||||
|
||||
def _make_roar_matching_key( val ): |
||||
"""Generate a key value that will be used to match ROAR scenarios.""" |
||||
if not val: |
||||
return val |
||||
return re.sub( "[^a-z0-9]", "", val.lower() ) |
||||
|
||||
_roar_scenarios = DownloadedFile( "ROAR", 1*24, |
||||
"roar-scenario-index.json", |
||||
"https://vasl-templates.org/services/roar/scenario-index.json", |
||||
_build_roar_scenario_index, |
||||
extra_args = { "index": None } |
||||
) |
||||
|
||||
# --------------------------------------------------------------------- |
||||
|
||||
@app.route( "/scenario-index" ) |
||||
def get_scenario_index(): |
||||
"""Return the scenario index.""" |
||||
|
||||
def add_field( entry, key, val ): #pylint: disable=missing-docstring |
||||
if val: |
||||
entry[ key ] = val |
||||
def make_entry( scenario ): |
||||
"""Make an entry for the scenario index.""" |
||||
entry = { "scenario_id": scenario["scenario_id"] } |
||||
add_field( entry, "scenario_name", _make_scenario_name( scenario ) ) |
||||
add_field( entry, "scenario_display_id", scenario.get( "sc_id" ) ) |
||||
add_field( entry, "scenario_location", scenario.get( "scen_location" ) ) |
||||
add_field( entry, "scenario_date", _parse_date( scenario.get( "scen_date" ) ) ) |
||||
add_field( entry, "publication_name", scenario.get( "pub_name" ) ) |
||||
add_field( entry, "publication_id", scenario.get( "pub_id" ) ) |
||||
add_field( entry, "publication_date", _parse_date( scenario.get( "published_date" ) ) ) |
||||
add_field( entry, "publisher_name", scenario.get( "publisher_name" ) ) |
||||
add_field( entry, "publisher_id", scenario.get( "publisher_id" ) ) |
||||
return entry |
||||
|
||||
# generate the scenario index |
||||
with _asa_scenarios: |
||||
if _asa_scenarios.index is None: |
||||
return _make_not_available_response( |
||||
"The scenario index is not available.", _asa_scenarios.error_msg |
||||
) |
||||
return jsonify( [ |
||||
make_entry( scenario ) |
||||
for scenario in _asa_scenarios.index.values() |
||||
] ) |
||||
|
||||
@app.route( "/roar/scenario-index" ) |
||||
def get_roar_scenario_index(): |
||||
"""Return the ROAR scenario index.""" |
||||
with _roar_scenarios: |
||||
if _roar_scenarios.index is None: |
||||
return _make_not_available_response( |
||||
"The ROAR scenarios are not available.", _roar_scenarios.error_msg |
||||
) |
||||
return jsonify( _roar_scenarios.index ) |
||||
|
||||
def _make_not_available_response( msg, msg2 ): |
||||
"""Generate a "not available" response.""" |
||||
resp = { "error": msg } |
||||
if msg2: |
||||
resp[ "message" ] = msg2 |
||||
return jsonify( resp ) |
||||
|
||||
# --------------------------------------------------------------------- |
||||
|
||||
@app.route( "/scenario/<scenario_id>" ) |
||||
def get_scenario( scenario_id ): |
||||
"""Return a scenario.""" |
||||
|
||||
# get the parameters |
||||
roar_override = request.args.get( "roar" ) |
||||
|
||||
# get the basic scenario information |
||||
scenario, args = _do_get_scenario( scenario_id ) |
||||
args[ "scenario_date_iso" ] = _parse_date_iso( scenario.get( "scen_date" ) ) |
||||
args[ "defender_name" ] = scenario.get( "defender" ) |
||||
args[ "attacker_name" ] = scenario.get( "attacker" ) |
||||
args = { k.lower(): v for k,v in args.items() } |
||||
|
||||
def get_win_score( key ): |
||||
"""Get a player's win percentage.""" |
||||
nWins = parse_int( playings.get( key+"_wins" ), -1 ) |
||||
if nWins < 0: |
||||
return None |
||||
score = 100 * nWins / nGames |
||||
return int( score + 0.5 ) |
||||
|
||||
# get the ASL Scenario Archive playings |
||||
playings = scenario.get( "playings", [ {} ] )[ 0 ] |
||||
nGames = parse_int( playings.get( "totalGames" ), 0 ) |
||||
if playings and nGames > 0: |
||||
# NOTE: The player names are display names, only shown in the balance graphs, |
||||
# so it doesn't matter if we know about them or not. |
||||
args[ "balance" ] = [ { |
||||
"name": scenario.get( "defender" ), |
||||
"wins": playings.get( "defender_wins" ), |
||||
"percentage": get_win_score( "defender" ) |
||||
}, { |
||||
"name": scenario.get( "attacker" ), |
||||
"wins": playings.get( "attacker_wins" ), |
||||
"percentage": get_win_score( "attacker" ) |
||||
} ] |
||||
|
||||
# try to match the scenario with one in ROAR |
||||
roar_id = None |
||||
if roar_override == "auto-match": |
||||
matches = _match_roar_scenario( scenario ) |
||||
if matches: |
||||
roar_id = matches[0][ "roar_id" ] |
||||
else: |
||||
roar_id = roar_override |
||||
if roar_id: |
||||
args[ "roar" ] = _get_roar_info( roar_id ) |
||||
|
||||
return jsonify( args ) |
||||
|
||||
def _do_get_scenario( scenario_id ): |
||||
"""Return the basic details for the specified scenario.""" |
||||
scenario = _get_scenario( scenario_id ) |
||||
url_template = app.config[ "ASA_SCENARIO_URL" ] |
||||
scenario_url = url_template.replace( "{ID}", scenario_id ) |
||||
return scenario, { |
||||
"SCENARIO_ID": scenario_id, |
||||
"SCENARIO_URL": scenario_url, |
||||
"SCENARIO_NAME": _make_scenario_name( scenario ), |
||||
"SCENARIO_DISPLAY_ID": scenario.get( "sc_id" ), |
||||
"SCENARIO_LOCATION": scenario.get( "scen_location" ), |
||||
"SCENARIO_DATE": _parse_date( scenario.get( "scen_date" ) ), |
||||
"THEATER": scenario.get( "theatre" ), |
||||
"DEFENDER_DESC": scenario.get( "def_desc" ), |
||||
"ATTACKER_DESC": scenario.get( "att_desc" ), |
||||
} |
||||
|
||||
def _match_roar_scenario( scenario ): |
||||
"""Try to match the scenario with a ROAR scenario.""" |
||||
|
||||
def get_result_count( scenario ): |
||||
"""Get the number of playings for a ROAR scenario.""" |
||||
results = scenario.get( "results", [] ) |
||||
return sum( r[1] for r in results ) |
||||
|
||||
with _roar_scenarios: |
||||
# try to match by scenario title |
||||
title = scenario.get( "title" ) |
||||
if not title: |
||||
return None |
||||
matches = _roar_scenarios.title_matching.get( _make_roar_matching_key( title ) ) |
||||
if not matches: |
||||
return [] |
||||
elif len( matches ) == 1: |
||||
# there was exactly one match - return it |
||||
roar_id = next( iter( matches ) ) |
||||
return [ _roar_scenarios.index[ roar_id ] ] |
||||
else: |
||||
# we found multiple scenarios with the same title, filter by ID |
||||
matches2 = _roar_scenarios.id_matching.get( _make_roar_matching_key( scenario.get("sc_id") ), set() ) |
||||
if matches2: |
||||
matches = matches.intersection( matches2 ) |
||||
matches = [ _roar_scenarios.index[m] for m in matches ] |
||||
matches.sort( key=get_result_count, reverse=True ) |
||||
return matches |
||||
|
||||
def _get_roar_info( roar_id ): |
||||
"""Get the information for the specified ROAR scenario.""" |
||||
|
||||
def get_balance( player_no ): |
||||
"""Get a player's balance stats.""" |
||||
# NOTE: The player names are display names, only shown in the balance graphs, |
||||
# so it doesn't matter if we know about them or not. |
||||
balance = { |
||||
"name": playings[ player_no ][0], |
||||
"wins": playings[ player_no ][1] |
||||
} |
||||
if nGames > 0: |
||||
balance[ "percentage" ] = int( 100 * playings[player_no][1] / nGames + 0.5 ) |
||||
return balance |
||||
|
||||
with _roar_scenarios: |
||||
|
||||
# find the ROAR scenario |
||||
index = _roar_scenarios.index or {} |
||||
scenario = index.get( roar_id ) |
||||
if not scenario: |
||||
abort( 404 ) |
||||
|
||||
# return the scenario details |
||||
results = { |
||||
"scenario_id": roar_id, |
||||
"scenario_display_id": scenario.get( "scenario_id" ), |
||||
"name": scenario.get( "name" ), |
||||
"url": scenario.get( "url" ) |
||||
} |
||||
playings = scenario.get( "results" ) |
||||
if playings: |
||||
nGames = playings[0][1] + playings[1][1] |
||||
results[ "balance" ] = [ get_balance(0), get_balance(1) ] |
||||
|
||||
return results |
||||
|
||||
# --------------------------------------------------------------------- |
||||
|
||||
@app.route( "/scenario-card/<scenario_id>" ) |
||||
def get_scenario_card( scenario_id ): #pylint: disable=too-many-branches |
||||
"""Return a scenario card (HTML).""" |
||||
|
||||
# get the arguments |
||||
brief_mode = request.args.get( "brief" ) |
||||
|
||||
# find the specified scenario |
||||
scenario, args = _do_get_scenario( scenario_id ) |
||||
|
||||
# prepare the template parameters |
||||
args[ "DESIGNER" ] = scenario.get( "author" ) |
||||
args[ "PUBLICATION" ] = scenario.get( "pub_name" ) |
||||
args[ "PUBLISHER" ] = scenario.get( "publisher_name" ) |
||||
args[ "PUBLICATION_DATE" ] = _parse_date( scenario.get( "published_date" ) ) |
||||
args[ "PREV_PUBLICATION" ] = scenario.get( "prior_publication" ) |
||||
args[ "REVISED_PUBLICATION" ] = scenario.get( "revision" ) |
||||
args[ "OVERVIEW" ] = scenario.get( "overview" ) |
||||
if brief_mode: |
||||
args[ "OVERVIEW_BRIEF" ] = _make_brief_overview( scenario.get( "overview" ) ) |
||||
args[ "DEFENDER_NAME" ] = scenario.get( "defender" ) |
||||
args[ "ATTACKER_NAME" ] = scenario.get( "attacker" ) |
||||
args[ "BOARDS" ] = ", ".join( str(m) for m in scenario.get("maps",[]) ) |
||||
args[ "MAP_IMAGES" ] = scenario.get( "mapImages" ) |
||||
overlays = ", ".join( str(o) for o in scenario.get("overlays",[]) ) |
||||
if overlays.upper() == "NONE": |
||||
overlays = None |
||||
if overlays: |
||||
args[ "OVERLAYS" ] = overlays |
||||
args[ "EXTRA_RULES" ] = scenario.get( "misc" ) |
||||
args[ "ERRATA" ] = scenario.get( "errata" ) |
||||
|
||||
# prepare the template parameters |
||||
if scenario.get( "pub_id" ): |
||||
url_template = app.config[ "ASA_PUBLICATION_URL" ] |
||||
args[ "PUBLICATION_URL" ] = url_template.replace( "{ID}", scenario["pub_id"] ) |
||||
if scenario.get( "publisher_id" ): |
||||
url_template = app.config[ "ASA_PUBLISHER_URL" ] |
||||
args[ "PUBLISHER_URL" ] = url_template.replace( "{ID}", scenario["publisher_id"] ) |
||||
playing_time = scenario.get( "time_to_play", "0" ) |
||||
if not str( playing_time ).startswith( "0" ): |
||||
args[ "PLAYING_TIME" ] = friendly_fractions( playing_time, "hour", "hours" ) |
||||
|
||||
# prepare the turn count |
||||
min_turns = scenario.get( "min_turns", "0" ) |
||||
max_turns = scenario.get( "max_turns", "0" ) |
||||
if min_turns != "0": |
||||
if min_turns == max_turns or max_turns == "0": |
||||
args[ "TURN_COUNT" ] = friendly_fractions( min_turns, "turn", "turns" ) |
||||
elif max_turns != "0": |
||||
args[ "TURN_COUNT" ] = "{}-{} turns".format( friendly_fractions(min_turns), friendly_fractions(max_turns) ) |
||||
|
||||
# prepare any info icons |
||||
icons = {} |
||||
if scenario.get( "oba" ) in ("D","B"): |
||||
icons[ "DEFENDER_OBA" ] = True |
||||
if scenario.get( "oba" ) in ("A","B"): |
||||
icons[ "ATTACKER_OBA" ] = True |
||||
if scenario.get( "night" ) == "1": |
||||
icons[ "IS_NIGHT" ] = True |
||||
if scenario.get( "aslsk" ) == "1": |
||||
icons[ "IS_ASLSK" ] = True |
||||
if scenario.get( "deluxe" ) == "1": |
||||
icons[ "IS_DELUXE" ] = True |
||||
if icons: |
||||
args[ "ICONS" ] = icons |
||||
|
||||
# prepare the lat/long co-ordinates |
||||
is_valid_coord = lambda val: val and val != "-99.99" |
||||
if is_valid_coord( scenario.get("gps_lat") ) and is_valid_coord( scenario.get("gps_long") ): |
||||
url_template = app.config.get( "MAP_URL", "https://maps.google.com/maps?q={LAT},{LONG}&z=4&output=embed" ) |
||||
args["MAP_URL"] = url_template.replace( "{LAT}", scenario["gps_lat"] ).replace( "{LONG}", scenario["gps_long"] ) |
||||
|
||||
# process the template and return the generated HTML |
||||
return render_template( "scenario-card.html", **args ) |
||||
|
||||
def _make_brief_overview( content ): |
||||
"""Truncate the scenario overview.""" |
||||
if not content: |
||||
return None |
||||
threshold = parse_int( app.config.get( "BRIEF_CONTENT_THRESHOLD" ), 200 ) |
||||
if threshold <= 0 or len(content) < threshold: |
||||
return None |
||||
regex = re.compile( "[.?!]+" ) |
||||
mo = regex.search( content, threshold ) |
||||
if not mo: |
||||
return content[:threshold] + "..." |
||||
val = content[ : mo.start() + len(mo.group()) ] |
||||
if val == content: |
||||
return None |
||||
return val |
||||
|
||||
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
||||
|
||||
def _get_scenario( scenario_id ): |
||||
"""Get the specified scenario.""" |
||||
with _asa_scenarios: |
||||
index = _asa_scenarios.index or {} |
||||
scenario = index.get( scenario_id ) |
||||
if not scenario: |
||||
abort( 404 ) |
||||
return scenario |
||||
|
||||
def _make_scenario_name( scenario ): |
||||
"""Get the scenario's name.""" |
||||
return scenario.get( "title" ) or "Untitled scenario (#{})".format( scenario["scenario_id"] ) |
||||
|
||||
# --------------------------------------------------------------------- |
||||
|
||||
@app.route( "/scenario/nat-report" ) |
||||
def scenario_nat_report(): |
||||
"""Generate the scenario nationalities report (for testing porpoises).""" |
||||
return render_template( "scenario-nat-report.html" ) |
||||
|
||||
# --------------------------------------------------------------------- |
||||
|
||||
def _parse_date( val ): |
||||
"""Parse a date string.""" |
||||
parts = _split_date_parts( val ) |
||||
if not parts: |
||||
return None |
||||
return "{} {}, {}".format( |
||||
make_formatted_day_of_month( parts[0] ), |
||||
get_month_name( parts[1] ), |
||||
parts[2] |
||||
) |
||||
|
||||
def _parse_date_iso( val ): |
||||
"""Parse a date string.""" |
||||
parts = _split_date_parts( val ) |
||||
if not parts: |
||||
return None |
||||
return "{:04}-{:02}-{:02}".format( parts[2], parts[1], parts[0] ) |
||||
|
||||
def _split_date_parts( val ): |
||||
"""Split a date into its component parts.""" |
||||
if val is None: |
||||
return None |
||||
mo = re.search( r"^(\d{4})-(\d{2})-(\d{2})", val ) |
||||
if not mo: |
||||
return None |
||||
if mo.group(1) == "1901": |
||||
return None # nb: 1901-01-01 seems to be used as a "invalid date" marker |
||||
return [ int(mo.group(3)), int(mo.group(2)), int(mo.group(1)) ] |
@ -0,0 +1,87 @@ |
||||
/* scenario card */ |
||||
.scenario-card { display: flex ; flex-direction: column ; position: relative ; } |
||||
.scenario-card a { text-decoration: none ; } |
||||
|
||||
/* scenario card - header */ |
||||
.scenario-card .header { |
||||
position: relative ; |
||||
border: 1px solid #b0b080 ; border-radius: 5px ; |
||||
background: #f0f0c0 ; padding: 0.25em 0.5em ; |
||||
margin-bottom: 0.35em ; |
||||
} |
||||
.scenario-card .scenario-name { font-size: 150% ; font-weight: bold ; } |
||||
.scenario-card .scenario-name a:focus { outline: 0 ; } |
||||
.scenario-card .scenario-id { font-style: italic ; } |
||||
.scenario-card .scenario-date { font-size: 80% ; font-style: italic ; } |
||||
.scenario-card .info { |
||||
position: absolute ; |
||||
border: 1px solid #c0c0a0 ; border-radius: 4px ; |
||||
padding: 0.25em 0.5em ; |
||||
background: #fff ; |
||||
font-size: 80% ; |
||||
} |
||||
.scenario-card .icons { margin-top: 0.25em ; opacity: 0.75 ; } |
||||
.scenario-card .icons img { height: 0.8em ; } |
||||
.scenario-card .icons img.oba { height: 0.7em ; margin-left: -3px ; } |
||||
|
||||
/* scenario card - sub-header */ |
||||
.scenario-card .header2 { margin: 0 0.5em ; font-size: 90% ; color: #666 ; } |
||||
.scenario-card .publisher { font-size: 90% ; font-style: italic ; } |
||||
.scenario-card .publication-date { font-size: 90% ; font-style: italic ; } |
||||
|
||||
/* scenario card - content */ |
||||
.scenario-card .content { margin: 0.5em 0.5em 0 0.5em ; padding-right: 0.5em ; overflow-y: auto ; } |
||||
.scenario-card .overview { margin-bottom: 1em ; text-align: justify ; } |
||||
.scenario-card .overview .more { display: inline-block ; font-size: 80% ; font-style: italic ; color: #666 ; cursor: pointer ; } |
||||
.scenario-card .map { float: right ; width: 30% ; min-width: 200px ; margin: 0 0 0.25em 1.5em ; } |
||||
.scenario-card .map iframe { width: 100% ; border: 0 ; } |
||||
|
||||
/* scenario card - player info */ |
||||
.scenario-card .player-info { margin-bottom: 1em ; } |
||||
.scenario-card .player-info .players td { vertical-align: top ; } |
||||
.scenario-card .player-info .players .label { font-weight: bold ; padding-right: 0.25em ; } |
||||
.scenario-card .player-info .players .flag { width: 1px ; text-align: center ; } |
||||
.scenario-card .player-info .players .name { padding-left: 0.1em ; } |
||||
.scenario-card .player-info .players .desc { font-size: 90% ; font-style: italic ; } |
||||
|
||||
.scenario-card .player-info .oba { clear: both ; float: right ; font-size: 90% ; border: 1px solid #c0c0a0 ; border-radius: 5px ; } |
||||
.scenario-card .player-info .oba .oba-header { border-bottom: 1px dotted #c0c0a0 ; padding: 2px 5px 5px 5px ; font-weight: bold ; text-align: center ; white-space: nowrap ; } |
||||
.scenario-card .player-info .oba td { line-height: 1em ; } |
||||
.scenario-card .player-info .oba .name { width: 1px ; padding-left: 5px ; font-size: 90% ; font-weight: bold ; white-space: nowrap ; } |
||||
.scenario-card .player-info .oba .count { width: 1px ; padding-left: 0.5em ; text-align: right ; } |
||||
.scenario-card .player-info .oba .comments { padding-left: 10px ; font-size: 80% ; font-style: italic ; color: #444 ; } |
||||
.scenario-card .player-info .oba .date-warning { padding-top: 0.5em ; font-size: 80% ; font-style: italic ; } |
||||
.scenario-card .player-info .oba .date-warning img { float: left ; height: 1.25em ; margin: -2px 0.5em 0 0 ; } |
||||
|
||||
.scenario-card .balance-graphs { display: inline-block ; border: 1px solid #c0c0a0 ; border-radius: 5px ; padding: 4px 8px ; } |
||||
.scenario-card .balance-graph { display: table-row ; font-size: 90% ; } |
||||
.scenario-card .balance-graph .player { display: table-cell ; } |
||||
.scenario-card .balance-graph .player.player1 { text-align: right ; } |
||||
.scenario-card .balance-graph .wins { display: table-cell ; text-align: right ; font-size: 70% ; font-style: italic ; color: #666 ; } |
||||
.scenario-card .balance-graph .wrapper { display: table-cell ; } |
||||
.scenario-card .balance-graph .progressbar { display: inline-block ; position: relative ; width: 4em ; height: 0.8em ; margin-bottom: -2px ; border: 1px solid #c0c0a0 ; background: #f0f0c0 ; } |
||||
.scenario-card .balance-graph a:focus { outline: 0 ; } |
||||
.scenario-card .balance-graph .progressbar.player1 { background: #f0f0c0 ; border-top-right-radius: 0 ; border-bottom-right-radius: 0 ; border-right: 0 ; } |
||||
.scenario-card .balance-graph .progressbar.player1 .ui-progressbar-value { background: #fff ; border-right: 1px solid #eee ; } |
||||
.scenario-card .balance-graph .progressbar.player2 { background: #fff ; border-top-left-radius: 0 ; border-bottom-left-radius: 0 ; } |
||||
.scenario-card .balance-graph .progressbar.player2 .ui-progressbar-value { background: #f0f0c0 ; border-right: 1px solid #eee ; } |
||||
.scenario-card .balance-graph .progressbar .score { position: absolute ; font-size: 70% ; font-style: italic ; color: #666 ; left: 35% ; } |
||||
|
||||
/* scenario card - misc */ |
||||
.scenario-card a.map-preview img { height: 0.75em ; margin-left: 0.25em ; } |
||||
.scenario-card a.map-preview:focus { outline: 0 ; } |
||||
.scenario-card .errata ul { margin-top: 0 ; } |
||||
.scenario-card .errata .source { font-size: 90% ; font-style: italic ; color: #666 ; } |
||||
|
||||
/* scenario info dialog */ |
||||
.ui-dialog.scenario-info { border-radius: 10px ; } |
||||
.ui-dialog.scenario-info .ui-dialog-titlebar { display: none ; } |
||||
.ui-dialog.scenario-info .credit { float: left ; margin-right: 0.5em ; display: flex ; align-items: center ; } |
||||
.ui-dialog.scenario-info .credit img { height: 1.4em ; margin-right: 0.5em ; opacity: 0.7 ; } |
||||
.ui-dialog.scenario-info .credit .caption { font-size: 70% ; line-height: 1em ; margin-top: -4px ; } |
||||
.ui-dialog.scenario-info .credit a { text-decoration: none ; font-style: italic ; color: #666 ; } |
||||
.ui-dialog.scenario-info .credit a:focus { outline: 0 ; } |
||||
#scenario-info-dialog .scenario-card { height: 100% ; overflow-y: hidden ; } |
||||
#scenario-info-dialog .connect-roar { display: inline-block ; margin-top: 0.25em ; font-size: 80% ; color: #444 ; cursor: pointer ; } |
||||
#scenario-info-dialog .connect-roar img { height: 0.75em ; Xmargin-right: 0.25em ; opacity: 0.7 ; } |
||||
#scenario-info-dialog .disconnect-roar img { height: 0.5em ; margin-left: 0.5em ; cursor: pointer ; } |
@ -0,0 +1,71 @@ |
||||
/* jQuery dialog */ |
||||
.ui-dialog.scenario-search .ui-dialog-titlebar { background: #e0e0a0 ; } |
||||
#scenario-search { display: flex ; } |
||||
#scenario-search .left { min-width: 300px ; display: flex ; flex-direction: column ; } |
||||
#scenario-search .left .scenarios { flex: 1 ; } |
||||
#scenario-search .right { min-width: 400px ; flex: 1 ; display: flex ; flex-direction: column ; } |
||||
#scenario-search .scenario-card { flex: 1 ; overflow-y: hidden ; } |
||||
|
||||
/* splitter */ |
||||
#scenario-search .gutter { position: relative ; margin: 0 0.5em ; background-color: #ccc ; cursor: col-resize ; } |
||||
#scenario-search .gutter img { position: absolute ; top: 45% ; left: -2px ; } |
||||
|
||||
/* search control */ |
||||
#scenario-search .select2-container { top: 10px !important ; } |
||||
#scenario-search .select2-selection { display: none ; } |
||||
#scenario-search .select2-dropdown { border: none ; } |
||||
#scenario-search .select2-search--dropdown { padding: 0 0 0.5em 0 ; } |
||||
|
||||
#scenario-search .select2-results__option { padding-left: 4px ; } |
||||
#scenario-search .select2-results__option--highlighted[aria-selected] { margin: 2px 2px -4px 0 ; padding-top:4px ; color: #f0f0f0 ; } |
||||
#scenario-search .select2-results__option--highlighted[aria-selected] .scenario-date { color: #f0f0f0 ; } |
||||
#scenario-search .select2-results__option--highlighted[aria-selected] .publication-name { color: #f0f0f0 ; } |
||||
#scenario-search .select2-results__option--highlighted[aria-selected] .publisher-name { color: #f0f0f0 ; } |
||||
#scenario-search .select2-results__option--highlighted[aria-selected] .publication-date { color: #f0f0f0 ; } |
||||
#scenario-search .select2-results__option[aria-selected] { background: #fff ; } |
||||
#scenario-search .select2-results__option--highlighted[aria-selected] { background: #5897fb ; } |
||||
|
||||
#scenario-search .select2-results__option { padding-bottom: 0 ; } |
||||
#scenario-search .select2-results__option--highlighted { background: #5897fb ; margin-bottom: -4px ; } |
||||
#scenario-search .select2-results__option--highlighted .search-result { border: none ; background: transparent ; } |
||||
#scenario-search .search-result { border: 1px dotted #888 ; padding: 0 0.5em 0.25em 0.5em ; background: #f0f0f0 ; font-size: 95% ; } |
||||
#scenario-search .search-result .scenario-name { font-weight: bold ; } |
||||
#scenario-search .search-result .scenario-id { font-size: 80% ; font-style: italic ; } |
||||
#scenario-search .search-result .scenario-location { font-size: 90% ; } |
||||
#scenario-search .search-result .scenario-date { font-size: 80% ; font-style: italic ; color: #444 ; } |
||||
#scenario-search .search-result .publication-name { font-size: 80% ; font-style: italic ; color: #666 ; } |
||||
#scenario-search .search-result .publisher-name { font-size: 80% ; font-style: italic ; color: #666 ; } |
||||
#scenario-search .search-result .publication-date { font-size: 80% ; font-style: italic ; color: #666 ; } |
||||
|
||||
/* footer */ |
||||
#scenario-search .footer { |
||||
margin-bottom: -0.5em ; |
||||
padding-top: 0.5em ; |
||||
font-size: 70% ; font-style: italic ; |
||||
opacity: 0.8 ; |
||||
} |
||||
#scenario-search .footer img.logo { height: 1.5em ; } |
||||
#scenario-search .footer .caption { display: inline-block ; line-height: 1em ; } |
||||
#scenario-search .footer a { color: #666 ; } |
||||
#scenario-search .footer a:link { text-decoration: none ; } |
||||
#scenario-search .footer a:focus { outline: 0 ; } |
||||
|
||||
/* import control */ |
||||
#scenario-search .import-control { margin-top: 0.5em ; padding-top: 0.25em ; border-top: 1px dotted #666 ; } |
||||
#scenario-search .import-control .buttons button { float: right ; margin-left: 0.5em ; padding: 0.1em 0.5em ; } |
||||
#scenario-search .import-control .buttons button.import { height: 2em ; display: flex ; align-items: center ; } |
||||
#scenario-search .import-control .buttons button.import img { height: 1em ; margin-right: 0.5em ; } |
||||
#scenario-search .import-control .buttons button.ok { background: #ddd ; } |
||||
#scenario-search .import-control .buttons button.ok:hover { background: #ccc ; } |
||||
#scenario-search .import-control .warnings { margin-bottom: 0.5em ; padding: 0.25em 0 0 10px ; font-size: 90% ; } |
||||
#scenario-search .import-control .warnings .header { display: flex ; align-items: center ; height: 1.5em ; margin-left: -5px ; font-size: 120% ; } |
||||
#scenario-search .import-control .warnings .header img { height: 1.5em ; margin: 0 0.5em 0.5em 0 ; } |
||||
#scenario-search .import-control .warnings .header .caption { margin-top: -5px ; } |
||||
#scenario-search .import-control .warnings input[type="checkbox"] { margin-right: 0.25em ; } |
||||
#scenario-search .import-control .warnings .hint { |
||||
font-size: 90% ; font-style: italic ; color: #444 ; |
||||
white-space: nowrap ; overflow: hidden ; text-overflow: ellipsis ; |
||||
} |
||||
#scenario-search .import-control .warnings div.hint { margin-left: 20px ; } |
||||
#scenario-search .import-control .warnings div.hint img { height: 1em ; margin: 0 0.25em -2px 0 ; } |
||||
#scenario-search .import-control .warnings .warning2 img { height: 1.25em ; margin: 0 0.25em -2px -2px ; } |
@ -0,0 +1,48 @@ |
||||
div.jquery-image-zoom { |
||||
line-height: 0; |
||||
font-size: 0; |
||||
|
||||
z-index: 1000; |
||||
|
||||
border: 5px solid #fff; |
||||
background: #eee; /* TM 25jan15: Added this to make it easier to see images with transparent backgrounds. */ |
||||
margin: -5px; |
||||
|
||||
-webkit-box-shadow: 0 0 10px rgba(0, 0, 0, 0.3); |
||||
-moz-box-shadow: 0 0 10px rgba(0, 0, 0, 0.3); |
||||
box-shadow: 0 0 10px rgba(0, 0, 0, 0.3); |
||||
} |
||||
|
||||
div.jquery-image-zoom a { |
||||
background: url(/static/imageZoom/jquery.imageZoom.png) no-repeat; |
||||
|
||||
display: block; |
||||
width: 25px; |
||||
height: 25px; |
||||
|
||||
position: absolute; |
||||
left: -17px; |
||||
top: -17px; |
||||
/* IE-users are prolly used to close-link in right-hand corner */ |
||||
*left: auto; |
||||
*right: -17px; |
||||
|
||||
text-decoration: none; |
||||
text-indent: -100000px; |
||||
outline: 0; |
||||
|
||||
z-index: 11; |
||||
} |
||||
|
||||
div.jquery-image-zoom a:hover { |
||||
background-position: left -25px; |
||||
} |
||||
|
||||
div.jquery-image-zoom img, |
||||
div.jquery-image-zoom embed, |
||||
div.jquery-image-zoom object, |
||||
div.jquery-image-zoom div { |
||||
width: 100%; |
||||
height: 100%; |
||||
margin: 0; |
||||
} |
@ -0,0 +1 @@ |
||||
jQuery.fn.imageZoom=function(c,b){var a=c.extend({speed:200,dontFadeIn:1,hideClicked:1,imageMargin:30,className:"jquery-image-zoom",loading:"Loading..."},b);a.doubleSpeed=a.speed/4;c(document).keydown(function(d){if(d.keyCode==27){c("div.jquery-image-zoom a").click()}});return this.click(function(k){var h=c(k.target);var g=h.is("a")?h:h.parents("a");g=(g&&g.is("a")&&g.attr("href").search(/(.*)\.(jpg|jpeg|gif|png|bmp|tif|tiff)$/gi)!=-1)?g:false;var i=(g&&g.find("img").length)?g.find("img"):false;c("div.jquery-image-zoom a").click();if(g){g.oldText=g.text();g.setLoadingImg=function(){if(i){i.css({opacity:"0.5"})}else{g.text(a.loading)}};g.setNotLoadingImg=function(){if(i){i.css({opacity:"1"})}else{g.text(g.oldText)}};var d=g.attr("href");if(c("div."+a.className+' img[src="'+d+'"]').length){return false}var j=function(l){g.setNotLoadingImg();var u=i?i:g;var q=i?a.hideClicked:0;var p=u.offset();var n={width:u.outerWidth(),height:u.outerHeight(),left:p.left,top:p.top};var o=c('<div><img src="'+d+'" alt=""/></div>').css("position","absolute").appendTo(document.body);var m={width:l.width,height:l.height};var s={width:c(window).width(),height:c(window).height()};if(m.width>(s.width-a.imageMargin*2)){var r=s.width-a.imageMargin*2;m.height=(r/m.width)*m.height;m.width=r}if(m.height>(s.height-a.imageMargin*2)){var t=s.height-a.imageMargin*2;m.width=(t/m.height)*m.width;m.height=t}m.left=(s.width-m.width)/2+c(window).scrollLeft();m.top=(s.height-m.height)/2+c(window).scrollTop();var e=c('<a href="#">Close</a>').appendTo(o).hide();if(q){g.css("visibility","hidden")}o.addClass(a.className).css(n).animate(m,a.speed,function(){e.fadeIn(a.doubleSpeed)});var v=function(){e.fadeOut(a.doubleSpeed,function(){o.animate(n,a.speed,function(){g.css("visibility","visible");o.remove()})});return false};o.click(v);e.click(v)};var f=new Image();f.src=d;if(f.complete){j(f)}else{g.setLoadingImg();f.onload=function(){j(f)}}return false}})}; |
After Width: | Height: | Size: 1.7 KiB |
After Width: | Height: | Size: 13 KiB |
After Width: | Height: | Size: 4.7 KiB |
Before Width: | Height: | Size: 139 B After Width: | Height: | Size: 139 B |
After Width: | Height: | Size: 355 B |
After Width: | Height: | Size: 4.8 KiB |
After Width: | Height: | Size: 3.2 KiB |
After Width: | Height: | Size: 2.4 KiB |
After Width: | Height: | Size: 14 KiB |
After Width: | Height: | Size: 2.4 KiB |
@ -0,0 +1,27 @@ |
||||
/* jshint esnext: true */ |
||||
|
||||
// --------------------------------------------------------------------
|
||||
|
||||
class PerformanceTimer |
||||
{ |
||||
constructor( msg ) { |
||||
// initialize
|
||||
if ( msg ) |
||||
console.log( "Starting timer:", msg ) ; |
||||
this.startTimer() ; |
||||
} |
||||
|
||||
startTimer() { |
||||
// start the timer
|
||||
this.startTime = window.performance.now() ; |
||||
return this.startTime ; |
||||
} |
||||
|
||||
stopTimer() { |
||||
// stop the timer
|
||||
var elapsedTime = window.performance.now() - this.startTime ; |
||||
this.startTime = null ; |
||||
return elapsedTime ; |
||||
} |
||||
|
||||
} |
@ -1,120 +0,0 @@ |
||||
<!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,153 @@ |
||||
<div class="header"> <!-- begin header --> |
||||
|
||||
<div> |
||||
<span class="scenario-name"> {%if SCENARIO_NAME%} |
||||
{%if SCENARIO_URL%}<a href="{{SCENARIO_URL}}">{%endif%}{{SCENARIO_NAME}}{%if SCENARIO_URL%}</a>{%endif%} |
||||
{%else%} |
||||
Untitled scenario |
||||
{%endif%} </span> |
||||
{%if SCENARIO_DISPLAY_ID%} <span class="scenario-id"> ({{SCENARIO_DISPLAY_ID}}) </span> {%endif%} |
||||
</div> |
||||
{%if SCENARIO_LOCATION%} <div> |
||||
<span class="scenario-location"> {{SCENARIO_LOCATION}} </span> |
||||
{%if SCENARIO_DATE%} <span class="scenario-date"> ({{SCENARIO_DATE}}) </span> {%endif%} |
||||
</div> {%endif%} |
||||
|
||||
<!-- info box --> |
||||
{%if THEATER or TURN_COUNT or PLAYING_TIME or ICONS is defined%} <div class="info" style="display:none;"> |
||||
{%if THEATER%} <div class="theater"> {{THEATER}} </div> {%endif%} |
||||
{%if TURN_COUNT%}<div class="turn-count"> {{TURN_COUNT|safe}} </div> {%endif%} |
||||
{%if PLAYING_TIME%} <div class="playing-time"> {{PLAYING_TIME|safe}} </div> {%endif%} |
||||
{%if ICONS is defined%} <div class="icons"> |
||||
{%if ICONS.IS_DELUXE%} <img src="{{url_for('static',filename='images/icons/deluxe.png')}}" title="Deluxe ASL"> {%endif%} |
||||
{%if ICONS.IS_ASLSK%} <img src="{{url_for('static',filename='images/icons/aslsk.png')}}" title="Starter Kit"> {%endif%} |
||||
{%if ICONS.IS_NIGHT%} <img src="{{url_for('static',filename='images/icons/night.png')}}" title="Night scenario"> {%endif%} |
||||
{%if ICONS.DEFENDER_OBA or ICONS.ATTACKER_OBA%} <img src="{{url_for('static',filename='images/icons/oba.png')}}" class="oba" title="Has OBA"> {%endif%} |
||||
</div> {%endif%} |
||||
</div> {%endif%} |
||||
|
||||
</div> <!-- end header --> |
||||
|
||||
<div class="header2"> <!-- begin header2 --> |
||||
|
||||
{%if DESIGNER%} |
||||
<div class="designer"> Designer: {{DESIGNER}} </div> |
||||
{%endif%} |
||||
|
||||
{%if PUBLICATION%} <div> |
||||
<span class="publication"> |
||||
{%if PUBLICATION_URL%}<a href="{{PUBLICATION_URL}}">{%endif%}{{PUBLICATION}}{%if PUBLICATION_URL%}</a>{%endif%} |
||||
</span> |
||||
{%if PUBLISHER%} <span class="publisher"> |
||||
({%if PUBLISHER_URL%}<a href="{{PUBLISHER_URL}}">{%endif%}{{PUBLISHER}}{%if PUBLISHER_URL%}</a>{%endif%}) |
||||
</span> {%endif%} |
||||
{%if PUBLICATION_DATE%} <span class="publication-date"> ({{PUBLICATION_DATE}}) </span> {%endif%} |
||||
</div> {%endif%} |
||||
|
||||
{%if PREV_PUBLICATION%} |
||||
<div class="prev-publication"> Previously: {{PREV_PUBLICATION}} </div> |
||||
{%endif%} |
||||
|
||||
{%if REVISED_PUBLICATION%} |
||||
<div class="revised-publication"> Revised: {{REVISED_PUBLICATION}} </div> |
||||
{%endif%} |
||||
|
||||
</div> <!-- end header2 --> |
||||
|
||||
<div class="content"> <!-- begin content --> |
||||
|
||||
{%if MAP_URL%} <div class="map"> |
||||
{%if MAP_URL.startswith("http") %} |
||||
<iframe class="map" src="{{MAP_URL}}"></iframe> |
||||
{%else%} |
||||
<div style="border:1px dotted #888;padding:0.5em;text-justify:none;"> {{MAP_URL}} </div> |
||||
{%endif%} |
||||
</div> {%endif%} |
||||
|
||||
{%if OVERVIEW%} <div class="overview"> |
||||
{%if OVERVIEW_BRIEF%} |
||||
<div class="full" style="display:none;"> {{OVERVIEW|safe}} </div> |
||||
<div class="brief"> {{OVERVIEW_BRIEF|safe}} </div> |
||||
<div class="more"> more... </div> |
||||
{%else%} |
||||
{{OVERVIEW|safe}} |
||||
{%endif%} |
||||
</div> {%endif%} |
||||
|
||||
<div class="player-info"> <!-- begin player-info --> |
||||
|
||||
{%if ICONS is defined and (ICONS.DEFENDER_OBA or ICONS.ATTACKER_OBA)%} |
||||
<table class="oba" style="display:none;"> |
||||
<tr> <td class="oba-header" colspan="4"> Off-board Artillery |
||||
{%if ICONS.DEFENDER_OBA%} |
||||
<tr class="defender"> <td class="name"> {{DEFENDER_NAME or "Defender"}} <td class="count black"> ? <td class="count red"> ? <td class="spacer"> |
||||
<tr class="defender"> <td class="comments" colspan="4"> |
||||
{%endif%} |
||||
{%if ICONS.ATTACKER_OBA%} |
||||
<tr class="attacker"> <td class="name"> {{ATTACKER_NAME or "Attacker"}} <td class="count black"> ? <td class="count red"> ? <td class="spacer"> |
||||
<tr class="attacker"> <td class="comments" colspan="4"> |
||||
{%endif%} |
||||
<tr> <td colspan="4" class="date-warning" style="display:none;"> |
||||
<img src="{{url_for('static',filename='images/warning.gif')}}"> |
||||
Based on a scenario date of <span class="val"></span>. |
||||
</table> |
||||
{%endif%} |
||||
|
||||
<table class="players"> |
||||
{%if DEFENDER_NAME%} |
||||
<tr class="defender"> |
||||
<td class="label"> Defender: <td class="flag"> <td class="name"> {{DEFENDER_NAME}} |
||||
{%if DEFENDER_DESC%} |
||||
<tr class="defender"> |
||||
<td> <td class="desc" colspan="2"> {{DEFENDER_DESC}} |
||||
{%endif%} |
||||
{%endif%} |
||||
{%if ATTACKER_NAME%} |
||||
<tr class="attacker"> |
||||
<td class="label"> Attacker: <td class="flag"> <td class="name"> {{ATTACKER_NAME}} |
||||
{%if ATTACKER_DESC%} |
||||
<tr class="attacker"> |
||||
<td> <td class="desc" colspan="2"> {{ATTACKER_DESC}} |
||||
{%endif%} |
||||
{%endif%} |
||||
</table> |
||||
|
||||
<div class="balance-graphs"> |
||||
<div class='balance-graph roar'> </div> |
||||
<div class='balance-graph asa'> </div> |
||||
<div class="connect-roar" style="display:none;"> |
||||
<img src="{{url_for('static',filename='images/connect-roar.png')}}"> |
||||
Connect to ROAR |
||||
</div> |
||||
</div> |
||||
|
||||
</div> <!-- end player-info --> |
||||
|
||||
{%if BOARDS%} <div class="boards"> |
||||
<b>Boards:</b> {{BOARDS}} |
||||
{%if MAP_IMAGES%} {%for map in MAP_IMAGES%} |
||||
<a href="{{map}}" class="map-preview"><img src="{{url_for('static',filename='images/map-preview.png')}}" title="Board preview"></a> |
||||
{%endfor%} {%endif%} |
||||
</div> {%endif%} |
||||
|
||||
{%if OVERLAYS%} <div class="overlays"> <b>Overlays:</b> {{OVERLAYS}} </div> {%endif%} |
||||
|
||||
{%if EXTRA_RULES%} <div class="extra-rules"> <b>Rules:</b> {{EXTRA_RULES}} </div> {%endif%} |
||||
|
||||
{%if ERRATA%} <div class="errata"> |
||||
{%if ERRATA|length == 1%} |
||||
<b>Errata:</b> |
||||
<span class="text">{{ERRATA[0].text}}</span> |
||||
{%if ERRATA[0].source%} <span class="source">[{{ERRATA[0].source}}]</span> {%endif%} |
||||
{%elif ERRATA|length >= 2%} |
||||
<b>Errata:</b> <ul> |
||||
{%for errata in ERRATA%} <li> |
||||
<span class="text">{{errata.text}}</span> |
||||
{%if errata.source%}<span class="source">[{{errata.source}}]</span> {%endif%} |
||||
{%endfor%} |
||||
</ul> |
||||
{%endif%} |
||||
|
||||
</div> {%endif%} |
||||
|
||||
</div> <!-- end content --> |
@ -0,0 +1,3 @@ |
||||
<div id="scenario-info-dialog" style="display:none;"> |
||||
<div class="scenario-card"></div> |
||||
</div> |
@ -0,0 +1,132 @@ |
||||
<!doctype html> <!-- NOTE: For testing porpoises only! --> |
||||
<html lang="en"> |
||||
|
||||
<head> |
||||
<meta charset="utf-8"> |
||||
<title> Scenario nationalities</title> |
||||
<style> |
||||
table { margin: 0 0 0 2em ; } |
||||
th { padding: 2px 5px ; text-align: left ; background: #eee ; } |
||||
td { padding: 2px 5px 2px 1em ; text-align: right ; } |
||||
</style> |
||||
</head> |
||||
|
||||
<body> |
||||
|
||||
<div id="results"></div> |
||||
|
||||
</body> |
||||
|
||||
<script src="{{url_for('static',filename='jquery/jquery-3.3.1.min.js')}}"></script> |
||||
<script src="{{url_for('static',filename='scenarios.js')}}"></script> |
||||
|
||||
<script> |
||||
var gAppConfig, gTemplatePack, gScenarioIndex ; |
||||
$(document).ready( function () { |
||||
// get the information we need |
||||
var nResponsesPending = 3 ; |
||||
function onResponse() { |
||||
if ( -- nResponsesPending == 0 ) |
||||
generateReport() ; |
||||
} |
||||
$.getJSON( "{{url_for('get_app_config')}}", function( resp ) { |
||||
gAppConfig = resp ; |
||||
onResponse() ; |
||||
} ).fail( function( xhr, status, errorMsg ) { |
||||
alert( "Can't get the app config:\n\n" + errorMsg ) ; |
||||
} ) ; |
||||
$.getJSON( "{{url_for('get_template_pack')}}", function( resp ) { |
||||
gTemplatePack = resp ; |
||||
onResponse() ; |
||||
} ).fail( function( xhr, status, errorMsg ) { |
||||
alert( "Can't get the template pack:\n\n" + errorMsg ) ; |
||||
} ) ; |
||||
$.getJSON( "{{url_for('get_scenario_index')}}", function( resp ) { |
||||
gScenarioIndex = resp ; |
||||
onResponse() ; |
||||
} ).fail( function( xhr, status, errorMsg ) { |
||||
alert( "Can't get the scenario index:\n\n" + errorMsg ) ; |
||||
} ) ; |
||||
} ) ; |
||||
</script> |
||||
|
||||
<script> |
||||
|
||||
function generateReport() { |
||||
|
||||
// initialize |
||||
var exactMatches={}, mappedMatches={}, unknownNats={} ; |
||||
|
||||
function updateCount( counts, key ) { |
||||
if ( counts[key] === undefined ) |
||||
counts[ key ] = 1 ; |
||||
else |
||||
++ counts[ key ] ; |
||||
} |
||||
function checkPlayerNat( playerName ) { |
||||
if ( ! playerName ) |
||||
return ; |
||||
var effectiveNat = getEffectivePlayerNat( playerName ) ; |
||||
if ( ! effectiveNat ) { |
||||
// record an unknown player nationality |
||||
updateCount( unknownNats, playerName ) ; |
||||
} |
||||
else if ( effectiveNat[1] == "exactMatch" || effectiveNat[1] == "partialMatch" ) { |
||||
// record an exact match |
||||
updateCount( exactMatches, effectiveNat[0] ) ; |
||||
} else if ( effectiveNat[1] == "exactMapping" || effectiveNat[1] == "partialMapping" ) { |
||||
// record a mapping |
||||
updateCount( mappedMatches, effectiveNat[2] ) ; |
||||
} else { |
||||
alert( "Unknown effective nat type: " + effectiveNat[1] ) ; |
||||
} |
||||
} |
||||
|
||||
// process each scenario |
||||
var $results = $( "#results" ) ; |
||||
var maxScenarios = gScenarioIndex.length ; |
||||
function processScenario( scenarioNo ) { |
||||
if ( scenarioNo >= maxScenarios ) { |
||||
// we've processed all the scenarios - output the results |
||||
outputResults() ; |
||||
return ; |
||||
} |
||||
// get the next scenario |
||||
$results.text( "Generating the report: " + (1+scenarioNo) + "/" + gScenarioIndex.length ) ; |
||||
var url = "{{url_for('get_scenario',scenario_id='ID')}}".replace( "ID", gScenarioIndex[scenarioNo].scenario_id ) ; |
||||
$.getJSON( url, function( resp ) { |
||||
// check the 2 players |
||||
checkPlayerNat( resp.defender_name ) ; |
||||
checkPlayerNat( resp.attacker_name ) ; |
||||
// process the next scenario |
||||
processScenario( scenarioNo + 1 ) ; |
||||
} ).fail( function( xhr, status, errorMsg ) { |
||||
alert( "Can't get the scenario:\n\n" + errorMsg ) ; |
||||
} ) ; |
||||
} |
||||
processScenario( 0 ) ; |
||||
|
||||
function outputResults() { |
||||
// output the results |
||||
$results.text( "Processed " + gScenarioIndex.length + " scenarios." ) ; |
||||
makeTable( "Exact/partial matches", exactMatches ) ; |
||||
makeTable( "Mapped matches", mappedMatches ) ; |
||||
makeTable( "Unknown", unknownNats ) ; |
||||
} |
||||
function makeTable( caption, vals, cmp ) { |
||||
var rows = [] ; |
||||
for ( key in vals ) |
||||
rows.push( [ key, vals[key] ] ) ; |
||||
rows.sort( function( lhs, rhs ) { return lhs[1] < rhs[1] ; } ) ; |
||||
var buf = [ "<h2>", caption, "</h2>", "<table>" ] ; |
||||
rows.forEach( function( row ) { |
||||
buf.push( "<tr>", "<th>", row[0], "<td>", row[1] ) ; |
||||
} ) ; |
||||
buf.push( "</table>" ) ; |
||||
$results.append( $( buf.join("") ) ) ; |
||||
} |
||||
|
||||
} |
||||
</script> |
||||
|
||||
</html> |
@ -0,0 +1,34 @@ |
||||
<div id="scenario-search" style="display:none;"> |
||||
|
||||
<div class="split left"> <!-- left panel --> |
||||
|
||||
<div class="scenarios"> <select></select> </div> |
||||
|
||||
<div class="footer"> |
||||
<a href="https://aslscenarioarchive.com"> |
||||
<img class="logo" src="{{url_for('static',filename='images/asl-scenario-archive.png')}}"> |
||||
<div class="caption"> Powered by the ASL Scenario Archive </div> |
||||
</a> |
||||
</div> |
||||
|
||||
</div> <!-- end left panel --> |
||||
|
||||
<div class="split right"> <!-- right panel --> |
||||
|
||||
<div class="scenario-card"> </div> |
||||
|
||||
<div class="import-control"> |
||||
<div class="warnings" style="display:none;"></div> |
||||
<div class="buttons"> |
||||
<button class="import" title="Import this scenario and link it to the ASL Scenario Archive."> |
||||
<img class="logo" src="{{url_for('static',filename='images/sortable-add.png')}}"> |
||||
Import |
||||
</button> |
||||
<button class="cancel-import"> Cancel </button> |
||||
<button class="confirm-import ok"> OK </button> |
||||
</div> |
||||
</div> |
||||
|
||||
</div> <!-- end right panel --> |
||||
|
||||
</div> |
@ -0,0 +1,137 @@ |
||||
{ |
||||
|
||||
"_generatedBy_": "(test data)", |
||||
"_generatedAt_": "(just now)", |
||||
|
||||
"scenarios": [ |
||||
|
||||
{ "scenario_id": "1", |
||||
"title": "Full content scenario", |
||||
"sc_id": "FCS-1", |
||||
"scen_location": "Some place", |
||||
"scen_date": "1945-12-31 00:00:00", |
||||
"theatre": "PTO", |
||||
"time_to_play": 1.25, |
||||
"min_turns": "6", "max_turns": "6", |
||||
"oba": "B", "night": "1", "deluxe": "1", "aslsk": "1", |
||||
"author": "Joe Author", |
||||
"pub_name": "ASL Journal", |
||||
"pub_id": "PUB-1", |
||||
"publisher_name": "Avalon Hill", |
||||
"publisher_id": "42", |
||||
"published_date": "2001-02-03", |
||||
"prior_publication": "Prior version", |
||||
"revision": "Revised version", |
||||
"gps_lat": "1.23", "gps_long": "4.56", |
||||
"overview": "This is a really good scenario.", |
||||
"defender": "Dutch", |
||||
"def_desc": "1st Dutch Army", |
||||
"attacker": "Romanian", |
||||
"att_desc": "1st Romanian Army", |
||||
"maps": [ 1, "2", "RB" ], |
||||
"mapImages": [ |
||||
"http://localhost:5010/static/images/asl-scenario-archive.png" |
||||
], |
||||
"overlays": [ 1, "2", "OG1" ], |
||||
"misc": "Some extra rules", |
||||
"errata": [ |
||||
{ "text": "Errata #1", "source": "over there" }, |
||||
{ "text": "Errata #2", "source": "right here" } |
||||
], |
||||
"playings": [ { |
||||
"totalGames": "10", |
||||
"defender_wins": "3", |
||||
"attacker_wins": "7" |
||||
} ] |
||||
}, |
||||
|
||||
{ "scenario_id": "2", |
||||
"title": "Fighting Withdrawal", |
||||
"sc_id": "FW", |
||||
"scen_location": "Sestroretsk Road, Terijoki, Finland", |
||||
"pub_name": "Beyond Valor", |
||||
"overview": "The Finns, seeking restitution for the Winter War of 1939, had erupted across the borders and breached the Soviet Karelian front even as the crisis to the south of Leningrad came.", |
||||
"attacker": "Finnish", |
||||
"defender": "Russians", |
||||
"playings": [ { |
||||
"totalGames": "134", |
||||
"defender_wins": "78", |
||||
"attacker_wins": "56" |
||||
} ] |
||||
}, |
||||
|
||||
{ "scenario_id": "3a", |
||||
"title": "MTO Scenario", |
||||
"theatre": "MTO" |
||||
}, |
||||
{ "scenario_id": "3b", |
||||
"title": "African Scenario", |
||||
"theatre": "Africa" |
||||
}, |
||||
|
||||
{ "scenario_id": "4a", |
||||
"title": "Unknown players", |
||||
"attacker": "Oceania", |
||||
"defender": "Eastasia", |
||||
"playings": [ { |
||||
"totalGames": "3", |
||||
"defender_wins": "2", |
||||
"attacker_wins": "1" |
||||
} ] |
||||
}, |
||||
{ "scenario_id": "4b", |
||||
"title": "Test partial nationality matches", |
||||
"defender": "Massed Russian Hordes", |
||||
"attacker": "The Japanese" |
||||
}, |
||||
{ "scenario_id": "4c", |
||||
"title": "Test nationality mapping", |
||||
"defender": "Gurkha", |
||||
"attacker": "Oh, Canada!" |
||||
}, |
||||
|
||||
{ "scenario_id": "5a", |
||||
"title": "OBA test", |
||||
"theatre": "ETO", |
||||
"scen_location": "Some place", |
||||
"scen_date": "1940-02-01 00:00:00", |
||||
"defender": "Finnish", |
||||
"attacker": "German", |
||||
"oba": "B" |
||||
}, |
||||
{ "scenario_id": "5b", |
||||
"title": "Defender OBA only", |
||||
"theatre": "ETO", |
||||
"scen_location": "Some place", |
||||
"scen_date": "1940-01-01 00:00:00", |
||||
"defender": "Burmese", |
||||
"oba": "D" |
||||
}, |
||||
{ "scenario_id": "5c", |
||||
"title": "Attacker OBA only", |
||||
"theatre": "ETO", |
||||
"scen_location": "Some place", |
||||
"scen_date": "1940-01-01 00:00:00", |
||||
"attacker": "Russian", |
||||
"defender": "The Other Guy", |
||||
"oba": "B" |
||||
}, |
||||
|
||||
{ "scenario_id": "6a", |
||||
"title": "ROAR Exact Match", |
||||
"sc_id": "EXACT-MATCH" |
||||
}, |
||||
{ "scenario_id": "6b", |
||||
"title": "ROAR Exact Match 2", |
||||
"sc_id": "EXACT-MATCH-2" |
||||
}, |
||||
{ "scenario_id": "6c", |
||||
"title": "ROAR Multiple Matches" |
||||
}, |
||||
|
||||
{ "scenario_id": "no-content" |
||||
} |
||||
|
||||
] |
||||
|
||||
} |
@ -1,2 +1,2 @@ |
||||
player1=[{{PLAYER_1}}:{{PLAYER_1_NAME}}] ; ELR=[{{PLAYER_1_ELR}}] ; SAN=[{{PLAYER_1_SAN}}] |
||||
player2=[{{PLAYER_2}}:{{PLAYER_2_NAME}}] ; ELR=[{{PLAYER_2_ELR}}] ; SAN=[{{PLAYER_2_SAN}}] |
||||
player1=[{{PLAYER_1}}:{{PLAYER_1_NAME}}] ; ELR=[{{PLAYER_1_ELR}}] ; SAN=[{{PLAYER_1_SAN}}] ; description=[{{PLAYER_1_DESCRIPTION}}] |
||||
player2=[{{PLAYER_2}}:{{PLAYER_2_NAME}}] ; ELR=[{{PLAYER_2_ELR}}] ; SAN=[{{PLAYER_2_SAN}}] ; description=[{{PLAYER_2_DESCRIPTION}}] |
||||
|
@ -1,29 +1,44 @@ |
||||
{ |
||||
|
||||
"1": { "scenario_id": "TEST 1", "name": "Fighting Withdrawal", |
||||
"_lastUpdated_": "some time ago", |
||||
"_nPlayings_": 123, |
||||
"_generatedBy_": "(test data)", |
||||
"_generatedAt_": "(just now)", |
||||
|
||||
"100": { |
||||
"scenario_id": "FW", "name": "Fighting Withdrawal", |
||||
"publication": "Beyond Valor", |
||||
"results": [ [ "Finnish", 200 ], [ "Russian", 300 ] ], |
||||
"url": "http://test.com/1" |
||||
"results": [ [ "Finnish", 279 ], [ "Russian", 325 ] ], |
||||
"url": "http://asl-roar.com/100" |
||||
}, |
||||
|
||||
"2": { "scenario_id": "TEST 2", "name": "Whitewash 1", |
||||
"results": [ [ "American", 10 ], [ "Japanese", 0 ] ], |
||||
"url": "http://test.com/2" |
||||
"101": { |
||||
"scenario_id": "ANOTHER", "name": "Another ROAR scenario", |
||||
"results": [ [ "British", 2 ], [ "French", 1 ] ], |
||||
"url": "http://asl-roar.com/101" |
||||
}, |
||||
|
||||
"3": { "scenario_id": "TEST 3", "name": "Whitewash 2", |
||||
"results": [ [ "American", 0 ], [ "Japanese", 10 ] ], |
||||
"url": "http://test.com/3" |
||||
"200": { |
||||
"name": "!! ROAR exact-match !!" |
||||
}, |
||||
|
||||
"4": { "scenario_id": "TEST 4", "name": "No playings", |
||||
"results": [ [ "British", 0 ], [ "French", 0 ] ], |
||||
"url": "http://test.com/4" |
||||
"210": { |
||||
"name": "ROAR Exact Match 2", |
||||
"scenario_id": "foo" |
||||
}, |
||||
|
||||
"5": { "scenario_id": "TEST 5", "name": "Unknown nationality", |
||||
"results": [ [ "American", 1 ], [ "Martian", 1 ] ], |
||||
"url": "http://test.com/5" |
||||
"211": { |
||||
"name": "ROAR Exact Match 2", |
||||
"scenario_id": "EXACT-MATCH-2" |
||||
}, |
||||
"220": { |
||||
"name": "ROAR Multiple Matches", |
||||
"results": [ [ "British", 5 ], [ "French", 2 ] ] |
||||
}, |
||||
"221": { |
||||
"name": "ROAR Multiple Matches" |
||||
}, |
||||
"222": { |
||||
"name": "ROAR Multiple Matches", |
||||
"results": [ [ "German", 10 ], [ "Russian", 5 ] ] |
||||
} |
||||
|
||||
} |
||||
|
@ -1,148 +0,0 @@ |
||||
"""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 |
@ -0,0 +1,756 @@ |
||||
"""" Test scenario search. """ |
||||
|
||||
import os |
||||
import time |
||||
|
||||
import pytest |
||||
from selenium.webdriver.common.action_chains import ActionChains |
||||
from selenium.webdriver.common.keys import Keys |
||||
from selenium.common.exceptions import ElementClickInterceptedException, ElementNotInteractableException |
||||
|
||||
from vasl_templates.webapp.tests.test_scenario_persistence import save_scenario, load_scenario |
||||
from vasl_templates.webapp.tests.utils import init_webapp, select_tab, new_scenario, \ |
||||
set_player, set_template_params, set_scenario_date, get_player_nat, get_theater, set_theater, \ |
||||
wait_for, wait_for_elem, find_child, find_children, get_css_classes |
||||
|
||||
# --------------------------------------------------------------------- |
||||
|
||||
def test_scenario_cards( webapp, webdriver ): |
||||
"""Test showing scenario cards.""" |
||||
|
||||
# initialize |
||||
init_webapp( webapp, webdriver ) |
||||
|
||||
# search for the "full" scenario and check the scenario card |
||||
_do_scenario_search( "full", [1], webdriver ) |
||||
card = _unload_scenario_card() |
||||
assert card == { |
||||
"scenario_name": "Full content scenario", "scenario_id": "FCS-1", |
||||
"scenario_url": "https://aslscenarioarchive.com/scenario.php?id=1", |
||||
"scenario_location": "Some place", "scenario_date": "31st December, 1945", |
||||
"theater": "PTO", "turn_count": "6", "playing_time": "1\u00bc hours", |
||||
"icons": [ "aslsk.png", "deluxe.png", "night.png", "oba.png" ], |
||||
"designer": "Joe Author", |
||||
"publication": "ASL Journal", |
||||
"publication_url": "https://aslscenarioarchive.com/viewPub.php?id=PUB-1", |
||||
"publication_date": "3rd February, 2001", |
||||
"publisher": "Avalon Hill", |
||||
"publisher_url": "https://aslscenarioarchive.com/viewPublisher.php?id=42", |
||||
"prev_publication": "Prior version", "revised_publication": "Revised version", |
||||
"overview": "This is a really good scenario.", |
||||
"map_url": "MAP:[1.23,4.56]", |
||||
"defender_name": "Dutch", "defender_desc": "1st Dutch Army", |
||||
"attacker_name": "Romanian", "attacker_desc": "1st Romanian Army", |
||||
"balances": { "asa": [ |
||||
{ "name": "Dutch", "wins": 3, "percentage": 30 }, { "name": "Romanian", "wins": 7, "percentage": 70 } |
||||
] }, |
||||
"oba": [ |
||||
[ "Dutch", "1B", "1R" ], [ "Romanian", "-", "-" ] |
||||
], |
||||
"boards": "1, 2, RB", |
||||
"map_previews": [ "asl-scenario-archive.png" ], |
||||
"overlays": "1, 2, OG1", |
||||
"extra_rules": "Some extra rules", |
||||
"errata": [ |
||||
[ "Errata #1", "over there" ], |
||||
[ "Errata #2", "right here" ] |
||||
] |
||||
} |
||||
|
||||
# search for the "empty" scenario and check the scenario card |
||||
_do_scenario_search( "Untitled", ["no-content"], webdriver ) |
||||
card = _unload_scenario_card() |
||||
assert card == { |
||||
"scenario_name": "Untitled scenario (#no-content)", |
||||
"scenario_url": "https://aslscenarioarchive.com/scenario.php?id=no-content", |
||||
} |
||||
|
||||
# --------------------------------------------------------------------- |
||||
|
||||
def test_import_scenario( webapp, webdriver ): |
||||
"""Test importing a scenario.""" |
||||
|
||||
# initialize |
||||
init_webapp( webapp, webdriver ) |
||||
|
||||
# import the "full" scenario |
||||
dlg = _do_scenario_search( "full", [1], webdriver ) |
||||
find_child( "button.import", dlg ).click() |
||||
_check_scenario( |
||||
SCENARIO_NAME="Full content scenario", SCENARIO_ID="FCS-1", |
||||
SCENARIO_LOCATION="Some place", |
||||
PLAYER_1="dutch", PLAYER_1_DESCRIPTION="1st Dutch Army", |
||||
PLAYER_2="romanian", PLAYER_2_DESCRIPTION="1st Romanian Army", |
||||
THEATER="PTO" |
||||
) |
||||
|
||||
# import the "empty" scenario |
||||
_unlink_scenario() |
||||
dlg = _do_scenario_search( "Untitled", ["no-content"], webdriver ) |
||||
find_child( "button.import", dlg ).click() |
||||
find_child( "button.confirm-import", dlg ).click() |
||||
# NOTE: Since there are no players defined in the scenario, what's on-screen will be left unchanged. |
||||
_check_scenario( |
||||
SCENARIO_NAME="Untitled scenario (#no-content)", SCENARIO_ID="", |
||||
SCENARIO_LOCATION="", |
||||
PLAYER_1="dutch", PLAYER_1_DESCRIPTION="", |
||||
PLAYER_2="romanian", PLAYER_2_DESCRIPTION="", |
||||
THEATER="ETO" |
||||
) |
||||
|
||||
def _check_scenario( **kwargs ): |
||||
"""Check the scenario import.""" |
||||
keys = [ "SCENARIO_NAME", "SCENARIO_ID", "SCENARIO_LOCATION", "PLAYER_1_DESCRIPTION", "PLAYER_2_DESCRIPTION" ] |
||||
for key in keys: |
||||
elem = find_child( "input[name='{}']".format( key ) ) |
||||
assert elem.get_attribute( "value" ) == kwargs[ key ] |
||||
assert get_player_nat( 1 ) == kwargs[ "PLAYER_1" ] |
||||
assert get_player_nat( 2 ) == kwargs[ "PLAYER_2" ] |
||||
assert get_theater() == kwargs[ "THEATER" ] |
||||
|
||||
# --------------------------------------------------------------------- |
||||
|
||||
def test_import_warnings( webapp, webdriver ): #pylint: disable=too-many-statements |
||||
"""Test warnings when importing a scenario.""" |
||||
|
||||
# initialize |
||||
init_webapp( webapp, webdriver, scenario_persistence=1 ) |
||||
|
||||
# import a scenario on top of an empty scenario |
||||
dlg = _do_scenario_search( "full", [1], webdriver ) |
||||
find_child( "button.import", dlg ).click() |
||||
wait_for( 2, lambda: not dlg.is_displayed() ) |
||||
|
||||
def do_test( param_name, expected_warning, expected_val, curr_val="CURR-VAL" ): #pylint: disable=missing-docstring |
||||
|
||||
# start with a new scenario |
||||
new_scenario() |
||||
|
||||
# set the scenario parameter |
||||
set_template_params( { param_name: curr_val } ) |
||||
|
||||
# import a scenario |
||||
_do_scenario_search( "full", [1], webdriver ) |
||||
find_child( "#scenario-search button.import" ).click() |
||||
|
||||
# check if any warnings were expected |
||||
elem = find_child( "[name='{}']".format( param_name ) ) |
||||
if expected_warning: |
||||
# yup - make sure they are being shown |
||||
warnings = find_children( ".warnings input[type='checkbox']", dlg ) |
||||
if expected_warning: |
||||
assert [ w.get_attribute( "name" ) for w in warnings ] == [ expected_warning ] |
||||
else: |
||||
assert not warnings |
||||
# cancel the import |
||||
find_child( "button.cancel-import", dlg ).click() |
||||
wait_for( 2, lambda: not find_child( ".warnings", dlg ).is_displayed() ) |
||||
# do the import again, and accept it |
||||
find_child( "#scenario-search button.import" ).click() |
||||
find_child( "button.confirm-import", dlg ).click() |
||||
assert not dlg.is_displayed() |
||||
assert elem.get_attribute( "value" ) == expected_val |
||||
else: |
||||
# nope - check that the import was done |
||||
assert not dlg.is_displayed() |
||||
assert elem.get_attribute( "value" ) == expected_val |
||||
|
||||
# do the tests |
||||
do_test( "SCENARIO_NAME", "scenario_name", "Full content scenario" ) |
||||
do_test( "SCENARIO_ID", "scenario_display_id", "FCS-1" ) |
||||
do_test( "SCENARIO_LOCATION", "scenario_location", "Some place" ) |
||||
do_test( "SCENARIO_DATE", "scenario_date_iso", "12/31/1945", curr_val="01/02/1940" ) |
||||
do_test( "SCENARIO_THEATER", None, "PTO", curr_val="Burma" ) |
||||
do_test( "PLAYER_1", None, "dutch", curr_val="german" ) |
||||
do_test( "PLAYER_1_DESCRIPTION", "defender_desc", "1st Dutch Army" ) |
||||
do_test( "PLAYER_2", None, "romanian", curr_val="german" ) |
||||
do_test( "PLAYER_2_DESCRIPTION", "attacker_desc", "1st Romanian Army" ) |
||||
|
||||
# test importing a scenario on top of existing OB owned by the same nationality |
||||
new_scenario() |
||||
load_scenario( { |
||||
"PLAYER_1": "dutch", |
||||
} ) |
||||
_do_scenario_search( "full", [1], webdriver ) |
||||
find_child( "#scenario-search button.import" ).click() |
||||
warnings = find_children( ".warnings input[type='checkbox']", dlg ) |
||||
assert [ w.get_attribute( "name" ) for w in warnings ] == [] |
||||
|
||||
# test importing a scenario on top of existing OB owned by the same nationality |
||||
new_scenario() |
||||
load_scenario( { |
||||
"PLAYER_1": "dutch", |
||||
"OB_SETUPS_1": [ { "caption": "Dutch setup note" } ] |
||||
} ) |
||||
_do_scenario_search( "full", [1], webdriver ) |
||||
find_child( "#scenario-search button.import" ).click() |
||||
wait_for( 2, lambda: not dlg.is_displayed() ) |
||||
|
||||
# test importing a scenario on top of existing OB owned by the different nationality |
||||
new_scenario() |
||||
load_scenario( { |
||||
"PLAYER_1": "german", |
||||
"OB_SETUPS_1": [ { "caption": "German setup note" } ] |
||||
} ) |
||||
_do_scenario_search( "full", [1], webdriver ) |
||||
find_child( "#scenario-search button.import", dlg ).click() |
||||
warnings = wait_for( 2, lambda: find_children( ".warnings input[type='checkbox']", dlg ) ) |
||||
assert [ w.get_attribute( "name" ) for w in warnings ] == [ "defender_name" ] |
||||
assert not warnings[0].is_selected() |
||||
try: |
||||
warnings[0].click() |
||||
except (ElementClickInterceptedException, ElementNotInteractableException): |
||||
# FUDGE! We sometimes get a "Other element would receive the click" (div.warning) error, |
||||
# I suspect because the warnings panel is still sliding up. |
||||
time.sleep( 0.5 ) |
||||
warnings[0].click() |
||||
find_child( "button.confirm-import", dlg ).click() |
||||
assert not dlg.is_displayed() |
||||
assert get_player_nat( 1 ) == "dutch" |
||||
|
||||
# --------------------------------------------------------------------- |
||||
|
||||
def test_oba_info( webapp, webdriver ): |
||||
"""Test showing OBA info in the scenario card.""" |
||||
|
||||
# initialize |
||||
init_webapp( webapp, webdriver ) |
||||
|
||||
def check_oba_info( card, expected ): |
||||
"""Check the OBA info.""" |
||||
assert card["oba"] == expected |
||||
assert "oba.png" in card["icons"] |
||||
|
||||
# search for the "OBA test" scenario |
||||
dlg = _do_scenario_search( "OBA test", ["5a"], webdriver ) |
||||
expected = [ |
||||
[ "Finnish", "6B", "3R", "Plentiful Ammo included" ], |
||||
[ "German", "1B", "2R", "a comment", "another comment" ] |
||||
] |
||||
check_oba_info( _unload_scenario_card(), expected ) |
||||
|
||||
# import the scenario and check the OBA info |
||||
find_child( "button.import" ).click() |
||||
wait_for( 2, lambda: not dlg.is_displayed() ) |
||||
check_oba_info( _get_scenario_info(), expected ) |
||||
|
||||
# change the scenario date and check the OBA info |
||||
set_scenario_date( "01/02/1943" ) |
||||
check_oba_info( _get_scenario_info(), [ |
||||
[ "Finnish", "8B", "3R", "Plentiful Ammo included" ], |
||||
[ "German", "1B", "2R", "a comment", "another comment" ], |
||||
"Based on a scenario date of 1/43." |
||||
] ) |
||||
|
||||
# change the scenario date and check the OBA info |
||||
set_scenario_date( "" ) |
||||
check_oba_info( _get_scenario_info(), expected ) |
||||
|
||||
# check a scenario where only the defender has OBA |
||||
dlg = _do_scenario_search( "Defender OBA", ["5b"], webdriver ) |
||||
check_oba_info( _unload_scenario_card(), [ |
||||
[ "Burmese", "-", "-" ], |
||||
None |
||||
] ) |
||||
|
||||
# check a scenario where the attacker has OBA, the defender is an unknwon nationality |
||||
dlg = _do_scenario_search( "Attacker OBA", ["5c"], webdriver ) |
||||
check_oba_info( _unload_scenario_card(), [ |
||||
[ "The Other Guy", "?", "?" ], |
||||
[ "Russian", "3B", "4R" ] |
||||
] ) |
||||
|
||||
# --------------------------------------------------------------------- |
||||
|
||||
def test_unknown_theaters( webapp, webdriver ): |
||||
"""Test importing scenarios with unknown theaters.""" |
||||
|
||||
# initialize |
||||
init_webapp( webapp, webdriver ) |
||||
|
||||
# search for the "MTO" scenario (this has a theater mapping) |
||||
set_theater( "Korea" ) |
||||
dlg = _do_scenario_search( "MTO", ["3a"], webdriver ) |
||||
find_child( "button.import", dlg ).click() |
||||
wait_for( 2, lambda: not dlg.is_displayed() ) |
||||
assert get_theater() == "ETO" |
||||
|
||||
# search for the "Africa" scenario (this has no theater mapping) |
||||
new_scenario() |
||||
set_theater( "Korea" ) |
||||
dlg = _do_scenario_search( "Africa", ["3b"], webdriver ) |
||||
find_child( "button.import", dlg ).click() |
||||
_check_warnings( [], ["Unknown theater: Africa"] ) |
||||
find_child( "button.confirm-import", dlg ).click() |
||||
assert get_theater() == "other" |
||||
|
||||
# --------------------------------------------------------------------- |
||||
|
||||
def test_unknown_nats( webapp, webdriver ): |
||||
"""Test importing scenarios with unknown player nationalities.""" |
||||
|
||||
# initialize |
||||
init_webapp( webapp, webdriver ) |
||||
|
||||
# test importing a scenario with 2 completely unknown player nationalities |
||||
set_player( 1, "french" ) |
||||
set_player( 2, "italian" ) |
||||
dlg = _do_scenario_search( "Unknown players", ["4a"], webdriver ) |
||||
find_child( "button.import", dlg ).click() |
||||
_check_warnings( [], ["Unknown player: Eastasia","Unknown player: Oceania"] ) |
||||
expected_bgraphs = { "asa": [ |
||||
{ "name": "Eastasia", "wins": 2, "percentage": 67 }, |
||||
{ "name": "Oceania", "wins": 1, "percentage": 33 } |
||||
] } |
||||
assert _unload_balance_graphs( dlg ) == expected_bgraphs |
||||
find_child( "button.confirm-import", dlg ).click() |
||||
wait_for( 2, lambda: not find_child( "#scenario-search" ).is_displayed() ) |
||||
assert get_player_nat( 1 ) == "french" |
||||
assert get_player_nat( 2 ) == "italian" |
||||
|
||||
# test matching nationalities (partial name matches) |
||||
new_scenario() |
||||
dlg = _do_scenario_search( "partial nationality matches", ["4b"], webdriver ) |
||||
find_child( "button.import", dlg ).click() |
||||
wait_for( 2, lambda: not find_child( "#scenario-search" ).is_displayed() ) |
||||
assert get_player_nat( 1 ) == "russian" |
||||
assert get_player_nat( 2 ) == "japanese" |
||||
|
||||
# test nationality mapping |
||||
new_scenario() |
||||
dlg = _do_scenario_search( "nationality mapping", ["4c"], webdriver ) |
||||
find_child( "button.import", dlg ).click() |
||||
wait_for( 2, lambda: not find_child( "#scenario-search" ).is_displayed() ) |
||||
assert get_player_nat( 1 ) == "british" |
||||
assert get_player_nat( 2 ) == "british~canadian" |
||||
|
||||
# --------------------------------------------------------------------- |
||||
|
||||
def test_roar_matching( webapp, webdriver ): |
||||
"""Test matching scenarios with ROAR scenarios.""" |
||||
|
||||
# initialize |
||||
init_webapp( webapp, webdriver ) |
||||
|
||||
# search for the "full" scenario |
||||
_do_scenario_search( "full", [1], webdriver ) |
||||
card = _unload_scenario_card() |
||||
assert card["balances"] == { "asa": [ |
||||
{ "name": "Dutch", "wins": 3, "percentage": 30 }, |
||||
{ "name": "Romanian", "wins": 7, "percentage": 70 } |
||||
] } |
||||
|
||||
# search for the "empty" scenario |
||||
_do_scenario_search( "Untitled", ["no-content"], webdriver ) |
||||
card = _unload_scenario_card() |
||||
assert "balances" not in card |
||||
|
||||
# search for "Fighting Withdrawal" |
||||
_do_scenario_search( "Withdrawal", ["2"], webdriver ) |
||||
card = _unload_scenario_card() |
||||
assert card["balances"] == { |
||||
# NOTE: The 2 sides in the ROAR balance graph should have been swapped around |
||||
# to match what's in the ASL Scenario Archive. |
||||
"roar": [ |
||||
{ "name": "Russian", "wins": 325, "percentage": 54 }, |
||||
{ "name": "Finnish", "wins": 279, "percentage": 46 } |
||||
], |
||||
"asa": [ |
||||
{ "name": "Russians", "wins": 78, "percentage": 58 }, # nb: the player nationality has a trailing "s" |
||||
{ "name": "Finnish", "wins": 56, "percentage": 42 } |
||||
] |
||||
} |
||||
|
||||
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
||||
|
||||
@pytest.mark.skipif( pytest.config.option.server_url is not None, reason="--server-url specified" ) #pylint: disable=no-member |
||||
def test_roar_matching2( webapp, webdriver ): |
||||
"""Test matching scenarios with ROAR scenarios.""" |
||||
|
||||
# initialize |
||||
init_webapp( webapp, webdriver ) |
||||
|
||||
from vasl_templates.webapp.scenarios import _asa_scenarios, _match_roar_scenario |
||||
def do_test( scenario_name, expected ): #pylint: disable=missing-docstring |
||||
scenarios = [ |
||||
s for s in _asa_scenarios.index.values() #pylint: disable=no-member |
||||
if s.get( "title" ) == scenario_name |
||||
] |
||||
assert len(scenarios) == 1 |
||||
matches = _match_roar_scenario( scenarios[0] ) |
||||
if not isinstance( expected, list ): |
||||
expected = [ expected ] |
||||
assert [ ( m["roar_id"], m["name"] ) for m in matches ] == expected |
||||
|
||||
with _asa_scenarios: |
||||
|
||||
# check for no match |
||||
do_test( "Full content scenario", [] ) |
||||
|
||||
# check for an exact match |
||||
do_test( "ROAR Exact Match", ("200","!! ROAR exact-match !!") ) |
||||
|
||||
# check for multiple matches, resolved by the scenario ID |
||||
do_test( "ROAR Exact Match 2", ("211","ROAR Exact Match 2") ) |
||||
|
||||
# check for multiple matches |
||||
# NOTE: These should be sorted in descending order of number of playings. |
||||
do_test( "ROAR Multiple Matches", [ |
||||
("222","ROAR Multiple Matches"), ("220","ROAR Multiple Matches"), ("221","ROAR Multiple Matches") |
||||
] ) |
||||
|
||||
# --------------------------------------------------------------------- |
||||
|
||||
def test_roar_linking( webapp, webdriver ): |
||||
"""Test linking scenarios with ROAR scenarios.""" |
||||
|
||||
# initialize |
||||
init_webapp( webapp, webdriver ) |
||||
|
||||
def check( bgraph, connect, disconnect ): |
||||
"""Check the scenario card.""" |
||||
|
||||
# unload the scenario card |
||||
if find_child( "#scenario-search" ).is_displayed(): |
||||
parent = find_child( "#scenario-search .scenario-card" ) |
||||
elif find_child( "#scenario-info-dialog" ).is_displayed(): |
||||
parent = find_child( "#scenario-info-dialog .scenario-card" ) |
||||
else: |
||||
assert False |
||||
card = _unload_scenario_card() |
||||
|
||||
# check if the balance graph is shown |
||||
if bgraph: |
||||
balance = card["balances"]["roar"] |
||||
assert balance[0]["name"] == bgraph[0] |
||||
assert balance[1]["name"] == bgraph[1] |
||||
else: |
||||
assert "roar" not in card["balances"] |
||||
|
||||
# check if the "connect to ROAR" button is shown |
||||
elem = find_child( ".connect-roar", parent ) |
||||
if connect: |
||||
assert elem.is_displayed() |
||||
else: |
||||
assert not elem.is_displayed() |
||||
|
||||
# check if the "disconnect from ROAR" is shown |
||||
elem = find_child( ".disconnect-roar", parent ) |
||||
if disconnect: |
||||
assert elem.is_displayed() |
||||
else: |
||||
assert not elem or not elem.is_displayed() |
||||
|
||||
# import the "Fighting Withdrawal" scenario |
||||
_do_scenario_search( "withdrawal", [2], webdriver ) |
||||
check( ["Russian","Finnish"], False, False ) |
||||
find_child( "#scenario-search button.import" ).click() |
||||
|
||||
# connect to another ROAR scenario |
||||
find_child( "button.scenario-search" ).click() |
||||
check( ["Russian","Finnish"], False, True ) |
||||
find_child( "#scenario-info-dialog .disconnect-roar" ).click() |
||||
check( None, True, False ) |
||||
find_child( "#scenario-info-dialog .connect-roar" ).click() |
||||
dlg = wait_for_elem( 2, ".ui-dialog.select-roar-scenario" ) |
||||
find_child( ".select2-search__field", dlg ).send_keys( "another" ) |
||||
find_child( ".select2-search__field", dlg ).send_keys( Keys.RETURN ) |
||||
check( ["British","French"], False, True ) |
||||
find_child( ".ui-dialog.scenario-info button.ok" ).click() |
||||
|
||||
# disconnect from the ROAR scenario |
||||
find_child( "button.scenario-search" ).click() |
||||
check( ["British","French"], False, True ) |
||||
find_child( "#scenario-info-dialog .disconnect-roar" ).click() |
||||
check( None, True, False ) |
||||
find_child( ".ui-dialog.scenario-info button.ok" ).click() |
||||
|
||||
# connect to a ROAR scenario |
||||
find_child( "button.scenario-search" ).click() |
||||
check( None, True, False ) |
||||
find_child( "#scenario-info-dialog .connect-roar" ).click() |
||||
dlg = wait_for_elem( 2, ".ui-dialog.select-roar-scenario" ) |
||||
elem = find_child( ".select2-search__field", dlg ) |
||||
elem.send_keys( "withdrawal" ) |
||||
elem.send_keys( Keys.RETURN ) |
||||
check( ["Russian","Finnish"], False, True ) |
||||
find_child( ".ui-dialog.scenario-info button.ok" ).click() |
||||
|
||||
# --------------------------------------------------------------------- |
||||
|
||||
def test_scenario_linking( webapp, webdriver ): |
||||
"""Test linking scenarios with the ASL Scenario Archive.""" |
||||
|
||||
# initialize |
||||
init_webapp( webapp, webdriver, scenario_persistence=1 ) |
||||
|
||||
def get_asa_id(): |
||||
"""Get the ASL Scenario Archive scenario ID.""" |
||||
return find_child( "input[name='ASA_ID']" ).get_attribute( "value" ) |
||||
|
||||
def check( asa_id ): |
||||
"""Check the current state of the scenario.""" |
||||
|
||||
# check that the ASL Scenario Archive scenario ID has been set |
||||
wait_for( 2, lambda: get_asa_id() == asa_id ) |
||||
|
||||
# check that the ASL Scenario Archive scenario ID is saved correctly |
||||
saved_scenario = save_scenario() |
||||
assert saved_scenario[ "ASA_ID" ] == asa_id |
||||
|
||||
# reset the scenario |
||||
new_scenario() |
||||
assert get_asa_id() == "" |
||||
|
||||
# check that the ASL Scenario Archive scenario ID is loaded correctly |
||||
load_scenario( saved_scenario ) |
||||
assert get_asa_id() == asa_id |
||||
|
||||
# import the "full" scenario |
||||
_do_scenario_search( "full", [1], webdriver ) |
||||
find_child( "#scenario-search button.import" ).click() |
||||
check( "1" ) |
||||
|
||||
# import the "empty" scenario (on top of the current scenario) |
||||
_do_scenario_search( "Untitled", ["no-content"], webdriver ) |
||||
find_child( "#scenario-search button.import" ).click() |
||||
find_child( "#scenario-search button.confirm-import" ).click() |
||||
check( "no-content" ) |
||||
|
||||
# import the "Fighting Withdrawal" scenario (on top of the current scenario) |
||||
_do_scenario_search( "Fighting Withdrawal", [2], webdriver ) |
||||
find_child( "#scenario-search button.import" ).click() |
||||
find_child( "#scenario-search button.confirm-import" ).click() |
||||
check( "2" ) |
||||
|
||||
# unlink the scenario |
||||
_unlink_scenario() |
||||
check( "" ) |
||||
|
||||
# --------------------------------------------------------------------- |
||||
|
||||
def _do_scenario_search( query, expected, webdriver ): |
||||
"""Do a scenario search.""" |
||||
|
||||
# find the dialog |
||||
dlg = find_child( "#scenario-search" ) |
||||
if not dlg.is_displayed(): |
||||
select_tab( "scenario" ) |
||||
btn = find_child( "button.scenario-search" ) |
||||
ActionChains( webdriver ).key_down( Keys.SHIFT ).click( btn ).perform() |
||||
dlg = wait_for_elem( 2, "#scenario-search" ) |
||||
ActionChains( webdriver ).key_up( Keys.SHIFT ).perform() |
||||
|
||||
# do the search and check the results |
||||
elem = find_child( "input.select2-search__field", dlg ) |
||||
elem.clear() |
||||
elem.send_keys( query ) |
||||
results = _unload_search_results() |
||||
assert [ r[0] for r in results ] == [ str(e) for e in expected ] |
||||
|
||||
return dlg |
||||
|
||||
def _unload_search_results(): |
||||
"""Unload the current search results.""" |
||||
results = [] |
||||
for sr in find_children( "#scenario-search .select2-results .search-result" ): |
||||
results.append( ( sr.get_attribute("data-id"), sr ) ) |
||||
return results |
||||
|
||||
def _unload_scenario_card(): #pylint: disable=too-many-branches,too-many-locals |
||||
"""Unload the scenario card.""" |
||||
|
||||
if find_child( "#scenario-search" ).is_displayed(): |
||||
card = find_child( "#scenario-search .scenario-card" ) |
||||
elif find_child( "#scenario-info-dialog" ).is_displayed(): |
||||
card = find_child( "#scenario-info-dialog .scenario-card" ) |
||||
else: |
||||
assert False |
||||
results = {} |
||||
|
||||
# unload the basic text content |
||||
def unload_text_field( key, sel, trim_prefix=None, trim_postfix=None, trim_parens=False ): |
||||
"""Unload a text field from the scenario card.""" |
||||
elem = find_child( sel, card ) |
||||
if not elem: |
||||
return |
||||
val = elem.text.strip() |
||||
if val: |
||||
if trim_parens: |
||||
assert val.startswith( "(" ) and val.endswith( ")" ) |
||||
val = val[1:-1] |
||||
if trim_prefix: |
||||
assert val.startswith( trim_prefix ) |
||||
val = val[ len(trim_prefix) : ] |
||||
if trim_postfix: |
||||
assert val.endswith( trim_postfix ) |
||||
val = val[ : -len(trim_postfix) ] |
||||
results[ key ] = val.strip() |
||||
def unload_attr( key, sel, attr ): |
||||
"""Unload a element's attribute from the scenario card.""" |
||||
elem = find_child( sel, card ) |
||||
if not elem: |
||||
return |
||||
results[ key ] = elem.get_attribute( attr ) |
||||
unload_text_field( "scenario_name", ".scenario-name" ) |
||||
unload_attr( "scenario_url", ".scenario-name a", "href" ) |
||||
unload_text_field( "scenario_id", ".scenario-id", trim_parens=True ) |
||||
unload_text_field( "scenario_location", ".scenario-location" ) |
||||
unload_text_field( "scenario_date", ".scenario-date", trim_parens=True ) |
||||
unload_text_field( "theater", ".info .theater" ) |
||||
unload_text_field( "turn_count", ".info .turn-count", trim_postfix="turns" ) |
||||
unload_text_field( "playing_time", ".info .playing-time" ) |
||||
unload_text_field( "designer", ".designer", trim_prefix="Designer:" ) |
||||
unload_text_field( "publication", ".publication" ) |
||||
unload_attr( "publication_url", ".publication a", "href" ) |
||||
unload_text_field( "publication_date", ".publication-date", trim_parens=True ) |
||||
unload_text_field( "publisher", ".publisher", trim_parens=True ) |
||||
unload_attr( "publisher_url", ".publisher a", "href" ) |
||||
unload_text_field( "prev_publication", ".prev-publication", trim_prefix="Previously:" ) |
||||
unload_text_field( "revised_publication", ".revised-publication", trim_prefix="Revised:" ) |
||||
unload_text_field( "map_url", ".map" ) # nb: we don't show a real map in test mode |
||||
unload_text_field( "overview", ".overview" ) |
||||
unload_text_field( "defender_name", ".defender .name" ) |
||||
unload_text_field( "defender_desc", ".defender .desc" ) |
||||
unload_text_field( "attacker_name", ".attacker .name" ) |
||||
unload_text_field( "attacker_desc", ".attacker .desc" ) |
||||
unload_text_field( "boards", ".boards", trim_prefix="Boards:" ) |
||||
unload_text_field( "overlays", ".overlays", trim_prefix="Overlays:" ) |
||||
unload_text_field( "extra_rules", ".extra-rules", trim_prefix="Rules:" ) |
||||
|
||||
# unload the balance graphs |
||||
balances = _unload_balance_graphs( card ) |
||||
if balances: |
||||
results[ "balances" ] = balances |
||||
|
||||
# FUDGE! We just show the lat/long in test mode, not a real map, so we have to remove it |
||||
# from the overview content. |
||||
if "overview" in results and "map_url" in results: |
||||
results["overview"] = results["overview"].replace( results["map_url"], "" ).strip() |
||||
|
||||
# unload the icons |
||||
icons = set( |
||||
c.get_attribute( "src" ) |
||||
for c in find_children( ".info .icons img", card ) |
||||
) |
||||
if icons: |
||||
results[ "icons" ] = sorted( os.path.split(i)[1] for i in icons ) |
||||
|
||||
# unload the OBA info |
||||
oba = find_child( ".player-info .oba", card ) |
||||
if oba and oba.is_displayed(): |
||||
oba_info = [] |
||||
for player in ["defender","attacker"]: |
||||
row = find_child( ".{}".format( player ), oba ) |
||||
if not row: |
||||
oba_info.append( None ) |
||||
continue |
||||
oba_info.append( [ |
||||
find_child( ".name", row ).text, |
||||
find_child( ".black", row ).text, |
||||
find_child( ".red", row ).text |
||||
] ) |
||||
comments = find_child( ".{} .comments".format(player), oba ).text |
||||
if comments: |
||||
oba_info[-1].extend( comments.split( "\n" ) ) |
||||
elem = find_child( ".date-warning", oba ) |
||||
if elem.is_displayed(): |
||||
oba_info.append( elem.text ) |
||||
results[ "oba" ] = oba_info |
||||
|
||||
# unload any map preview images |
||||
elems = find_children( ".map-preview", card ) |
||||
if elems: |
||||
urls = [ e.get_attribute("href") for e in elems ] |
||||
results[ "map_previews" ] = [ os.path.basename(u) for u in urls ] |
||||
|
||||
# unload any errata |
||||
def get_source( val ): #pylint: disable=missing-docstring |
||||
assert val.startswith( "[" ) and val.endswith( "]" ) |
||||
return val[1:-1] |
||||
elems1 = find_children( ".errata .text", card ) |
||||
elems2 = find_children( ".errata .source", card ) |
||||
assert len(elems1) == len(elems2) |
||||
if len(elems1) > 0: |
||||
results[ "errata" ] = [ |
||||
[ e1.text, get_source(e2.text) ] |
||||
for e1,e2 in zip(elems1,elems2) |
||||
] |
||||
|
||||
return results |
||||
|
||||
def _unload_balance_graphs( parent ): |
||||
"""Unload balance graph(s).""" |
||||
|
||||
def get_player_no( elem ): |
||||
"""Figure out what player an element belongs to.""" |
||||
# FUDGE! Selenium doesn't seem to let us select elements using things like ".wins.player1", |
||||
# so we have to iterate over all ".wins" elements, and figure out which player each one belongs to :-/ |
||||
classes = elem.get_attribute( "class" ) |
||||
if "player1" in classes: |
||||
return 0 |
||||
elif "player2" in classes: |
||||
return 1 |
||||
else: |
||||
assert False |
||||
return -1 |
||||
|
||||
# unload the balance graphs |
||||
balances = {} |
||||
balance_graphs = find_children( ".balance-graph", parent ) or [] |
||||
for bgraph in balance_graphs: |
||||
if not bgraph.is_displayed(): |
||||
continue |
||||
balance = [ {}, {} ] |
||||
for elem in find_children( ".player", bgraph ): |
||||
balance[ get_player_no(elem) ][ "name" ] = elem.text |
||||
for elem in find_children( ".wins", bgraph ): |
||||
wins = elem.text |
||||
assert wins.startswith( "(" ) and wins.endswith( ")" ) |
||||
balance[ get_player_no(elem) ][ "wins" ] = int( wins[1:-1] ) |
||||
for elem in find_children( ".progressbar", bgraph ): |
||||
percentage = int( elem.get_attribute( "aria-valuenow" ) ) |
||||
player_no = get_player_no( elem ) |
||||
if player_no == 0: |
||||
percentage = 100 - percentage |
||||
balance[ player_no ][ "percentage" ] = percentage |
||||
classes = [ c for c in get_css_classes(bgraph) if c != "balance-graph" ] |
||||
assert len(classes) == 1 |
||||
balances[ classes[0] ] = balance |
||||
|
||||
return balances |
||||
|
||||
def _check_warnings( expected, expected2 ): |
||||
"""Check any import warnings being shown.""" |
||||
def do_check_warnings(): #pylint: |
||||
"""Get import warnings.""" |
||||
warnings = [ |
||||
c.get_attribute( "name" ) |
||||
for c in find_children( "#scenario-search .warnings input[type='checkbox']" ) |
||||
] |
||||
warnings2 = [ |
||||
c.text |
||||
for c in find_children( "#scenario-search .warnings .warning2" ) |
||||
] |
||||
return warnings == expected and warnings2 == expected2 |
||||
wait_for( 2, do_check_warnings ) |
||||
|
||||
def _get_scenario_info(): |
||||
"""Open the scenario info and unload the information.""" |
||||
btn = find_child( "button.scenario-search" ) |
||||
assert find_child( "img", btn ).get_attribute( "src" ).endswith( "/info.gif" ) |
||||
btn.click() |
||||
wait_for_elem( 2, "#scenario-info-dialog" ) |
||||
card = _unload_scenario_card() |
||||
btn = find_children( ".ui-dialog .ui-dialog-buttonpane button" )[0] |
||||
assert btn.text == "OK" |
||||
btn.click() |
||||
return card |
||||
|
||||
def _unlink_scenario(): |
||||
"""Unlink the scenario from the ASL Scenario Archive.""" |
||||
find_child( "button.scenario-search" ).click() |
||||
wait_for_elem( 2, "#scenario-info-dialog" ) |
||||
btn = find_children( ".ui-dialog .ui-dialog-buttonpane button" )[1] |
||||
assert btn.text == "Unlink" |
||||
btn.click() |
@ -0,0 +1,37 @@ |
||||
"""Test utility functions.""" |
||||
|
||||
from vasl_templates.webapp.utils import friendly_fractions |
||||
|
||||
# --------------------------------------------------------------------- |
||||
|
||||
def test_friendly_fractions(): |
||||
"""Test generating friendly fractions.""" |
||||
|
||||
def do_test( val, expected, singular=False ): #pylint: disable=missing-docstring |
||||
# test without a postfix |
||||
assert friendly_fractions( val ) == expected |
||||
# test the singular/plural postfixes |
||||
expected = expected+" foo" if singular else expected+" foos" |
||||
assert friendly_fractions( val, "foo", "foos" ) == expected |
||||
|
||||
# do the test |
||||
do_test( 0, "0" ) |
||||
do_test( 0.124, "0" ) |
||||
do_test( 0.125, "¼", singular=True ) |
||||
do_test( 0.374, "¼", singular=True ) |
||||
do_test( 0.375, "½", singular=True ) |
||||
do_test( 0.624, "½", singular=True ) |
||||
do_test( 0.625, "¾", singular=True ) |
||||
do_test( 0.874, "¾", singular=True ) |
||||
do_test( 0.875, "1", singular=True ) |
||||
do_test( 1.124, "1", singular=True ) |
||||
do_test( 1.125, "1¼" ) |
||||
do_test( 1.374, "1¼" ) |
||||
do_test( 1.375, "1½" ) |
||||
do_test( 1.624, "1½" ) |
||||
do_test( 1.625, "1¾" ) |
||||
do_test( 1.874, "1¾" ) |
||||
do_test( 1.875, "2" ) |
||||
do_test( 2.125, "2¼" ) |
||||
do_test( 2.5, "2½" ) |
||||
do_test( 2.75, "2¾" ) |