diff --git a/.gitignore b/.gitignore
index f59e6d2..125b476 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,5 @@
_work_/
+_releases_/
.venv*
*.pyc
diff --git a/.pylintrc b/.pylintrc
index f0690bf..c863eaf 100644
--- a/.pylintrc
+++ b/.pylintrc
@@ -139,7 +139,8 @@ disable=print-statement,
invalid-name,
wrong-import-position,
global-statement,
- too-few-public-methods
+ too-few-public-methods,
+ duplicate-code, # can't get it to shut up about @pytest.mark.skipif's :-/
# Enable the message, report, category or checker with the given id(s). You can
# either give multiple identifier separated by comma (,) or put this option
diff --git a/_freeze.py b/_freeze.py
index 8b2bb94..a657250 100755
--- a/_freeze.py
+++ b/_freeze.py
@@ -73,6 +73,7 @@ args = [
"--onefile",
"--name", target_name,
]
+args.extend( [ "--add-data", "vassal-shim/release/vassal-shim.jar" + os.pathsep + "vasl_templates/webapp" ] )
# NOTE: We also need to include the config/ and data/ subdirectories, but we would like to
# make them available to the user, so we include them ourself in the final release archive.
def map_dir( src, dest ): #pylint: disable=missing-docstring
diff --git a/conftest.py b/conftest.py
index 03b9155..697889b 100644
--- a/conftest.py
+++ b/conftest.py
@@ -43,12 +43,21 @@ def pytest_addoption( parser ):
"--short-tests", action="store_true", dest="short_tests", default=False,
help="Run a shorter version of the test suite."
)
+
# NOTE: Some tests require the VASL module file(s). We don't want to put these into source control,
# so we provide this option to allow the caller to specify where they live.
parser.addoption(
"--vasl-mods", action="store", dest="vasl_mods", default=None,
help="Directory containing the VASL .vmod file(s)."
)
+
+ # 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).
+ parser.addoption(
+ "--vassal", action="store", dest="vassal", default=None,
+ help="Directory containing VASSAL installation(s)."
+ )
+
# NOTE: It's not good to have the code run differently to how it will normally,
# but using the clipboard to retrieve snippets causes more trouble than it's worth :-/
# since any kind of clipboard activity while the tests are running could cause them to fail
diff --git a/setup.py b/setup.py
index 013a76d..626fea6 100644
--- a/setup.py
+++ b/setup.py
@@ -22,13 +22,13 @@ setup(
"PyQT5==5.10.0",
"pyyaml==3.13",
"pillow==5.3.0",
+ "selenium==3.12.0",
"click==6.7",
],
extras_require = {
"dev": [
"pytest==3.6.0",
"tabulate==0.8.2",
- "selenium==3.12.0",
"lxml==4.2.4",
"pylint==1.9.2",
"pytest-pylint==0.9.0",
diff --git a/vasl_templates/file_dialog.py b/vasl_templates/file_dialog.py
new file mode 100644
index 0000000..0af494f
--- /dev/null
+++ b/vasl_templates/file_dialog.py
@@ -0,0 +1,83 @@
+""" Manage loading and saving files. """
+
+import os
+
+from PyQt5.QtWidgets import QFileDialog
+
+# ---------------------------------------------------------------------
+
+# NOTE: While loading/saving files works fine when handled by the embedded browser,
+# we can't get the full path of the file loaded (because of browser security).
+# This means that we can't do things like default to saving a scenario to the same file
+# it was loaded from, or retrying a failed save. This is such a lousy UX,
+# we handle load/save operations ourself, where we can manage things like this.
+
+class FileDialog:
+ """Manage loading and saving files."""
+
+ def __init__( self, parent, object_name, default_extn, filters, default_fname ):
+ self.parent = parent
+ self.object_name = object_name
+ self.default_extn = default_extn
+ self.filters = filters
+ self.curr_fname = default_fname
+
+ def load_file( self, binary ):
+ """Load a file."""
+
+ # ask the user which file to load
+ fname, _ = QFileDialog.getOpenFileName(
+ self.parent, "Load {}".format( self.object_name ),
+ self.curr_fname,
+ self.filters
+ )
+ if not fname:
+ return None
+
+ # load the file
+ try:
+ with open( fname, "rb" ) as fp:
+ data = fp.read()
+ except Exception as ex: #pylint: disable=broad-except
+ self.parent.showErrorMsg( "Can't load the {}:\n\n{}".format( self.object_name, ex ) )
+ return None
+ if not binary:
+ data = data.decode( "utf-8" )
+ self.curr_fname = fname
+
+ return data
+
+ def save_file( self, data ):
+ """Save data to a file."""
+
+ # initialize
+ if isinstance( data, str ):
+ data = data.encode( "utf-8" )
+
+ while True: # nb: keep trying until the save succeeds or the user cancels the operation
+
+ # ask the user where to save the file
+ fname, _ = QFileDialog.getSaveFileName(
+ self.parent, "Save {}".format( self.object_name),
+ self.curr_fname,
+ self.filters
+ )
+ if not fname:
+ return False
+
+ # check the file extension
+ extn = os.path.splitext( fname )[1]
+ if not extn:
+ fname += self.default_extn
+ elif fname.endswith( "." ):
+ fname = fname[:-1]
+
+ # save the file
+ try:
+ with open( fname, "wb", ) as fp:
+ fp.write( data )
+ except Exception as ex: #pylint: disable=broad-except
+ self.parent.showErrorMsg( "Can't save the {}:\n\n{}".format( self.object_name, ex ) )
+ continue
+ self.curr_fname = fname
+ return True
diff --git a/vasl_templates/main.py b/vasl_templates/main.py
index 8c73f03..14b7573 100755
--- a/vasl_templates/main.py
+++ b/vasl_templates/main.py
@@ -15,6 +15,8 @@ from PyQt5.QtCore import Qt, QSettings, QDir
import PyQt5.QtCore
import click
+from vasl_templates.webapp.utils import SimpleError
+
# FUDGE! This needs to be created before showing any UI elements e.g. an error message box.
qt_app = QApplication( sys.argv )
@@ -86,7 +88,7 @@ def _do_main( template_pack, default_scenario, remote_debugging, debug ): #pylin
# configure the default scenario
if default_scenario:
if not os.path.isfile( default_scenario ):
- raise RuntimeError( "Can't find the default scenario file." )
+ raise SimpleError( "Can't find the default scenario file." )
webapp_main.default_scenario = default_scenario
# configure remote debugging
@@ -134,7 +136,7 @@ def _do_main( template_pack, default_scenario, remote_debugging, debug ): #pylin
except: #pylint: disable=bare-except
resp = None
if resp:
- raise RuntimeError( "The application is already running." )
+ raise SimpleError( "The application is already running." )
# start the webapp server
def webapp_thread():
diff --git a/vasl_templates/main_window.py b/vasl_templates/main_window.py
index 6e119e7..6d90ea1 100644
--- a/vasl_templates/main_window.py
+++ b/vasl_templates/main_window.py
@@ -5,15 +5,16 @@ import os
import re
import json
import io
+import base64
import logging
from PyQt5.QtWidgets import QWidget, QVBoxLayout, QMenuBar, QAction, QLabel, QMessageBox
from PyQt5.QtWebEngineWidgets import QWebEngineView, QWebEngineProfile, QWebEnginePage
from PyQt5.QtWebChannel import QWebChannel
from PyQt5.QtGui import QDesktopServices, QIcon
-from PyQt5.QtCore import Qt, QUrl, QMargins, pyqtSlot
+from PyQt5.QtCore import Qt, QUrl, QMargins, pyqtSlot, QVariant
-from vasl_templates.webapp.config.constants import APP_NAME
+from vasl_templates.webapp.config.constants import APP_NAME, IS_FROZEN
from vasl_templates.main import app_settings
from vasl_templates.web_channel import WebChannelHandler
from vasl_templates.utils import log_exceptions
@@ -55,7 +56,7 @@ class MainWindow( QWidget ):
# initialize the main window
self.setWindowTitle( APP_NAME )
- if getattr( sys, "frozen", False ):
+ if IS_FROZEN:
dname = sys._MEIPASS #pylint: disable=no-member,protected-access
else:
dname = os.path.join( os.path.split(__file__)[0], "webapp" )
@@ -249,6 +250,25 @@ class MainWindow( QWidget ):
"""Called when the user wants to save a scenario."""
return self._web_channel_handler.save_scenario( data )
+ @pyqtSlot( result=QVariant )
+ @log_exceptions( caption="SLOT EXCEPTION" )
+ def load_vsav( self ):
+ """Called when the user wants to update a VASL scenario."""
+ fname, data = self._web_channel_handler.load_vsav()
+ if data is None:
+ return None
+ return QVariant( {
+ "filename": fname,
+ "data": base64.b64encode( data ).decode( "utf-8" )
+ } )
+
+ @pyqtSlot( str, str, result=bool )
+ @log_exceptions( caption="SLOT EXCEPTION" )
+ def save_updated_vsav( self, fname, data ):
+ """Called when a VASL scenario has been updated and is ready to be saved."""
+ data = base64.b64decode( data )
+ return self._web_channel_handler.save_updated_vsav( fname, data )
+
@pyqtSlot( str )
@log_exceptions( caption="SLOT EXCEPTION" )
def on_user_settings_change( self, user_settings ): #pylint: disable=no-self-use
diff --git a/vasl_templates/server_settings.py b/vasl_templates/server_settings.py
index bd165f0..7fd6cb0 100644
--- a/vasl_templates/server_settings.py
+++ b/vasl_templates/server_settings.py
@@ -9,6 +9,7 @@ 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
@@ -26,39 +27,96 @@ class ServerSettingsDialog( QDialog ):
base_dir = os.path.split( __file__ )[0]
dname = os.path.join( base_dir, "ui/server_settings.ui" )
uic.loadUi( dname, self )
- self.select_vasl_mod_button.setIcon(
- QIcon( os.path.join( base_dir, "resources/file_browser.png" ) )
- )
+ for btn in ["vassal_dir","vasl_mod","boards_dir","java","webdriver"]:
+ getattr( self, "select_{}_button".format(btn) ).setIcon(
+ QIcon( os.path.join( base_dir, "resources/file_browser.png" ) )
+ )
self.setMinimumSize( self.size() )
# initialize handlers
+ self.select_vassal_dir_button.clicked.connect( self.on_select_vassal_dir )
self.select_vasl_mod_button.clicked.connect( self.on_select_vasl_mod )
+ self.select_boards_dir_button.clicked.connect( self.on_select_boards_dir )
+ self.select_java_button.clicked.connect( self.on_select_java )
+ self.select_webdriver_button.clicked.connect( self.on_select_webdriver )
self.ok_button.clicked.connect( self.on_ok )
self.cancel_button.clicked.connect( self.on_cancel )
# load the current server settings
+ self.vassal_dir.setText( app_settings.value( "ServerSettings/vassal-dir" ) )
+ self.vassal_dir.setToolTip(
+ "Supported versions: {}".format( SUPPORTED_VASSAL_VERSIONS_DISPLAY )
+ )
self.vasl_mod.setText( app_settings.value( "ServerSettings/vasl-mod" ) )
self.vasl_mod.setToolTip(
"Supported versions: {}".format( SUPPORTED_VASL_MOD_VERSIONS_DISPLAY )
)
+ self.boards_dir.setText( app_settings.value( "ServerSettings/boards-dir" ) )
+ self.java_path.setText( app_settings.value( "ServerSettings/java-path" ) )
+ self.webdriver_path.setText( app_settings.value( "ServerSettings/webdriver-path" ) )
+ self.webdriver_path.setToolTip( "Configure either geckodriver or chromedriver here." )
+
+ def on_select_vassal_dir( self ):
+ """Let the user locate the VASSAL installation directory."""
+ dname = QFileDialog.getExistingDirectory(
+ self, "Select VASSAL installation directory",
+ self.vassal_dir.text(),
+ QFileDialog.ShowDirsOnly
+ )
+ if dname:
+ self.vassal_dir.setText( dname )
def on_select_vasl_mod( self ):
"""Let the user select a VASL module."""
fname = QFileDialog.getOpenFileName(
self, "Select VASL module",
- app_settings.value( "ServerSettings/vasl-mod" ),
- "VASL module files (*.vmod)|All files (*.*)"
+ self.vasl_mod.text(),
+ "VASL module files (*.vmod);;All files (*.*)"
)[0]
if fname:
self.vasl_mod.setText( fname )
+ def on_select_boards_dir( self ):
+ """Let the user locate the VASL boards directory."""
+ dname = QFileDialog.getExistingDirectory(
+ self, "Select VASL boards directory",
+ self.boards_dir.text(),
+ QFileDialog.ShowDirsOnly
+ )
+ if dname:
+ self.boards_dir.setText( dname )
+
+ def on_select_java( self ):
+ """Let the user locate the Java executable."""
+ fname = QFileDialog.getOpenFileName(
+ self, "Select Java executable",
+ self.java_path.text(),
+ _make_exe_filter_string()
+ )[0]
+ if fname:
+ self.java_path.setText( fname )
+
+ def on_select_webdriver( self ):
+ """Let the user locate the webdriver executable."""
+ fname = QFileDialog.getOpenFileName(
+ self, "Select webdriver",
+ self.webdriver_path.text(),
+ _make_exe_filter_string()
+ )[0]
+ if fname:
+ self.webdriver_path.setText( fname )
+
def on_ok( self ):
"""Accept the new server settings."""
# save the new settings
+ app_settings.setValue( "ServerSettings/vassal-dir", self.vassal_dir.text() )
fname = self.vasl_mod.text().strip()
vasl_mod_changed = fname != app_settings.value( "ServerSettings/vasl-mod" )
app_settings.setValue( "ServerSettings/vasl-mod", fname )
+ app_settings.setValue( "ServerSettings/boards-dir", self.boards_dir.text() )
+ app_settings.setValue( "ServerSettings/java-path", self.java_path.text() )
+ app_settings.setValue( "ServerSettings/webdriver-path", self.webdriver_path.text() )
# install the new settings
# NOTE: We should really do this before saving the new settings, but that's more trouble
@@ -80,11 +138,27 @@ class ServerSettingsDialog( QDialog ):
"""Cancel the dialog."""
self.close()
+def _make_exe_filter_string():
+ """Make a file filter string for executables."""
+ buf = []
+ if os.name == "nt":
+ buf.append( "Executable files (*.exe)" )
+ buf.append( "All files (*.*)" )
+ return ";;".join( buf )
+
# ---------------------------------------------------------------------
def install_server_settings():
"""Install the server settings."""
+ # install the server settings
+ from vasl_templates.webapp import app as app
+ app.config["VASSAL_DIR"] = app_settings.value( "ServerSettings/vassal-dir" )
+ app.config["VASL_MOD"] = app_settings.value( "ServerSettings/vasl-mod" )
+ app.config["BOARDS_DIR"] = app_settings.value( "ServerSettings/boards-dir" )
+ app.config["JAVA_PATH"] = app_settings.value( "ServerSettings/java-path" )
+ app.config["WEBDRIVER_PATH"] = app_settings.value( "ServerSettings/webdriver-path" )
+
# load the VASL module
fname = app_settings.value( "ServerSettings/vasl-mod" )
if fname:
diff --git a/vasl_templates/ui/server_settings.ui b/vasl_templates/ui/server_settings.ui
index 0e4a54d..025b9ed 100644
--- a/vasl_templates/ui/server_settings.ui
+++ b/vasl_templates/ui/server_settings.ui
@@ -10,7 +10,7 @@
0
0
500
- 90
+ 199
@@ -21,100 +21,227 @@
-
-
-
-
- 0
- 0
-
-
-
-
- 0
- 30
-
-
-
-
- 16777215
- 30
-
-
-
-
- 5
-
-
- 0
-
-
- 0
-
-
- 0
-
-
- 0
-
-
-
-
-
- &VASL module:
-
-
- vasl_mod
-
-
-
- -
-
-
- -
-
-
-
- 0
- 0
-
-
-
-
- 25
- 25
-
-
-
-
- 25
- 25
-
-
-
-
-
-
- true
-
-
-
-
- vasl_mod
- label
- select_vasl_mod_button
-
-
- -
-
-
- Qt::Vertical
-
-
-
- 20
- 40
-
+
+
+ 2
-
+
-
+
+
+ VA&SSAL installation:
+
+
+ vassal_dir
+
+
+
+ -
+
+
+ 2
+
+
-
+
+
+ -
+
+
+
+ 22
+ 22
+
+
+
+
+ 22
+ 22
+
+
+
+
+
+
+
+
+
+ -
+
+
+ &VASL module:
+
+
+ vasl_mod
+
+
+
+ -
+
+
+ 2
+
+
-
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ 22
+ 22
+
+
+
+
+ 22
+ 22
+
+
+
+
+
+
+ true
+
+
+
+
+
+ -
+
+
+ VASL &boards:
+
+
+ boards_dir
+
+
+
+ -
+
+
+ 2
+
+
-
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ 22
+ 22
+
+
+
+
+ 22
+ 22
+
+
+
+
+
+
+
+
+
+ -
+
+
+ &Java:
+
+
+ java_path
+
+
+
+ -
+
+
+ 2
+
+
-
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ 22
+ 22
+
+
+
+
+ 22
+ 22
+
+
+
+
+
+
+
+
+
+ -
+
+
+ &Web driver:
+
+
+ webdriver_path
+
+
+
+ -
+
+
+ 2
+
+
-
+
+
+ -
+
+
+
+ 22
+ 22
+
+
+
+
+ 22
+ 22
+
+
+
+
+
+
+
+
+
+
-
@@ -199,6 +326,20 @@
+
+ vassal_dir
+ select_vassal_dir_button
+ vasl_mod
+ select_vasl_mod_button
+ boards_dir
+ select_boards_dir_button
+ java_path
+ select_java_button
+ webdriver_path
+ select_webdriver_button
+ ok_button
+ cancel_button
+
diff --git a/vasl_templates/web_channel.py b/vasl_templates/web_channel.py
index 9a39b7b..a69f638 100644
--- a/vasl_templates/web_channel.py
+++ b/vasl_templates/web_channel.py
@@ -2,87 +2,57 @@
import os
-from PyQt5.QtWidgets import QFileDialog
-
from vasl_templates.webapp.config.constants import APP_NAME
+from vasl_templates.file_dialog import FileDialog
# ---------------------------------------------------------------------
class WebChannelHandler:
"""Handle web channel requests."""
- _FILE_FILTERS = "Scenario files (*.json);;All files (*)"
-
- def __init__( self, window ):
-
- # initialize
- self._window = window
-
- # NOTE: While loading/saving scenarios works fine when handled by the embedded browser,
- # we can't get the full path of the file saved loaded (because of browser security).
- # This means that we can't e.g. default saving a scenario to the same file it was loaded from.
- # This is such a lousy UX, we handle load/save operations ourself, where we can manage this.
- self._curr_scenario_fname = None
+ def __init__( self, parent ):
+ self.parent = parent
+ self.scenario_file_dialog = FileDialog(
+ self.parent,
+ "scenario", ".json",
+ "Scenario files (*.json);;All files (*)",
+ "scenario.json"
+ )
+ self.updated_vsav_file_dialog = FileDialog(
+ self.parent,
+ "VASL scenario", ".vsav",
+ "VASL scenario files (*.vsav);;All files (*)",
+ "scenario.vsav"
+ )
def on_new_scenario( self ):
"""Called when the scenario is reset."""
- self._curr_scenario_fname = None
+ self.scenario_file_dialog.curr_fname = None
def load_scenario( self ):
"""Called when the user wants to load a scenario."""
-
- # ask the user which file to load
- fname, _ = QFileDialog.getOpenFileName(
- self._window, "Load scenario",
- os.path.split(self._curr_scenario_fname)[0] if self._curr_scenario_fname else None,
- WebChannelHandler._FILE_FILTERS
- )
- if not fname:
- return None
-
- # load the scenario
- try:
- with open( fname, "r", encoding="utf-8" ) as fp:
- data = fp.read()
- except Exception as ex: #pylint: disable=broad-except
- self._window.showErrorMsg( "Can't load the scenario:\n\n{}".format( ex ) )
- return None
- self._curr_scenario_fname = fname
-
- return data
+ return self.scenario_file_dialog.load_file( False )
def save_scenario( self, data ):
"""Called when the user wants to save a scenario."""
-
- # ask the user where to save the scenario
- fname, _ = QFileDialog.getSaveFileName(
- self._window, "Save scenario",
- self._curr_scenario_fname,
- WebChannelHandler._FILE_FILTERS
- )
- if not fname:
- return False
-
- # check the file extension
- extn = os.path.splitext( fname )[1]
- if not extn:
- fname += ".json"
- elif fname.endswith( "." ):
- fname = fname[:-1]
-
- # save the file
- try:
- with open( fname, "w", encoding="utf-8" ) as fp:
- fp.write( data )
- except Exception as ex: #pylint: disable=broad-except
- self._window.showErrorMsg( "Can't save the scenario:\n\n{}".format( ex ) )
- return False
- self._curr_scenario_fname = fname
-
- return True
+ return self.scenario_file_dialog.save_file( data )
def on_scenario_name_change( self, val ):
"""Update the main window title to show the scenario name."""
- self._window.setWindowTitle(
+ self.parent.setWindowTitle(
"{} - {}".format( APP_NAME, val ) if val else APP_NAME
)
+
+ def load_vsav( self ):
+ """Called when the user wants to load a VASL scenario to update."""
+ data = self.updated_vsav_file_dialog.load_file( True )
+ if data is None:
+ return None, None
+ fname = os.path.split( self.updated_vsav_file_dialog.curr_fname )[1]
+ return fname, data
+
+ def save_updated_vsav( self, fname, data ):
+ """Called when a VASL scenario has been updated and is ready to be saved."""
+ dname = os.path.split( self.updated_vsav_file_dialog.curr_fname )[0]
+ self.updated_vsav_file_dialog.curr_fname = os.path.join( dname, fname )
+ return self.updated_vsav_file_dialog.save_file( data )
diff --git a/vasl_templates/webapp/__init__.py b/vasl_templates/webapp/__init__.py
index baaa53e..4abf579 100644
--- a/vasl_templates/webapp/__init__.py
+++ b/vasl_templates/webapp/__init__.py
@@ -59,6 +59,7 @@ import vasl_templates.webapp.main #pylint: disable=cyclic-import
import vasl_templates.webapp.vo #pylint: disable=cyclic-import
import vasl_templates.webapp.snippets #pylint: disable=cyclic-import
import vasl_templates.webapp.files #pylint: disable=cyclic-import
+import vasl_templates.webapp.vassal #pylint: disable=cyclic-import
# ---------------------------------------------------------------------
diff --git a/vasl_templates/webapp/config/constants.py b/vasl_templates/webapp/config/constants.py
index 3bd8c93..317bb79 100644
--- a/vasl_templates/webapp/config/constants.py
+++ b/vasl_templates/webapp/config/constants.py
@@ -8,7 +8,9 @@ APP_VERSION = "v0.5" # nb: also update setup.py
APP_DESCRIPTION = "Generate HTML for use in VASL scenarios."
if getattr( sys, "frozen", False ):
+ IS_FROZEN = True
BASE_DIR = os.path.split( sys.executable )[0]
else:
+ IS_FROZEN = False
BASE_DIR = os.path.abspath( os.path.join( os.path.split(__file__)[0], ".." ) )
DATA_DIR = os.path.join( BASE_DIR, "data" )
diff --git a/vasl_templates/webapp/config/site.cfg.example b/vasl_templates/webapp/config/site.cfg.example
index a36efd2..ef2e777 100644
--- a/vasl_templates/webapp/config/site.cfg.example
+++ b/vasl_templates/webapp/config/site.cfg.example
@@ -1,4 +1,10 @@
[Site Config]
; Enable VASL counter images in the UI by configuring a VASL .vmod file here.
-VASL_MOD = ...
+VASL_MOD = ...configure the VASL module (e.g. vasl-6.4.3.vmod)...
+
+; Configure VASSAL to be able to automatically update labels in a VASL scenario.
+VASSAL_DIR = ...configure the VASSAL installation directory...
+BOARDS_DIR = ...configure the VASL boards directory...
+WEBDRIVER_PATH = ...configure either geckodriver or chromedriver here...
+; JAVA_PATH = ...configure the Java executable here (optional, must be in the PATH otherwise)...
diff --git a/vasl_templates/webapp/data/default-template-pack/atmm.j2 b/vasl_templates/webapp/data/default-template-pack/atmm.j2
index 4a58873..a800135 100644
--- a/vasl_templates/webapp/data/default-template-pack/atmm.j2
+++ b/vasl_templates/webapp/data/default-template-pack/atmm.j2
@@ -1,4 +1,4 @@
-
+
diff --git a/vasl_templates/webapp/data/default-template-pack/baz.j2 b/vasl_templates/webapp/data/default-template-pack/baz.j2
index 2b1db6b..1b11cc1 100644
--- a/vasl_templates/webapp/data/default-template-pack/baz.j2
+++ b/vasl_templates/webapp/data/default-template-pack/baz.j2
@@ -1,4 +1,4 @@
-
+
diff --git a/vasl_templates/webapp/data/default-template-pack/extras/blank-space.j2 b/vasl_templates/webapp/data/default-template-pack/extras/blank-space.j2
index 6832123..e2e7c74 100644
--- a/vasl_templates/webapp/data/default-template-pack/extras/blank-space.j2
+++ b/vasl_templates/webapp/data/default-template-pack/extras/blank-space.j2
@@ -1,4 +1,4 @@
-
+
diff --git a/vasl_templates/webapp/data/default-template-pack/extras/hip-guns.j2 b/vasl_templates/webapp/data/default-template-pack/extras/hip-guns.j2
old mode 100755
new mode 100644
index a13de2c..a46a49c
--- a/vasl_templates/webapp/data/default-template-pack/extras/hip-guns.j2
+++ b/vasl_templates/webapp/data/default-template-pack/extras/hip-guns.j2
@@ -1,4 +1,4 @@
-
+
diff --git a/vasl_templates/webapp/data/default-template-pack/extras/kgs/grenade-bundles.j2 b/vasl_templates/webapp/data/default-template-pack/extras/kgs/grenade-bundles.j2
index 999f7f3..1a29d0a 100644
--- a/vasl_templates/webapp/data/default-template-pack/extras/kgs/grenade-bundles.j2
+++ b/vasl_templates/webapp/data/default-template-pack/extras/kgs/grenade-bundles.j2
@@ -1,4 +1,4 @@
-
+
diff --git a/vasl_templates/webapp/data/default-template-pack/extras/kgs/molotov-cocktails.j2 b/vasl_templates/webapp/data/default-template-pack/extras/kgs/molotov-cocktails.j2
index dfff9ab..b62f371 100644
--- a/vasl_templates/webapp/data/default-template-pack/extras/kgs/molotov-cocktails.j2
+++ b/vasl_templates/webapp/data/default-template-pack/extras/kgs/molotov-cocktails.j2
@@ -1,4 +1,4 @@
-
+
diff --git a/vasl_templates/webapp/data/default-template-pack/extras/pf-count.j2 b/vasl_templates/webapp/data/default-template-pack/extras/pf-count.j2
index cda7253..b146f71 100644
--- a/vasl_templates/webapp/data/default-template-pack/extras/pf-count.j2
+++ b/vasl_templates/webapp/data/default-template-pack/extras/pf-count.j2
@@ -1,4 +1,4 @@
-
+
diff --git a/vasl_templates/webapp/data/default-template-pack/extras/turn-track-shading.j2 b/vasl_templates/webapp/data/default-template-pack/extras/turn-track-shading.j2
index f76afee..2dda338 100644
--- a/vasl_templates/webapp/data/default-template-pack/extras/turn-track-shading.j2
+++ b/vasl_templates/webapp/data/default-template-pack/extras/turn-track-shading.j2
@@ -1,4 +1,4 @@
-
+
diff --git a/vasl_templates/webapp/data/default-template-pack/mol-p.j2 b/vasl_templates/webapp/data/default-template-pack/mol-p.j2
index 7ae4e4c..b6ba16e 100644
--- a/vasl_templates/webapp/data/default-template-pack/mol-p.j2
+++ b/vasl_templates/webapp/data/default-template-pack/mol-p.j2
@@ -1,4 +1,4 @@
-
+
diff --git a/vasl_templates/webapp/data/default-template-pack/mol.j2 b/vasl_templates/webapp/data/default-template-pack/mol.j2
index 7854db6..09cbb18 100644
--- a/vasl_templates/webapp/data/default-template-pack/mol.j2
+++ b/vasl_templates/webapp/data/default-template-pack/mol.j2
@@ -1,4 +1,4 @@
-
+
diff --git a/vasl_templates/webapp/data/default-template-pack/ob_note.j2 b/vasl_templates/webapp/data/default-template-pack/ob_note.j2
index c83315f..36baf07 100644
--- a/vasl_templates/webapp/data/default-template-pack/ob_note.j2
+++ b/vasl_templates/webapp/data/default-template-pack/ob_note.j2
@@ -1,4 +1,4 @@
-
+
diff --git a/vasl_templates/webapp/data/default-template-pack/ob_ordnance.j2 b/vasl_templates/webapp/data/default-template-pack/ob_ordnance.j2
index d390d79..52052a8 100644
--- a/vasl_templates/webapp/data/default-template-pack/ob_ordnance.j2
+++ b/vasl_templates/webapp/data/default-template-pack/ob_ordnance.j2
@@ -1,4 +1,4 @@
-
+
diff --git a/vasl_templates/webapp/data/default-template-pack/ob_setup.j2 b/vasl_templates/webapp/data/default-template-pack/ob_setup.j2
index 2990e5d..6cb9242 100644
--- a/vasl_templates/webapp/data/default-template-pack/ob_setup.j2
+++ b/vasl_templates/webapp/data/default-template-pack/ob_setup.j2
@@ -1,4 +1,4 @@
-
+
diff --git a/vasl_templates/webapp/data/default-template-pack/ob_vehicles.j2 b/vasl_templates/webapp/data/default-template-pack/ob_vehicles.j2
index ab3eb98..88f32a3 100644
--- a/vasl_templates/webapp/data/default-template-pack/ob_vehicles.j2
+++ b/vasl_templates/webapp/data/default-template-pack/ob_vehicles.j2
@@ -1,4 +1,4 @@
-
+
diff --git a/vasl_templates/webapp/data/default-template-pack/pf.j2 b/vasl_templates/webapp/data/default-template-pack/pf.j2
index 4319337..cba73db 100644
--- a/vasl_templates/webapp/data/default-template-pack/pf.j2
+++ b/vasl_templates/webapp/data/default-template-pack/pf.j2
@@ -1,4 +1,4 @@
-
+
diff --git a/vasl_templates/webapp/data/default-template-pack/piat.j2 b/vasl_templates/webapp/data/default-template-pack/piat.j2
index 219bfb0..5a12515 100644
--- a/vasl_templates/webapp/data/default-template-pack/piat.j2
+++ b/vasl_templates/webapp/data/default-template-pack/piat.j2
@@ -1,4 +1,4 @@
-
+
diff --git a/vasl_templates/webapp/data/default-template-pack/players.j2 b/vasl_templates/webapp/data/default-template-pack/players.j2
index a04d8e0..be51942 100644
--- a/vasl_templates/webapp/data/default-template-pack/players.j2
+++ b/vasl_templates/webapp/data/default-template-pack/players.j2
@@ -1,4 +1,4 @@
-
+
diff --git a/vasl_templates/webapp/data/default-template-pack/psk.j2 b/vasl_templates/webapp/data/default-template-pack/psk.j2
index d250d98..398eb50 100644
--- a/vasl_templates/webapp/data/default-template-pack/psk.j2
+++ b/vasl_templates/webapp/data/default-template-pack/psk.j2
@@ -1,4 +1,4 @@
-
+
diff --git a/vasl_templates/webapp/data/default-template-pack/scenario.j2 b/vasl_templates/webapp/data/default-template-pack/scenario.j2
index 412f030..e6d2588 100644
--- a/vasl_templates/webapp/data/default-template-pack/scenario.j2
+++ b/vasl_templates/webapp/data/default-template-pack/scenario.j2
@@ -1,4 +1,4 @@
-
+
diff --git a/vasl_templates/webapp/data/default-template-pack/scenario_note.j2 b/vasl_templates/webapp/data/default-template-pack/scenario_note.j2
index 3d905ee..4c5b8af 100644
--- a/vasl_templates/webapp/data/default-template-pack/scenario_note.j2
+++ b/vasl_templates/webapp/data/default-template-pack/scenario_note.j2
@@ -1,4 +1,4 @@
-
+
diff --git a/vasl_templates/webapp/data/default-template-pack/ssr.j2 b/vasl_templates/webapp/data/default-template-pack/ssr.j2
index f325a3c..d989502 100644
--- a/vasl_templates/webapp/data/default-template-pack/ssr.j2
+++ b/vasl_templates/webapp/data/default-template-pack/ssr.j2
@@ -1,4 +1,4 @@
-
+
diff --git a/vasl_templates/webapp/data/default-template-pack/victory_conditions.j2 b/vasl_templates/webapp/data/default-template-pack/victory_conditions.j2
index 2640bcc..bed9d31 100644
--- a/vasl_templates/webapp/data/default-template-pack/victory_conditions.j2
+++ b/vasl_templates/webapp/data/default-template-pack/victory_conditions.j2
@@ -1,4 +1,4 @@
-
+
diff --git a/vasl_templates/webapp/file_server/vasl_mod.py b/vasl_templates/webapp/file_server/vasl_mod.py
index 7b30edd..f91936d 100644
--- a/vasl_templates/webapp/file_server/vasl_mod.py
+++ b/vasl_templates/webapp/file_server/vasl_mod.py
@@ -11,8 +11,8 @@ _logger = logging.getLogger( "vasl_mod" )
from vasl_templates.webapp.file_server.utils import get_vo_gpids, get_effective_gpid
-SUPPORTED_VASL_MOD_VERSIONS = [ "6.3.3", "6.4.0", "6.4.1", "6.4.2", "6.4.3" ]
-SUPPORTED_VASL_MOD_VERSIONS_DISPLAY = "6.3.3, 6.4.0-6.4.3"
+SUPPORTED_VASL_MOD_VERSIONS = [ "6.4.0", "6.4.1", "6.4.2", "6.4.3" ]
+SUPPORTED_VASL_MOD_VERSIONS_DISPLAY = "6.4.0-6.4.3"
# ---------------------------------------------------------------------
diff --git a/vasl_templates/webapp/static/css/main.css b/vasl_templates/webapp/static/css/main.css
index bf2c7f5..7578a1b 100644
--- a/vasl_templates/webapp/static/css/main.css
+++ b/vasl_templates/webapp/static/css/main.css
@@ -14,7 +14,7 @@ label { height: 1.25em ; margin-top: -3px ; }
#menu { position: absolute ; top: 15px ; right: 8px ; z-index: 1 ; }
#menu input[type='image'] { height: 30px ; }
-.PopMenu-Item { width: 11em ; }
+.PopMenu-Item { width: 12em ; }
.PopMenu-Item a { padding: 5px 10px 5px 10px ; }
.PopMenu-Icon { display: none ; }
@@ -26,7 +26,8 @@ label { height: 1.25em ; margin-top: -3px ; }
.select2-dropdown { color: #444 ; }
-.snippet-control button.generate { height: 26px ; padding: 2px 10px 2px 5px ; }
+.snippet-control button.generate { height: 26px ; padding: 2px 10px 2px 5px ; color: #000 ; }
+.snippet-control button.generate.inactive { color: #aaa ; }
.snippet-control button.generate img { height: 20px ; margin-right: 5px ; vertical-align: middle ; }
.snippet-control .ui-selectmenu-button { padding: 2px 10px ; }
.snippet-control-menu-item { font-size: 75% ; font-style: italic ; }
diff --git a/vasl_templates/webapp/static/css/vassal.css b/vasl_templates/webapp/static/css/vassal.css
new file mode 100644
index 0000000..96b7ebd
--- /dev/null
+++ b/vasl_templates/webapp/static/css/vassal.css
@@ -0,0 +1,9 @@
+.ui-dialog.update-vsav .ui-dialog-titlebar { display: none ; }
+#update-vsav { display: flex ; align-items: center ; }
+#update-vsav img { margin-right: 1em ; }
+
+#vassal-shim-error textarea { width: 100% ; height: 15em ; min-height: 5em ; resize: none ; padding: 2px ; font-family: monospace ; font-size: 80% ; }
+.ui-dialog.vassal-shim-error .ui-dialog-titlebar { background: #f5af41 ; }
+.ui-dialog.vassal-shim-error .ui-dialog-content { display: flex ; flex-direction: column ; }
+.ui-dialog.vassal-shim-error .ui-dialog-content textarea { flex-grow: 1 ; }
+.ui-dialog.vassal-shim-error .ui-dialog-buttonpane { border: none ; margin-top: 0 !important ; padding-top: 0 !important ; }
diff --git a/vasl_templates/webapp/static/extras.js b/vasl_templates/webapp/static/extras.js
index 2b6647f..44a0bec 100644
--- a/vasl_templates/webapp/static/extras.js
+++ b/vasl_templates/webapp/static/extras.js
@@ -147,20 +147,30 @@ function _parse_extra_template( template_id, template )
function fixup_template_parameters( template )
{
// identify any non-standard template parameters
- var matches = [] ;
var regex = /\{\{([A-Z0-9_]+?):.*?\}\}/g ;
+ var matches = [] ;
var match ;
while( (match = regex.exec( template )) !== null )
matches.push( [ regex.lastIndex-match[0].length, match[0].length, match[1] ] ) ;
// fix them up
+ var i ;
if ( matches.length > 0 ) {
- for ( var i=matches.length-1 ; i >= 0 ; --i )
+ for ( i=matches.length-1 ; i >= 0 ; --i )
template = template.substr(0,matches[i][0]) + "{{"+matches[i][2]+"}}" + template.substr(matches[i][0]+matches[i][1]) ;
}
- // remove comments
- template = template.replace( /\n*/g, "" ) ;
+ // remove all our special comments, except for the snippet ID
+ regex = /\n*/g ;
+ matches = [] ;
+ while( (match = regex.exec( template )) !== null ) {
+ if ( match[1] !== "id" )
+ matches.push( [ regex.lastIndex-match[0].length, match[0].length ] ) ;
+ }
+ if ( matches.length > 0 ) {
+ for ( i=matches.length-1 ; i >= 0 ; --i )
+ template = template.substr(0,matches[i][0]) + template.substr(matches[i][0]+matches[i][1]) ;
+ }
return template ;
}
diff --git a/vasl_templates/webapp/static/help/index.html b/vasl_templates/webapp/static/help/index.html
index d89321c..e360d4a 100644
--- a/vasl_templates/webapp/static/help/index.html
+++ b/vasl_templates/webapp/static/help/index.html
@@ -296,6 +296,16 @@ pytest --webdriver chrome --headless
NOTE: Internet Explorer is also supported as a WebDriver, but due to differences in the way it works, some tests are currently failing for this.
+
Compiling the VASSAL shim
+
+ The program uses VASSAL to update VASL scenarios (.vsav files), and since this is written in Java, a helper program has been written in Java to do this.
+
To compile the program, go to the $/vassal-shim directory and type:
+
+make all VASSAL_DIR=...
+
+where VASSAL_DIR points to VASSAL's lib/ directory (the program needs Vengine.jar).
+ Since this program doesn't change very often, the resulting artifact (vassal-shim.jar) is checked into source control, so that it can be used without needing to install a JDK and compiling it first.
+
Code lint'ing
Python code is checked using pylint (installed during the pip install above), which should be run from the root directory of the repo.
diff --git a/vasl_templates/webapp/static/main.js b/vasl_templates/webapp/static/main.js
index f0a2b7a..6899c61 100644
--- a/vasl_templates/webapp/static/main.js
+++ b/vasl_templates/webapp/static/main.js
@@ -9,7 +9,7 @@ gVaslPieceInfo = {} ;
gWebChannelHandler = null ;
gEmSize = null ;
-var _NATIONALITY_SPECIFIC_BUTTONS = {
+var NATIONALITY_SPECIFIC_BUTTONS = {
"russian": [ "mol", "mol-p" ],
"german": [ "pf", "psk", "atmm" ],
"american": [ "baz" ],
@@ -39,6 +39,7 @@ $(document).ready( function () {
new_scenario: { label: "New scenario", action: function() { on_new_scenario() ; } },
load_scenario: { label: "Load scenario", action: on_load_scenario },
save_scenario: { label: "Save scenario", action: on_save_scenario },
+ update_vsav: { label: "Update VASL scenario", action: on_update_vsav },
separator: { type: "separator" },
template_pack: { label: "Load template pack", action: on_template_pack },
separator2: { type: "separator" },
@@ -63,10 +64,10 @@ $(document).ready( function () {
} ) ;
}
} ) ;
- // add a handler for when the "load scenario" file has been selected
+ // add handlers
$("#load-scenario").change( on_load_scenario_file_selected ) ;
- // add a handler for when the "load template pack" file has been selected
$("#load-template-pack").change( on_template_pack_file_selected ) ;
+ $("#load-vsav").change( on_load_vsav_file_selected ) ;
// all done - we can show the menu now
$("#menu").show() ;
@@ -421,10 +422,10 @@ function update_page_load_status( id )
// check if the vehicle/ordnance listings have finished loading
if ( gPageLoadStatus.indexOf( "vehicle-listings" ) === -1 && gPageLoadStatus.indexOf( "ordnance-listings" ) === -1 ) {
- // NOTE: If the default scanerio contains any vehicles or ordnance, it will look up the V/O listings,
+ // NOTE: If the default scenario contains any vehicles or ordnance, it will look up the V/O listings,
// so we need to wait until those have arrived. Note that while the default scenario will normally
// be empty, having stuff in it is very useful during development.
- do_on_new_scenario() ;
+ do_on_new_scenario( false ) ;
}
// check if the page has finished loading
@@ -555,9 +556,9 @@ function on_player_change( player_no )
var player_nat = update_ob_tab_header( player_no ) ;
// show/hide the nationality-specific buttons
- for ( var nat in _NATIONALITY_SPECIFIC_BUTTONS ) {
- for ( var i=0 ; i < _NATIONALITY_SPECIFIC_BUTTONS[nat].length ; ++i ) {
- var button_id = _NATIONALITY_SPECIFIC_BUTTONS[nat][i] ;
+ for ( var nat in NATIONALITY_SPECIFIC_BUTTONS ) {
+ for ( var i=0 ; i < NATIONALITY_SPECIFIC_BUTTONS[nat].length ; ++i ) {
+ var button_id = NATIONALITY_SPECIFIC_BUTTONS[nat][i] ;
var $elem = $( "#panel-ob_notes_" + player_no + " div.snippet-control[data-id='" + button_id + "']" ) ;
$elem.css( "display", nat == player_nat ? "inline-block" : "none" ) ;
}
diff --git a/vasl_templates/webapp/static/simple_notes.js b/vasl_templates/webapp/static/simple_notes.js
index 6234a19..c12fd10 100644
--- a/vasl_templates/webapp/static/simple_notes.js
+++ b/vasl_templates/webapp/static/simple_notes.js
@@ -88,6 +88,13 @@ function _do_edit_simple_note( $sortable2, $entry, default_width )
// create a new note
if ( caption !== "" ) {
data = { caption: caption, width: width } ;
+ if ( note_type === "scenario_notes" || note_type === "ob_setups" || note_type === "ob_notes" ) {
+ var usedIds = {} ;
+ $sortable2.find( "li" ).each( function() {
+ usedIds[ $(this).data("sortable2-data").id ] = true ;
+ } ) ;
+ data.id = auto_assign_id( usedIds ) ;
+ }
_do_add_simple_note( $sortable2, data ) ;
}
}
@@ -132,23 +139,32 @@ function _make_simple_note( note_type, caption )
// add a handler for the snippet button
$content.children("img.snippet").click( function() {
- var data = $(this).parent().parent().data( "sortable2-data" ) ;
- var key ;
- if ( note_type === "scenario_notes" )
- key = "SCENARIO_NOTE" ;
- else if ( note_type === "ob_setups" )
- key = "OB_SETUP" ;
- else if ( note_type == "ob_notes" )
- key = "OB_NOTE" ;
- var extra_params = {} ;
- extra_params[key] = data.caption ;
- extra_params[key+"_WIDTH"] = data.width ;
+ var extra_params = get_simple_note_snippet_extra_params( $(this) ) ;
generate_snippet( $(this), extra_params ) ;
} ) ;
return $content ;
}
+function get_simple_note_snippet_extra_params( $img )
+{
+ // get the extra parameters needed to generate the simple note's snippet
+ var extra_params = {} ;
+ var $sortable2 = $img.closest( ".sortable" ) ;
+ var note_type = _get_note_type_for_sortable( $sortable2 ) ;
+ var key ;
+ if ( note_type === "scenario_notes" )
+ key = "SCENARIO_NOTE" ;
+ else if ( note_type === "ob_setups" )
+ key = "OB_SETUP" ;
+ else if ( note_type == "ob_notes" )
+ key = "OB_NOTE" ;
+ var data = $img.parent().parent().data( "sortable2-data" ) ;
+ extra_params[key] = data.caption ;
+ extra_params[key+"_WIDTH"] = data.width ;
+ return extra_params ;
+}
+
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
function _get_note_type_for_sortable( $sortable2 )
diff --git a/vasl_templates/webapp/static/snippets.js b/vasl_templates/webapp/static/snippets.js
index c01526a..fd83256 100644
--- a/vasl_templates/webapp/static/snippets.js
+++ b/vasl_templates/webapp/static/snippets.js
@@ -22,29 +22,48 @@ var gLastSavedScenarioFilename = null;
function generate_snippet( $btn, extra_params )
{
- // unload the template parameters
+ // generate the snippet
+ var snippet = make_snippet( $btn, extra_params, true ) ;
+
+ // copy the snippet to the clipboard
+ try {
+ copyToClipboard( snippet ) ;
+ }
+ catch( ex ) {
+ showErrorMsg( "Can't copy to the clipboard:
" + escapeHTML(ex) + "
" ) ;
+ return ;
+ }
+ showInfoMsg( "The HTML snippet has been copied to the clipboard." ) ;
+}
+
+function make_snippet( $btn, extra_params, show_date_warnings )
+{
+ // initialize
var template_id = $btn.data( "id" ) ;
var params = unload_snippet_params( true, template_id ) ;
// set player-specific parameters
- var curr_tab = $("#tabs .ui-tabs-active a").attr( "href" ) ;
- var colors ;
- if ( curr_tab === "#tabs-ob1" ) {
- params.PLAYER_NAME = get_nationality_display_name( params.PLAYER_1 ) ;
- colors = get_player_colors( 1 ) ;
- params.OB_COLOR = colors[0] ;
- params.OB_COLOR_2 = colors[2] ;
- if ( gUserSettings["include-flags-in-snippets"] )
- params.PLAYER_FLAG = make_player_flag_url( get_player_nat( 1 ) ) ;
- } else if ( curr_tab === "#tabs-ob2" ) {
- params.PLAYER_NAME = get_nationality_display_name( params.PLAYER_2 ) ;
- colors = get_player_colors( 2 ) ;
+ var player_no = get_player_no_for_element( $btn ) ;
+ if ( player_no ) {
+ params.PLAYER_NAME = get_nationality_display_name( params["PLAYER_"+player_no] ) ;
+ var colors = get_player_colors( player_no ) ;
params.OB_COLOR = colors[0] ;
params.OB_COLOR_2 = colors[2] ;
if ( gUserSettings["include-flags-in-snippets"] )
- params.PLAYER_FLAG = make_player_flag_url( get_player_nat( 2 ) ) ;
+ params.PLAYER_FLAG = make_player_flag_url( get_player_nat( player_no ) ) ;
}
+ // set the snippet ID
+ var data ;
+ if ( template_id === "ob_setup" || template_id === "ob_note" ) {
+ data = $btn.parent().parent().data( "sortable2-data" ) ;
+ params.SNIPPET_ID = template_id + "_" + player_no + "." + data.id ;
+ } else if ( template_id === "scenario_note" ) {
+ data = $btn.parent().parent().data( "sortable2-data" ) ;
+ params.SNIPPET_ID = template_id + "." + data.id ;
+ } else
+ params.SNIPPET_ID = template_id ;
+
// set player-specific parameters
if ( template_id == "ob_vehicles_1" ) {
template_id = "ob_vehicles" ;
@@ -129,14 +148,16 @@ function generate_snippet( $btn, extra_params )
}
// check for date-specific parameters
- if ( template_id === "pf" && ! is_pf_available() )
- showWarningMsg( "PF are only available after September 1943." ) ;
- if ( template_id === "psk" && ! is_psk_available() )
- showWarningMsg( "PSK are only available after September 1943." ) ;
- if ( template_id === "baz" && ! is_baz_available() )
- showWarningMsg( "BAZ are only available from November 1942." ) ;
- if ( template_id === "atmm" && ! is_atmm_available() )
- showWarningMsg( "ATMM are only available from 1944." ) ;
+ if ( show_date_warnings ) {
+ if ( template_id === "pf" && ! is_pf_available() )
+ showWarningMsg( "PF are only available after September 1943." ) ;
+ if ( template_id === "psk" && ! is_psk_available() )
+ showWarningMsg( "PSK are only available after September 1943." ) ;
+ if ( template_id === "baz" && ! is_baz_available() )
+ showWarningMsg( "BAZ are only available from November 1942." ) ;
+ if ( template_id === "atmm" && ! is_atmm_available() )
+ showWarningMsg( "ATMM are only available from 1944." ) ;
+ }
// add in any extra parameters
if ( extra_params )
@@ -149,44 +170,38 @@ function generate_snippet( $btn, extra_params )
// get the template to generate the snippet from
var templ = get_template( template_id, true ) ;
if ( templ === null )
- return ;
+ return "" ;
var func ;
try {
func = jinja.compile( templ ).render ;
}
catch( ex ) {
showErrorMsg( "Can't compile template:" + escapeHTML(ex) + "
" ) ;
- return ;
+ return "[error: can't compile template]" ;
}
// process the template
- var val ;
+ var snippet ;
try {
// NOTE: While it's generally not a good idea to disable auto-escaping, the whole purpose
// of this application is to generate HTML snippets, and so virtually every single
// template parameter would have to be piped through the "safe" filter :-/ We never render
// any of the generated HTML, so any risk exists only when the user pastes the HTML snippet
// into a VASL scenario, which uses an ancient HTML engine (with probably no Javascript)...
- val = func( params, {
+ snippet = func( params, {
autoEscape: false,
filters: {
- join: function(val,sep) { return val.join(sep) ; }
+ join: function(snippet,sep) { return snippet.join(sep) ; }
} ,
} ) ;
- val = val.trim() ;
+ snippet = snippet.trim() ;
}
catch( ex ) {
showErrorMsg( "Can't process template: " + template_id + "" + escapeHTML(ex) + "
" ) ;
- return ;
+ return "[error: can't process template'" ;
}
- try {
- copyToClipboard( val ) ;
- }
- catch( ex ) {
- showErrorMsg( "Can't copy to the clipboard:" + escapeHTML(ex) + "
" ) ;
- return ;
- }
- showInfoMsg( "The HTML snippet has been copied to the clipboard." ) ;
+
+ return snippet ;
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
@@ -696,7 +711,7 @@ function do_load_scenario( data, fname )
{
// NOTE: We reset the scenario first, in case the loaded scenario is missing fields,
// so that those fields will be reset to their default values (instead of just staying unchanged).
- do_on_new_scenario() ;
+ do_on_new_scenario( false ) ;
// load the scenario
try {
@@ -715,6 +730,14 @@ function do_load_scenario_data( params )
// reset the scenario
reset_scenario() ;
+ // auto-assign ID's to the OB setup notes and notes
+ // NOTE: We do this here to handle scenarios that were created before these ID's were implemented.
+ auto_assign_ids( params.SCENARIO_NOTES ) ;
+ auto_assign_ids( params.OB_SETUPS_1 ) ;
+ auto_assign_ids( params.OB_NOTES_1 ) ;
+ auto_assign_ids( params.OB_SETUPS_2 ) ;
+ auto_assign_ids( params.OB_NOTES_2 ) ;
+
// load the scenario parameters
var params_loaded = {} ;
var warnings = [] ;
@@ -860,6 +883,50 @@ function do_load_scenario_data( params )
on_scenario_date_change() ;
}
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+function auto_assign_ids( vals )
+{
+ if ( ! vals )
+ return ;
+
+ // NOTE: These ID's are used to uniquely identify OB setup notes and OB notes, since they are generated
+ // from the same template ("ob_setup" and "ob_note") and so the template_id alone won't be enough. We need
+ // to be able to uniquely identify each snippet so that we can match them with labels in the VASL scenario.
+ // However, we need to be able to handle the following situation:
+ // - the scenario has, say, 5 OB notes, with ID's 1-5
+ // - the user deletes #3, and creates a new one
+ // If we track the highest ID ever used across the life of the scenario, the new snippet will be assigned ID #6,
+ // but when we inject the snippets into the VASL scenario, the label corresponding to snippet #3 will be left
+ // as it is, and a new label created for snippet #6, which is not what the user will want. Instead, we re-use
+ // ID 3 and give it to the new snippet, so that when we inject snippets, the old label corresponding to snippet #3
+ // will simply be updated with the contents of the new snippet #6.
+
+ // identify which ID's are currently in use
+ var usedIds = {} ;
+ for ( var i=0 ; i < vals.length ; ++i ) {
+ if ( vals[i].id )
+ usedIds[ vals[i].id ] = true ;
+ }
+
+ // assign ID's to entries that don't have one
+ for ( i=0 ; i < vals.length ; ++i ) {
+ if ( ! vals[i].id )
+ vals[i].id = auto_assign_id( usedIds ) ;
+ }
+}
+
+function auto_assign_id( usedIds )
+{
+ // assign the next available ID
+ for ( var i=1 ; ; ++i ) {
+ if ( ! usedIds[i] ) {
+ usedIds[i] = true ;
+ return i ;
+ }
+ }
+}
+
// --------------------------------------------------------------------
function on_save_scenario()
@@ -959,7 +1026,7 @@ function on_new_scenario()
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-function do_on_new_scenario( verbose ) {
+function do_on_new_scenario( user_requested ) {
// load the default scenario
if ( gDefaultScenario )
do_load_scenario_data( gDefaultScenario ) ;
@@ -976,11 +1043,11 @@ function do_on_new_scenario( verbose ) {
// flag that we have a new scenario
gLastSavedScenarioFilename = null ;
- if ( gWebChannelHandler )
+ if ( gWebChannelHandler && user_requested )
gWebChannelHandler.on_new_scenario() ;
// provide some feedback to the user
- if ( verbose )
+ if ( user_requested )
showInfoMsg( "The scenario was reset." ) ;
}
@@ -1218,7 +1285,10 @@ function on_scenario_date_change()
// (by SSR) even outside the normal time.
function update_ui( id, is_available ) {
var $btn = $( "button.generate[data-id='" + id + "']" ) ;
- $btn.css( "color", is_available?"#000":"#aaa" ) ;
+ if ( is_available )
+ $btn.removeClass( "inactive" ) ;
+ else
+ $btn.addClass( "inactive" ) ;
$btn.children( "img" ).each( function() {
$(this).attr( "src", gImagesBaseUrl + (is_available?"/snippet.png":"/snippet-disabled.png") ) ;
} ) ;
diff --git a/vasl_templates/webapp/static/utils.js b/vasl_templates/webapp/static/utils.js
index 8f98f34..06a6ff9 100644
--- a/vasl_templates/webapp/static/utils.js
+++ b/vasl_templates/webapp/static/utils.js
@@ -37,7 +37,7 @@ function make_player_flag_url( player_nat ) {
function get_player_no_for_element( $elem )
{
- // get the player colors (if any) for the specified element
+ // get the player that owns the specified element
if ( $.contains( $("#tabs-ob1")[0], $elem[0] ) )
return 1 ;
if ( $.contains( $("#tabs-ob2")[0], $elem[0] ) )
diff --git a/vasl_templates/webapp/static/vassal.js b/vasl_templates/webapp/static/vassal.js
new file mode 100644
index 0000000..7fb4eb9
--- /dev/null
+++ b/vasl_templates/webapp/static/vassal.js
@@ -0,0 +1,291 @@
+
+// --------------------------------------------------------------------
+
+function on_update_vsav()
+{
+ // FOR TESTING PORPOISES! We can't control a file upload from Selenium (since
+ // the browser will use native controls), so we get the data from a