Compare commits

...

18 Commits

  1. 2
      .pylintrc
  2. 14
      Dockerfile
  3. 23
      conftest.py
  4. 1
      docker/config/site.cfg
  5. 6
      docker/run.sh
  6. 14
      requirements-dev.txt
  7. 13
      requirements.txt
  8. 2
      setup.py
  9. 5
      vasl_templates/about.py
  10. 3
      vasl_templates/main.py
  11. 26
      vasl_templates/ui/about.ui
  12. 10
      vasl_templates/webapp/__init__.py
  13. 3
      vasl_templates/webapp/config/constants.py
  14. 2
      vasl_templates/webapp/config/site.cfg.example
  15. 3
      vasl_templates/webapp/data/default-template-pack/national-capabilities.json
  16. 6
      vasl_templates/webapp/data/ordnance/french.json
  17. 4
      vasl_templates/webapp/data/vasl-6.6.6/piece-info.json
  18. 72
      vasl_templates/webapp/data/vasl-6.6.7/expected-multiple-images.json
  19. 2
      vasl_templates/webapp/data/vasl-6.6.7/online-counter-images.json
  20. 71
      vasl_templates/webapp/data/vasl-6.6.7/piece-info.json
  21. 147
      vasl_templates/webapp/data/vasl-6.6.7/vasl-overrides.json
  22. 72
      vasl_templates/webapp/data/vasl-6.6.8/expected-multiple-images.json
  23. 2
      vasl_templates/webapp/data/vasl-6.6.8/online-counter-images.json
  24. 71
      vasl_templates/webapp/data/vasl-6.6.8/piece-info.json
  25. 147
      vasl_templates/webapp/data/vasl-6.6.8/vasl-overrides.json
  26. 6
      vasl_templates/webapp/main.py
  27. 2
      vasl_templates/webapp/scenarios.py
  28. BIN
      vasl_templates/webapp/static/help/images/asl-rulebook2.png
  29. BIN
      vasl_templates/webapp/static/help/images/asl-rulebook2.small.png
  30. 31
      vasl_templates/webapp/static/help/index.html
  31. 2
      vasl_templates/webapp/static/main.js
  32. 83
      vasl_templates/webapp/templates/index.html
  33. 2
      vasl_templates/webapp/tests/control_tests.py
  34. 2
      vasl_templates/webapp/tests/control_tests_servicer.py
  35. 0
      vasl_templates/webapp/tests/fixtures/counters/6.5.0.txt
  36. 0
      vasl_templates/webapp/tests/fixtures/counters/6.5.1.txt
  37. 5
      vasl_templates/webapp/tests/fixtures/counters/6.6.0.txt
  38. 5
      vasl_templates/webapp/tests/fixtures/counters/6.6.1.txt
  39. 5
      vasl_templates/webapp/tests/fixtures/counters/6.6.2.txt
  40. 5
      vasl_templates/webapp/tests/fixtures/counters/6.6.3.txt
  41. 5
      vasl_templates/webapp/tests/fixtures/counters/6.6.4.txt
  42. 5
      vasl_templates/webapp/tests/fixtures/counters/6.6.5.txt
  43. 5
      vasl_templates/webapp/tests/fixtures/counters/6.6.6.txt
  44. 1363
      vasl_templates/webapp/tests/fixtures/counters/6.6.7.txt
  45. 1363
      vasl_templates/webapp/tests/fixtures/counters/6.6.8.txt
  46. 1610
      vasl_templates/webapp/tests/proto/generated/control_tests_pb2.py
  47. 2
      vasl_templates/webapp/tests/proto/set_data_dir_request.py
  48. 2
      vasl_templates/webapp/tests/proto/set_default_template_pack_request.py
  49. 2
      vasl_templates/webapp/tests/proto/set_vasl_version_request.py
  50. 2
      vasl_templates/webapp/tests/proto/set_veh_ord_notes_dir_request.py
  51. 4
      vasl_templates/webapp/tests/test_counters.py
  52. 4
      vasl_templates/webapp/tests/test_national_capabilities.py
  53. 5
      vasl_templates/webapp/tests/test_scenario_search.py
  54. 2
      vasl_templates/webapp/tests/test_vasl_extensions.py
  55. 38
      vasl_templates/webapp/tests/test_vassal.py
  56. 9
      vasl_templates/webapp/tests/utils.py
  57. 6
      vasl_templates/webapp/vasl_mod.py
  58. 12
      vasl_templates/webapp/vassal.py
  59. 14
      vasl_templates/webapp/webdriver.py
  60. BIN
      vassal-shim/release/vassal-shim.jar
  61. 8
      vassal-shim/src/vassal_shim/VassalShim.java

@ -480,4 +480,4 @@ known-third-party=enchant
# Exceptions that will emit a warning when being caught. Defaults to
# "Exception"
overgeneral-exceptions=Exception
overgeneral-exceptions=builtins.Exception

@ -1,28 +1,28 @@
# NOTE: Use the run-container.sh script to build and launch this container.
# NOTE: Multi-stage builds require Docker >= 17.05.
FROM rockylinux:8.5 AS base
FROM rockylinux:9.1 AS base
# update packages and install requirements
RUN dnf -y upgrade-minimal && \
dnf install -y python39
dnf install -y python3.11
# NOTE: We don't need the following stuff for the build step, but it's nice to not have to re-install
# it all every time we change the requirements :-/
# install Java
ARG JAVA_URL=https://download.oracle.com/java/17/archive/jdk-17.0.2_linux-x64_bin.tar.gz
RUN curl -s "$JAVA_URL" | tar -xz -C /usr/bin/
RUN dnf install -y java-17-openjdk
# install Firefox
ARG FIREFOX_URL=https://ftp.mozilla.org/pub/firefox/releases/94.0.2/linux-x86_64/en-US/firefox-94.0.2.tar.bz2
# NOTE: We could install this using dnf, but the version of geckodriver needs to match it.
ARG FIREFOX_URL=https://ftp.mozilla.org/pub/firefox/releases/117.0.1/linux-x86_64/en-US/firefox-117.0.1.tar.bz2
RUN dnf install -y bzip2 xorg-x11-server-Xvfb gtk3 dbus-glib && \
curl -s "$FIREFOX_URL" | tar -jx -C /usr/local/ && \
ln -s /usr/local/firefox/firefox /usr/bin/firefox && \
echo "exclude=firefox" >>/etc/dnf/dnf.conf
# install geckodriver
ARG GECKODRIVER_URL=https://github.com/mozilla/geckodriver/releases/download/v0.31.0/geckodriver-v0.31.0-linux64.tar.gz
ARG GECKODRIVER_URL=https://github.com/mozilla/geckodriver/releases/download/v0.33.0/geckodriver-v0.33.0-linux64.tar.gz
RUN curl -sL "$GECKODRIVER_URL" | tar -xz -C /usr/bin/
# clean up
@ -33,7 +33,7 @@ RUN dnf clean all
FROM base AS build
# set up a virtualenv
RUN python3 -m venv /opt/venv
RUN python3.11 -m venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH"
RUN pip install --upgrade pip

