Added support for HTML vehicle/ordnance notes.

master
Pacman Ghost 5 years ago
parent 3690cc06cb
commit ec3c9ca2f1
  1. 4
      vasl_templates/webapp/__init__.py
  2. 16
      vasl_templates/webapp/data/default-template-pack/ma_note.css
  3. 2
      vasl_templates/webapp/data/default-template-pack/mol-p.j2
  4. 2
      vasl_templates/webapp/data/default-template-pack/mol.j2
  5. 7
      vasl_templates/webapp/data/default-template-pack/ob_ordnance.j2
  6. 9
      vasl_templates/webapp/data/default-template-pack/ob_ordnance_ma_notes.j2
  7. 3
      vasl_templates/webapp/data/default-template-pack/ob_ordnance_note.j2
  8. 3
      vasl_templates/webapp/data/default-template-pack/ob_vehicle_note.j2
  9. 7
      vasl_templates/webapp/data/default-template-pack/ob_vehicles.j2
  10. 9
      vasl_templates/webapp/data/default-template-pack/ob_vehicles_ma_notes.j2
  11. 3
      vasl_templates/webapp/data/default-template-pack/ssr.j2
  12. 5
      vasl_templates/webapp/data/default-template-pack/vo.css
  13. 21
      vasl_templates/webapp/data/default-template-pack/vo_note.css
  14. 4
      vasl_templates/webapp/data/vehicles/russian.lend-lease.json
  15. 6
      vasl_templates/webapp/files.py
  16. 1
      vasl_templates/webapp/globvars.py
  17. 32
      vasl_templates/webapp/snippets.py
  18. BIN
      vasl_templates/webapp/static/images/bullet.png
  19. 68
      vasl_templates/webapp/static/snippets.js
  20. 3
      vasl_templates/webapp/static/user_settings.js
  21. 24
      vasl_templates/webapp/static/utils.js
  22. 10
      vasl_templates/webapp/static/vo.js
  23. 4
      vasl_templates/webapp/templates/user-settings-dialog.html
  24. 5
      vasl_templates/webapp/templates/vo-notes-report.html
  25. 5
      vasl_templates/webapp/tests/fixtures/data/default-template-pack/nationalities.json
  26. 2
      vasl_templates/webapp/tests/fixtures/data/default-template-pack/ob_ordnance_note.j2
  27. 2
      vasl_templates/webapp/tests/fixtures/data/default-template-pack/ob_vehicle_note.j2
  28. 16
      vasl_templates/webapp/tests/fixtures/data/vehicles/allied-minor/greek.json
  29. BIN
      vasl_templates/webapp/tests/fixtures/vo-notes/allied-minor/vehicles/201.png
  30. 1
      vasl_templates/webapp/tests/fixtures/vo-notes/allied-minor/vehicles/202.html
  31. 1
      vasl_templates/webapp/tests/fixtures/vo-notes/allied-minor/vehicles/203.html
  32. BIN
      vasl_templates/webapp/tests/fixtures/vo-notes/allied-minor/vehicles/203.png
  33. 4
      vasl_templates/webapp/tests/fixtures/vo-reports/vehicles/russian/1940.txt
  34. 4
      vasl_templates/webapp/tests/fixtures/vo-reports/vehicles/russian/1941.txt
  35. 4
      vasl_templates/webapp/tests/fixtures/vo-reports/vehicles/russian/1942.txt
  36. 4
      vasl_templates/webapp/tests/fixtures/vo-reports/vehicles/russian/1943.txt
  37. 4
      vasl_templates/webapp/tests/fixtures/vo-reports/vehicles/russian/1944.txt
  38. 4
      vasl_templates/webapp/tests/fixtures/vo-reports/vehicles/russian/1945.txt
  39. 8
      vasl_templates/webapp/tests/remote.py
  40. 55
      vasl_templates/webapp/tests/test_user_settings.py
  41. 14
      vasl_templates/webapp/tests/test_vasl_extensions.py
  42. 4
      vasl_templates/webapp/tests/test_vassal.py
  43. 34
      vasl_templates/webapp/tests/test_vo_notes.py
  44. 7
      vasl_templates/webapp/tests/utils.py
  45. 175
      vasl_templates/webapp/vo_notes.py
  46. 22
      vasl_templates/webapp/webdriver.py

@ -17,6 +17,10 @@ from vasl_templates.webapp.config.constants import BASE_DIR
def _on_startup(): def _on_startup():
"""Do startup initialization.""" """Do startup initialization."""
# load the default template_pack
from vasl_templates.webapp.snippets import load_default_template_pack
load_default_template_pack()
# configure the VASL module # configure the VASL module
fname = app.config.get( "VASL_MOD" ) fname = app.config.get( "VASL_MOD" )
if fname: if fname:

