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():
"""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
fname = app.config.get( "VASL_MOD" )
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.c { text-align: center ; }
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>
</head>

@ -4,7 +4,7 @@
<meta charset="utf-8">
<style>
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>
</head>

@ -2,12 +2,7 @@
<head>
<meta charset="utf-8">
<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>
<style> {{VO_CSS}} </style>
</head>
<table style="

@ -2,14 +2,7 @@
<head>
<meta charset="utf-8">
<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>
<style> {{MA_NOTE_CSS}} </style>
</head>
<table style="

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

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

@ -2,12 +2,7 @@
<head>
<meta charset="utf-8">
<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>
<style> {{VO_CSS}} </style>
</head>
<table style="

@ -2,14 +2,7 @@
<head>
<meta charset="utf-8">
<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>
<style> {{MA_NOTE_CSS}} </style>
</head>
<table style="

@ -2,6 +2,9 @@
<head>
<meta charset="utf-8">
<style>
ul { margin: 0 ; padding-left: 0 ; list-style-image: url("{{IMAGES_BASE_URL}}/bullet.png") ; }
</style>
</head>
<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",
"copy_from": "br/v:042",
"name": "Valentine V(b)",
"note_number": "52\u2020",
"note_number": "52.1\u2020",
"extra_notes": [ "LL" ],
"gpid": [ 726, 728, 7432, 7434 ]
},
{ "id": "ru/v:080",
"copy_from": "br/v:043",
"name": "Valentine VIII(b)",
"note_number": "52\u2020",
"note_number": "52.2\u2020",
"extra_notes": [ "LL" ],
"gpid": [ 730, 7111 ]
},

@ -43,8 +43,10 @@ class FileServer:
return send_file( buf, mimetype=mime_type )
else:
path = path.replace( "\\", "/" ) # nb: for Windows :-/
if ignore_empty and is_empty_file( os.path.join( self.base_dir, path ) ):
return None
if ignore_empty:
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 )
@staticmethod

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

@ -11,7 +11,7 @@ import threading
from flask import request, jsonify, send_file, abort
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.config.constants import DATA_DIR
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,
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
# 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.
dname = os.path.join( base_dir, "extras" )
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():
data["templates"]["extras/"+key] = val
@ -61,13 +67,14 @@ def get_template_pack():
# check if we're loading the template pack from a directory
if os.path.isdir( dname ):
# 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["templates"] = templates
data["css"] = css
else:
# extract the template pack files from the specified ZIP file
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"] = {}
with zipfile.ZipFile( dname, "r" ) as zip_file:
for fname in zip_file.namelist():
@ -82,7 +89,7 @@ def get_template_pack():
fname2 = "extras/" + fname2
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 )
if not os.path.isdir( dname ):
abort( 404 )
nationalities, templates = {}, {}
nationalities, templates, css = {}, {}, {}
for root,_,fnames in os.walk(dname):
for fname in fnames:
# 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 )
with open( fname, "r" ) as fp:
if fname.lower() == "nationalities.json":
nationalities = json.load( fp )
continue
if words[1] == ".j2":
fname2 = words[0]
if extn == ".j2":
relpath = os.path.relpath( os.path.abspath(fname), dname )
if relpath.startswith( "extras" + os.sep ):
fname2 = "extras/" + fname2
templates[fname2] = fp.read()
return nationalities, templates
fname_stem = "extras/" + fname_stem
templates[fname_stem] = fp.read()
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 snippet_save_name = null ;
// add simple parameters
params.IMAGES_BASE_URL = APP_URL_BASE + gImagesBaseUrl ;
// set player-specific parameters
var player_no = get_player_no_for_element( $btn ) ;
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 key = (vo_type === "vehicles") ? "VEHICLE" : "ORDNANCE" ;
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 ;
}
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" )
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
var pos ;
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
if ( extra_params )
$.extend( true, params, extra_params ) ;
params.IMAGES_BASE_URL = APP_URL_BASE + gImagesBaseUrl ;
// check that the players have different nationalities
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
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 {
content: snippet,
@ -401,7 +428,7 @@ function get_vo_note_key( vo_entry )
return key ;
}
function make_vo_note_key_url( vo_type, nat, key )
function get_vo_note( vo_type, nat, key )
{
if ( ! key )
return null ;
@ -421,8 +448,13 @@ function make_vo_note_key_url( vo_type, nat, key )
if ( !( key in gVehicleOrdnanceNotes[ vo_type ][ nat ] ) )
return null ;
// generate the URL
return APP_URL_BASE + "/" + vo_type + "/" + nat + "/note/" + key ;
var vo_note = gVehicleOrdnanceNotes[ vo_type ][ nat ][ 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 )
@ -1625,7 +1657,7 @@ function on_template_pack()
var pos = data.indexOf( "|" ) ;
var fname = data.substring( 0, pos ).trim() ;
data = data.substring( pos+1 ).trim() ;
if ( fname.substring(fname.length-4) === ".zip" )
if ( getFilenameExtn( fname ) === ".zip" )
data = atob( data ) ;
do_load_template_pack( fname, data ) ;
return ;
@ -1664,6 +1696,7 @@ function do_load_template_pack( fname, data )
var template_pack = {
nationalities: $.extend( true, {}, gDefaultTemplatePack.nationalities ),
templates: {},
css: {},
} ;
// 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 ) ;
return ;
}
if ( fname.substring(fname.length-3) != ".j2" ) {
var extn = getFilenameExtn( fname ) ;
if ( [".j2",".css"].indexOf( extn ) === -1 ) {
invalid_filename_extns.push( fname ) ;
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
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
@ -1746,7 +1784,7 @@ function do_load_template_pack( fname, data )
// check if we have a ZIP file
fname = fname.toLowerCase() ;
if ( fname.substring(fname.length-4) === ".zip" ) {
if ( getFilenameExtn( fname ) === ".zip" ) {
// yup - process each file in the ZIP
var nFiles = 0 ;
JSZip.loadAsync( data ).then( function( zip ) {

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

@ -365,6 +365,30 @@ function pluralString( n, 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()
{
// 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>"
] ;
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 ) ;
if ( ! vo_note_url ) {
var vo_note = get_vo_note( vo_type, nat, vo_note_key ) ;
if ( ! vo_note ) {
// 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
// just checking their corresponding common vehicles/ordnance.
var nat_type = gTemplatePack.nationalities[ nat ].type ;
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" ;
if ( is_template_available( template_id ) ) {
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 + "'>"
) ;
}
data.vo_note_url = vo_note_url ;
data.vo_note = vo_note ;
}
buf.push( "</div>" ) ;
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.
</div>
<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>
</div>

@ -146,9 +146,10 @@ function load_vo_notes( vo_entries )
for ( var i=0 ; i < keys.length ; ++i ) {
if ( keys[i] === "multi-applicable" )
continue ;
var vo_note = vo_notes[ keys[i] ] ;
buf.push( "<tr>",
"<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>" ) ;
@ -181,7 +182,7 @@ function load_vo_notes( vo_entries )
buf.push( "<td class='vo-note-raw'>", vo_entry.note_number) ;
var vo_note_key = get_vo_note_key( vo_entry ) ;
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)" ;
}
buf.push( "<td class='vo-note'>", vo_note_key ) ;

@ -45,6 +45,11 @@
"ob_colors": [ "OBCOL:dutch","OBCOL2:dutch", "OBCOL-BORDER:dutch" ],
"type": "allied-minor"
},
"greek": {
"display_name": "Greek",
"ob_colors": [ "OBCOL:greek","OBCOL2:greek", "OBCOL-BORDER:greek" ],
"type": "allied-minor"
},
"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
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 V(b) sM8 CS 4 sM8 CS 4 52† 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 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.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
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

@ -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
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 V(b) sM8 CS 4 sM8 CS 4 52† 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 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.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
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

@ -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
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 V(b) sM8 CS 4 sM8 CS 4 52† 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 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.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
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

@ -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
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 V(b) sM8 CS 4 sM8 CS 4 52† 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 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.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
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

@ -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
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 V(b) sM8 CS 4 sM8 CS 4 52† 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 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.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
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

@ -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
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 V(b) sM8 CS 4 sM8 CS 4 52† 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 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.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
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

@ -47,7 +47,7 @@ class ControlTests:
def __getattr__( self, name ):
"""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
if self.server_url:
# 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."""
_logger.info( "Returning the vasl_mod startup warnings: %s", 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. """
import json
import re
from selenium.webdriver.support.ui import Select
from selenium.webdriver.common.keys import Keys
from vasl_templates.webapp.tests.utils import \
init_webapp, 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
from vasl_templates.webapp.tests.test_vehicles_ordnance import add_vo
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 ):
"""Check that a user setting was stored in the cookies correctly."""
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_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, "B", "This vehicle uses Re" ),
( 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" ),
] )
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, "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, "Br H", 'As signified by "Inf' ),
( 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" ),
] )
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, "B", "This vehicle uses Re" ),
( True, "C", "Although a captured " ),
@ -285,7 +285,7 @@ def test_bfp_extensions( webapp, webdriver ):
( True, "A", "The (a) indicates U." ),
( True, "C", "Although a captured " ),
( 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 )
# test the Chapter H note
@ -293,9 +293,9 @@ def test_bfp_extensions( webapp, webdriver ):
elems = find_children( "li img.snippet", vehicles_sortable )
assert len(elems) == 2
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()
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)
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, {
"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

@ -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 ):
"""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 ):
"""Extract the details from a vehicle/ordnance note snippet."""
mo = re.search( "^(.+?): http://.+?/(.*)$", clipboard )
return ( mo.group(1), mo.group(2) )
mo = re.search( r'^(.+?): \<img src="http://.+?/(.*)"\>$', clipboard )
if mo:
return ( mo.group(1), mo.group(2) )
else:
return clipboard

