Added support for VASL extensions.

master
Pacman Ghost 5 years ago
parent 3dc9a61251
commit 0f85f9dfca
  1. 1
      Dockerfile
  2. 4
      conftest.py
  3. 3
      docker/config/debug.cfg
  4. 1
      docker/config/site.cfg
  5. 2
      vasl_templates/main.py
  6. 32
      vasl_templates/server_settings.py
  7. 12
      vasl_templates/utils.py
  8. 21
      vasl_templates/webapp/data/extensions/kgs-v1.1.json
  9. 34
      vasl_templates/webapp/file_server/utils.py
  10. 256
      vasl_templates/webapp/file_server/vasl_mod.py
  11. 18
      vasl_templates/webapp/files.py
  12. 26
      vasl_templates/webapp/main.py
  13. 38
      vasl_templates/webapp/static/main.js
  14. 4
      vasl_templates/webapp/static/snippets.js
  15. 4
      vasl_templates/webapp/static/vo.js
  16. 3
      vasl_templates/webapp/templates/index.html
  17. 12
      vasl_templates/webapp/testing.py
  18. 2
      vasl_templates/webapp/tests/fixtures/invalid-vo-image-ids/invalid-gpid.json
  19. 2
      vasl_templates/webapp/tests/fixtures/invalid-vo-image-ids/long-gpid.json
  20. 6
      vasl_templates/webapp/tests/fixtures/vasl-extensions/good-match/test-extn.json
  21. 6
      vasl_templates/webapp/tests/fixtures/vasl-extensions/mismatched-id/test-extn.json
  22. 6
      vasl_templates/webapp/tests/fixtures/vasl-extensions/mismatched-version/test-extn.json
  23. 5
      vasl_templates/webapp/tests/fixtures/vasl-extensions/test-extn.xml
  24. 90
      vasl_templates/webapp/tests/remote.py
  25. 26
      vasl_templates/webapp/tests/test_counters.py
  26. 193
      vasl_templates/webapp/tests/test_vasl_extensions.py
  27. 4
      vasl_templates/webapp/tests/test_vehicles_ordnance.py
  28. 3
      vasl_templates/webapp/tests/utils.py
  29. 39
      vasl_templates/webapp/utils.py
  30. 43
      vasl_templates/webapp/vassal.py
  31. 46
      vasl_templates/webapp/vo.py
  32. 1
      vasl_templates/webapp/vo_notes.py

@ -6,6 +6,7 @@
# docker run --rm -it --name vasl-templates \
# -p 5010:5010 \
# -v .../vasl-6.4.3.vmod:/data/vasl.vmod \
# -v .../vasl-extensions:/data/vasl-extensions \
# vasl-templates
# If you have Chapter H data, add the following:
# -v .../chapter-h-notes:/data/chapter-h-notes

@ -54,6 +54,10 @@ def pytest_addoption( parser ):
"--vasl-mods", action="store", dest="vasl_mods", default=None,
help="Directory containing the VASL .vmod file(s)."
)
parser.addoption(
"--vasl-extensions", action="store", dest="vasl_extensions", default=None,
help="Directory containing the VASL extensions."
)
# NOTE: Some tests require VASSAL to be installed. This option allows the caller to specify
# where it is (multiple installations can be placed in sub-directories).

@ -1,3 +1,4 @@
[Debug]
TEST_VASL_MODS = /test-data/vasl-vmods
TEST_VASL_MODS = /test-data/vasl-vmods/
TEST_VASL_EXTENSIONS_DIR = /test-data/vasl-extensions/

@ -3,4 +3,5 @@
FLASK_HOST = 0.0.0.0
VASL_MOD = /data/vasl.vmod
VASL_EXTENSIONS = /data/vasl-extensions/
CHAPTER_H_NOTES = /data/chapter-h-notes/