@ -0,0 +1,16 @@
.ma-note { text-align: justify ; }
.ma-note .key { font-weight: bold ; }
.ma-note table { margin-left: 10px ; }
.ma-note td { padding: 0 ; }
.ma-note ul { padding-left: 10px ; list-style-image: url("{{IMAGES_BASE_URL}}/bullet.png") ; }
.ma-note li { margin-bottom: 2px ; }
.ma-note .example { font-size: 90% ; font-style: italic ; }
.ma-note table { margin-left: 10px ; margin-top: -5px ; }
.ma-note table th { padding: 2px 10px 2px 5px ; text-align: left ; background: #f0f0f0 ; }
.ma-note table td { padding: 0 10px 0 5px ; }
.extra-notes-caption { border: 1px solid #e0e0e0 ; background: #fcfcfc ; font-weight: bold ; padding: 2px 5px ; }
.slashed { text-decoration: line-through ; }
ul { margin: 0 0 0 15px ; padding: 0 ; }
sup { font-size: 75% ; }

@ -6,7 +6,7 @@
td { margin: 0 ; padding: 0 ; } td { margin: 0 ; padding: 0 ; }
td.c { text-align: center ; } td.c { text-align: center ; }
td.r { text-align: right ; } td.r { text-align: right ; }
ul { margin: 0 0 0 10px ; padding: 0 ; } ul { margin: 0 ; padding-left: 10px ; list-style-image: url("{{IMAGES_BASE_URL}}/bullet.png") ; }
</style> </style>
</head> </head>

@ -4,7 +4,7 @@
<meta charset="utf-8"> <meta charset="utf-8">
<style> <style>
td { margin: 0 ; padding: 0 ; } td { margin: 0 ; padding: 0 ; }
ul { margin: 0 0 0 10px ; padding: 0 ; } ul { margin: 0 ; padding-left: 10px ; list-style-image: url("{{IMAGES_BASE_URL}}/bullet.png") ; }
</style> </style>
</head> </head>

@ -2,12 +2,7 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<style> <style> {{VO_CSS}} </style>
td { margin: 0 ; padding: 0 ; }
.note { margin-top: 2px ; font-size: 90% ; font-style: italic ; color: #808080 ; }
.comment { font-size: 90% ; font-style: italic ; color: #404040 ; }
sup { font-size: 75% ; }
</style>
</head> </head>
<table style=" <table style="

@ -2,14 +2,7 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<style> <style> {{MA_NOTE_CSS}} </style>
.ma-note .key { font-weight: bold ; }
.ma-note table { margin-left: 10px ; }
.ma-note td { padding: 0 ; }
.extra-notes-caption { border: 1px solid #e0e0e0 ; background: #fcfcfc ; font-weight: bold ; padding: 2px 5px ; }
ul { margin: 0 0 0 15px ; padding: 0 ; }
sup { font-size: 75% ; }
</style>
</head> </head>
<table style=" <table style="

@ -2,6 +2,7 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<style> {{VO_NOTE_CSS}} </style>
</head> </head>
<table> <table>
@ -16,7 +17,7 @@
{%if PLAYER_FLAG%}<img src="{{PLAYER_FLAG}}">&nbsp;{%endif%}{{ORDNANCE_NAME}} {%if PLAYER_FLAG%}<img src="{{PLAYER_FLAG}}">&nbsp;{%endif%}{{ORDNANCE_NAME}}
<tr> <tr>
<td> <img src="{{ORDNANCE_NOTE_URL}}"> <td> {{ORDNANCE_NOTE_HTML}}
</table> </table>

@ -2,6 +2,7 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<style> {{VO_NOTE_CSS}} </style>
</head> </head>
<table> <table>
@ -16,7 +17,7 @@
{%if PLAYER_FLAG%}<img src="{{PLAYER_FLAG}}">&nbsp;{%endif%}{{VEHICLE_NAME}} {%if PLAYER_FLAG%}<img src="{{PLAYER_FLAG}}">&nbsp;{%endif%}{{VEHICLE_NAME}}
<tr> <tr>
<td> <img src="{{VEHICLE_NOTE_URL}}"> <td> {{VEHICLE_NOTE_HTML}}
</table> </table>

@ -2,12 +2,7 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<style> <style> {{VO_CSS}} </style>
td { margin: 0 ; padding: 0 ; }
.note { margin-top: 2px ; font-size: 90% ; font-style: italic ; color: #808080 ; }
.comment { font-size: 90% ; font-style: italic ; color: #404040 ; }
sup { font-size: 75% ; }
</style>
</head> </head>
<table style=" <table style="

@ -2,14 +2,7 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<style> <style> {{MA_NOTE_CSS}} </style>
.ma-note .key { font-weight: bold ; }
.ma-note table { margin-left: 10px ; }
.ma-note td { padding: 0 ; }
.extra-notes-caption { border: 1px solid #e0e0e0 ; background: #fcfcfc ; font-weight: bold ; padding: 2px 5px ; }
ul { margin: 0 0 0 15px ; padding: 0 ; }
sup { font-size: 75% ; }
</style>
</head> </head>
<table style=" <table style="

@ -2,6 +2,9 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<style>
ul { margin: 0 ; padding-left: 0 ; list-style-image: url("{{IMAGES_BASE_URL}}/bullet.png") ; }
</style>
</head> </head>
<table style=" <table style="

@ -0,0 +1,5 @@
td { margin: 0 ; padding: 0 ; }
sup { font-size: 75% ; }
.note { margin-top: 2px ; font-size: 90% ; font-style: italic ; color: #808080 ; }
.comment { font-size: 90% ; font-style: italic ; color: #404040 ; }

@ -0,0 +1,21 @@
img.piece { float: left ; margin-right: 0.5em ; }
.header { margin-bottom: 0.25em ; }
.header .note-number { font-weight: bold ; }
.header .name { font-weight: bold ; font-style: italic ; }
.content { text-align: justify ; }
.content p { margin-top: 5px ; }
.content ul { margin-left: 0 ; padding-left: 25px ; list-style-image: url("{{IMAGES_BASE_URL}}/bullet.png") ; }
.content li { margin-bottom: 2px ; }
.content .example { font-size: 90% ; font-style: italic ; }
.content .rf { font-style: italic ; color: #444 ; }
.content .lfloat { float: left ; margin-right: 0.5em ; }
.content .rfloat { float: right ; margin-left: 0.5em ; }
.content table { margin: 0 10px 0 10px ; margin-top: -0.5em ; }
.content table th { padding: 2px 10px 2px 5px ; text-align: left ; background: #f0f0f0 ; }
.content table td { padding: 0 10px 0 5px ; }
table.layout td { padding: 0 5px 0 5px ; }
.content .rf { display: none ; }

@ -59,14 +59,14 @@
{ "id": "ru/v:079", { "id": "ru/v:079",
"copy_from": "br/v:042", "copy_from": "br/v:042",
"name": "Valentine V(b)", "name": "Valentine V(b)",
"note_number": "52\u2020", "note_number": "52.1\u2020",
"extra_notes": [ "LL" ], "extra_notes": [ "LL" ],
"gpid": [ 726, 728, 7432, 7434 ] "gpid": [ 726, 728, 7432, 7434 ]
}, },
{ "id": "ru/v:080", { "id": "ru/v:080",
"copy_from": "br/v:043", "copy_from": "br/v:043",
"name": "Valentine VIII(b)", "name": "Valentine VIII(b)",
"note_number": "52\u2020", "note_number": "52.2\u2020",
"extra_notes": [ "LL" ], "extra_notes": [ "LL" ],
"gpid": [ 730, 7111 ] "gpid": [ 730, 7111 ]
}, },

@ -43,8 +43,10 @@ class FileServer:
return send_file( buf, mimetype=mime_type ) return send_file( buf, mimetype=mime_type )
else: else:
path = path.replace( "\\", "/" ) # nb: for Windows :-/ path = path.replace( "\\", "/" ) # nb: for Windows :-/
if ignore_empty and is_empty_file( os.path.join( self.base_dir, path ) ): if ignore_empty:
return None fname = os.path.join( self.base_dir, path )
if os.path.isfile( fname ) and is_empty_file( fname ):
return None
return send_from_directory( self.base_dir, path ) return send_from_directory( self.base_dir, path )
@staticmethod @staticmethod

@ -3,6 +3,7 @@
from vasl_templates.webapp import app from vasl_templates.webapp import app
from vasl_templates.webapp.config.constants import APP_NAME, APP_VERSION from vasl_templates.webapp.config.constants import APP_NAME, APP_VERSION
template_pack = None
vasl_mod = None vasl_mod = None
vo_listings = None vo_listings = None
vo_notes = None vo_notes = None

@ -11,7 +11,7 @@ import threading
from flask import request, jsonify, send_file, abort from flask import request, jsonify, send_file, abort
from PIL import Image from PIL import Image
from vasl_templates.webapp import app from vasl_templates.webapp import app, globvars
from vasl_templates.webapp.utils import SimpleError from vasl_templates.webapp.utils import SimpleError
from vasl_templates.webapp.config.constants import DATA_DIR from vasl_templates.webapp.config.constants import DATA_DIR
from vasl_templates.webapp.webdriver import WebDriver from vasl_templates.webapp.webdriver import WebDriver
@ -29,6 +29,12 @@ def get_template_pack():
If, in the future, we support loading other template packs from the backend, If, in the future, we support loading other template packs from the backend,
we can add a parameter here to specify which one to return. we can add a parameter here to specify which one to return.
""" """
if not globvars.template_pack:
load_default_template_pack()
return jsonify( globvars.template_pack )
def load_default_template_pack():
"""Load the default template pack."""
# initialize # initialize
# NOTE: We always start with the default nationalities data. Unlike template files, # NOTE: We always start with the default nationalities data. Unlike template files,
@ -46,7 +52,7 @@ def get_template_pack():
# can add to them, or modify existing ones, but not remove them. # can add to them, or modify existing ones, but not remove them.
dname = os.path.join( base_dir, "extras" ) dname = os.path.join( base_dir, "extras" )
if os.path.isdir( dname ): if os.path.isdir( dname ):
_, extra_templates = _do_get_template_pack( dname ) _, extra_templates, _ = _do_get_template_pack( dname )
for key,val in extra_templates.items(): for key,val in extra_templates.items():
data["templates"]["extras/"+key] = val data["templates"]["extras/"+key] = val
@ -61,13 +67,14 @@ def get_template_pack():
# check if we're loading the template pack from a directory # check if we're loading the template pack from a directory
if os.path.isdir( dname ): if os.path.isdir( dname ):
# yup - return the files in it # yup - return the files in it
nat, templates =_do_get_template_pack( dname ) nat, templates, css =_do_get_template_pack( dname )
data["nationalities"].update( nat ) data["nationalities"].update( nat )
data["templates"] = templates data["templates"] = templates
data["css"] = css
else: else:
# extract the template pack files from the specified ZIP file # extract the template pack files from the specified ZIP file
if not os.path.isfile( dname ): if not os.path.isfile( dname ):
return jsonify( { "error": "Can't find template pack: {}".format(dname) } ) raise RuntimeError( "Can't find template pack: {}".format( dname ) )
data["templates"] = {} data["templates"] = {}
with zipfile.ZipFile( dname, "r" ) as zip_file: with zipfile.ZipFile( dname, "r" ) as zip_file:
for fname in zip_file.namelist(): for fname in zip_file.namelist():
@ -82,7 +89,7 @@ def get_template_pack():
fname2 = "extras/" + fname2 fname2 = "extras/" + fname2
data["templates"][ fname2 ] = fdata data["templates"][ fname2 ] = fdata
return jsonify( data ) globvars.template_pack = data
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
@ -91,23 +98,24 @@ def _do_get_template_pack( dname ):
dname = os.path.abspath( dname ) dname = os.path.abspath( dname )
if not os.path.isdir( dname ): if not os.path.isdir( dname ):
abort( 404 ) abort( 404 )
nationalities, templates = {}, {} nationalities, templates, css = {}, {}, {}
for root,_,fnames in os.walk(dname): for root,_,fnames in os.walk(dname):
for fname in fnames: for fname in fnames:
# add the next file to the results # add the next file to the results
words = os.path.splitext( fname ) fname_stem, extn = os.path.splitext( fname )
fname = os.path.join( root, fname ) fname = os.path.join( root, fname )
with open( fname, "r" ) as fp: with open( fname, "r" ) as fp:
if fname.lower() == "nationalities.json": if fname.lower() == "nationalities.json":
nationalities = json.load( fp ) nationalities = json.load( fp )
continue continue
if words[1] == ".j2": if extn == ".j2":
fname2 = words[0]
relpath = os.path.relpath( os.path.abspath(fname), dname ) relpath = os.path.relpath( os.path.abspath(fname), dname )
if relpath.startswith( "extras" + os.sep ): if relpath.startswith( "extras" + os.sep ):
fname2 = "extras/" + fname2 fname_stem = "extras/" + fname_stem
templates[fname2] = fp.read() templates[fname_stem] = fp.read()
return nationalities, templates elif extn == ".css":
css[fname_stem] = fp.read().strip()
return nationalities, templates, css
# --------------------------------------------------------------------- # ---------------------------------------------------------------------

Binary file not shown.

After

Width:  |  Height:  |  Size: 168 B

@ -123,6 +123,9 @@ function make_snippet( $btn, params, extra_params, show_date_warnings )
var template_id = $btn.data( "id" ) ; var template_id = $btn.data( "id" ) ;
var snippet_save_name = null ; var snippet_save_name = null ;
// add simple parameters
params.IMAGES_BASE_URL = APP_URL_BASE + gImagesBaseUrl ;
// set player-specific parameters // set player-specific parameters
var player_no = get_player_no_for_element( $btn ) ; var player_no = get_player_no_for_element( $btn ) ;
if ( player_no ) { if ( player_no ) {
@ -174,7 +177,19 @@ function make_snippet( $btn, params, extra_params, show_date_warnings )
var data = $btn.parent().parent().data( "sortable2-data" ) ; var data = $btn.parent().parent().data( "sortable2-data" ) ;
var key = (vo_type === "vehicles") ? "VEHICLE" : "ORDNANCE" ; var key = (vo_type === "vehicles") ? "VEHICLE" : "ORDNANCE" ;
params[ key + "_NAME" ] = data.vo_entry.name ; params[ key + "_NAME" ] = data.vo_entry.name ;
params[ key + "_NOTE_URL" ] = data.vo_note_url ; if ( data.vo_note.substr( 0, 7 ) === "http://" )
params[ key + "_NOTE_HTML" ] = '<img src="' + data.vo_note + '">' ;
else {
if ( gUserSettings["vo-notes-as-images"] ) {
// show the vehicle/ordnance note as an image
var nat = params[ "PLAYER_" + player_no ] ;
var url = APP_URL_BASE + "/" + vo_type + "/" + nat + "/note/" + get_vo_note_key(data.vo_entry) ;
params[ key + "_NOTE_HTML" ] = '<img src="' + url + '">' ;
} else {
// insert the raw HTML into the snippet
params[ key + "_NOTE_HTML" ] = data.vo_note ;
}
}
snippet_save_name = data.vo_entry.name ; snippet_save_name = data.vo_entry.name ;
} }
if ( template_id === "ob_vehicle_note" ) if ( template_id === "ob_vehicle_note" )
@ -182,6 +197,18 @@ function make_snippet( $btn, params, extra_params, show_date_warnings )
else if ( template_id === "ob_ordnance_note" ) else if ( template_id === "ob_ordnance_note" )
set_vo_note( "ordnance" ) ; set_vo_note( "ordnance" ) ;
// install the CSS
function install_css( key ) {
if ( gTemplatePack.css[ key ] ) {
params[ key.toUpperCase() + "_CSS" ] = strReplaceAll(
gTemplatePack.css[key], "{{IMAGES_BASE_URL}}", params.IMAGES_BASE_URL
) ;
}
}
install_css( "vo" ) ;
install_css( "vo_note" ) ;
install_css( "ma_note" ) ;
// generate snippets for multi-applicable vehicle/ordnance notes // generate snippets for multi-applicable vehicle/ordnance notes
var pos ; var pos ;
function add_ma_notes( ma_notes, keys, param_name, nat, vo_type ) { function add_ma_notes( ma_notes, keys, param_name, nat, vo_type ) {
@ -335,7 +362,6 @@ function make_snippet( $btn, params, extra_params, show_date_warnings )
// add in any extra parameters // add in any extra parameters
if ( extra_params ) if ( extra_params )
$.extend( true, params, extra_params ) ; $.extend( true, params, extra_params ) ;
params.IMAGES_BASE_URL = APP_URL_BASE + gImagesBaseUrl ;
// check that the players have different nationalities // check that the players have different nationalities
if ( params.PLAYER_1 === params.PLAYER_2 ) if ( params.PLAYER_1 === params.PLAYER_2 )
@ -376,7 +402,8 @@ function make_snippet( $btn, params, extra_params, show_date_warnings )
} }
// fixup any user file URL's // fixup any user file URL's
snippet = snippet.replace( "{{USER_FILES}}", APP_URL_BASE + "/user" ) ; snippet = strReplaceAll( snippet, "{{USER_FILES}}", APP_URL_BASE+"/user" ) ;
snippet = strReplaceAll( snippet, "{{CHAPTER_H}}", APP_URL_BASE+"/chapter-h" ) ;
return { return {
content: snippet, content: snippet,
@ -401,7 +428,7 @@ function get_vo_note_key( vo_entry )
return key ; return key ;
} }
function make_vo_note_key_url( vo_type, nat, key ) function get_vo_note( vo_type, nat, key )
{ {
if ( ! key ) if ( ! key )
return null ; return null ;
@ -421,8 +448,13 @@ function make_vo_note_key_url( vo_type, nat, key )
if ( !( key in gVehicleOrdnanceNotes[ vo_type ][ nat ] ) ) if ( !( key in gVehicleOrdnanceNotes[ vo_type ][ nat ] ) )
return null ; return null ;
// generate the URL var vo_note = gVehicleOrdnanceNotes[ vo_type ][ nat ][ key ] ;
return APP_URL_BASE + "/" + vo_type + "/" + nat + "/note/" + key ; // FUDGE! We need to detect between a full HTML note and an image-based one.
// This is not great, but it'll do... :-/
if ( vo_note.substr( 0, nat.length+1 ) === nat+"/" )
return APP_URL_BASE + "/" + vo_type + "/" + nat + "/note/" + key ;
else
return vo_note ;
} }
function get_ma_notes_keys( nat, vo_entries, vo_type ) function get_ma_notes_keys( nat, vo_entries, vo_type )
@ -1625,7 +1657,7 @@ function on_template_pack()
var pos = data.indexOf( "|" ) ; var pos = data.indexOf( "|" ) ;
var fname = data.substring( 0, pos ).trim() ; var fname = data.substring( 0, pos ).trim() ;
data = data.substring( pos+1 ).trim() ; data = data.substring( pos+1 ).trim() ;
if ( fname.substring(fname.length-4) === ".zip" ) if ( getFilenameExtn( fname ) === ".zip" )
data = atob( data ) ; data = atob( data ) ;
do_load_template_pack( fname, data ) ; do_load_template_pack( fname, data ) ;
return ; return ;
@ -1664,6 +1696,7 @@ function do_load_template_pack( fname, data )
var template_pack = { var template_pack = {
nationalities: $.extend( true, {}, gDefaultTemplatePack.nationalities ), nationalities: $.extend( true, {}, gDefaultTemplatePack.nationalities ),
templates: {}, templates: {},
css: {},
} ; } ;
// NOTE: We always start with the default extras templates; user-defined template packs // NOTE: We always start with the default extras templates; user-defined template packs
@ -1687,17 +1720,22 @@ function do_load_template_pack( fname, data )
$.extend( true, template_pack.nationalities, nationalities ) ; $.extend( true, template_pack.nationalities, nationalities ) ;
return ; return ;
} }
if ( fname.substring(fname.length-3) != ".j2" ) { var extn = getFilenameExtn( fname ) ;
if ( [".j2",".css"].indexOf( extn ) === -1 ) {
invalid_filename_extns.push( fname ) ; invalid_filename_extns.push( fname ) ;
return ; return ;
} }
var template_id = fname.substring( 0, fname.length-3 ).toLowerCase() ;
if ( gValidTemplateIds.indexOf( template_id ) === -1 && template_id.substr(0,7) !== "extras/" ) {
unknown_template_ids.push( fname ) ;
return ;
}
// save the template pack file // save the template pack file
template_pack.templates[template_id] = data ; var template_id = fname.substring( 0, fname.length-extn.length ).toLowerCase() ;
if ( extn === ".css" )
template_pack.css[template_id] = data ;
else {
if ( gValidTemplateIds.indexOf( template_id ) === -1 && template_id.substr(0,7) !== "extras/" ) {
unknown_template_ids.push( fname ) ;
return ;
}
template_pack.templates[template_id] = data ;
}
} }
// initialize // initialize
@ -1746,7 +1784,7 @@ function do_load_template_pack( fname, data )
// check if we have a ZIP file // check if we have a ZIP file
fname = fname.toLowerCase() ; fname = fname.toLowerCase() ;
if ( fname.substring(fname.length-4) === ".zip" ) { if ( getFilenameExtn( fname ) === ".zip" ) {
// yup - process each file in the ZIP // yup - process each file in the ZIP
var nFiles = 0 ; var nFiles = 0 ;
JSZip.loadAsync( data ).then( function( zip ) { JSZip.loadAsync( data ).then( function( zip ) {

@ -5,6 +5,7 @@ USER_SETTINGS = {
"hide-unavailable-ma-notes": "checkbox", "hide-unavailable-ma-notes": "checkbox",
"include-vasl-images-in-snippets": "checkbox", "include-vasl-images-in-snippets": "checkbox",
"include-flags-in-snippets": "checkbox", "include-flags-in-snippets": "checkbox",
"vo-notes-as-images": "checkbox",
} ; } ;
// -------------------------------------------------------------------- // --------------------------------------------------------------------
@ -44,7 +45,7 @@ function user_settings()
dialogClass: "user-settings", dialogClass: "user-settings",
modal: true, modal: true,
width: 440, width: 440,
height: 270, height: 300,
resizable: false, resizable: false,
create: function() { create: function() {
init_dialog( $(this), "OK", true ) ; init_dialog( $(this), "OK", true ) ;

@ -365,6 +365,30 @@ function pluralString( n, str1, str2 )
return (n == 1) ? str1 : str2 ; return (n == 1) ? str1 : str2 ;
} }
function strReplaceAll( val, searchFor, replaceWith )
{
// str.replace() only replaces a single instance!?!? :wtf:
if ( ! searchFor )
return val ;
var pos = 0 ;
for ( ; ; ) {
pos = val.indexOf( searchFor, pos ) ;
if ( pos === -1 )
return val ;
val = val.substr(0,pos) + replaceWith + val.substr(pos+searchFor.length) ;
}
}
function getFilenameExtn( fname )
{
// get the filename extension
var pos = fname.lastIndexOf( "." ) ;
if ( pos !== -1 )
return fname.substr( pos ) ;
else
return null ;
}
function isIE() function isIE()
{ {
// check if we're running in IE :-/ // check if we're running in IE :-/

@ -165,17 +165,17 @@ function do_add_vo( vo_type, player_no, vo_entry, vo_image_id, elite, custom_cap
"</div>" "</div>"
] ; ] ;
var vo_note_key = get_vo_note_key( vo_entry ) ; var vo_note_key = get_vo_note_key( vo_entry ) ;
var vo_note_url = make_vo_note_key_url( vo_type, nat, vo_note_key ) ; var vo_note = get_vo_note( vo_type, nat, vo_note_key ) ;
if ( ! vo_note_url ) { if ( ! vo_note ) {
// NOTE: Note numbers seem to be distinct across all Allied Minor or all Axis Minor vehicles/ordnance, // NOTE: Note numbers seem to be distinct across all Allied Minor or all Axis Minor vehicles/ordnance,
// so if we don't find a note in a given nationality's normal vehicles/ordnance, we can get away with // so if we don't find a note in a given nationality's normal vehicles/ordnance, we can get away with
// just checking their corresponding common vehicles/ordnance. // just checking their corresponding common vehicles/ordnance.
var nat_type = gTemplatePack.nationalities[ nat ].type ; var nat_type = gTemplatePack.nationalities[ nat ].type ;
if ( ["allied-minor","axis-minor"].indexOf( nat_type ) !== -1 ) { if ( ["allied-minor","axis-minor"].indexOf( nat_type ) !== -1 ) {
vo_note_url = make_vo_note_key_url( vo_type, nat_type, vo_note_key ) ; vo_note = get_vo_note( vo_type, nat_type, vo_note_key ) ;
} }
} }
if ( vo_note_url ) { if ( vo_note ) {
var template_id = (vo_type === "vehicles") ? "ob_vehicle_note" : "ob_ordnance_note" ; var template_id = (vo_type === "vehicles") ? "ob_vehicle_note" : "ob_ordnance_note" ;
if ( is_template_available( template_id ) ) { if ( is_template_available( template_id ) ) {
buf.push( buf.push(
@ -183,7 +183,7 @@ function do_add_vo( vo_type, player_no, vo_entry, vo_image_id, elite, custom_cap
" class='snippet' data-id='" + template_id + "' title='" + GENERATE_SNIPPET_HINT + "'>" " class='snippet' data-id='" + template_id + "' title='" + GENERATE_SNIPPET_HINT + "'>"
) ; ) ;
} }
data.vo_note_url = vo_note_url ; data.vo_note = vo_note ;
} }
buf.push( "</div>" ) ; buf.push( "</div>" ) ;
var $content = $( buf.join("") ) ; var $content = $( buf.join("") ) ;

@ -17,7 +17,9 @@
If you enable any of these options, this program must be running before you load the scenario in VASL. If you enable any of these options, this program must be running before you load the scenario in VASL.
</div> </div>
<input type="checkbox" name="include-vasl-images-in-snippets">&nbsp;Include VASL images in snippets <br> <input type="checkbox" name="include-vasl-images-in-snippets">&nbsp;Include VASL images in snippets <br>
<input type="checkbox" name="include-flags-in-snippets">&nbsp;Include flags in snippets <input type="checkbox" name="include-flags-in-snippets">&nbsp;Include flags in snippets <br>
<div style="height:0.25em;"> &nbsp; </div>
<input type="checkbox" name="vo-notes-as-images">&nbsp;Show Chapter H vehicle/ordnance notes as images
</fieldset> </fieldset>
</div> </div>

@ -146,9 +146,10 @@ function load_vo_notes( vo_entries )
for ( var i=0 ; i < keys.length ; ++i ) { for ( var i=0 ; i < keys.length ; ++i ) {
if ( keys[i] === "multi-applicable" ) if ( keys[i] === "multi-applicable" )
continue ; continue ;
var vo_note = vo_notes[ keys[i] ] ;
buf.push( "<tr>", buf.push( "<tr>",
"<td class='key'>", keys[i]+":", "<td class='key'>", keys[i]+":",
"<td>", vo_notes[keys[i]] "<td>", vo_note.substr(vo_note.length-4) === ".png" ? vo_note : "(HTML content)"
) ; ) ;
} }
buf.push( "</table>" ) ; buf.push( "</table>" ) ;
@ -181,7 +182,7 @@ function load_vo_notes( vo_entries )
buf.push( "<td class='vo-note-raw'>", vo_entry.note_number) ; buf.push( "<td class='vo-note-raw'>", vo_entry.note_number) ;
var vo_note_key = get_vo_note_key( vo_entry ) ; var vo_note_key = get_vo_note_key( vo_entry ) ;
if ( vo_note_key ) { if ( vo_note_key ) {
if ( ! make_vo_note_key_url( vo_type, nat, vo_note_key ) ) if ( ! get_vo_note( vo_type, nat, vo_note_key ) )
vo_note_key += " (missing)" ; vo_note_key += " (missing)" ;
} }
buf.push( "<td class='vo-note'>", vo_note_key ) ; buf.push( "<td class='vo-note'>", vo_note_key ) ;

@ -45,6 +45,11 @@
"ob_colors": [ "OBCOL:dutch","OBCOL2:dutch", "OBCOL-BORDER:dutch" ], "ob_colors": [ "OBCOL:dutch","OBCOL2:dutch", "OBCOL-BORDER:dutch" ],
"type": "allied-minor" "type": "allied-minor"
}, },
"greek": {
"display_name": "Greek",
"ob_colors": [ "OBCOL:greek","OBCOL2:greek", "OBCOL-BORDER:greek" ],
"type": "allied-minor"
},
"romanian": { "romanian": {
"display_name": "Romanian", "display_name": "Romanian",

@ -1 +1 @@
{{ORDNANCE_NAME}}: {{ORDNANCE_NOTE_URL}} {{ORDNANCE_NAME}}: {{ORDNANCE_NOTE_HTML}}

@ -1 +1 @@
{{VEHICLE_NAME}}: {{VEHICLE_NOTE_URL}} {{VEHICLE_NAME}}: {{VEHICLE_NOTE_HTML}}

@ -0,0 +1,16 @@
[
{ "name": "PNG note",
"note_number": "201",
"id": "gr/v:001"
},
{ "name": "HTML note",
"note_number": "202",
"id": "gr/v:002"
},
{ "name": "PNG + HTML notes",
"note_number": "203",
"id": "gr/v:003"
}
]

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

@ -82,8 +82,8 @@ Sherman III(a) WP6[J4+]† s8 CS 5[brewup] s8 CS 5[bre
Sherman III(L)(a) WP7 s5 sM8 CS 6[brewup] WP7 s5 sM8 CS 6[brewup] 50.1 N O R† LL Sherman III(L)(a) WP7 s5 sM8 CS 6[brewup] WP7 s5 sM8 CS 6[brewup] 50.1 N O R† LL
Matilda II(b) sD6 CS 5 sD6 CS 5 51† M†<sup>1</sup> N LL Matilda II(b) sD6 CS 5 sD6 CS 5 51† M†<sup>1</sup> N LL
Valentine II(b) sM8 CS 4 sM8 CS 4 52 Br N LL Valentine II(b) sM8 CS 4 sM8 CS 4 52 Br N LL
Valentine V(b) sM8 CS 4 sM8 CS 4 52† Br K†<sup>1</sup> Br N<sup>T</sup> LL Valentine V(b) sM8 CS 4 sM8 CS 4 52.1† Br K†<sup>1</sup> Br N<sup>T</sup> LL
Valentine VIII(b) HE7 sD6 CS 4 HE7 sD6 CS 4 52† Br N<sup>T</sup> LL Valentine VIII(b) HE7 sD6 CS 4 HE7 sD6 CS 4 52.2† Br N<sup>T</sup> LL
Churchill III(b) D6[J4]7[5]† HE7[F3]8[4+]† sD6[4+] sM8† CS 7 sM8† CS 7 53† N LL Churchill III(b) D6[J4]7[5]† HE7[F3]8[4+]† sD6[4+] sM8† CS 7 sM8† CS 7 53† N LL
M3A1 Scout Car(a) CS 4 CS 4 54 US E† US H US I† US N LL M3A1 Scout Car(a) CS 4 CS 4 54 US E† US H US I† US N LL
M5(a) cs 5†[1] cs 5†[1] 55 Br A Br I†<sup>1</sup> Br N LL M5(a) cs 5†[1] cs 5†[1] 55 Br A Br I†<sup>1</sup> Br N LL

@ -82,8 +82,8 @@ Sherman III(a) WP6[J4+]† s8 CS 5[brewup] s8 CS 5[bre
Sherman III(L)(a) WP7 s5 sM8 CS 6[brewup] WP7 s5 sM8 CS 6[brewup] 50.1 N O R† LL Sherman III(L)(a) WP7 s5 sM8 CS 6[brewup] WP7 s5 sM8 CS 6[brewup] 50.1 N O R† LL
Matilda II(b) sD6 CS 5 sD6 CS 5 51† M†<sup>1</sup> N LL Matilda II(b) sD6 CS 5 sD6 CS 5 51† M†<sup>1</sup> N LL
Valentine II(b) sM8 CS 4 sM8 CS 4 52 Br N LL Valentine II(b) sM8 CS 4 sM8 CS 4 52 Br N LL
Valentine V(b) sM8 CS 4 sM8 CS 4 52† Br K†<sup>1</sup> Br N<sup>T</sup> LL Valentine V(b) sM8 CS 4 sM8 CS 4 52.1† Br K†<sup>1</sup> Br N<sup>T</sup> LL
Valentine VIII(b) HE7 sD6 CS 4 HE7 sD6 CS 4 52† Br N<sup>T</sup> LL Valentine VIII(b) HE7 sD6 CS 4 HE7 sD6 CS 4 52.2† Br N<sup>T</sup> LL
Churchill III(b) D6[J4]7[5]† HE7[F3]8[4+]† sD6[4+] sM8† CS 7 sM8† CS 7 53† N LL Churchill III(b) D6[J4]7[5]† HE7[F3]8[4+]† sD6[4+] sM8† CS 7 sM8† CS 7 53† N LL
M3A1 Scout Car(a) CS 4 CS 4 54 US E† US H US I† US N LL M3A1 Scout Car(a) CS 4 CS 4 54 US E† US H US I† US N LL
M5(a) cs 5†[1] cs 5†[1] 55 Br A Br I†<sup>1</sup> Br N LL M5(a) cs 5†[1] cs 5†[1] 55 Br A Br I†<sup>1</sup> Br N LL

@ -82,8 +82,8 @@ Sherman III(a) WP6[J4+]† s8 CS 5[brewup] s8 CS 5[bre
Sherman III(L)(a) WP7 s5 sM8 CS 6[brewup] WP7 s5 sM8 CS 6[brewup] 50.1 N O R† LL Sherman III(L)(a) WP7 s5 sM8 CS 6[brewup] WP7 s5 sM8 CS 6[brewup] 50.1 N O R† LL
Matilda II(b) sD6 CS 5 sD6 CS 5 51† M†<sup>1</sup> N LL Matilda II(b) sD6 CS 5 sD6 CS 5 51† M†<sup>1</sup> N LL
Valentine II(b) sM8 CS 4 sM8 CS 4 52 Br N LL Valentine II(b) sM8 CS 4 sM8 CS 4 52 Br N LL
Valentine V(b) sM8 CS 4 sM8 CS 4 52† Br K†<sup>1</sup> Br N<sup>T</sup> LL Valentine V(b) sM8 CS 4 sM8 CS 4 52.1† Br K†<sup>1</sup> Br N<sup>T</sup> LL
Valentine VIII(b) HE7 sD6 CS 4 HE7 sD6 CS 4 52† Br N<sup>T</sup> LL Valentine VIII(b) HE7 sD6 CS 4 HE7 sD6 CS 4 52.2† Br N<sup>T</sup> LL
Churchill III(b) D6[J4]7[5]† HE7[F3]8[4+]† sD6[4+] sM8† CS 7 sM8† CS 7 53† N LL Churchill III(b) D6[J4]7[5]† HE7[F3]8[4+]† sD6[4+] sM8† CS 7 sM8† CS 7 53† N LL
M3A1 Scout Car(a) CS 4 CS 4 54 US E† US H US I† US N LL M3A1 Scout Car(a) CS 4 CS 4 54 US E† US H US I† US N LL
M5(a) cs 5†[1] cs 5†[1] 55 Br A Br I†<sup>1</sup> Br N LL M5(a) cs 5†[1] cs 5†[1] 55 Br A Br I†<sup>1</sup> Br N LL

@ -82,8 +82,8 @@ Sherman III(a) WP6[J4+]† s8 CS 5[brewup] s8 CS 5[bre
Sherman III(L)(a) WP7 s5 sM8 CS 6[brewup] WP7 s5 sM8 CS 6[brewup] 50.1 N O R† LL Sherman III(L)(a) WP7 s5 sM8 CS 6[brewup] WP7 s5 sM8 CS 6[brewup] 50.1 N O R† LL
Matilda II(b) sD6 CS 5 sD6 CS 5 51† M†<sup>1</sup> N LL Matilda II(b) sD6 CS 5 sD6 CS 5 51† M†<sup>1</sup> N LL
Valentine II(b) sM8 CS 4 sM8 CS 4 52 Br N LL Valentine II(b) sM8 CS 4 sM8 CS 4 52 Br N LL
Valentine V(b) sM8 CS 4 sM8 CS 4 52† Br K†<sup>1</sup> Br N<sup>T</sup> LL Valentine V(b) sM8 CS 4 sM8 CS 4 52.1† Br K†<sup>1</sup> Br N<sup>T</sup> LL
Valentine VIII(b) HE7 sD6 CS 4 HE7 sD6 CS 4 52† Br N<sup>T</sup> LL Valentine VIII(b) HE7 sD6 CS 4 HE7 sD6 CS 4 52.2† Br N<sup>T</sup> LL
Churchill III(b) D6[J4]7[5]† HE7[F3]8[4+]† sD6[4+] sM8† CS 7 sM8† CS 7 53† N LL Churchill III(b) D6[J4]7[5]† HE7[F3]8[4+]† sD6[4+] sM8† CS 7 sM8† CS 7 53† N LL
M3A1 Scout Car(a) CS 4 CS 4 54 US E† US H US I† US N LL M3A1 Scout Car(a) CS 4 CS 4 54 US E† US H US I† US N LL
M5(a) cs 5†[1] cs 5†[1] 55 Br A Br I†<sup>1</sup> Br N LL M5(a) cs 5†[1] cs 5†[1] 55 Br A Br I†<sup>1</sup> Br N LL

@ -82,8 +82,8 @@ Sherman III(a) WP6[J4+]† s8 CS 5[brewup] s8 CS 5[bre
Sherman III(L)(a) WP7 s5 sM8 CS 6[brewup] WP7 s5 sM8 CS 6[brewup] 50.1 N O R† LL Sherman III(L)(a) WP7 s5 sM8 CS 6[brewup] WP7 s5 sM8 CS 6[brewup] 50.1 N O R† LL
Matilda II(b) sD6 CS 5 sD6 CS 5 51† M†<sup>1</sup> N LL Matilda II(b) sD6 CS 5 sD6 CS 5 51† M†<sup>1</sup> N LL
Valentine II(b) sM8 CS 4 sM8 CS 4 52 Br N LL Valentine II(b) sM8 CS 4 sM8 CS 4 52 Br N LL
Valentine V(b) sM8 CS 4 sM8 CS 4 52† Br K†<sup>1</sup> Br N<sup>T</sup> LL Valentine V(b) sM8 CS 4 sM8 CS 4 52.1† Br K†<sup>1</sup> Br N<sup>T</sup> LL
Valentine VIII(b) HE7 sD6 CS 4 HE7 sD6 CS 4 52† Br N<sup>T</sup> LL Valentine VIII(b) HE7 sD6 CS 4 HE7 sD6 CS 4 52.2† Br N<sup>T</sup> LL
Churchill III(b) D6[J4]7[5]† HE7[F3]8[4+]† sD6[4+] sM8† CS 7 HE8† sD6 sM8† CS 7 53† N LL Churchill III(b) D6[J4]7[5]† HE7[F3]8[4+]† sD6[4+] sM8† CS 7 HE8† sD6 sM8† CS 7 53† N LL
M3A1 Scout Car(a) CS 4 CS 4 54 US E† US H US I† US N LL M3A1 Scout Car(a) CS 4 CS 4 54 US E† US H US I† US N LL
M5(a) cs 5†[1] cs 5†[1] 55 Br A Br I†<sup>1</sup> Br N LL M5(a) cs 5†[1] cs 5†[1] 55 Br A Br I†<sup>1</sup> Br N LL

@ -82,8 +82,8 @@ Sherman III(a) WP6[J4+]† s8 CS 5[brewup] WP6† s8 C
Sherman III(L)(a) WP7 s5 sM8 CS 6[brewup] WP7 s5 sM8 CS 6[brewup] 50.1 N O R† LL Sherman III(L)(a) WP7 s5 sM8 CS 6[brewup] WP7 s5 sM8 CS 6[brewup] 50.1 N O R† LL
Matilda II(b) sD6 CS 5 sD6 CS 5 51† M†<sup>1</sup> N LL Matilda II(b) sD6 CS 5 sD6 CS 5 51† M†<sup>1</sup> N LL
Valentine II(b) sM8 CS 4 sM8 CS 4 52 Br N LL Valentine II(b) sM8 CS 4 sM8 CS 4 52 Br N LL
Valentine V(b) sM8 CS 4 sM8 CS 4 52† Br K†<sup>1</sup> Br N<sup>T</sup> LL Valentine V(b) sM8 CS 4 sM8 CS 4 52.1† Br K†<sup>1</sup> Br N<sup>T</sup> LL
Valentine VIII(b) HE7 sD6 CS 4 HE7 sD6 CS 4 52† Br N<sup>T</sup> LL Valentine VIII(b) HE7 sD6 CS 4 HE7 sD6 CS 4 52.2† Br N<sup>T</sup> LL
Churchill III(b) D6[J4]7[5]† HE7[F3]8[4+]† sD6[4+] sM8† CS 7 D7† HE8† sD6 sM8† CS 7 53† N LL Churchill III(b) D6[J4]7[5]† HE7[F3]8[4+]† sD6[4+] sM8† CS 7 D7† HE8† sD6 sM8† CS 7 53† N LL
M3A1 Scout Car(a) CS 4 CS 4 54 US E† US H US I† US N LL M3A1 Scout Car(a) CS 4 CS 4 54 US E† US H US I† US N LL
M5(a) cs 5†[1] cs 5†[1] 55 Br A Br I†<sup>1</sup> Br N LL M5(a) cs 5†[1] cs 5†[1] 55 Br A Br I†<sup>1</sup> Br N LL

@ -47,7 +47,7 @@ class ControlTests:
def __getattr__( self, name ): def __getattr__( self, name ):
"""Generic entry point for handling control requests.""" """Generic entry point for handling control requests."""
if name.startswith( ("get_","set_") ): if name.startswith( ("get_","set_","reset_") ):
# check if we are talking to a local or remote server # check if we are talking to a local or remote server
if self.server_url: if self.server_url:
# remote: return a function that will invoke the handler function on the remote server # remote: return a function that will invoke the handler function on the remote server
@ -281,3 +281,9 @@ class ControlTests:
"""Get the vasl_mod startup warnings.""" """Get the vasl_mod startup warnings."""
_logger.info( "Returning the vasl_mod startup warnings: %s", vasl_mod_module.warnings ) _logger.info( "Returning the vasl_mod startup warnings: %s", vasl_mod_module.warnings )
return vasl_mod_module.warnings return vasl_mod_module.warnings
def _reset_template_pack( self ):
"""Force the default template pack to be reloaded."""
_logger.info( "Reseting the default template pack." )
globvars.template_pack = None
return self

@ -1,12 +1,13 @@
""" Test the user settings. """ """ Test the user settings. """
import json import json
import re
from selenium.webdriver.support.ui import Select from selenium.webdriver.support.ui import Select
from selenium.webdriver.common.keys import Keys from selenium.webdriver.common.keys import Keys
from vasl_templates.webapp.tests.utils import \ from vasl_templates.webapp.tests.utils import \
init_webapp, find_child, wait_for_clipboard, \ init_webapp, find_child, find_children, wait_for_clipboard, \
select_tab, select_menu_option, set_player, click_dialog_button, add_simple_note select_tab, select_menu_option, set_player, click_dialog_button, add_simple_note
from vasl_templates.webapp.tests.test_vehicles_ordnance import add_vo from vasl_templates.webapp.tests.test_vehicles_ordnance import add_vo
from vasl_templates.webapp.tests.test_scenario_persistence import save_scenario, load_scenario from vasl_templates.webapp.tests.test_scenario_persistence import save_scenario, load_scenario
@ -231,6 +232,58 @@ def test_hide_unavailable_ma_notes( webapp, webdriver ):
# --------------------------------------------------------------------- # ---------------------------------------------------------------------
def test_vo_notes_as_images( webapp, webdriver ):
"""Test showing vehicle/ordnance notes as HTML/images."""
# initialize
init_webapp( webapp, webdriver, scenario_persistence=1,
reset = lambda ct: ct.set_vo_notes_dir( dtype="test" )
)
# load the test vehicle
load_scenario( {
"PLAYER_1": "greek",
"OB_VEHICLES_1": [ { "name": "HTML note" } ],
} )
select_tab( "ob1" )
def check_snippet( expected ):
"""Generate and check the vehicle note snippet."""
sortable = find_child( "#ob_vehicles-sortable_1" )
elems = find_children( "li", sortable )
assert len(elems) == 1
btn = find_child( "img.snippet", elems[0] )
btn.click()
contains = True if isinstance( expected, str ) else None
wait_for_clipboard( 2, expected, contains=contains )
# generate the vehicle snippet (should get the raw HTML)
check_snippet( "This is an HTML vehicle note (202)." )
# enable "show vehicle/ordnance notes as images"
select_menu_option( "user_settings" )
elem = find_child( ".ui-dialog.user-settings input[name='vo-notes-as-images']" )
assert not elem.is_selected()
elem.click()
click_dialog_button( "OK" )
_check_cookies( webdriver, "vo-notes-as-images", True )
# generate the vehicle snippet (should get a link to return an image)
check_snippet( re.compile( r"http://.+?:\d+/vehicles/greek/note/202" ) )
# disable "show vehicle/ordnance notes as images"
select_menu_option( "user_settings" )
elem = find_child( ".ui-dialog.user-settings input[name='vo-notes-as-images']" )
assert elem.is_selected()
elem.click()
click_dialog_button( "OK" )
_check_cookies( webdriver, "vo-notes-as-images", False )
# generate the vehicle snippet (should get the raw HTML)
check_snippet( "This is an HTML vehicle note (202)." )
# ---------------------------------------------------------------------
def _check_cookies( webdriver, name, expected ): def _check_cookies( webdriver, name, expected ):
"""Check that a user setting was stored in the cookies correctly.""" """Check that a user setting was stored in the cookies correctly."""
cookies = [ c for c in webdriver.get_cookies() if c["name"] == "user-settings" ] cookies = [ c for c in webdriver.get_cookies() if c["name"] == "user-settings" ]

@ -124,7 +124,7 @@ def test_dedupe_ma_notes( webapp, webdriver ):
# do the tests # do the tests
do_test( [ "Type 92A (Tt)", "M3(a) (LT)" ], [ do_test( [ "Type 92A (Tt)", "M3(a) (LT)" ], [
( False, "A", "The MA <i>and all</i" ), ( False, "A", "The MA and <i>all MG" ),
( True, "A", "The (a) indicates U." ), ( True, "A", "The (a) indicates U." ),
( True, "B", "This vehicle uses Re" ), ( True, "B", "This vehicle uses Re" ),
( True, "C", "Although a captured " ), ( True, "C", "Although a captured " ),
@ -132,7 +132,7 @@ def test_dedupe_ma_notes( webapp, webdriver ):
( True, "US B", "Due to two of the MG" ), ( True, "US B", "Due to two of the MG" ),
] ) ] )
do_test( [ "Type 92A (Tt)", "Type 98 MCT (AAtr)" ], [ do_test( [ "Type 92A (Tt)", "Type 98 MCT (AAtr)" ], [
( False, "A", "The MA <i>and all</i" ), ( False, "A", "The MA and <i>all MG" ),
( True, "Br H", 'As signified by "Inf' ), ( True, "Br H", 'As signified by "Inf' ),
( True, "Ge A", "MA and CMG (if so eq" ), # nb: this is "Ge A", which is different to the Japanese "A" ( True, "Ge A", "MA and CMG (if so eq" ), # nb: this is "Ge A", which is different to the Japanese "A"
] ) ] )
@ -142,11 +142,11 @@ def test_dedupe_ma_notes( webapp, webdriver ):
( True, "C", "Although a captured " ), ( True, "C", "Although a captured " ),
( True, "Br H", 'As signified by "Inf' ), ( True, "Br H", 'As signified by "Inf' ),
( True, "Ge A", "MA and CMG (if so eq" ), ( True, "Ge A", "MA and CMG (if so eq" ),
( True, "Jp A", "The MA <i>and all</i" ), ( True, "Jp A", "The MA and <i>all MG" ),
( True, "US B", "Due to two of the MG" ), ( True, "US B", "Due to two of the MG" ),
] ) ] )
do_test( [ "Type 92A (Tt)", "M3(a) (LT)", "Type 98 MCT (AAtr)" ], [ do_test( [ "Type 92A (Tt)", "M3(a) (LT)", "Type 98 MCT (AAtr)" ], [
( False, "A", "The MA <i>and all</i" ), ( False, "A", "The MA and <i>all MG" ),
( True, "A", "The (a) indicates U." ), ( True, "A", "The (a) indicates U." ),
( True, "B", "This vehicle uses Re" ), ( True, "B", "This vehicle uses Re" ),
( True, "C", "Although a captured " ), ( True, "C", "Although a captured " ),
@ -285,7 +285,7 @@ def test_bfp_extensions( webapp, webdriver ):
( True, "A", "The (a) indicates U." ), ( True, "A", "The (a) indicates U." ),
( True, "C", "Although a captured " ), ( True, "C", "Although a captured " ),
( True, "Ch F", "This vehicle, despit" ), ( True, "Ch F", "This vehicle, despit" ),
( True, "Jp A", "The MA <i>and all</i" ), ( True, "Jp A", "The MA and <i>all MG" ),
], transform=_extract_extn_ma_notes ) ], transform=_extract_extn_ma_notes )
# test the Chapter H note # test the Chapter H note
@ -293,9 +293,9 @@ def test_bfp_extensions( webapp, webdriver ):
elems = find_children( "li img.snippet", vehicles_sortable ) elems = find_children( "li img.snippet", vehicles_sortable )
assert len(elems) == 2 assert len(elems) == 2
elems[0].click() elems[0].click()
wait_for_clipboard( 2, re.compile( r'<img src=".*?/vehicles/japanese/note/8">' ) ) wait_for_clipboard( 2, "By 1935 the latest European tanks", contains=True )
elems[1].click() elems[1].click()
wait_for_clipboard( 2, re.compile( r'<img src=".*?/vehicles/japanese/note/adf-bj:17">' ) ) wait_for_clipboard( 2, "The Japanese captured hundreds of vehicles", contains=True )
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

@ -277,7 +277,9 @@ def test_latw_update( webapp, webdriver ):
# update the scenario (German/Russian, no date) # update the scenario (German/Russian, no date)
load_scenario_params( { "scenario": { "PLAYER_1": "german", "PLAYER_2": "russian", "SCENARIO_DATE": "" } } ) load_scenario_params( { "scenario": { "PLAYER_1": "german", "PLAYER_2": "russian", "SCENARIO_DATE": "" } } )
updated_vsav_dump = _update_vsav_and_dump( fname, { "created": 3, "updated": 2, "deleted": 2 } ) # NOTE: We changed the MOL-P template (to add custom list bullets), so the snippet is different
# to when this test was originally written, and so #updated changed from 2 to 3.
updated_vsav_dump = _update_vsav_and_dump( fname, { "created": 3, "updated": 3, "deleted": 2 } )
_check_vsav_dump( updated_vsav_dump, { _check_vsav_dump( updated_vsav_dump, {
"pf": "Panzerfaust", "psk": "Panzerschrek", "atmm": "ATMM check:", # nb: the PF label now has a snippet ID "pf": "Panzerfaust", "psk": "Panzerschrek", "atmm": "ATMM check:", # nb: the PF label now has a snippet ID
"mol": "Kindling Attempt:", "mol-p": "TH#", # nb: the MOL label now has a snippet ID "mol": "Kindling Attempt:", "mol-p": "TH#", # nb: the MOL label now has a snippet ID

@ -112,6 +112,33 @@ def test_ma_notes( webapp, webdriver ):
# --------------------------------------------------------------------- # ---------------------------------------------------------------------
def test_ma_html_notes( webapp, webdriver ):
"""Test how we load vehicle/ordnance notes (HTML vs. PNG)."""
# initialize
init_webapp( webapp, webdriver, scenario_persistence=1,
reset = lambda ct: ct.set_vo_notes_dir( dtype="test" )
)
# load the test scenario
load_scenario( {
"PLAYER_1": "greek",
"OB_VEHICLES_1": [
{ "name": "PNG note" },
{ "name": "HTML note" },
{ "name": "PNG + HTML notes" }
],
} )
# check the snippets
_check_vo_snippets( 1, "vehicles", [
( "PNG note", "vehicles/allied-minor/note/201" ),
"HTML note: <table width='500'><tr><td>\nThis is an HTML vehicle note (202).\n</table>",
"PNG + HTML notes: <table width='500'><tr><td>\nThis is an HTML vehicle note (203).\n</table>",
] )
# ---------------------------------------------------------------------
def test_common_vo_notes( webapp, webdriver ): def test_common_vo_notes( webapp, webdriver ):
"""Test handling of Allied/Axis Minor common vehicles/ordnance.""" """Test handling of Allied/Axis Minor common vehicles/ordnance."""
@ -550,5 +577,8 @@ def _check_vo_snippets( player_no, vo_type, expected ):
def _extract_vo_note( clipboard ): def _extract_vo_note( clipboard ):
"""Extract the details from a vehicle/ordnance note snippet.""" """Extract the details from a vehicle/ordnance note snippet."""
mo = re.search( "^(.+?): http://.+?/(.*)$", clipboard ) mo = re.search( r'^(.+?): \<img src="http://.+?/(.*)"\>$', clipboard )
return ( mo.group(1), mo.group(2) ) if mo:
return ( mo.group(1), mo.group(2) )
else:
return clipboard

@ -46,9 +46,12 @@ _webdriver = None
def init_webapp( webapp, webdriver, **options ): def init_webapp( webapp, webdriver, **options ):
"""Initialize the webapp.""" """Initialize the webapp."""
# initialize
global _webapp, _webdriver global _webapp, _webdriver
_webapp = webapp _webapp = webapp
_webdriver = webdriver _webdriver = webdriver
# reset the server # reset the server
# NOTE: We have to do this manually, since we can't use pytest's monkeypatch'ing, # NOTE: We have to do this manually, since we can't use pytest's monkeypatch'ing,
# since we could be talking to a remote server (see ControlTests for more details). # since we could be talking to a remote server (see ControlTests for more details).
@ -65,6 +68,10 @@ def init_webapp( webapp, webdriver, **options ):
if "reset" in options: if "reset" in options:
options.pop( "reset" )( control_tests ) options.pop( "reset" )( control_tests )
# force the default template pack to be reloaded (using the new settings)
control_tests.reset_template_pack()
# load the webapp
webdriver.get( webapp.url_for( "main", **options ) ) webdriver.get( webapp.url_for( "main", **options ) )
wait_for( 5, lambda: find_child("#_page-loaded_") is not None ) wait_for( 5, lambda: find_child("#_page-loaded_") is not None )

@ -2,13 +2,17 @@
# Pokhara, Nepal (DEC/18). # Pokhara, Nepal (DEC/18).
import os import os
import pathlib
import io
import re
import logging import logging
from collections import defaultdict from collections import defaultdict
from flask import render_template, jsonify, abort from flask import request, render_template, jsonify, send_file, abort, Response, url_for
from vasl_templates.webapp import app, globvars from vasl_templates.webapp import app, globvars
from vasl_templates.webapp.files import FileServer from vasl_templates.webapp.files import FileServer
from vasl_templates.webapp.webdriver import WebDriver
from vasl_templates.webapp.utils import resize_image_response, is_image_file, is_empty_file from vasl_templates.webapp.utils import resize_image_response, is_image_file, is_empty_file
# --------------------------------------------------------------------- # ---------------------------------------------------------------------
@ -32,7 +36,7 @@ def load_vo_notes(): #pylint: disable=too-many-statements,too-many-locals,too-ma
dname = app.config.get( "CHAPTER_H_NOTES_DIR" ) dname = app.config.get( "CHAPTER_H_NOTES_DIR" )
if not dname: if not dname:
globvars.vo_notes = { "vehicles": {}, "ordnance": {} } globvars.vo_notes = { "vehicles": {}, "ordnance": {} }
globvars.file_server = None globvars.vo_notes_file_server = None
return return
dname = os.path.abspath( dname ) dname = os.path.abspath( dname )
if not os.path.isdir( dname ): if not os.path.isdir( dname ):
@ -68,9 +72,12 @@ def load_vo_notes(): #pylint: disable=too-many-statements,too-many-locals,too-ma
# multi-applicable notes, so we force them to appear in the final results. # multi-applicable notes, so we force them to appear in the final results.
vo_notes["vehicles"]["anzac"] = {} vo_notes["vehicles"]["anzac"] = {}
vo_notes["ordnance"]["indonesian"] = {} vo_notes["ordnance"]["indonesian"] = {}
vo_note_layout_width = app.config.get( "VO_NOTE_LAYOUT_WIDTH", 500 )
# load the vehicle/ordnance notes # load the vehicle/ordnance notes
for root,_,fnames in os.walk( dname, followlinks=True ): for root,_,fnames in os.walk( dname, followlinks=True ):
# initialize
dname2, vo_type2 = os.path.split( root ) dname2, vo_type2 = os.path.split( root )
if vo_type2 in extn_ids: if vo_type2 in extn_ids:
extn_id = vo_type2 extn_id = vo_type2
@ -86,38 +93,77 @@ def load_vo_notes(): #pylint: disable=too-many-statements,too-many-locals,too-ma
vo_type2, nat2 = "vehicles", "landing-craft" vo_type2, nat2 = "vehicles", "landing-craft"
else: else:
nat2 = nat nat2 = nat
# process each file in the next directory
ma_notes = {} ma_notes = {}
for fname in fnames: for fname in fnames:
# ignore placeholder files
fname = os.path.join( root, fname )
if is_empty_file( fname ):
continue
# figure out what kind of file we have
extn = os.path.splitext( fname )[1].lower() extn = os.path.splitext( fname )[1].lower()
if is_image_file( extn ): if is_image_file( extn ):
key = os.path.splitext(fname)[0]
if not all( ch.isdigit() or ch in (".") for ch in key ): # image file - check if this looks like a vehicle/ordnance note
logging.warning( "Unexpected vehicle/ordnance note key: %s", key ) key = os.path.splitext( os.path.split( fname )[1] )[0]
fname = os.path.join( root, fname ) if not all( ch.isdigit() or ch == "." for ch in key ):
if is_empty_file( fname ): # nope (this could be e.g. an image that's part of an HTML vehicle/ordnance note)
continue # nb: ignore placeholder files continue
prefix = os.path.commonpath( [ dname, fname ] )
if prefix: # yup - save it as a vehicle/ordnance note
if extn_id:
key = "{}:{}".format( extn_id, key )
vo_notes[vo_type2][nat2][key] = fname[len(prefix)+1:]
else:
logging.warning( "Unexpected vehicle/ordnance note path: %s", fname )
elif extn == ".html":
key = get_ma_note_key( nat2, fname )
if extn_id: if extn_id:
key = "{}:{}".format( extn_id, key ) key = "{}:{}".format( extn_id, key )
# NOTE: We only do this if we don't already have an HTML version.
if not vo_notes.get( vo_type2, {} ).get( nat2, {} ).get( key ):
rel_path = pathlib.PosixPath( fname ).relative_to( dname )
vo_notes[vo_type2][nat2][key] = str(rel_path)
elif extn == ".html":
# HTML file - read the content
fname = os.path.join( root, fname ) fname = os.path.join( root, fname )
with open( fname, "r" ) as fp: with open( fname, "r" ) as fp:
buf = fp.read().strip() html_content = fp.read().strip()
if not buf: if "&half;" in html_content:
continue # nb: ignore placeholder files # NOTE: VASSAL doesn't like this, use "frac12;" :-/
if buf.startswith( "<p>" ): logging.warning( "Found &half; in HTML: %s", fname )
buf = buf[3:].strip()
if "&half;" in buf: # check what kind of file we have
# NOTE: VASSAL doesn't like this, use "frac12;" :-/ key = get_ma_note_key( nat2, os.path.split(fname)[1] )
logging.warning( "Found &half; in HTML: %s", fname ) if re.search( r"^\d+(\.\d+)?$", key ):
ma_notes[key] = buf
# check if the content is specifying its own layout
if "<!-- vasl-templates:manual-layout -->" not in html_content:
# nope - use the default one
html_content = "<table width='{}'><tr><td>\n{}\n</table>".format(
vo_note_layout_width, html_content
)
# save it as a vehicle/ordnance note
if extn_id:
key = "{}:{}".format( extn_id, key )
rel_path = pathlib.PosixPath( os.path.split(fname)[0] ).relative_to( dname )
vo_notes[ vo_type2 ][ nat2 ][ key ] = _fixup_urls(
html_content,
"{{CHAPTER_H}}/" + str(rel_path) + "/"
)
else:
# save it as a multi-applicable note
if extn_id:
key = "{}:{}".format( extn_id, key )
if html_content.startswith( "<p>" ):
html_content = html_content[3:].strip()
rel_path = pathlib.PosixPath( os.path.split(fname)[0] ).relative_to( dname )
ma_notes[ key ] = _fixup_urls(
html_content,
"{{CHAPTER_H}}/" + str(rel_path) + "/"
)
if "multi-applicable" in vo_notes[ vo_type2 ][ nat2 ]: if "multi-applicable" in vo_notes[ vo_type2 ][ nat2 ]:
vo_notes[ vo_type2 ][ nat2 ][ "multi-applicable" ].update( ma_notes ) vo_notes[ vo_type2 ][ nat2 ][ "multi-applicable" ].update( ma_notes )
else: else:
@ -131,7 +177,14 @@ def load_vo_notes(): #pylint: disable=too-many-statements,too-many-locals,too-ma
# install the vehicle/ordnance notes # install the vehicle/ordnance notes
globvars.vo_notes = { k: dict(v) for k,v in vo_notes.items() } globvars.vo_notes = { k: dict(v) for k,v in vo_notes.items() }
globvars.file_server = file_server globvars.vo_notes_file_server = file_server
def _fixup_urls( html, url_stem ):
"""Fixup URL's to Chapter H files."""
matches = list( re.finditer( r"<img [^>]*src=(['\"])(.*?)\1", html ) )
for mo in reversed(matches):
html = html[:mo.start(2)] + url_stem+ html[mo.start(2):]
return html
# --------------------------------------------------------------------- # ---------------------------------------------------------------------
@ -139,19 +192,69 @@ def load_vo_notes(): #pylint: disable=too-many-statements,too-many-locals,too-ma
def get_vo_note( vo_type, nat, key ): def get_vo_note( vo_type, nat, key ):
"""Return a Chapter H vehicle/ordnance note.""" """Return a Chapter H vehicle/ordnance note."""
# locate the file # get the vehicle/ordnance note
vo_notes = globvars.vo_notes[ vo_type ] vo_notes = globvars.vo_notes[ vo_type ]
fname = vo_notes.get( nat, {} ).get( key ) vo_note = vo_notes.get( nat, {} ).get( key )
if not fname: if not vo_note:
abort( 404 )
if not globvars.file_server:
abort( 404 ) abort( 404 )
resp = globvars.file_server.serve_file( fname, ignore_empty=True ) if not globvars.vo_notes_file_server:
if not resp:
abort( 404 ) abort( 404 )
default_scaling = app.config.get( "CHAPTER_H_IMAGE_SCALING", 100 ) # serve the file
return resize_image_response( resp, default_scaling=default_scaling ) if is_image_file( vo_note ):
resp = globvars.vo_notes_file_server.serve_file( vo_note, ignore_empty=True )
if not resp:
abort( 404 )
default_scaling = app.config.get( "CHAPTER_H_IMAGE_SCALING", 100 )
return resize_image_response( resp, default_scaling=default_scaling )
else:
buf = _make_vo_note_html( vo_note )
if request.args.get( "f" ) == "html":
# return the content as HTML
return Response( buf, mimetype="text/html" )
else:
# return the content as an image
# NOTE: We offer this option since VASSAL's HTML engine is so ancient, it doesn't support
# floating images (which we really need), either via CSS "float", or the HTML "align" attribute.
# NOTE: We need our own WebDriver instance in case the user is trying to generate a snippet image,
# which will use the shared instance (thus locking it), but vehicle/ordnance notes can contain
# a link that calls us here to generate the Chapter H content as an image, and if this 2nd request
# gets handled in a different thread (which it certainly will, since the 1st request is still
# in progress), we will deadlock waiting for the shared instance to become available.
with WebDriver.get_instance( "vo_note" ) as webdriver:
img = webdriver.get_snippet_screenshot( None, buf )
buf = io.BytesIO()
img.save( buf, format="PNG" )
buf.seek( 0 )
return send_file( buf, mimetype="image/png" )
def _make_vo_note_html( vo_note ):
"""Generate the HTML for a vehicle/ordnance note."""
# initialize
url_root = request.url_root
if url_root.endswith( "/" ):
url_root = url_root[:-1]
# inject the CSS (we do it like this since VASSAL doesn't support <link> :-/)
css = globvars.template_pack.get( "css", {} ).get( "vo_note" )
if css:
vo_note = "<head>\n<style>\n{}\n</style>\n</head>\n\n{}".format( css, vo_note )
# update any parameters
vo_note = vo_note.replace( "{{CHAPTER_H}}", url_root+"/chapter-h" )
vo_note = vo_note.replace( "{{IMAGES_BASE_URL}}", url_root+url_for("static",filename="images") )
return vo_note
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
@app.route( "/chapter-h/<path:path>" )
def get_chapter_h_file( path ):
"""Return a Chapter H file."""
if not globvars.vo_notes_file_server:
abort( 404 )
return globvars.vo_notes_file_server.serve_file( path, ignore_empty=True )
# --------------------------------------------------------------------- # ---------------------------------------------------------------------

@ -19,10 +19,10 @@ _logger = logging.getLogger( "webdriver" )
class WebDriver: class WebDriver:
"""Wrapper for a Selenium webdriver.""" """Wrapper for a Selenium webdriver."""
# NOTE: The thread-safety lock controls access to the _shared_instance variable, # NOTE: The thread-safety lock controls access to the _shared_instances variable,
# not the WebDriver it points to (it has its own lock). # not the WebDriver it points to (it has its own lock).
_shared_instance_lock = threading.RLock() _shared_instances_lock = threading.RLock()
_shared_instance = None _shared_instances = {}
def __init__( self ): def __init__( self ):
self.driver = None self.driver = None
@ -159,7 +159,7 @@ class WebDriver:
return self.get_screenshot( snippet, window_size, window_size2 ) return self.get_screenshot( snippet, window_size, window_size2 )
@staticmethod @staticmethod
def get_instance(): def get_instance( key="default" ):
"""Return the shared WebDriver instance. """Return the shared WebDriver instance.
A Selenium webdriver has a hefty startup time, so we create one on first use, and then re-use it. A Selenium webdriver has a hefty startup time, so we create one on first use, and then re-use it.
@ -178,26 +178,26 @@ class WebDriver:
if app.config.get( "DISABLE_SHARED_WEBDRIVER" ): if app.config.get( "DISABLE_SHARED_WEBDRIVER" ):
return WebDriver() return WebDriver()
with WebDriver._shared_instance_lock: with WebDriver._shared_instances_lock:
# check if we've already created the shared WebDriver # check if we've already created the shared WebDriver
if WebDriver._shared_instance: if key in WebDriver._shared_instances:
# yup - just return it (nb: the caller is responsible for locking it) # yup - just return it (nb: the caller is responsible for locking it)
_logger.info( "Returning shared WebDriver: %x", id(WebDriver._shared_instance) ) _logger.info( "Returning shared WebDriver (%s): %x", key, id(WebDriver._shared_instances[key]) )
return WebDriver._shared_instance return WebDriver._shared_instances[ key ]
# nope - create a new WebDriver instance # nope - create a new WebDriver instance
# NOTE: We start it here to keep it alive even after the caller has finished with it, # NOTE: We start it here to keep it alive even after the caller has finished with it,
# and take steps to make sure it gets stopped and cleaned up when the program exits. # and take steps to make sure it gets stopped and cleaned up when the program exits.
wdriver = WebDriver() wdriver = WebDriver()
_logger.info( "Created shared WebDriver: %x", id(wdriver) ) _logger.info( "Created shared WebDriver (%s): %x", key, id(wdriver) )
wdriver._do_start() #pylint: disable=protected-access wdriver._do_start() #pylint: disable=protected-access
WebDriver._shared_instance = wdriver WebDriver._shared_instances[ key ] = wdriver
# make sure the shared WebDriver gets cleaned up # make sure the shared WebDriver gets cleaned up
def cleanup(): #pylint: disable=missing-docstring def cleanup(): #pylint: disable=missing-docstring
_logger.info( "Cleaning up shared WebDriver: %x", id(wdriver) ) _logger.info( "Cleaning up shared WebDriver (%s): %x", key, id(wdriver) )
wdriver._do_stop() #pylint: disable=protected-access wdriver._do_stop() #pylint: disable=protected-access
atexit.register( cleanup ) atexit.register( cleanup )
globvars.cleanup_handlers.append( cleanup ) globvars.cleanup_handlers.append( cleanup )

Loading…
Cancel
Save