@ -1,6 +1,7 @@
""" pytest support functions. """
import os
import shutil
import threading
import json
import re
@ -10,8 +11,6 @@ import urllib.request
from urllib.error import URLError
import pytest
from flask import url_for
from vasl_templates.webapp import app
from vasl_templates.webapp.tests import utils
from vasl_templates.webapp.tests.control_tests import ControlTests
@ -19,6 +18,7 @@ from vasl_templates.webapp.tests.control_tests import ControlTests
FLASK_WEBAPP_PORT = 5011
_pytest_options = None
_orig_url_for = app.url_for
# ---------------------------------------------------------------------
@ -129,7 +129,9 @@ def _make_webapp():
# NOTE: It's not a bad idea to bypass the clipboard, even when running in a browser,
# to avoid problems if something else uses the clipboard while the tests are running.
kwargs["store_clipboard"] = 1
url = url_for( endpoint, _external=True, **kwargs )
if kwargs.get( "_external" ) is None:
kwargs["_external"] = True
url = _orig_url_for( endpoint, **kwargs )
url = url.replace( "http://localhost", app.base_url )
return url
app.url_for = make_webapp_url
@ -187,9 +189,9 @@ def _make_webapp():
# we won't have even got this far, since it needs to be there to drive the browser.
# NOTE: This will have no effect if we're talking to a remote server, but we can live with that.
if _pytest_options.webdriver == "firefox":
app.config[ "WEBDRIVER_PATH" ] = "geckodriver"
app.config[ "WEBDRIVER_PATH" ] = shutil.which( "geckodriver" )
elif _pytest_options.webdriver == "chrome":
app.config[ "WEBDRIVER_PATH" ] = "chromedriver"
app.config[ "WEBDRIVER_PATH" ] = shutil.which( "chromedriver" )
return app
@ -216,15 +218,18 @@ def webdriver( request ):
# initialize
driver = request.config.getoption( "--webdriver" )
from selenium import webdriver as wb
log_fname = os.path.join( tempfile.gettempdir(), "webdriver-pytest.log" )
if driver == "firefox":
service = wb.firefox.service.Service(
log_output = os.path.join( tempfile.gettempdir(), "webdriver-pytest.log" )
)
options = wb.FirefoxOptions()
options.headless = _pytest_options.headless
service = wb.firefox.service.Service( log_path=log_fname )
if _pytest_options.headless:
options.add_argument( "--headless" )
driver = wb.Firefox( options=options, service=service )
elif driver == "chrome":
options = wb.ChromeOptions()
options.headless = _pytest_options.headless
if _pytest_options.headless:
options.add_argument( "--headless" )
options.add_argument( "--disable-gpu" )
driver = wb.Chrome( options=options )
else:

@ -2,5 +2,4 @@
IS_CONTAINER = 1
JAVA_PATH = /usr/bin/jdk-17.0.2/bin/java
WEBDRIVER_PATH = /usr/bin/geckodriver

@ -6,6 +6,10 @@ export DISPLAY=:10.0
Xvfb :10 -ac 1>/tmp/xvfb.log 2>/tmp/xvfb.err &
# run the webapp server
python3 /app/vasl_templates/webapp/run_server.py \
# IMPORTANT! This script runs as PID 1, which is the only process that will receive signals,
# so we must replace it with the Python webserver process if it is to receive e.g. SIGTERM,
# which we must handle if "docker stop" and scaling down in Kubernetes is to work (otherwise
# things timeout and we get SIGKILL'ed).
exec python3 /app/vasl_templates/webapp/run_server.py \
--addr 0.0.0.0 \
--force-init-delay 30

@ -1,7 +1,7 @@
pytest==7.1.2
grpcio-tools==1.46.3
tabulate==0.8.9
lxml==4.9.0
pylint==2.14.1
pytest-pylint==0.18.0
pyinstaller==5.1
pytest==7.4.2
grpcio-tools==1.58.0
tabulate==0.9.0
lxml==4.9.3
pylint==2.17.5
pytest-pylint==0.19.0
pyinstaller==5.13.2

@ -1,9 +1,10 @@
# python 3.10.7
# python 3.11.4
flask==2.1.2
pyyaml==6.0
pillow==9.1.1
selenium==4.2.0
flask==2.3.3
pyyaml==6.0.1
# NOTE: Pillow 9.5.0 is the last version that provides 32-bit wheels.
pillow==9.5.0
selenium==4.12.0
waitress==2.1.2
appdirs==1.4.4
click==8.1.3
click==8.1.7