@ -46,9 +46,12 @@ _webdriver = None
def init_webapp( webapp, webdriver, **options ):
"""Initialize the webapp."""
# initialize
global _webapp, _webdriver
_webapp = webapp
_webdriver = webdriver
# reset the server
# 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).
@ -65,6 +68,10 @@ def init_webapp( webapp, webdriver, **options ):
if "reset" in options:
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 ) )
wait_for( 5, lambda: find_child("#_page-loaded_") is not None )

@ -2,13 +2,17 @@
# Pokhara, Nepal (DEC/18).
import os
import pathlib
import io
import re
import logging
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.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
# ---------------------------------------------------------------------
@ -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" )
if not dname:
globvars.vo_notes = { "vehicles": {}, "ordnance": {} }
globvars.file_server = None
globvars.vo_notes_file_server = None
return
dname = os.path.abspath( 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.
vo_notes["vehicles"]["anzac"] = {}
vo_notes["ordnance"]["indonesian"] = {}
vo_note_layout_width = app.config.get( "VO_NOTE_LAYOUT_WIDTH", 500 )
# load the vehicle/ordnance notes
for root,_,fnames in os.walk( dname, followlinks=True ):
# initialize
dname2, vo_type2 = os.path.split( root )
if vo_type2 in extn_ids:
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"
else:
nat2 = nat
# process each file in the next directory
ma_notes = {}
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()
if is_image_file( extn ):
key = os.path.splitext(fname)[0]
if not all( ch.isdigit() or ch in (".") for ch in key ):
logging.warning( "Unexpected vehicle/ordnance note key: %s", key )
fname = os.path.join( root, fname )
if is_empty_file( fname ):
continue # nb: ignore placeholder files
prefix = os.path.commonpath( [ dname, fname ] )
if prefix:
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 )
# image file - check if this looks like a vehicle/ordnance note
key = os.path.splitext( os.path.split( fname )[1] )[0]
if not all( ch.isdigit() or ch == "." for ch in key ):
# nope (this could be e.g. an image that's part of an HTML vehicle/ordnance note)
continue
# yup - save it as a vehicle/ordnance note
if extn_id:
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 )
with open( fname, "r" ) as fp:
buf = fp.read().strip()
if not buf:
continue # nb: ignore placeholder files
if buf.startswith( "<p>" ):
buf = buf[3:].strip()
if "&half;" in buf:
# NOTE: VASSAL doesn't like this, use "frac12;" :-/
logging.warning( "Found &half; in HTML: %s", fname )
ma_notes[key] = buf
html_content = fp.read().strip()
if "&half;" in html_content:
# NOTE: VASSAL doesn't like this, use "frac12;" :-/
logging.warning( "Found &half; in HTML: %s", fname )
# check what kind of file we have
key = get_ma_note_key( nat2, os.path.split(fname)[1] )
if re.search( r"^\d+(\.\d+)?$", key ):
# 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 ]:
vo_notes[ vo_type2 ][ nat2 ][ "multi-applicable" ].update( ma_notes )
else:
@ -131,7 +177,14 @@ def load_vo_notes(): #pylint: disable=too-many-statements,too-many-locals,too-ma
# install the vehicle/ordnance notes
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 ):
"""Return a Chapter H vehicle/ordnance note."""
# locate the file
# get the vehicle/ordnance note
vo_notes = globvars.vo_notes[ vo_type ]
fname = vo_notes.get( nat, {} ).get( key )
if not fname:
abort( 404 )
if not globvars.file_server:
vo_note = vo_notes.get( nat, {} ).get( key )
if not vo_note:
abort( 404 )
resp = globvars.file_server.serve_file( fname, ignore_empty=True )
if not resp:
if not globvars.vo_notes_file_server:
abort( 404 )
default_scaling = app.config.get( "CHAPTER_H_IMAGE_SCALING", 100 )
return resize_image_response( resp, default_scaling=default_scaling )
# serve the file
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:
"""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).
_shared_instance_lock = threading.RLock()
_shared_instance = None
_shared_instances_lock = threading.RLock()
_shared_instances = {}
def __init__( self ):
self.driver = None
@ -159,7 +159,7 @@ class WebDriver:
return self.get_screenshot( snippet, window_size, window_size2 )
@staticmethod
def get_instance():
def get_instance( key="default" ):
"""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.
@ -178,26 +178,26 @@ class WebDriver:
if app.config.get( "DISABLE_SHARED_WEBDRIVER" ):
return WebDriver()
with WebDriver._shared_instance_lock:
with WebDriver._shared_instances_lock:
# 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)
_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
# 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.
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
WebDriver._shared_instance = wdriver
WebDriver._shared_instances[ key ] = wdriver
# make sure the shared WebDriver gets cleaned up
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
atexit.register( cleanup )
globvars.cleanup_handlers.append( cleanup )

Loading…
Cancel
Save