@ -114,7 +114,7 @@ def _do_main( template_pack, default_scenario, remote_debugging, debug ): #pylin
# install the server settings
try:
from vasl_templates.server_settings import install_server_settings #pylint: disable=cyclic-import
install_server_settings()
install_server_settings( True )
except Exception as ex: #pylint: disable=broad-except
from vasl_templates.main_window import MainWindow #pylint: disable=cyclic-import
MainWindow.showErrorMsg(

@ -8,10 +8,10 @@ from PyQt5.QtGui import QIcon
from vasl_templates.main import app_settings
from vasl_templates.main_window import MainWindow
from vasl_templates.webapp.config.constants import DATA_DIR
from vasl_templates.webapp.vassal import SUPPORTED_VASSAL_VERSIONS_DISPLAY
from vasl_templates.webapp.file_server.vasl_mod import VaslMod, SUPPORTED_VASL_MOD_VERSIONS_DISPLAY
from vasl_templates.webapp.files import install_vasl_mod
from vasl_templates.utils import show_msg_store
from vasl_templates.webapp.vassal import VassalShim, SUPPORTED_VASSAL_VERSIONS_DISPLAY
from vasl_templates.webapp.utils import MsgStore
from vasl_templates.webapp.file_server.vasl_mod import set_vasl_mod, SUPPORTED_VASL_MOD_VERSIONS_DISPLAY
# ---------------------------------------------------------------------
@ -122,7 +122,7 @@ class ServerSettingsDialog( QDialog ):
# NOTE: We should really do this before saving the new settings, but that's more trouble
# than it's worth at this stage... :-/
try:
install_server_settings()
install_server_settings( False )
except Exception as ex: #pylint: disable=broad-except
MainWindow.showErrorMsg( "Couldn't install the server settings:\n\n{}".format( ex ) )
return
@ -148,7 +148,7 @@ def _make_exe_filter_string():
# ---------------------------------------------------------------------
def install_server_settings():
def install_server_settings( is_startup ):
"""Install the server settings."""
# install the server settings
@ -159,10 +159,20 @@ def install_server_settings():
app.config["JAVA_PATH"] = app_settings.value( "ServerSettings/java-path" )
app.config["WEBDRIVER_PATH"] = app_settings.value( "ServerSettings/webdriver-path" )
# initialize
if is_startup:
# nb: we let the web page show startup messages
msg_store = None
else:
msg_store = MsgStore()
# load the VASL module
fname = app_settings.value( "ServerSettings/vasl-mod" )
if fname:
vasl_mod = VaslMod( fname, DATA_DIR )
else:
vasl_mod = None
install_vasl_mod( vasl_mod )
set_vasl_mod( fname, msg_store )
# check the VASSAL version
VassalShim.check_vassal_version( msg_store )
# show any messages
if msg_store:
show_msg_store( msg_store )

@ -27,3 +27,15 @@ def catch_exceptions( caption="EXCEPTION", retval=None ):
return retval
return wrapper
return decorator
# ---------------------------------------------------------------------
def show_msg_store( msg_store ):
"""Show messages in a MsgStore."""
# NOTE: It would be nice to show a single dialog with all the messages, each one tagged with
# a pretty little icon, but for now, we just show a message box for each message :-/
from vasl_templates.main_window import MainWindow
for msg_type in ("error","warning"):
for msg in msg_store.get_msgs( msg_type ):
MainWindow.showErrorMsg( msg )

@ -0,0 +1,21 @@
{
"extensionId": "f97",
"version": "v1.1",
"vehicles": [
{
"_comment_": "Matilda II(b)",
"id": "ru/v:078",
"gpid": [ "f97:178", "f97:184" ]
},
{
"_comment_": "T-60 M40",
"id": "ru/v:004",
"gpid": [ "f97:186" ]
}
]
}

@ -5,28 +5,40 @@ import json
# ---------------------------------------------------------------------
def get_vo_gpids( data_dir ):
def get_vo_gpids( data_dir, extns ): #pylint: disable=too-many-locals,too-many-branches
"""Get the GPID's for the vehicles/ordnance."""
gpids = set()
for vo_type in ("vehicles","ordnance"):
dname = os.path.join( data_dir, vo_type )
for vo_type in ("vehicles","ordnance"): #pylint: disable=too-many-nested-blocks
# process each file
dname = os.path.join( data_dir, vo_type )
for root,_,fnames in os.walk(dname):
for fname in fnames:
if os.path.splitext(fname)[1] != ".json":
continue
# load the GPID's from the next file
# NOTE: We originally assumed that GPID's are integers, but the main VASL build file started
# to have non-numeric values, as do, apparently, extensions :-/ For back-compat, we support both.
entries = json.load( open( os.path.join(root,fname), "r" ) )
for entry in entries:
if isinstance( entry["gpid"], list):
gpids.update( get_effective_gpid(gpid) for gpid in entry["gpid"] )
else:
gpids.add( entry["gpid"] )
gpids.remove( None )
entry_gpids = entry[ "gpid" ]
if not isinstance( entry_gpids, list ):
entry_gpids = [ entry_gpids ]
for gpid in entry_gpids:
if gpid:
gpids.add( get_effective_gpid( str(gpid) ) )
# process any extensions
if extns:
for extn in extns:
extn_info = extn[1]
for vo_type in ["vehicles","ordnance"]:
if vo_type not in extn_info:
continue
for piece in extn_info[vo_type]:
gpids.update( piece["gpid"] )
return gpids
@ -40,8 +52,8 @@ def get_vo_gpids( data_dir ):
# will break. This kind of thing is going to happen again, so we provide a generic mechanism
# for dealing with this kind of thing...
GPID_REMAPPINGS = {
7140: 2775, # SdKfz 10/5
7146: 2772, # SdKfz 10/4
"7140": "2775", # SdKfz 10/5
"7146": "2772", # SdKfz 10/4
}
def get_effective_gpid( gpid ):

@ -1,7 +1,9 @@
""" Serve files from a VASL module file. """
""" Wrapper around a VASL module file and extensions. """
import os
import threading
import json
import glob
import zipfile
import re
import xml.etree.ElementTree
@ -9,6 +11,8 @@ import xml.etree.ElementTree
import logging
_logger = logging.getLogger( "vasl_mod" )
from vasl_templates.webapp import app
from vasl_templates.webapp.config.constants import DATA_DIR
from vasl_templates.webapp.file_server.utils import get_vo_gpids, get_effective_gpid
SUPPORTED_VASL_MOD_VERSIONS = [ "6.4.0", "6.4.1", "6.4.2", "6.4.3" ]
@ -16,16 +20,150 @@ SUPPORTED_VASL_MOD_VERSIONS_DISPLAY = "6.4.0-6.4.3"
# ---------------------------------------------------------------------
# NOTE: The lock only controls access to the _vasl_mod variable, not the VaslMod object it points to.
# In practice this doesn't really matter, since it will be loaded once at startup, then never changes;
# it's only the tests that are constantly changing the underlying object.
_vasl_mod_lock = threading.RLock()
_vasl_mod = None
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
def get_vasl_mod():
"""Return the global VaslMod object."""
with _vasl_mod_lock:
global _vasl_mod
if _vasl_mod is None:
# check if a VASL module has been configured
# NOTE: We will be doing this check every time someone wants the global VaslMod object,
# even if one hasn't been configured, but in all likelihood, everyone will have it configured,
# in which case, the check will only be done once, and the global _vasl_mod variable set.
fname = app.config.get( "VASL_MOD" )
if fname:
# yup - load it
from vasl_templates.webapp.main import startup_msg_store #pylint: disable=cyclic-import
set_vasl_mod( fname, startup_msg_store )
return _vasl_mod
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
def set_vasl_mod( vmod_fname, msg_store ):
"""Install a new global VaslMod object."""
with _vasl_mod_lock:
global _vasl_mod
if vmod_fname:
extns_dir = app.config.get( "VASL_EXTENSIONS_DIR" )
extns = _load_vasl_extns( extns_dir, msg_store )
_vasl_mod = VaslMod( vmod_fname, DATA_DIR, extns )
if _vasl_mod.vasl_version not in SUPPORTED_VASL_MOD_VERSIONS:
msg_store.warning(
"VASL {} is unsupported.<p>Things might work, but they might not...".format(
_vasl_mod.vasl_version
)
)
else:
_vasl_mod = None
def _load_vasl_extns( extn_dir, msg_store ): #pylint: disable=too-many-locals
"""Locate VASL extensions and their corresponding vehicle/ordnance info files."""
if not extn_dir:
return []
# load our extension info files
all_extn_info = {}
if "_VASL_EXTN_INFO_DIR_" in app.config:
dname = app.config["_VASL_EXTN_INFO_DIR_"] # nb: for the test suite
else:
dname = os.path.join( DATA_DIR, "extensions" )
for fname in glob.glob( os.path.join(dname,"*.json") ):
_logger.debug( "Loading VASL extension info: %s", fname )
with open( fname, "r" ) as fp:
extn_info = json.load( fp )
all_extn_info[ ( extn_info["extensionId"], extn_info["version"] ) ] = extn_info
_logger.debug( "- id=%s ; version=%s", extn_info["extensionId"], extn_info["version"] )
# figure out what filename extensions we will recognize
valid_fname_extns = app.config.get( "VASL_EXTENSION_FILENAME_EXTNS", ".mdx .zip" )
valid_fname_extns = valid_fname_extns.replace( ";", " " ).replace( ",", " " ).split()
# process each VASL extension
extns = []
for extn_fname in os.listdir( extn_dir ):
# check if this is a file we're interested in
if os.path.splitext(extn_fname)[1] not in valid_fname_extns:
continue
extn_fname = os.path.join( extn_dir, extn_fname )
# try to load the extension
_logger.debug( "Loading VASL extension: %s", extn_fname )
try:
zip_file = zipfile.ZipFile( extn_fname, "r" )
except zipfile.BadZipFile:
msg_store.warning( "Can't load VASL extension (not a ZIP file): {}", extn_fname, logger=_logger )
continue
try:
build_info = zip_file.read( "buildFile" )
except KeyError:
msg_store.warning( "Missing buildFile: {}", extn_fname, logger=_logger )
continue
doc = xml.etree.ElementTree.fromstring( build_info )
node = doc.findall( "." )[0]
if node.tag != "VASSAL.build.module.ModuleExtension":
msg_store.warning( "Unexpected root node ({}) for VASL extension: {}",
node.tag, extn_fname, logger=_logger
)
continue
# get the extension's ID and version string
extn_id = node.attrib.get( "extensionId" )
if not extn_id:
msg_store.warning( "Can't find ID for VASL extension: {}", extn_fname, logger=_logger )
continue
extn_version = node.attrib.get( "version" )
if not extn_version:
msg_store.warning( "Can't find version for VASL extension: {}", extn_fname, logger=_logger )
continue
_logger.debug( "- id=%s ; version=%s", extn_id, extn_version )
# check if we have a corresponding info file
extn_info = all_extn_info.get( ( extn_id, extn_version ) )
if not extn_info:
msg_store.warning( "Not loading VASL extension \"{}\".<p>No extension info file for {}/{}.".format(
os.path.split(extn_fname)[1], extn_id, extn_version
) )
_logger.warning( "Not loading VASL extension (no info file for %s/%s): %s",
extn_id, extn_version, extn_fname
)
continue
# yup - add the extension to the list
extns.append( ( extn_fname, extn_info ) )
return extns
# ---------------------------------------------------------------------
class VaslMod:
"""Serve files from a VASL module file."""
"""Wrapper around a VASL module file and extensions."""
def __init__( self, fname, data_dir, extns ) :
def __init__( self, fname, data_dir ) :
# initialize
self.pieces = {}
# parse the VASL module file
_logger.info( "Loading VASL module: %s", fname )
self.zip_file = zipfile.ZipFile( fname, "r" )
self.vasl_version = self._parse_vmod( data_dir )
self.filename = fname
self.extns = extns
# initialize
self._pieces = {}
self._files = [ ( zipfile.ZipFile(fname,"r"), None ) ]
if extns:
for extn in extns:
self._files.append(
( zipfile.ZipFile(extn[0],"r"), extn[1] )
)
# load the VASL module and any extensions
self.vasl_version = self._load_vmod( data_dir )
if self.vasl_version not in SUPPORTED_VASL_MOD_VERSIONS:
_logger.warning( "Unsupported VASL version: %s", self.vasl_version )
@ -34,11 +172,11 @@ class VaslMod:
# get the image path
gpid = get_effective_gpid( gpid )
if gpid not in self.pieces:
if gpid not in self._pieces:
return None, None
entry = self.pieces[ get_effective_gpid( gpid ) ]
piece = self._pieces[ get_effective_gpid( gpid ) ]
assert side in ("front","back")
image_paths = entry[ side+"_images" ]
image_paths = piece[ side + "_images" ]
if not image_paths:
return None, None
if not isinstance( image_paths, list ):
@ -50,7 +188,7 @@ class VaslMod:
# load the image data
image_path = os.path.join( "images", image_path )
image_path = re.sub( r"[\\/]+", "/", image_path ) # nb: in case we're on Windows :-/
image_data = self.zip_file.read( image_path )
image_data = piece[ "zip_file" ].read( image_path )
return image_path, image_data
@ -68,11 +206,19 @@ class VaslMod:
"back_images": image_count( p, "back_images" ),
"is_small": p["is_small"],
}
for p in self.pieces.values()
for p in self._pieces.values()
}
def _parse_vmod( self, data_dir ): #pylint: disable=too-many-branches,too-many-locals
"""Parse a .vmod file."""
def get_extns( self ):
"""Return the loaded VASL extensions."""
return [
( files[0].filename, files[1] )
for files in self._files
if files[1]
]
def _load_vmod( self, data_dir ): #pylint: disable=too-many-branches,too-many-locals
"""Load a VASL module file and any extensions."""
# load our overrides
fname = os.path.join( data_dir, "vasl-overrides.json" )
@ -81,30 +227,54 @@ class VaslMod:
expected_multiple_images = json.load( open( fname, "r" ) )
# figure out which pieces we're interested in
target_gpids = get_vo_gpids( data_dir )
target_gpids = get_vo_gpids( data_dir, self.get_extns() )
# parse the VASL module and any extensions
for i,files in enumerate( self._files ):
_logger.info( "Loading VASL %s: %s", ("module" if i == 0 else "extension"), files[0].filename )
version = self._parse_zip_file( files[0], target_gpids, vasl_overrides, expected_multiple_images )
if i == 0:
vasl_version = version
# make sure we found all the pieces we need
_logger.info( "Loaded %d pieces.", len(self._pieces) )
if target_gpids:
_logger.warning( "Couldn't find pieces: %s", target_gpids )
# make sure all the overrides defined were used
if vasl_overrides:
gpids = ", ".join( vasl_overrides.keys() )
_logger.warning( "Unused VASL overrides: %s", gpids )
if expected_multiple_images:
gpids = ", ".join( expected_multiple_images.keys() )
_logger.warning( "Expected multiple images but didn't find them: %s", gpids )
return vasl_version
def _parse_zip_file( self, zip_file, target_gpids, vasl_overrides, expected_multiple_images ): #pylint: disable=too-many-locals
"""Parse a VASL module or extension."""
# load the build file
build_info = zip_file.read( "buildFile" )
doc = xml.etree.ElementTree.fromstring( build_info )
def check_override( gpid, piece, override ):
"""Check that the values in an override entry match what we have."""
for key in override:
if piece[key] != override[key]:
_logger.warning( "Unexpected value in VASL override for '%s' (gpid=%d): %s", key, gpid, piece[key] )
_logger.warning( "Unexpected value in VASL override for '%s' (gpid=%s): %s", key, gpid, piece[key] )
return False
return True
# parse the VASL build info
build_info = self.zip_file.read( "buildFile" )
doc = xml.etree.ElementTree.fromstring( build_info )
# iterate over each PieceSlot in the build file
for node in doc.iter( "VASSAL.build.widget.PieceSlot" ):
# load the next entry
# FUDGE! 6.4.3 introduced weird GPID's for "Hex Grid" pieces :-/
if node.attrib["gpid"].startswith( "4d0:" ):
continue
gpid = int( node.attrib["gpid"] )
gpid = node.attrib[ "gpid" ]
if gpid not in target_gpids:
continue
if gpid in self.pieces:
_logger.warning( "Found duplicate GPID: %d", gpid )
if gpid in self._pieces:
_logger.warning( "Found duplicate GPID: %s", gpid )
front_images, back_images = self._get_image_paths( gpid, node.text )
piece = {
"gpid": gpid,
@ -112,43 +282,31 @@ class VaslMod:
"front_images": front_images,
"back_images": back_images,
"is_small": int(node.attrib["height"]) <= 48,
"zip_file": zip_file,
}
# check if we want to override any values
override = vasl_overrides.get( str(gpid) )
override = vasl_overrides.get( gpid )
if override:
if check_override( gpid, piece, override["expected"] ):
for key in override["updated"]:
piece[key] = override["updated"][key]
del vasl_overrides[ str(gpid) ]
del vasl_overrides[ gpid ]
# save the loaded entry
self.pieces[gpid] = piece
self._pieces[ gpid ] = piece
target_gpids.remove( gpid )
_logger.debug( "- Loaded piece: %s", piece )
# check for multiple images
if isinstance(piece["front_images"],list) or isinstance(piece["back_images"],list):
expected = expected_multiple_images.get( str(gpid) )
expected = expected_multiple_images.get( gpid )
if expected:
check_override( gpid, piece, expected )
del expected_multiple_images[ str(gpid) ]
del expected_multiple_images[ gpid ]
else:
_logger.warning( "Found multiple images: %s", piece )
# make sure we found all the pieces we need
_logger.info( "Loaded %d pieces.", len(self.pieces) )
if target_gpids:
_logger.warning( "Couldn't find pieces: %s", target_gpids )
# make sure all the overrides defined were used
if vasl_overrides:
gpids = ", ".join( vasl_overrides.keys() )
_logger.warning( "Unused VASL overrides: %s", gpids )
if expected_multiple_images:
gpids = ", ".join( expected_multiple_images.keys() )
_logger.warning( "Expected multiple images but didn't find them: %s", gpids )
return doc.attrib.get( "version" )
@staticmethod
@ -180,7 +338,7 @@ class VaslMod:
fields = [ f for f in fields if f ]
return fields
if not fields:
_logger.warning( "Couldn't find any image paths for gpid=%d.", gpid )
_logger.warning( "Couldn't find any image paths for gpid=%s.", gpid )
return None, None
if len(fields) == 1:
# the piece only has front image(s)
@ -188,14 +346,14 @@ class VaslMod:
else:
# the piece has front and back image(s)
if len(fields) > 2:
_logger.warning( "Found > 2 image paths for gpid=%d", gpid )
_logger.warning( "Found > 2 image paths for gpid=%s", gpid )
front_images, back_images = split_fields(fields[1]), split_fields(fields[0])
# ignore dismantled ordnance
if len(front_images) > 1:
if front_images[-1].endswith( "dm" ):
if back_images[-1].endswith( "dmb" ):
_logger.debug( "Ignoring dismantled images: gpid=%d, front=%s, back=%s",
_logger.debug( "Ignoring dismantled images: gpid=%s, front=%s, back=%s",
gpid, front_images, back_images
)
front_images.pop()
@ -207,7 +365,7 @@ class VaslMod:
if len(front_images) > 1:
if front_images[-1].endswith( "l" ):
if back_images[-1].endswith( ("lb","l-b") ):
_logger.debug( "Ignoring limbered images: gpid=%d, front=%s, back=%s",
_logger.debug( "Ignoring limbered images: gpid=%s, front=%s, back=%s",
gpid, front_images, back_images
)
front_images.pop()
@ -216,7 +374,7 @@ class VaslMod:
_logger.warning( "Unexpected limbered images: %s %s", front_images, back_images )
elif front_images[-1].endswith( "B.png" ) and front_images[0] == front_images[-1][:-5]+".png":
# nb: this is for Finnish Guns
_logger.debug( "Ignoring limbered images: gpid=%d, front=%s, back=%s",
_logger.debug( "Ignoring limbered images: gpid=%s, front=%s, back=%s",
gpid, front_images, back_images
)
front_images.pop()

@ -9,12 +9,7 @@ import mimetypes
from flask import send_file, send_from_directory, jsonify, redirect, url_for, abort
from vasl_templates.webapp import app
from vasl_templates.webapp.file_server.vasl_mod import VaslMod
from vasl_templates.webapp.config.constants import DATA_DIR
vasl_mod = None
if app.config.get( "VASL_MOD" ):
vasl_mod = VaslMod( app.config["VASL_MOD"], DATA_DIR )
from vasl_templates.webapp.file_server.vasl_mod import get_vasl_mod
# ---------------------------------------------------------------------
@ -64,24 +59,18 @@ def get_user_file( path ):
# ---------------------------------------------------------------------
def install_vasl_mod( new_vasl_mod ):
"""Install a new VASL module."""
global vasl_mod
vasl_mod = new_vasl_mod
# ---------------------------------------------------------------------
@app.route( "/counter/<gpid>/<side>/<int:index>" )
@app.route( "/counter/<gpid>/<side>", defaults={"index":0} )
def get_counter_image( gpid, side, index ):
"""Get a counter image."""
# check if a VASL module has been configured
vasl_mod = get_vasl_mod()
if not vasl_mod:
return redirect( url_for( "static", filename="images/missing-image.png" ), code=302 )
# return the specified counter image
image_path, image_data = vasl_mod.get_piece_image( int(gpid), side, int(index) )
image_path, image_data = vasl_mod.get_piece_image( gpid, side, int(index) )
if not image_data:
abort( 404 )
return send_file(
@ -96,6 +85,7 @@ def get_vasl_piece_info():
"""Get information about the VASL pieces."""
# check if a VASL module has been configured
vasl_mod = get_vasl_mod()
if not vasl_mod:
return jsonify( {} )

@ -6,8 +6,12 @@ import json
from flask import request, render_template, jsonify, send_file, redirect, url_for, abort
from vasl_templates.webapp import app
from vasl_templates.webapp.utils import MsgStore
from vasl_templates.webapp.config.constants import DATA_DIR
startup_msg_store = MsgStore() # store messages generated during startup
_check_versions = True
# ---------------------------------------------------------------------
@app.route( "/" )
@ -17,6 +21,28 @@ def main():
# ---------------------------------------------------------------------
@app.route( "/startup-msgs" )
def get_startup_msgs():
"""Return any messages generated during startup."""
global _check_versions
if _check_versions:
_check_versions = False
# check the VASSAL version
from vasl_templates.webapp.vassal import VassalShim
VassalShim.check_vassal_version( startup_msg_store )
# collect all the startup messages
startup_msgs = {}
for msg_type in ("info","warning","error"):
msgs = startup_msg_store.get_msgs( msg_type )
if msgs:
startup_msgs[ msg_type ] = msgs
return jsonify( startup_msgs )
# ---------------------------------------------------------------------
@app.route( "/help" )
def show_help():
"""Show the help page."""

@ -281,20 +281,6 @@ $(document).ready( function () {
showErrorMsg( "Can't get the template pack:<div class='pre'>" + escapeHTML(errorMsg) + "</div>" ) ;
} ) ;
// check the VASSAL/VASL versions
$.get( gCheckVassalVersionUrl, function( resp ) {
if ( resp )
showWarningMsg( resp ) ;
} ).fail( function( xhr, status, errorMsg ) {
showErrorMsg( "Can't check the VASSAL version:<div class='pre'>" + escapeHTML(errorMsg) + "</div>" ) ;
} ) ;
$.get( gCheckVaslVersionUrl, function( resp ) {
if ( resp )
showWarningMsg( resp ) ;
} ).fail( function( xhr, status, errorMsg ) {
showErrorMsg( "Can't check the VASL version:<div class='pre'>" + escapeHTML(errorMsg) + "</div>" ) ;
} ) ;
// fixup the layout
var prevHeight = [] ;
$(window).resize( function() {
@ -452,7 +438,13 @@ gPageLoadStatus = [ "main", "vehicle-listings", "ordnance-listings", "vehicle-no
function update_page_load_status( id )
{
// track the page load progress
gPageLoadStatus.splice( gPageLoadStatus.indexOf(id), 1 ) ;
var pos = gPageLoadStatus.indexOf( id ) ;
if ( pos === -1 ) {
if ( id !== "default-scenario" )
console.log( "Multiple page-load status:", id ) ;
return ;
}
gPageLoadStatus.splice( pos, 1 ) ;
if ( id === "template-pack" )
$("fieldset[name='scenario']").fadeIn( 2*1000 ) ;
@ -464,6 +456,13 @@ function update_page_load_status( id )
do_on_new_scenario( false ) ;
}
function show_startup_msgs( msgs, msg_type ) {
if ( msg_type in msgs ) {
for ( var i=0 ; i < msgs[msg_type].length ; ++i )
doShowNotificationMsg( msg_type, msgs[msg_type][i] ) ;
}
}
// check if the page has finished loading
if ( gPageLoadStatus.length === 0 ) {
// yup - update the UI
@ -482,6 +481,15 @@ function update_page_load_status( id )
// notify the PyQT desktop application
if ( gWebChannelHandler )
gWebChannelHandler.on_app_loaded() ;
// show any startuup messages
$.get( gGetStartupMsgsUrl, function( resp ) {
$("body").append( $("<div id='_startup-msgs-ready_'></div>") ) ;
show_startup_msgs( resp, "error" ) ;
show_startup_msgs( resp, "warning" ) ;
show_startup_msgs( resp, "info" ) ;
} ).fail( function( xhr, status, errorMsg ) {
showErrorMsg( "Can't get the startup messages:<div class='pre'>" + escapeHTML(errorMsg) + "</div>" ) ;
} ) ;
}
}

@ -1039,9 +1039,9 @@ function do_load_scenario_data( params )
}
var vo_image_id = null ;
if ( "image_id" in params[key][i] ) {
var matches = params[key][i].image_id.match( /^(\d{3,4})\/(\d)$/ ) ;
var matches = params[key][i].image_id.match( /^([a-z0-9:]{3,10})\/(\d)$/ ) ;
if ( matches )
vo_image_id = [ parseInt(matches[1]), parseInt(matches[2]) ] ;
vo_image_id = [ matches[1], parseInt(matches[2]) ] ;
else
warnings.push( "Invalid V/O image ID for '" + params[key][i].name + "': " + params[key][i].image_id ) ;
}

@ -353,9 +353,9 @@ function _find_vo_image_id( vo_images, vo_image_id )
// find the specified V/O image ID (because indexOf() doesn't handle arrays :-/)
if ( vo_image_id === null )
return 0 ;
vo_image_id = vo_image_id.join(":") ;
vo_image_id = vo_image_id.join( ":" ) ;
for ( var i=0 ; i < vo_images.length ; ++i ) {
if ( vo_images[i].join(":") == vo_image_id )
if ( vo_images[i].join( ":" ) === vo_image_id )
return i ;
}
return -1 ;

@ -88,8 +88,7 @@
gAppName = "{{APP_NAME}}" ;
gAppVersion = "{{APP_VERSION}}" ;
gImagesBaseUrl = "{{url_for('static',filename='images')}}" ;
gCheckVassalVersionUrl = "{{url_for('check_vassal_version')}}" ;
gCheckVaslVersionUrl = "{{url_for('check_vasl_version')}}" ;
gGetStartupMsgsUrl = "{{url_for('get_startup_msgs')}}" ;
gGetTemplatePackUrl = "{{url_for('get_template_pack')}}" ;
gGetDefaultScenarioUrl = "{{url_for('get_default_scenario')}}" ;
gVehicleListingsUrl = "{{url_for('get_vehicle_listings',merge_common=1)}}" ;

@ -1,12 +1,15 @@
"""Webapp handlers for testing porpoises."""
import inspect
import base64
from flask import request, jsonify, abort
from vasl_templates.webapp import app
from vasl_templates.webapp.tests.remote import ControlTests
_control_tests = ControlTests( app )
# ---------------------------------------------------------------------
@app.route( "/control-tests/<action>" )
@ -18,8 +21,7 @@ def control_tests( action ):
abort( 404 )
# figure out what we're being asked to do
controller = ControlTests( app )
func = getattr( controller, action )
func = getattr( _control_tests, action )
if not func:
abort( 404 )
@ -27,8 +29,10 @@ def control_tests( action ):
sig = inspect.signature( func )
kwargs = {}
for param in sig.parameters.values():
if param.name in ("vengine","vmod","gpids","dtype","fname","dname"):
if param.name in ("vengine","vmod","gpids","dtype","fname","dname","extns_dtype","bin_data"):
kwargs[ param.name ] = request.args.get( param.name, param.default )
if param.name == "bin_data":
kwargs["bin_data"] = base64.b64decode( kwargs["bin_data"] )
# execute the command
resp = func( **kwargs )
@ -37,5 +41,5 @@ def control_tests( action ):
if isinstance( resp, (str,list,dict) ):
return jsonify( resp )
else:
assert resp == controller, "Methods should return self if there is no response data."
assert resp == _control_tests, "Methods should return self if there is no response data."
return "ok"

@ -4,7 +4,7 @@
{
"id": "ge/v:990",
"name": "a german vehicle",
"image_id": "abc123/0"
"image_id": "abc123!/0"
}
]
}

@ -4,7 +4,7 @@
{
"id": "ge/v:990",
"name": "a german vehicle",
"image_id": "12345/0"
"image_id": "12345678901234567890/0"
}
]
}

@ -0,0 +1,6 @@
{
"extensionId": "test",
"version": "v0.1"
}

@ -0,0 +1,6 @@
{
"extensionId": "unknown",
"version": "v0.1"
}

@ -0,0 +1,6 @@
{
"extensionId": "test",
"version": "v9.9"
}

@ -0,0 +1,5 @@
<!-- This is a dummy buildFile that represents our test VASL extension.
The test suite will convert it to a ZIP file, that will be loaded by the webapp.
-->
<VASSAL.build.module.ModuleExtension extensionId="test" version="v0.1" />

@ -10,6 +10,8 @@ import os
import urllib.request
import json
import glob
import base64
import tempfile
import logging
import random
@ -19,10 +21,9 @@ from vasl_templates.webapp import app
from vasl_templates.webapp.config.constants import DATA_DIR
from vasl_templates.webapp import main as webapp_main
from vasl_templates.webapp import snippets as webapp_snippets
from vasl_templates.webapp import files as webapp_files
from vasl_templates.webapp import vo_notes as webapp_vo_notes
from vasl_templates.webapp.file_server import utils as webapp_file_server_utils
from vasl_templates.webapp.file_server.vasl_mod import VaslMod
from vasl_templates.webapp.file_server.vasl_mod import set_vasl_mod
_logger = logging.getLogger( "control_tests" )
@ -39,6 +40,11 @@ class ControlTests:
self.server_url = pytest.config.option.server_url #pylint: disable=no-member
except AttributeError:
self.server_url = None
# set up a temp directory for our test VASL extensions
self._vasl_extns_temp_dir = tempfile.TemporaryDirectory()
def __del__( self ):
self._vasl_extns_temp_dir.cleanup()
def __getattr__( self, name ):
"""Generic entry point for handling control requests."""
@ -56,6 +62,8 @@ class ControlTests:
def _remote_test_control( self, action, **kwargs ):
"""Invoke a handler function on the remote server."""
if "bin_data" in kwargs:
kwargs["bin_data"] = base64.b64encode( kwargs["bin_data"] )
resp = urllib.request.urlopen(
self.webapp.url_for( "control_tests", action=action, **kwargs )
).read()
@ -117,29 +125,77 @@ class ControlTests:
try:
dname = pytest.config.option.vasl_mods #pylint: disable=no-member
except AttributeError:
dname = app.config["TEST_VASL_MODS"]
dname = app.config[ "TEST_VASL_MODS" ]
fspec = os.path.join( dname, "*.vmod" )
return glob.glob( fspec )
def _set_vasl_mod( self, vmod=None ):
def _set_vasl_mod( self, vmod=None, extns_dtype=None ):
"""Install a VASL module."""
if vmod is None:
_logger.info( "Installing VASL module: %s", vmod )
webapp_files.vasl_mod = None
if "VASL_MOD" in app.config:
del app.config[ "VASL_MOD" ]
# configure the VASL extensions
if extns_dtype:
if extns_dtype == "real":
try:
dname = pytest.config.option.vasl_extensions #pylint: disable=no-member
except AttributeError:
dname = app.config[ "TEST_VASL_EXTENSIONS_DIR" ]
elif extns_dtype == "test":
dname = self._vasl_extns_temp_dir.name
else:
assert False, "Unknown extensions directory type: "+extns_dtype
_logger.info( "Enabling VASL extensions: %s", dname )
app.config[ "VASL_EXTENSIONS_DIR" ] = dname
else:
fnames = self._do_get_vasl_mods()
_logger.info( "Disabling VASL extensions." )
app.config.pop( "VASL_EXTENSIONS_DIR", None )
# configure the VASL module
if vmod:
vmod_fnames = self._do_get_vasl_mods()
if vmod == "random":
# NOTE: Some tests require a VASL module to be loaded, and since they should all
# should behave in the same way, it doesn't matter which one we load.
fname = random.choice( fnames )
vmod = random.choice( vmod_fnames )
else:
assert vmod in fnames
fname = vmod
_logger.info( "Installing VASL module: %s", fname )
app.config[ "VASL_MOD" ] = fname
webapp_files.vasl_mod = VaslMod( fname, DATA_DIR )
assert vmod in vmod_fnames
app.config[ "VASL_MOD" ] = vmod
else:
app.config.pop( "VASL_MOD", None )
_logger.info( "Installing VASL module: %s", vmod )
# install the new VASL module
from vasl_templates.webapp.main import startup_msg_store
startup_msg_store.reset()
set_vasl_mod( vmod, startup_msg_store )
return self
def _get_vasl_extns( self ): #pylint: disable=no-self-use
"""Return the loaded VASL extensions."""
from vasl_templates.webapp.file_server.vasl_mod import get_vasl_mod
extns = get_vasl_mod().get_extns()
_logger.debug( "Returning VASL extensions:\n%s",
"\n".join( "- {}".format( e ) for e in extns )
)
return extns
def _set_test_vasl_extn( self, fname=None, bin_data=None ):
"""Set the test VASL extension."""
fname = os.path.join( self._vasl_extns_temp_dir.name, fname )
with open( fname, "wb" ) as fp:
fp.write( bin_data )
return self
def _set_vasl_extn_info_dir( self, dtype=None ):
"""Set the directory containing the VASL extension info files."""
if dtype:
dname = os.path.join( os.path.split(__file__)[0], "fixtures/vasl-extensions" )
dname = os.path.join( dname, dtype )
_logger.info( "Setting the default VASL extension info directory: %s", dname )
app.config[ "_VASL_EXTN_INFO_DIR_" ] = dname
else:
_logger.info( "Using the default VASL extension info directory." )
app.config.pop( "_VASL_EXTN_INFO_DIR_", None )
return self
def _get_vassal_engines( self ):
@ -155,7 +211,7 @@ class ControlTests:
try:
dname = pytest.config.option.vassal #pylint: disable=no-member
except AttributeError:
dname = app.config[ "TEST_VASSAL_ENGINES"]
dname = app.config[ "TEST_VASSAL_ENGINES" ]
vassal_engines = []
for root,_,fnames in os.walk( dname ):
for fname in fnames:

@ -32,7 +32,7 @@ def test_counter_images( webapp ):
# NOTE: This is ridiculously slow on Windows :-/
# figure out which pieces we're interested in
gpids = get_vo_gpids( DATA_DIR )
gpids = get_vo_gpids( DATA_DIR, None )
def check_images( check_front, check_back ): #pylint: disable=unused-argument
"""Check getting the front and back images for each counter."""
@ -61,20 +61,20 @@ def test_counter_images( webapp ):
# test each VASL module file in the specified directory
fname = os.path.join( os.path.split(__file__)[0], "fixtures/vasl-pieces.txt" )
expected_vasl_pieces = open( fname, "r" ).read()
vasl_mods = control_tests.get_vasl_mods()
for vasl_mod in vasl_mods:
vmod_fnames = control_tests.get_vasl_mods()
for vmod_fname in vmod_fnames:
# install the VASL module file
control_tests.set_vasl_mod( vmod=vasl_mod )
control_tests.set_vasl_mod( vmod=vmod_fname )
# NOTE: We assume we have access to the same VASL modules as the server, but the path on the webserver
# might be different to what it is locally, so we translate it here.
fname = os.path.split( vasl_mod )[1]
fname = os.path.split( vmod_fname )[1]
vasl_mods_dir = pytest.config.option.vasl_mods #pylint: disable=no-member
fname = os.path.join( vasl_mods_dir, fname )
# check the pieces loaded
vasl_mod = VaslMod( fname, DATA_DIR )
vasl_mod = VaslMod( fname, DATA_DIR, None )
buf = io.StringIO()
_dump_pieces( vasl_mod, buf )
assert buf.getvalue() == expected_vasl_pieces
@ -92,8 +92,10 @@ def _dump_pieces( vasl_mod, out ):
# dump the VASL pieces
results = [ [ "GPID", "Name", "Front images", "Back images"] ]
for gpid in sorted(vasl_mod.pieces.keys()):
piece = vasl_mod.pieces[ gpid ]
pieces = vasl_mod._pieces #pylint: disable=protected-access
gpids = sorted( pieces.keys(), key=int ) # nb: because GPID's changed from int to str :-/
for gpid in gpids:
piece = pieces[ gpid ]
assert piece["gpid"] == gpid
results.append( [ gpid, piece["name"], piece["front_images"], piece["back_images"] ] )
print( tabulate.tabulate( results, headers="firstrow" ), file=out )
@ -132,13 +134,13 @@ def test_gpid_remapping( webapp, webdriver ):
else:
assert check_gpid_image( gpid ) == 404
def do_test( vasl_mod, valid_images ):
def do_test( vmod_fname, valid_images ):
"""Do the test."""
# initialize (using the specified VASL vmod)
init_webapp( webapp, webdriver, scenario_persistence=1,
reset = lambda ct:
ct.set_data_dir( dtype="real" ) \
.set_vasl_mod( vmod=vasl_mod )
.set_vasl_mod( vmod=vmod_fname )
)
load_scenario( scenario_data )
# check that the German vehicles loaded correctly
@ -154,10 +156,10 @@ def test_gpid_remapping( webapp, webdriver ):
scenario_data = json.load( open( fname, "r" ) )
# locate the VASL modules
vasl_mods = control_tests.get_vasl_mods()
vmod_fnames = control_tests.get_vasl_mods()
def find_vasl_mod( version ):
"""Find the VASL module for the specified version."""
matches = [ vmod for vmod in vasl_mods if "vasl-{}.vmod".format(version) in vmod ]
matches = [ fname for fname in vmod_fnames if "vasl-{}.vmod".format(version) in fname ]
assert len(matches) == 1
return matches[0]

@ -0,0 +1,193 @@
""" Test VASL extensions. """
import os
import zipfile
import urllib
import json
import re
import typing
import pytest
from selenium.webdriver.common.action_chains import ActionChains
from selenium.webdriver.common.keys import Keys
from vasl_templates.webapp.utils import TempFile
from vasl_templates.webapp.tests.utils import init_webapp, set_player, find_child, find_children, wait_for
from vasl_templates.webapp.tests.test_vehicles_ordnance import add_vo
# ---------------------------------------------------------------------
def test_load_vasl_extensions( webapp, webdriver ):
"""Test loading VASL extensions."""
# initialize
control_tests = init_webapp( webapp, webdriver )
def do_test( build_info_fname, build_info, expected ): #pylint: disable=missing-docstring
# create the test VASL extension
_set_test_vasl_extn( control_tests, build_info, build_info_fname )
# reload the webapp
control_tests.set_vasl_mod( vmod="random", extns_dtype="test" )
webdriver.refresh()
_check_startup_messages( webapp, expected )
# try loading an extension that has no buildFile
do_test( "foo", "<foo />", "Missing buildFile:" )
# try loading extensions with missing information
do_test( "buildFile", '<VASSAL.build.module.ModuleExtension version="v0.1" />',
"Can't find ID for VASL extension:"
)
do_test( "buildFile", '<VASSAL.build.module.ModuleExtension extensionId="test" />',
"Can't find version for VASL extension:"
)
# try loading an extension with an unknown ID
do_test( "buildFile", '<VASSAL.build.module.ModuleExtension version="v0.1" extensionId="unknown" />',
re.compile( r'Not loading VASL extension "test\.zip".+No extension info file for unknown/v0\.1' )
)
# try loading something that's not a ZIP file
control_tests.set_test_vasl_extn( fname="test.zip", bin_data=b"This is not a ZIP file." ) \
.set_vasl_mod( vmod="random", extns_dtype="test" )
webdriver.refresh()
_check_startup_messages( webapp, "Can't load VASL extension (not a ZIP file):" )
# ---------------------------------------------------------------------
def test_vasl_extension_info( webapp, webdriver ):
"""Test matching VASL extensions with our extension info files."""
# initialize
control_tests = init_webapp( webapp, webdriver )
# prepare our test VASL extension
fname = os.path.join( os.path.split(__file__)[0], "fixtures/vasl-extensions/test-extn.xml" )
_set_test_vasl_extn( control_tests, open(fname,"r").read() )
def do_test( dtype, expected ): #pylint: disable=missing-docstring
control_tests.set_vasl_extn_info_dir( dtype=dtype ) \
.set_vasl_mod( vmod="random", extns_dtype="test" )
webdriver.refresh()
_check_startup_messages( webapp, expected )
# try loading the VASL extension, with no matching extension info
do_test( "mismatched-id",
re.compile( r'Not loading VASL extension.+No extension info file for test/v0\.1' )
)
do_test( "mismatched-version",
re.compile( r'Not loading VASL extension.+No extension info file for test/v0\.1' )
)
# try loading the VASL extension, with matching extension info
do_test( "good-match", None )
extns = control_tests.get_vasl_extns()
assert len(extns) == 1
extn = extns[0]
assert extn[1] == { "extensionId": "test", "version": "v0.1" }
# ---------------------------------------------------------------------
@pytest.mark.skipif(
not pytest.config.option.vasl_extensions, #pylint: disable=no-member
reason = "--vasl-extensions not specified"
)
def test_kgs_extensions( webapp, webdriver ):
"""Test the KGS extension."""
# initialize
control_tests = init_webapp( webapp, webdriver,
reset = lambda ct: ct.set_data_dir( dtype="real" )
)
def check_counter_images( veh_name, expected ):
"""Check the counter images available for the specified vehicle."""
# add the specified vehicle
add_vo( webdriver, "vehicles", 2, veh_name )
# edit the vehicle
vehicles_sortable = find_child( "#ob_vehicles-sortable_2" )
elems = find_children( "li", vehicles_sortable )
ActionChains(webdriver).double_click( elems[-1] ).perform()
dlg = find_child( ".ui-dialog.edit-vo" )
# check the currently-selected counter
image_url = find_child( "img.vasl-image", dlg ).get_attribute( "src" )
if expected:
assert image_url.endswith( "/counter/{}/front".format( expected[0] ) )
else:
assert image_url.endswith( "/missing-image.png" )
# check the available counters
btn = find_child( "input.select-vo-image", dlg )
if expected and len(expected) > 1:
btn.click()
dlg2 = find_child( ".ui-dialog.select-vo-image" )
image_urls = [
elem.get_attribute( "src" )
for elem in find_children( ".vo-images img", dlg2 )
]
assert len(image_urls) == len(expected)
for image_url,piece_id in zip( image_urls, expected ):
assert image_url.endswith( "/counter/{}/front/0".format(piece_id) )
dlg2.send_keys( Keys.ESCAPE )
else:
assert btn is None
dlg.send_keys( Keys.ESCAPE )
def do_test( enable_extns ): #pylint: disable=missing-docstring
# initialize
control_tests.set_vasl_mod( vmod="random",
extns_dtype = "real" if enable_extns else None
)
webdriver.refresh()
set_player( 2, "russian" )
# check the Matilda II(b)
check_counter_images( "Matilda II(b) (HT)",
["f97:178","f97:184"] if enable_extns else None
)
# check the T60-M40
check_counter_images( "T-60 M40 (Tt)",
["547","f97:186"] if enable_extns else ["547"]
)
# do the tests
do_test( True )
do_test( False )
# ---------------------------------------------------------------------
def _set_test_vasl_extn( control_tests, build_info, build_info_fname="buildFile" ):
"""Install a test VASL extension file."""
with TempFile() as temp_file:
with zipfile.ZipFile( temp_file.name, "w" ) as zip_file:
zip_file.writestr( build_info_fname, build_info )
temp_file.close()
with open( temp_file.name, "rb" ) as fp:
zip_data = fp.read()
control_tests.set_test_vasl_extn( fname="test.zip", bin_data=zip_data )
def _check_startup_messages( webapp, expected ):
"""Check that the startup messages are what we expect."""
# wait for the startup messages to become available
wait_for( 2, lambda: find_child("#_startup-msgs-ready_") is not None )
# check the startup messages
url = webapp.url_for( "get_startup_msgs" )
startup_msgs = json.load( urllib.request.urlopen( url ) )
if expected:
assert list(startup_msgs.keys()) == [ "warning" ]
assert len(startup_msgs["warning"]) == 1
if isinstance( expected, typing.re.Pattern ):
assert expected.search( startup_msgs["warning"][0] )
else:
assert startup_msgs["warning"][0].startswith( expected )
else:
assert not startup_msgs

@ -580,10 +580,10 @@ def test_vo_images( webapp, webdriver ): #pylint: disable=too-many-statements
load_scenario( saved_scenario )
check_sortable2_entries( 1, [
( "/counter/2602/front", "ge/v:035", None ),
( "/counter/2807/front/0", "ge/v:027", [2807,0] )
( "/counter/2807/front/0", "ge/v:027", ["2807",0] )
] )
check_sortable2_entries( 2, [
( "/counter/1555/front/1", "br/v:115", [1555,1] )
( "/counter/1555/front/1", "br/v:115", ["1555",1] )
] )
# ---------------------------------------------------------------------

@ -55,7 +55,8 @@ def init_webapp( webapp, webdriver, **options ):
.set_data_dir( dtype="test" ) \
.set_default_scenario( fname=None ) \
.set_default_template_pack( dname=None ) \
.set_vasl_mod( vmod=None ) \
.set_vasl_extn_info_dir( dtype=None ) \
.set_vasl_mod( vmod=None, extns_dtype=None ) \
.set_vassal_engine( vengine=None ) \
.set_vo_notes_dir( dtype=None ) \
.set_user_files_dir( dtype=None )

@ -3,6 +3,45 @@
import os
import tempfile
import pathlib
from collections import defaultdict
# ---------------------------------------------------------------------
class MsgStore:
"""Store different types of messages."""
def __init__( self ):
self._msgs = None
self.reset()
def reset( self ):
"""Reset the MsgStore."""
self._msgs = defaultdict( list )
def info( self, msg, *args, **kwargs ):
"""Add an informational message."""
self._add_msg( "info", msg, *args, **kwargs )
def warning( self, msg, *args, **kwargs ):
"""Add a warning message."""
self._add_msg( "warning", msg, *args, **kwargs )
def error( self, msg, *args, **kwargs ):
"""Add an error message."""
self._add_msg( "error", msg, *args, **kwargs )
def get_msgs( self, msg_type ):
"""Get stored messages."""
return self._msgs[ msg_type ]
def _add_msg( self, msg_type, msg, *args, **kwargs ):
"""Add a message to the store."""
logger = kwargs.pop( "logger", None )
msg = msg.format( *args, **kwargs )
self._msgs[ msg_type ].append( msg )
if logger:
func = getattr( logger, "warn" if msg_type == "warning" else msg_type )
func( msg )
# ---------------------------------------------------------------------

@ -16,8 +16,7 @@ from flask import request
from vasl_templates.webapp import app
from vasl_templates.webapp.config.constants import BASE_DIR, IS_FROZEN
from vasl_templates.webapp.files import vasl_mod
from vasl_templates.webapp.file_server.vasl_mod import SUPPORTED_VASL_MOD_VERSIONS
from vasl_templates.webapp.file_server.vasl_mod import get_vasl_mod
from vasl_templates.webapp.utils import TempFile, SimpleError
from vasl_templates.webapp.webdriver import WebDriver
@ -227,11 +226,8 @@ class VassalShim:
raise SimpleError( "Can't find the VASL boards: {}".format( self.boards_dir ) )
# locate the VASL module
self.vasl_mod = app.config.get( "VASL_MOD" )
if not self.vasl_mod:
if not get_vasl_mod():
raise SimpleError( "The VASL module has not been configured." )
if not os.path.isfile( self.vasl_mod ):
raise SimpleError( "Can't find VASL module: {}".format( self.vasl_mod ) )
# locate the VASSAL shim JAR
self.shim_jar = app.config.get( "VASSAL_SHIM" )
@ -283,7 +279,7 @@ class VassalShim:
args[0]
]
if args[0] in ("dump","update"):
args2.append( self.vasl_mod )
args2.append( get_vasl_mod().filename )
args2.extend( args[1:] )
# figure out how long to the let the VASSAL shim run
@ -340,6 +336,17 @@ class VassalShim:
raise VassalShimError( proc.returncode, stdout, stderr )
return stdout
@staticmethod
def check_vassal_version( msg_store ):
"""Check the version of VASSAL."""
if not app.config.get( "VASSAL_DIR" ) or not msg_store:
return
version = VassalShim().get_version()
if version not in SUPPORTED_VASSAL_VERSIONS:
msg_store.warning(
"VASSAL {} is unsupported.<p>Things might work, but they might not...".format( version )
)
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
class VassalShimError( Exception ):
@ -350,25 +357,3 @@ class VassalShimError( Exception ):
self.retcode = retcode
self.stdout = stdout
self.stderr = stderr
# ---------------------------------------------------------------------
@app.route( "/check-vassal-version" )
def check_vassal_version():
"""Check if we're running a supported version of VASSAL."""
vassal_dir = app.config.get( "VASSAL_DIR" )
if vassal_dir:
vassal_shim = VassalShim()
version = vassal_shim.get_version()
if version not in SUPPORTED_VASSAL_VERSIONS:
return "VASSAL {} is unsupported.<p>Things might work, but they might not...".format( version )
return ""
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
@app.route( "/check-vasl-version" )
def check_vasl_version():
"""Check if we're running a supported version of VASL."""
if vasl_mod and vasl_mod.vasl_version not in SUPPORTED_VASL_MOD_VERSIONS:
return "VASL {} is unsupported.<p>Things might work, but they might not...".format( vasl_mod.vasl_version )
return ""