@ -28,7 +28,7 @@ def parse_requirements( fname ):
setup(
name = "vasl_templates",
version = "1.11", # nb: also update constants.py
version = "1.13", # nb: also update constants.py
description = "Create HTML snippets for use in VASL.",
license = "AGPLv3",
url = "https://code.pacman-ghost.com/public/vasl-templates",

@ -11,7 +11,7 @@ from PyQt5.QtCore import QUrl
from PyQt5.QtGui import QDesktopServices, QIcon, QCursor
from PyQt5.QtWidgets import QDialog
from vasl_templates.webapp.config.constants import APP_NAME, APP_VERSION, APP_HOME_URL, IS_FROZEN
from vasl_templates.webapp.config.constants import APP_NAME, APP_VERSION, APP_HOME_URL, APP_ISSUES_URL, IS_FROZEN
from vasl_templates.utils import get_build_info
# ---------------------------------------------------------------------
@ -60,6 +60,9 @@ class AboutDialog( QDialog ):
self.home_url.setText( "Visit us at <a href='{}'>{}</a>.".format(
APP_HOME_URL, mo.group(1) if mo else APP_HOME_URL
) )
self.issues_url.setText( "Report a bug, request a feature, ask a question <a href='{}'>here</a>.".format(
APP_ISSUES_URL
) )
def on_app_icon_clicked( self, event ): #pylint: disable=unused-argument
"""Click handler."""

@ -243,6 +243,9 @@ def _do_main( template_pack, default_scenario, remote_debugging, debug ): #pylin
if is_windows():
QApplication.setStyle( "windowsvista" )
# disable the context help button in the title bar (Windows only)
QApplication.setAttribute( PyQt5.QtCore.Qt.AA_DisableWindowContextHelpButton )
# check if we should disable the embedded browser
disable_browser = webapp.config.get( "DISABLE_WEBENGINEVIEW" )

@ -10,7 +10,7 @@
<x>0</x>
<y>0</y>
<width>460</width>
<height>195</height>
<height>215</height>
</rect>
</property>
<property name="windowTitle">
@ -88,7 +88,7 @@
<property name="geometry">
<rect>
<x>19</x>
<y>152</y>
<y>172</y>
<width>425</width>
<height>31</height>
</rect>
@ -147,6 +147,28 @@
<bool>true</bool>
</property>
</widget>
<widget class="QLabel" name="issues_url">
<property name="geometry">
<rect>
<x>80</x>
<y>142</y>
<width>361</width>
<height>21</height>
</rect>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>*** ISSUES URL ***</string>
</property>
<property name="openExternalLinks">
<bool>true</bool>
</property>
</widget>
<widget class="QLabel" name="license">
<property name="geometry">
<rect>

@ -129,8 +129,8 @@ def _is_flask_child_process():
# ---------------------------------------------------------------------
def _on_sigint( signum, stack ): #pylint: disable=unused-argument
"""Clean up after a SIGINT."""
def _on_sig( signum, stack ): #pylint: disable=unused-argument
"""Clean up after a SIGINT/SIGTERM."""
# FUDGE! Since we added gRPC test control, we want to shutdown properly and clean things up (e.g. temp files
# created by the gRPC service), but the Flask reloader complicates what we have to do here horribly :-(
@ -230,6 +230,7 @@ _set_config_from_env( "VASL_EXTNS_DIR" )
_set_config_from_env( "BOARDS_DIR" )
_set_config_from_env( "CHAPTER_H_NOTES_DIR" )
_set_config_from_env( "USER_FILES_DIR" )
_set_config_from_env( "ASL_RULEBOOK2_BASE_URL" )
# NOTE: The Docker container also sets DEFAULT_TEMPLATE_PACK, but we read it directly from
# the environment variable, since it is not something that is stored in app.config.
@ -257,7 +258,10 @@ import vasl_templates.webapp.downloads #pylint: disable=cyclic-import
import vasl_templates.webapp.lfa #pylint: disable=cyclic-import
# install our signal handler (must be done in the main thread)
signal.signal( signal.SIGINT, _on_sigint )
signal.signal( signal.SIGINT, _on_sig )
# NOTE: We must handle SIGTERM, so that "docker stop" and scaling down in Kubernetes
# work properly (otherwise it times out and we get SIGKILL'ed :-/).
signal.signal( signal.SIGTERM, _on_sig )
# register startup initialization
app.before_request( _on_request )

@ -4,9 +4,10 @@ import sys
import os
APP_NAME = "VASL Templates"
APP_VERSION = "v1.11" # nb: also update setup.py
APP_VERSION = "v1.13" # nb: also update setup.py
APP_DESCRIPTION = "Generate HTML for use in VASL scenarios."
APP_HOME_URL = "https://vasl-templates.org"
APP_ISSUES_URL = "https://code.pacman-ghost.com/public/vasl-templates/issues"
if getattr( sys, "frozen", False ):
IS_FROZEN = True

@ -2,7 +2,7 @@
; configure VASSAL and VASL
VASSAL_DIR = ...configure the VASSAL installation directory...
VASL_MOD = ...configure the VASL module (e.g. vasl-6.6.6.vmod)...
VASL_MOD = ...configure the VASL module (e.g. vasl-6.6.7.vmod)...
VASL_EXTNS_DIR = ...configure the VASL extensions directory...
BOARDS_DIR = ...configure the VASL boards directory...

@ -306,7 +306,8 @@
},
"kfw-rok": {
"th_color": "{! -08/1950 = Red | 09/1950-04/1951 = Red (ROK) ; Black (KMC) | 05/1951- = Black | ??? !}",
"_comment_": "Errata (ASLJ 13 p48): KMC: '9/50+ black' s.b. '8/50+ black",
"th_color": "{! -07/1950 = Red | 08/1950-04/1951 = Red (ROK) ; Black (KMC) | 05/1951- = Black | ??? !}",
"oba": [ "{! 06/1950- = 10B | ??? !}", "3R",
"{? 09/1950- | Plentiful Ammo included | Plentiful Ammo included (KMC) | Plentiful Ammo included (ROK: 9/50+) ?}",
"{! 06/1950-08/1950 = ROK: 6B/3R !}"

@ -5,7 +5,7 @@
"note_number": "1",
"notes": [ "A", "B", "C\u2020", "E" ],
"id": "fr/o:000",
"gpid": 1636
"gpid": 12830
},
{ "name": "Mortier de 60 mle 35",
"type": "MTR",
@ -13,7 +13,7 @@
"note_number": "2\u2020",
"notes": [ "A", "B", "V" ],
"id": "fr/o:001",
"gpid": 1641
"gpid": 1633
},
{ "name": "Mortier de 81 mle 27/31",
"type": "MTR",
@ -22,7 +22,7 @@
"note_number": "3\u2020",
"notes": [ "A", "B", "D", "V" ],
"id": "fr/o:002",
"gpid": 1667
"gpid": [ 1665, 1667 ]
},
{ "name": "Fusil Antichar Boys",
"type": "ATR",

@ -16,9 +16,9 @@
"11359": {"is_small": true},
"3b5:11259": {"is_small": true},
"1632": {"is_small": true},
"1636": {"is_small": true},
"1641": {"is_small": true},
"1633": {"is_small": true},
"1648": {"is_small": true},
"12830": {"is_small": true},
"1982": {"is_small": true},
"1983": {"is_small": true},
"1984": {"is_small": true},

@ -0,0 +1,72 @@
{
"1555": {
"name": "2pdr Portee",
"front_images": [ "br/vehicles/portee.gif", "br/vehicles/portee0.gif" ],
"back_images": null
},
"2212": {
"name": "76* INF FRC",
"front_images": [ "al/gun/alINF76.gif", "al/gun/alINF76u.gif" ],
"back_images": "al/gun/alINF76b.gif"
},
"2698": {
"name": "SPW 251/10",
"front_images": "ge/veh/spw25110.gif",
"back_images": [ "No_ATR.gif", "No_PSK.gif" ]
},
"6765": {
"name": "81* MTR Krh/32",
"front_images": "fi/gun/fi81mmMTR.png",
"back_images": [ "fi/gun/fi81mmMTR.png", "fi/gun/fi81mmMTRB.png" ]
},
"6782": {
"name": "81* MTR Savu M42",
"front_images": [ "fi/gun/fi81mmMTR SavuB.png", "fi/gun/fi81mmMTR Savu.png" ],
"back_images": "fi/gun/fi81mmMTR SavuB.png"
},
"6797": {
"name": "20L (4) AA (g)",
"front_images": [ "fi/gun/fi20L4.png", "fi/gun/fi20L4L.png" ]
},
"6801": {
"name": "20L (6) AA (g)",
"front_images": "fi/gun/fi20L6.png",
"back_images": [ "fi/gun/fi20L6.png", "fi/gun/fi20L6L.png" ]
},
"7409": {
"name": "76 ItK/28 B(s)",
"front_images": "fi/gun/fiAA76L.png",
"back_images": [ "fi/gun/fiAA76L.png", "fi/gun/fiAA76LB.png" ]
},
"adf:1828": {
"name": "105 ART wz.29",
"front_images": "po/gun/poARTwz29-BFP.png",
"back_images": [ "po/gun/poARTwz29-BFP.png", "po/gun/poARTwz29-BFPb.png" ]
},
"adf:1829": {
"name": "120* ART wz09.31",
"front_images": "po/gun/poARTwz0931-BFP.png",
"back_images": [ "po/gun/poARTwz0931-BFP.png", "po/gun/poARTwz0931-BFPb.png" ]
},
"adf:1830": {
"name": "155 ART wz.17",
"front_images": "po/gun/poARTwz17-BFP.png",
"back_images": [ "po/gun/poARTwz17-BFP.png", "po/gun/poARTwz17-BFPb.png" ]
},
"3b5:3676": {
"name": "M19A1 MGMC",
"front_images": [ "us/veh/usM19A1MGMC(trailer)KFW.png", "us/veh/usM19A1MGMC(KFW).png" ],
"back_images": null
}
}

@ -0,0 +1,71 @@
{
"6996": {"is_small": true},
"485": {"is_small": true},
"850": {"is_small": true},
"849": {"is_small": true},
"12689": {"is_small": true},
"856": {"is_small": true},
"857": {"is_small": true},
"11336": {"is_small": true},
"858": {"is_small": true},
"11337": {"is_small": true},
"1149": {"is_small": true},
"1153": {"is_small": true},
"12687": {"is_small": true},
"3b5:7613": {"is_small": true},
"11359": {"is_small": true},
"3b5:11259": {"is_small": true},
"1632": {"is_small": true},
"1633": {"is_small": true},
"1648": {"is_small": true},
"12830": {"is_small": true},
"1982": {"is_small": true},
"1983": {"is_small": true},
"1984": {"is_small": true},
"1985": {"is_small": true},
"1986": {"is_small": true},
"1987": {"is_small": true},
"1988": {"is_small": true},
"2172": {"is_small": true},
"2173": {"is_small": true},
"2176": {"is_small": true},
"2179": {"is_small": true},
"11391": {"is_small": true},
"11392": {"is_small": true},
"11395": {"is_small": true},
"11396": {"is_small": true},
"11440": {"is_small": true},
"3b5:8401": {"is_small": true},
"3b5:8402": {"is_small": true},
"2465": {"is_small": true},
"2474": {"is_small": true},
"3252": {"is_small": true},
"3253": {"is_small": true},
"3263": {"is_small": true},
"3422": {"is_small": true},
"3428": {"is_small": true},
"6730": {"is_small": true},
"3605": {"is_small": true},
"3608": {"is_small": true},
"6763": {"is_small": true},
"3679": {"is_small": true},
"3680": {"is_small": true},
"3681": {"is_small": true},
"3682": {"is_small": true},
"3691": {"is_small": true},
"3692": {"is_small": true},
"3959": {"is_small": true},
"11558": {"is_small": true},
"11559": {"is_small": true},
"3b5:10150": {"is_small": true},
"3b5:10151": {"is_small": true},
"11600": {"is_small": true},
"11604": {"is_small": true},
"3b5:7871": {"is_small": true},
"adf:1948": {"is_small": true},
"adf:75": {"is_small": true},
"adf:77": {"is_small": true},
"adf:76": {"is_small": true},
"adf:1407": {"is_small": true},
"08d:75": {"is_small": true}
}

@ -0,0 +1,147 @@
{
"2474": {
"expected": {
"name": "Goliath",
"front_images": [ "ge/gegol.gif", "ge/gegolb.gif" ],
"back_images": null
},
"updated": {
"front_images": "ge/gegol.gif"
}
},
"1555": {
"expected": {
"name": "2pdr Portee",
"front_images": "br/vehicles/portee.gif",
"back_images": [ "br/vehicles/portee.gif", "br/vehicles/portee0.gif" ]
},
"updated": {
"front_images": [ "br/vehicles/portee.gif", "br/vehicles/portee0.gif" ],
"back_images": null
}
},
"3463": {
"expected": {
"name": "75L AA 75/46",
"front_images": [ "it/gun/itAA7546.gif", "it/gun/itAA7546b.gif" ],
"back_images": [ "it/gun/itAA7546b.gif", "it/gun/itAA7546lb.gif" ]
},
"updated": {
"front_images": "it/gun/itAA7546.gif",
"back_images": "it/gun/itAA7546b.gif"
}
},
"3776": {
"expected": {
"name": "37* INF Skoda IG",
"front_images": [ "ax/gun/buIN37s.gif", "ax/gun/buIN37s2.gif" ],
"back_images": "ax/gun/buIN37sb.gif"
},
"updated": {
"front_images": "ax/gun/buIN37s.gif"
}
},
"3777": {
"expected": {
"name": "70* INF Skoda IG",
"front_images": [ "ax/gun/buIN37s.gif", "ax/gun/buIN37s2.gif" ],
"back_images": "ax/gun/buIN37sb.gif"
},
"updated": {
"front_images": "ax/gun/buIN37s2.gif"
}
},
"6802": {
"expected": {
"name": "20L (4) AA",
"front_images": [ "fi/gun/fi20L4 _2.png", "fi/gun/fi20L4 _2 LIM.png" ],
"back_images": null
},
"updated": {
"front_images": "fi/gun/fi20L4 _2.png"
}
},
"6803": {
"expected": {
"name": "20L VKT (12) AA",
"front_images": [ "fi/gun/fi20L12.png", "fi/gun/fi20L12L.png" ],
"back_images": null
},
"updated": {
"front_images": "fi/gun/fi20L12.png"
}
},
"6804": {
"expected": {
"name": "40L Bofors AA (s)",
"front_images": [ "fi/gun/fi40L.png", "fi/gun/fi40LL.png" ],
"back_images": null
},
"updated": {
"front_images": "fi/gun/fi40L.png"
}
},
"adf:1824": {
"expected": {
"name": "37L AT PTP obr. 30",
"front_images": "ru/gun/ruAT37L.gif",
"back_images": "ru/gun/ruAT37Lb.gif"
},
"updated": {
"front_images": "ru/gun/ru37LPTPobr30.png"
}
},
"adf:1822": {
"expected": {
"name": "37* INF PP obr. 15R",
"front_images": "ru/gun/ruINF37s.gif",
"back_images": "ru/gun/ruINF37sb.gif"
},
"updated": {
"front_images": "ru/gun/ru37PPobr15R.png"
}
},
"adf:1823": {
"expected": {
"name": "76* INF PP obr. 27",
"front_images": "ru/gun/ruINF76s.gif",
"back_images": "ru/gun/ruINF76sb.gif"
},
"updated": {
"front_images": "ru/gun/ru76PPobr27.png"
}
},
"3b5:10093": {
"expected": {
"name": "SL truck",
"front_images": [ "sh/SL3b(KFW).png", "sh/SL4b(KFW).png", "sh/SL5b(KFW).png", "sh/SL6b(KFW).png", "sh/SL1b(KFW).png", "sh/SL2b(KFW).png" ],
"back_images": [ "sh/SL3(KFW).png", "sh/SL4(KFW).png", "sh/SL5(KFW).png", "sh/SL6(KFW).png", "sh/SL1(KFW).png", "sh/SL2(KFW).png" ]
},
"updated": {
"front_images": "us/veh/usSearchlight(KFW).png",
"back_images": null
}
},
"08d:75": {
"expected": {
"name": "RCL 75*",
"front_images": "amrcl75-malf.png",
"back_images": "dm-75rcl.gif"
},
"updated": {
"front_images": "amrcl75.png"
}
}
}

@ -0,0 +1,72 @@
{
"1555": {
"name": "2pdr Portee",
"front_images": [ "br/vehicles/portee.gif", "br/vehicles/portee0.gif" ],
"back_images": null
},
"2212": {
"name": "76* INF FRC",
"front_images": [ "al/gun/alINF76.gif", "al/gun/alINF76u.gif" ],
"back_images": "al/gun/alINF76b.gif"
},
"2698": {
"name": "SPW 251/10",
"front_images": "ge/veh/spw25110.gif",
"back_images": [ "No_ATR.gif", "No_PSK.gif" ]
},
"6765": {
"name": "81* MTR Krh/32",
"front_images": "fi/gun/fi81mmMTR.png",
"back_images": [ "fi/gun/fi81mmMTR.png", "fi/gun/fi81mmMTRB.png" ]
},
"6782": {
"name": "81* MTR Savu M42",
"front_images": [ "fi/gun/fi81mmMTR SavuB.png", "fi/gun/fi81mmMTR Savu.png" ],
"back_images": "fi/gun/fi81mmMTR SavuB.png"
},
"6797": {
"name": "20L (4) AA (g)",
"front_images": [ "fi/gun/fi20L4.png", "fi/gun/fi20L4L.png" ]
},
"6801": {
"name": "20L (6) AA (g)",
"front_images": "fi/gun/fi20L6.png",
"back_images": [ "fi/gun/fi20L6.png", "fi/gun/fi20L6L.png" ]
},
"7409": {
"name": "76 ItK/28 B(s)",
"front_images": "fi/gun/fiAA76L.png",
"back_images": [ "fi/gun/fiAA76L.png", "fi/gun/fiAA76LB.png" ]
},
"adf:1828": {
"name": "105 ART wz.29",
"front_images": "po/gun/poARTwz29-BFP.png",
"back_images": [ "po/gun/poARTwz29-BFP.png", "po/gun/poARTwz29-BFPb.png" ]
},
"adf:1829": {
"name": "120* ART wz09.31",
"front_images": "po/gun/poARTwz0931-BFP.png",
"back_images": [ "po/gun/poARTwz0931-BFP.png", "po/gun/poARTwz0931-BFPb.png" ]
},
"adf:1830": {
"name": "155 ART wz.17",
"front_images": "po/gun/poARTwz17-BFP.png",
"back_images": [ "po/gun/poARTwz17-BFP.png", "po/gun/poARTwz17-BFPb.png" ]
},
"3b5:3676": {
"name": "M19A1 MGMC",
"front_images": [ "us/veh/usM19A1MGMC(trailer)KFW.png", "us/veh/usM19A1MGMC(KFW).png" ],
"back_images": null
}
}

@ -0,0 +1,71 @@
{
"6996": {"is_small": true},
"485": {"is_small": true},
"850": {"is_small": true},
"849": {"is_small": true},
"12689": {"is_small": true},
"856": {"is_small": true},
"857": {"is_small": true},
"11336": {"is_small": true},
"858": {"is_small": true},
"11337": {"is_small": true},
"1149": {"is_small": true},
"1153": {"is_small": true},
"12687": {"is_small": true},
"3b5:7613": {"is_small": true},
"11359": {"is_small": true},
"3b5:11259": {"is_small": true},
"1632": {"is_small": true},
"1633": {"is_small": true},
"1648": {"is_small": true},
"12830": {"is_small": true},
"1982": {"is_small": true},
"1983": {"is_small": true},
"1984": {"is_small": true},
"1985": {"is_small": true},
"1986": {"is_small": true},
"1987": {"is_small": true},
"1988": {"is_small": true},
"2172": {"is_small": true},
"2173": {"is_small": true},
"2176": {"is_small": true},
"2179": {"is_small": true},
"11391": {"is_small": true},
"11392": {"is_small": true},
"11395": {"is_small": true},
"11396": {"is_small": true},
"11440": {"is_small": true},
"3b5:8401": {"is_small": true},
"3b5:8402": {"is_small": true},
"2465": {"is_small": true},
"2474": {"is_small": true},
"3252": {"is_small": true},
"3253": {"is_small": true},
"3263": {"is_small": true},
"3422": {"is_small": true},
"3428": {"is_small": true},
"6730": {"is_small": true},
"3605": {"is_small": true},
"3608": {"is_small": true},
"6763": {"is_small": true},
"3679": {"is_small": true},
"3680": {"is_small": true},
"3681": {"is_small": true},
"3682": {"is_small": true},
"3691": {"is_small": true},
"3692": {"is_small": true},
"3959": {"is_small": true},
"11558": {"is_small": true},
"11559": {"is_small": true},
"3b5:10150": {"is_small": true},
"3b5:10151": {"is_small": true},
"11600": {"is_small": true},
"11604": {"is_small": true},
"3b5:7871": {"is_small": true},
"adf:1948": {"is_small": true},
"adf:75": {"is_small": true},
"adf:77": {"is_small": true},
"adf:76": {"is_small": true},
"adf:1407": {"is_small": true},
"08d:75": {"is_small": true}
}

@ -0,0 +1,147 @@
{
"2474": {
"expected": {
"name": "Goliath",
"front_images": [ "ge/gegol.gif", "ge/gegolb.gif" ],
"back_images": null
},
"updated": {
"front_images": "ge/gegol.gif"
}
},
"1555": {
"expected": {
"name": "2pdr Portee",
"front_images": "br/vehicles/portee.gif",
"back_images": [ "br/vehicles/portee.gif", "br/vehicles/portee0.gif" ]
},
"updated": {
"front_images": [ "br/vehicles/portee.gif", "br/vehicles/portee0.gif" ],
"back_images": null
}
},
"3463": {
"expected": {
"name": "75L AA 75/46",
"front_images": [ "it/gun/itAA7546.gif", "it/gun/itAA7546b.gif" ],
"back_images": [ "it/gun/itAA7546b.gif", "it/gun/itAA7546lb.gif" ]
},
"updated": {
"front_images": "it/gun/itAA7546.gif",
"back_images": "it/gun/itAA7546b.gif"
}
},
"3776": {
"expected": {
"name": "37* INF Skoda IG",
"front_images": [ "ax/gun/buIN37s.gif", "ax/gun/buIN37s2.gif" ],
"back_images": "ax/gun/buIN37sb.gif"
},
"updated": {
"front_images": "ax/gun/buIN37s.gif"
}
},
"3777": {
"expected": {
"name": "70* INF Skoda IG",
"front_images": [ "ax/gun/buIN37s.gif", "ax/gun/buIN37s2.gif" ],
"back_images": "ax/gun/buIN37sb.gif"
},
"updated": {
"front_images": "ax/gun/buIN37s2.gif"
}
},
"6802": {
"expected": {
"name": "20L (4) AA",
"front_images": [ "fi/gun/fi20L4 _2.png", "fi/gun/fi20L4 _2 LIM.png" ],
"back_images": null
},
"updated": {
"front_images": "fi/gun/fi20L4 _2.png"
}
},
"6803": {
"expected": {
"name": "20L VKT (12) AA",
"front_images": [ "fi/gun/fi20L12.png", "fi/gun/fi20L12L.png" ],
"back_images": null
},
"updated": {
"front_images": "fi/gun/fi20L12.png"
}
},
"6804": {
"expected": {
"name": "40L Bofors AA (s)",
"front_images": [ "fi/gun/fi40L.png", "fi/gun/fi40LL.png" ],
"back_images": null
},
"updated": {
"front_images": "fi/gun/fi40L.png"
}
},
"adf:1824": {
"expected": {
"name": "37L AT PTP obr. 30",
"front_images": "ru/gun/ruAT37L.gif",
"back_images": "ru/gun/ruAT37Lb.gif"
},
"updated": {
"front_images": "ru/gun/ru37LPTPobr30.png"
}
},
"adf:1822": {
"expected": {
"name": "37* INF PP obr. 15R",
"front_images": "ru/gun/ruINF37s.gif",
"back_images": "ru/gun/ruINF37sb.gif"
},
"updated": {
"front_images": "ru/gun/ru37PPobr15R.png"
}
},
"adf:1823": {
"expected": {
"name": "76* INF PP obr. 27",
"front_images": "ru/gun/ruINF76s.gif",
"back_images": "ru/gun/ruINF76sb.gif"
},
"updated": {
"front_images": "ru/gun/ru76PPobr27.png"
}
},
"3b5:10093": {
"expected": {
"name": "SL truck",
"front_images": [ "sh/SL3b(KFW).png", "sh/SL4b(KFW).png", "sh/SL5b(KFW).png", "sh/SL6b(KFW).png", "sh/SL1b(KFW).png", "sh/SL2b(KFW).png" ],
"back_images": [ "sh/SL3(KFW).png", "sh/SL4(KFW).png", "sh/SL5(KFW).png", "sh/SL6(KFW).png", "sh/SL1(KFW).png", "sh/SL2(KFW).png" ]
},
"updated": {
"front_images": "us/veh/usSearchlight(KFW).png",
"back_images": null
}
},
"08d:75": {
"expected": {
"name": "RCL 75*",
"front_images": "amrcl75-malf.png",
"back_images": "dm-75rcl.gif"
},
"updated": {
"front_images": "amrcl75.png"
}
}
}

@ -235,9 +235,13 @@ def get_program_info():
return tstamp + timedelta( minutes=tz_offset )
# set the basic details
try:
vassal_version = VassalShim.get_version()
except Exception: #pylint: disable=broad-except
vassal_version = "???"
params = {
"APP_VERSION": vasl_templates.webapp.config.constants.APP_VERSION,
"VASSAL_VERSION": VassalShim.get_version()
"VASSAL_VERSION": vassal_version
}
build_git_info = get_build_git_info()
if build_git_info:

@ -539,7 +539,7 @@ def prepare_asa_upload(): #pylint: disable=too-many-locals
max_size = parse_int( app.config.get( "ASA_MAX_SCREENSHOT_SIZE" ), 200 ) * 1024
if len(screenshot_data) > max_size:
ratio = math.sqrt( float(max_size) / len(screenshot_data) )
img = img.resize( ( int(img.width * ratio), int(img.height * ratio) ), Image.ANTIALIAS )
img = img.resize( ( int(img.width * ratio), int(img.height * ratio) ), Image.LANCZOS )
# add a border
border_size = parse_int( app.config.get( "ASA_SCREENSHOT_BORDER_SIZE" ), 5 )
img = ImageOps.expand( img, border_size, (255,255,255,255) )

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

@ -54,8 +54,8 @@ If you have Docker installed, the webapp can be run in a container e.g.
<div class="code">
./run-container.sh \
--port 5010 \
--vassal ~/vassal-3.6.14/ \
--vasl ~/vasl/vasl-6.6.6.vmod \
--vassal ~/vassal-3.7.9/ \
--vasl ~/vasl/vasl-6.6.8.vmod \
--vasl-extensions ~/vasl/extensions/ \
--boards ~/vasl/boards/ \
--chapter-h ~/vasl/chapter-h/ \
@ -73,7 +73,7 @@ If you have Docker installed, the webapp can be run in a container e.g.
<p> You can also run the program directly from the source code. Get a copy from <a href="https://code.pacman-ghost.com/public/vasl-templates">here</a> in the usual way, by <tt>git clone</tt>'ing it, or downloading a ZIP and unpacking it somewhere.
<p> The web server was written and tested using Python 3.10.7, but it doesn't do anything particularly funky, so any recent version of Python <em>should</em> work.
<p> The web server was written and tested using Python 3.11.4, but it doesn't do anything particularly funky, so any recent version of Python <em>should</em> work.
<p> While not essential, it is <em>strongly</em> recommended that you set up a <a href="https://virtualenv.pypa.io/en/stable/">virtual environment</a> first. Then <tt>cd</tt> into the project's root directory, and install the requirements (note the trailing period):
<div class="code">
@ -101,6 +101,18 @@ pip install --editable .[gui]
<p> Then, run the <tt>vasl-templates</tt> command.
<a name="asl-rulebook2"></a>
<h2> Integrating with <tt>asl-rulebook2</tt></h2>
<p> <img src="images/asl-rulebook2.png" class="preview" style="float:right;">
If you are using <a href="https://code.pacman-ghost.com/public/asl-rulebook2"><tt>asl-rulebook2</tt></a>, you can connect it to this program, and vehicle/ordnance entries will show an icon that you can click to open their Chapter H entries.
<p> In your <tt>site.cfg</tt>, configure the base URL of the <tt>asl-rulebook2</tt> application e.g.
<div class="code">
[Site Config]
ASL_RULEBOOK2_BASE_URL = http://localhost:5020
</div>
<a name="install-webdriver"></a>
<h2>Installing a webdriver</h2>
@ -154,11 +166,11 @@ Configuring the program
<p> Choose <em>Settings</em> from the <em>File</em> menu and configure the highlighted settings. As a guide, here are some example settings:
<table class="settings">
<tr> <td class="key"> VASSAL&nbsp;installation: </td> <td class="val"> <nobr>C:\Program Files\VASSAL-3.6.14\</nobr>
<tr> <td class="key"> VASL&nbsp;module: </td> <td class="val"> <nobr>C:\bin\vasl\vasl-6.6.6.vmod</nobr>
<tr> <td class="key"> VASSAL&nbsp;installation: </td> <td class="val"> <nobr>C:\Program Files\VASSAL-3.7.9\</nobr>
<tr> <td class="key"> VASL&nbsp;module: </td> <td class="val"> <nobr>C:\bin\vasl\vasl-6.6.8.vmod</nobr>
<tr> <td class="key"> VASL&nbsp;extensions: </td> <td class="val"> <nobr>C:\bin\vasl\extensions\</nobr>
<tr> <td class="key"> VASL&nbsp;boards: </td> <td class="val"> <nobr>C:\bin\vasl\boards\</nobr>
<tr> <td class="key" valign="top"> Java: </td> <td class="val"> <nobr>C:\Program Files\VASSAL-3.6.14\jre\bin\java.exe</nobr>
<tr> <td class="key" valign="top"> Java: </td> <td class="val"> <nobr>C:\Program Files\VASSAL-3.7.9\jre\bin\java.exe</nobr>
<div class="hint" style="margin:0.5em 1em;"> Leave this field blank to use the Java that comes with VASSAL (Windows only), or on your PATH. </div>
<tr> <td class="key"> Web&nbsp;driver: </td> <td class="val"> <nobr>C:\bin\geckodriver.exe</nobr>
</table>
@ -548,6 +560,13 @@ When you're writing a new template file, it would be painful to have to ZIP up a
<a name="dev-setup"></a>
<h2 style="margin-top:0;"> Setting up </h2>
<div class="info" style="float:right;max-width:40%;margin-left:1em;">
If you want to run the GUI, you will also need to install the GUI requirements:
<div class="code">
pip install --editable .[gui]
</div>
</div>
<p> After cloning the repo, install the <em>developer</em> requirements:
<div class="code">
pip install --editable .[dev]

@ -77,8 +77,6 @@ $(document).ready( function () {
program_info: { label: "Program info", icon: imagesDir+"/info.png", action: show_program_info },
show_help: { label: "Help", icon: imagesDir+"/help.png", action: show_help },
} ;
if ( getUrlParam( "pyqt" ) )
delete menuItems.program_info ;
$menu.popmenu( menuItems ) ;
// nb: we only show the popmenu on left click (not the normal right-click)
$menu.off( "contextmenu" ) ;

@ -141,34 +141,66 @@
<script src="{{url_for('static',filename='trumbowyg/plugins/table/trumbowyg.table.min.js')}}"></script>
<script src="{{url_for('static',filename='DOMPurify/purify.min.js')}}"></script>
<script src="{{url_for('static',filename='utils.js')}}"></script>
<script>
function removeUrlTestParams( url )
{
// remove any URL parameters
// NOTE: We can't use URL.searchParams since we might be passed a relative URL.
// FUDGE! The test suite is giving us absolute, encoded URL's :-/
url = strReplaceAll( url, "&amp;", "&" ) ;
var match = url.match( new RegExp( "^http://.+?:\\d+?/" ) ) ;
if ( match )
url = url.substring( match[0].length - 1 ) ;
var pos = url.indexOf( "?" ) ;
if ( pos >= 0 ) {
params = url.substring( pos+1 ).split( "&" ) ;
url = url.substring( 0, pos ) ;
var nKeeps = 0 ;
params.forEach( function( param ) {
var keep = true ;
[ "store_msgs", "disable_close_window_check", "store_clipboard" ].forEach( function( key ) {
if ( param.substring( 0, key.length+1 ) === key + "=" )
keep = false ;
} ) ;
if ( keep ) {
url += ( nKeeps === 0 ) ? "?" : "&" ;
url += param ;
nKeeps += 1 ;
}
} ) ;
}
return url ;
}
gAppName = "{{APP_NAME}}" ;
gAppVersion = "{{APP_VERSION}}" ;
gImagesBaseUrl = "{{url_for('static',filename='images')}}" ;
gAppConfigUrl = "{{url_for('get_app_config')}}" ;
gGetStartupMsgsUrl = "{{url_for('get_startup_msgs')}}" ;
gGetTemplatePackUrl = "{{url_for('get_template_pack')}}" ;
gGetAslRulebook2VoNoteTargetsUrl = "{{url_for('get_asl_rulebook2_vo_note_targets')}}" ;
gShowAslRulebook2VoNoteUrl = "{{url_for('show_asl_rulebook2_target',target='TARGET')}}" ;
gGetDefaultScenarioUrl = "{{url_for('get_default_scenario')}}" ;
gVehicleListingsUrl = "{{url_for('get_vehicle_listings',merge_common=1)}}" ;
gOrdnanceListingsUrl = "{{url_for('get_ordnance_listings',merge_common=1)}}" ;
gVehicleNotesUrl = "{{url_for('get_vehicle_notes')}}" ;
gOrdnanceNotesUrl = "{{url_for('get_ordnance_notes')}}" ;
gGetVaslPieceInfoUrl = "{{url_for('get_vasl_piece_info')}}" ;
gGetOnlineCounterImagesUrl = "{{url_for('get_online_counter_images')}}" ;
gGetScenarioIndexUrl = "{{url_for('get_scenario_index')}}" ;
gGetScenarioUrl = "{{url_for('get_scenario',scenario_id='ID')}}" ;
gGetScenarioCardUrl = "{{url_for('get_scenario_card',scenario_id='ID')}}" ;
gGetRoarScenarioIndexUrl = "{{url_for('get_roar_scenario_index')}}" ;
gAnalyzeVsavUrl = "{{url_for('analyze_vsav')}}" ;
gAnalyzeVlogsUrl = "{{url_for('analyze_vlogs')}}" ;
gUpdateVsavUrl = "{{url_for('update_vsav')}}" ;
gPrepareAsaUploadUrl = "{{url_for('prepare_asa_upload')}}" ;
gOnSuccessfulAsaUploadUrl = "{{url_for('on_successful_asa_upload',scenario_id='ID')}}" ;
gMakeSnippetImageUrl = "{{url_for('make_snippet_image')}}" ;
gGetProgramInfoUrl = "{{url_for('get_program_info')}}" ;
gHelpUrl = "{{url_for('show_help')}}" ;
gImagesBaseUrl = removeUrlTestParams( "{{url_for('static',filename='images')}}" ) ;
gAppConfigUrl = removeUrlTestParams( "{{url_for('get_app_config')}}" ) ;
gGetStartupMsgsUrl = removeUrlTestParams( "{{url_for('get_startup_msgs')}}" ) ;
gGetTemplatePackUrl = removeUrlTestParams( "{{url_for('get_template_pack')}}" ) ;
gGetAslRulebook2VoNoteTargetsUrl = removeUrlTestParams( "{{url_for('get_asl_rulebook2_vo_note_targets')}}" ) ;
gShowAslRulebook2VoNoteUrl = removeUrlTestParams( "{{url_for('show_asl_rulebook2_target',target='TARGET')}}" ) ;
gGetDefaultScenarioUrl = removeUrlTestParams( "{{url_for('get_default_scenario')}}" ) ;
gVehicleListingsUrl = removeUrlTestParams( "{{url_for('get_vehicle_listings',merge_common=1)}}" ) ;
gOrdnanceListingsUrl = removeUrlTestParams( "{{url_for('get_ordnance_listings',merge_common=1)}}" ) ;
gVehicleNotesUrl = removeUrlTestParams( "{{url_for('get_vehicle_notes')}}" ) ;
gOrdnanceNotesUrl = removeUrlTestParams( "{{url_for('get_ordnance_notes')}}" ) ;
gGetVaslPieceInfoUrl = removeUrlTestParams( "{{url_for('get_vasl_piece_info')}}" ) ;
gGetOnlineCounterImagesUrl = removeUrlTestParams( "{{url_for('get_online_counter_images')}}" ) ;
gGetScenarioIndexUrl = removeUrlTestParams( "{{url_for('get_scenario_index')}}" ) ;
gGetScenarioUrl = removeUrlTestParams( "{{url_for('get_scenario',scenario_id='ID')}}" ) ;
gGetScenarioCardUrl = removeUrlTestParams( "{{url_for('get_scenario_card',scenario_id='ID')}}" ) ;
gGetRoarScenarioIndexUrl = removeUrlTestParams( "{{url_for('get_roar_scenario_index')}}" ) ;
gAnalyzeVsavUrl = removeUrlTestParams( "{{url_for('analyze_vsav')}}" ) ;
gAnalyzeVlogsUrl = removeUrlTestParams( "{{url_for('analyze_vlogs')}}" ) ;
gUpdateVsavUrl = removeUrlTestParams( "{{url_for('update_vsav')}}" ) ;
gPrepareAsaUploadUrl = removeUrlTestParams( "{{url_for('prepare_asa_upload')}}" ) ;
gOnSuccessfulAsaUploadUrl = removeUrlTestParams( "{{url_for('on_successful_asa_upload',scenario_id='ID')}}" ) ;
gMakeSnippetImageUrl = removeUrlTestParams( "{{url_for('make_snippet_image')}}" ) ;
gGetProgramInfoUrl = removeUrlTestParams( "{{url_for('get_program_info')}}" ) ;
gHelpUrl = removeUrlTestParams( "{{url_for('show_help')}}" ) ;
</script>
<script src="{{url_for('static',filename='main.js')}}"></script>
@ -190,7 +222,6 @@ gHelpUrl = "{{url_for('show_help')}}" ;
<script src="{{url_for('static',filename='us