@ -2,11 +2,13 @@
import os
import json
import logging
from flask import request, render_template, jsonify, abort
from vasl_templates.webapp import app
from vasl_templates.webapp.config.constants import DATA_DIR
from vasl_templates.webapp.file_server.vasl_mod import get_vasl_mod
# ---------------------------------------------------------------------
@ -22,7 +24,7 @@ def get_ordnance_listings():
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
def _do_get_listings( listings_type ): #pylint: disable=too-many-branches
def _do_get_listings( vo_type ): #pylint: disable=too-many-locals,too-many-branches
"""Load the vehicle/ordnance listings."""
# locate the data directory
@ -30,7 +32,7 @@ def _do_get_listings( listings_type ): #pylint: disable=too-many-branches
dname = DATA_DIR # nb: always use the real data for reports, not the test fixtures
else:
dname = app.config.get( "DATA_DIR", DATA_DIR )
dname = os.path.join( dname, listings_type )
dname = os.path.join( dname, vo_type )
if not os.path.isdir( dname ):
abort( 404 )
@ -61,7 +63,7 @@ def _do_get_listings( listings_type ): #pylint: disable=too-many-branches
listings[nat].extend( listings[minor_type+"-common"] )
del listings[ minor_type+"-common" ]
# merge landing craft
if listings_type == "vehicles":
if vo_type == "vehicles":
for lc in listings.get("landing-craft",[]):
if lc["name"] in ("Daihatsu","Shohatsu"):
listings["japanese"].append( lc )
@ -69,8 +71,46 @@ def _do_get_listings( listings_type ): #pylint: disable=too-many-branches
listings["american"].append( lc )
listings["british"].append( lc )
# apply any changes for VASL extensions
vasl_mod = get_vasl_mod()
if vasl_mod:
# build an index of the pieces
piece_index = {}
for nat,pieces in listings.items():
for piece in pieces:
piece_index[ piece["id"] ] = piece
# process each VASL extension
for extn in vasl_mod.get_extns():
if vo_type not in extn[1]:
continue
_apply_extn_info( extn[0], extn[1], piece_index, vo_type )
return jsonify( listings )
def _apply_extn_info( extn_fname, extn_info, piece_index, vo_type ):
"""Update the vehicle/ordnance listings for the specified VASL extension."""
# initialize
logger = logging.getLogger( "vasl_mod" )
logger.info( "Updating %s for VASL extension: %s", vo_type, os.path.split(extn_fname)[1] )
# process each entry
for entry in extn_info[vo_type]:
piece = piece_index.get( entry["id"] )
if piece:
# update an existing piece
logger.debug( "- Updating GPID's for %s: %s", entry["id"], entry["gpid"] )
if piece["gpid"]:
prev_gpids = piece["gpid"]
if not isinstance( piece["gpid"], list ):
piece["gpid"] = [ piece["gpid"] ]
piece["gpid"].extend( entry["gpid"] )
else:
prev_gpids = "(none)"
piece["gpid"] = entry["gpid"]
logger.debug( " - %s => %s", prev_gpids, piece["gpid"] )
else:
logger.warning( "- Updating V/O entry with extension info not supported: %s", entry["id"] )
# ---------------------------------------------------------------------
@app.route( "/<vo_type>/<nat>/<theater>/<int:year>", defaults={"month":1} )

@ -120,6 +120,7 @@ def get_vo_note( vo_type, nat, key ):
# locate the file
with _vo_notes_lock:
# NOTE: We assume that the client has already loaded the vehicle/ordnance notes.
if not _vo_notes_file_server:
abort( 404 )
vo_notes = _do_get_vo_notes( vo_type )

Loading…
Cancel
Save