Allow the user to choose between multiple VASL counter images.

master
Pacman Ghost 6 years ago
parent c1567fcbaa
commit c957f03739
  1. 12
      vasl_templates/webapp/file_server/vasl_mod.py
  2. 4
      vasl_templates/webapp/files.py
  3. 18
      vasl_templates/webapp/static/css/main.css
  4. 6
      vasl_templates/webapp/static/css/tabs-ob.css
  5. BIN
      vasl_templates/webapp/static/images/select-vo-image.png
  6. 93
      vasl_templates/webapp/static/snippets.js
  7. 2
      vasl_templates/webapp/static/sortable.js
  8. 10
      vasl_templates/webapp/static/utils.js
  9. 148
      vasl_templates/webapp/static/vo.js
  10. 4
      vasl_templates/webapp/templates/index.html
  11. 10
      vasl_templates/webapp/tests/fixtures/invalid-vo-image-ids/alphanumeric-gpid.json
  12. 10
      vasl_templates/webapp/tests/fixtures/invalid-vo-image-ids/alphanumeric-index.json
  13. 10
      vasl_templates/webapp/tests/fixtures/invalid-vo-image-ids/empty.json
  14. 10
      vasl_templates/webapp/tests/fixtures/invalid-vo-image-ids/long-gpid.json
  15. 10
      vasl_templates/webapp/tests/fixtures/invalid-vo-image-ids/missing-gpid.json
  16. 10
      vasl_templates/webapp/tests/fixtures/invalid-vo-image-ids/missing-index.json
  17. 10
      vasl_templates/webapp/tests/fixtures/invalid-vo-image-ids/short-gpid.json
  18. 8
      vasl_templates/webapp/tests/test_counters.py
  19. 14
      vasl_templates/webapp/tests/test_scenario_persistence.py
  20. 210
      vasl_templates/webapp/tests/test_vehicles_ordnance.py
  21. 22
      vasl_templates/webapp/tests/utils.py

@ -48,8 +48,18 @@ class VaslMod:
def get_piece_info( self ):
"""Get information about each piece."""
def image_count( piece, key ):
"""Return the number of images the specified piece has."""
if not piece[key]:
return 0
return len(piece[key]) if isinstance( piece[key], list ) else 1
return {
p["gpid"]: { "name": p["name"], "is_small": p["is_small"] }
p["gpid"]: {
"name": p["name"],
"front_images": image_count( p, "front_images" ),
"back_images": image_count( p, "back_images" ),
"is_small": p["is_small"],
}
for p in self.pieces.values()
}

@ -16,7 +16,7 @@ if app.config.get( "VASL_MOD" ):
# ---------------------------------------------------------------------
@app.route( "/counter/<gpid>/<side>/<int:index>" )
@app.route( "/counter/<gpid>/<side>", defaults={"index":1} )
@app.route( "/counter/<gpid>/<side>", defaults={"index":0} )
def get_counter_image( gpid, side, index ):
"""Get a counter image."""
@ -25,7 +25,7 @@ def get_counter_image( gpid, side, index ):
return app.send_static_file( "images/missing-image.png" )
# return the specified counter image
image_path, image_data = vasl_mod.get_piece_image( int(gpid), side, int(index)-1 )
image_path, image_data = vasl_mod.get_piece_image( int(gpid), side, int(index) )
if not image_data:
abort( 404 )
return send_file(

@ -122,12 +122,20 @@ button.edit-template img { height: 18px ; vertical-align: middle ; margin-right:
#select-vo .select2-search { padding: 0 0 5px 0 ; }
#select-vo .select2-results__options { max-height: none ; }
#select-vo .select2-dropdown { border: none ; }
#select-vo .select2-dropdown .vo-entry { display: flex ; align-items: center ; }
#select-vo .select2-dropdown .vo-entry img { width: 3.5em ; margin-right: 0.5em ; }
#select-vo .select2-dropdown .vo-entry.small-piece img { width: 2.7em ; margin-left: 0.4em ; margin-right: 0.9em ; }
#select-vo .select2-dropdown .vo-entry { display: flex ; }
#select-vo .select2-dropdown .vo-entry img { height: 3.5em ; margin-right: 0.5em ; }
#select-vo .select2-dropdown .vo-entry .content { display: flex ; flex-direction: column ; justify-content: center ; }
#select-vo .select2-dropdown .vo-entry.small-piece img { height: 2.7em ; margin-left: 0.4em ; margin-right: 0.9em ; }
#select-vo .select2-dropdown .vo-entry .vo-type { font-size: 80% ; font-style: italic ; color: #888 ; }
.ui-dialog.select-vo .ui-dialog-buttonpane { border: none ; padding: 0 ; font-size: 75% ; }
.ui-dialog.select-vo button { margin: 0 0 0 5px ; padding: 0.1em 0.2em ; }
#select-vo .select2-results__option--highlighted[aria-selected] .vo-type { color: #fff ; }
#select-vo .select2-dropdown .vo-entry input.select-vo-image { width: 15px ; position: relative ; top: 10px ; }
.ui-dialog.select-vo .ui-dialog-buttonpane { border: none ; padding: 0 ; }
.ui-dialog.select-vo .ui-dialog-buttonpane button { margin: 0 0 0 5px ; padding: 0.1em 0.2em ; }
.ui-dialog.select-vo-image { padding: 5px ; }
.ui-dialog.select-vo-image .ui-dialog-titlebar { display: none ; }
.ui-dialog.select-vo-image .ui-dialog-content { padding: 0 ; height: 100% !important ; overflow: hidden ; }
.ui-dialog.select-vo-image .vo-images img { margin: 5px ; padding: 10px ; border: 1px dotted #ddd ; }
.growl-title { display: none ; }
.growl .pre { font-family: monospace ; }

@ -17,7 +17,7 @@
.panel-ob_vehicles .footer { margin-top: 0.5em ; display: flex ; align-items: center ; }
.panel-ob_vehicles .sortable { font-size: 90% ; }
.panel-ob_vehicles .sortable img { display: inline-block ; vertical-align: middle ; width: 3.5em ; margin-right: 0.5em ; }
.panel-ob_vehicles .sortable img { display: inline-block ; vertical-align: middle ; height: 3.5em ; margin-right: 0.5em ; }
/* -------------------------------------------------------------------- */
@ -26,5 +26,5 @@
.panel-ob_ordnance .footer { margin-top: 0.5em ; display: flex ; align-items: center ; }
.panel-ob_ordnance .sortable { font-size: 90% ; }
.panel-ob_ordnance .sortable img { display: inline-block ; vertical-align: middle ; width: 3.5em ; margin-right: 0.5em ; }
.panel-ob_ordnance .sortable .small-piece img { width: 2.5em ; margin-left: 0.5em ; margin-right: 1em ; }
.panel-ob_ordnance .sortable img { display: inline-block ; vertical-align: middle ; height: 3.5em ; margin-right: 0.5em ; }
.panel-ob_ordnance .sortable .small-piece img { height: 2.5em ; margin-left: 0.5em ; margin-right: 1em ; }

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

@ -211,19 +211,21 @@ function unload_snippet_params( params, check_date_capabilities )
var $sortable2 = $( "#ob_" + vo_type + "-sortable_" + player_no ) ;
var objs = [] ;
$sortable2.children( "li" ).each( function() {
var entry = $(this).data( "sortable2-data" ).vo_entry ;
var vo_entry = $(this).data( "sortable2-data" ).vo_entry ;
var vo_image_id = $(this).data( "sortable2-data" ).vo_image_id ;
var obj = {
id: entry.id,
name: entry.name,
note_number: entry.note_number,
notes: entry.notes
id: vo_entry.id,
image_id: (vo_image_id !== null) ? vo_image_id[0]+"/"+vo_image_id[1] : null,
name: vo_entry.name,
note_number: vo_entry.note_number,
notes: vo_entry.notes
} ;
if ( entry.no_radio )
obj.no_radio = entry.no_radio ;
if ( entry.no_if ) {
if ( vo_entry.no_radio )
obj.no_radio = vo_entry.no_radio ;
if ( vo_entry.no_if ) {
obj.no_if = "no IF" ;
if ( typeof(entry.no_if) === "string" ) { // nb: only for the French B1-bis :-/
var no_if = entry.no_if ;
if ( typeof(vo_entry.no_if) === "string" ) { // nb: only for the French B1-bis :-/
var no_if = vo_entry.no_if ;
if ( no_if.substring(no_if.length-1) == "\u2020" )
obj.no_if += "<sup>"+no_if.substring(0,no_if.length-1)+"</sup>\u2020" ;
else
@ -239,7 +241,7 @@ function unload_snippet_params( params, check_date_capabilities )
// get a lot of use :-/
var nat = params[ "PLAYER_"+player_no ] ;
var capabilities = make_capabilities(
entry,
vo_entry,
nat,
params.SCENARIO_THEATER,
params.SCENARIO_YEAR, params.SCENARIO_MONTH, check_date_capabilities,
@ -248,7 +250,7 @@ function unload_snippet_params( params, check_date_capabilities )
if ( capabilities )
obj.capabilities = capabilities ;
capabilities = make_capabilities(
entry,
vo_entry,
nat,
params.SCENARIO_THEATER,
params.SCENARIO_YEAR, params.SCENARIO_MONTH, check_date_capabilities,
@ -256,7 +258,7 @@ function unload_snippet_params( params, check_date_capabilities )
) ;
if ( capabilities )
obj.raw_capabilities = capabilities ;
var crew_survival = make_crew_survival( entry ) ;
var crew_survival = make_crew_survival( vo_entry ) ;
if ( crew_survival )
obj.crew_survival = crew_survival ;
objs.push( obj ) ;
@ -274,29 +276,29 @@ function unload_snippet_params( params, check_date_capabilities )
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
function make_capabilities( entry, nat, scenario_theater, scenario_year, scenario_month, check_date_capabilities, raw )
function make_capabilities( vo_entry, nat, scenario_theater, scenario_year, scenario_month, check_date_capabilities, raw )
{
var capabilities = [] ;
// extract the static capabilities
var i ;
if ( "capabilities" in entry ) {
for ( i=0 ; i < entry.capabilities.length ; ++i )
capabilities.push( entry.capabilities[i] ) ;
if ( "capabilities" in vo_entry ) {
for ( i=0 ; i < vo_entry.capabilities.length ; ++i )
capabilities.push( vo_entry.capabilities[i] ) ;
}
// extract the variable capabilities
if ( "capabilities2" in entry ) {
if ( "capabilities2" in vo_entry ) {
var indeterminate_caps=[], unexpected_caps=[], invalid_caps=[] ;
for ( var key in entry.capabilities2 ) {
for ( var key in vo_entry.capabilities2 ) {
// check if the capability is dependent on the scenario date
if ( !( entry.capabilities2[key] instanceof Array ) ) {
capabilities.push( key + entry.capabilities2[key] ) ;
if ( !( vo_entry.capabilities2[key] instanceof Array ) ) {
capabilities.push( key + vo_entry.capabilities2[key] ) ;
continue ;
}
// check for LF
if ( key == "LF" ) {
var caps = $.extend( true, [], entry.capabilities2[key] ) ;
var caps = $.extend( true, [], vo_entry.capabilities2[key] ) ;
if ( caps[caps.length-1] == "\u2020" ) {
caps.pop() ;
capabilities.push( "LF\u2020" ) ;
@ -315,14 +317,14 @@ function make_capabilities( entry, nat, scenario_theater, scenario_year, scenari
raw = true ;
}
if ( raw ) {
capabilities.push( make_raw_capability( key, entry.capabilities2[key] ) ) ;
capabilities.push( make_raw_capability( key, vo_entry.capabilities2[key] ) ) ;
}
else {
var cap = _select_capability_by_date( entry.capabilities2[key], nat, scenario_theater, scenario_year, scenario_month ) ;
var cap = _select_capability_by_date( vo_entry.capabilities2[key], nat, scenario_theater, scenario_year, scenario_month ) ;
if ( cap === null )
continue ;
if ( cap == "<invalid>" ) {
invalid_caps.push( entry.name + ": " + key + ": " + entry.capabilities2[key] ) ;
invalid_caps.push( vo_entry.name + ": " + key + ": " + vo_entry.capabilities2[key] ) ;
continue ;
}
capabilities.push( key + cap ) ;
@ -352,14 +354,14 @@ function make_capabilities( entry, nat, scenario_theater, scenario_year, scenari
}
// extract any other capabilities
if ( "capabilities_other" in entry ) {
for ( i=0 ; i < entry.capabilities_other.length ; ++i )
capabilities.push( entry.capabilities_other[i] ) ;
if ( "capabilities_other" in vo_entry ) {
for ( i=0 ; i < vo_entry.capabilities_other.length ; ++i )
capabilities.push( vo_entry.capabilities_other[i] ) ;
}
// include damage points (for Landing Craft)
if ( "damage_points" in entry )
capabilities.push( "DP " + entry.damage_points ) ;
if ( "damage_points" in vo_entry )
capabilities.push( "DP " + vo_entry.damage_points ) ;
return capabilities.length > 0 ? capabilities : null ;
}
@ -510,7 +512,7 @@ function has_ref( val )
return null ;
}
function make_crew_survival( entry )
function make_crew_survival( vo_entry )
{
function make_cs_string( prefix, val ) {
if ( val.length === 2 && val[0] === null && val[1] === "\u2020" )
@ -521,10 +523,10 @@ function make_crew_survival( entry )
// check if the vehicle has a crew survival field
var crew_survival = null ;
if ( "CS#" in entry )
crew_survival = make_cs_string( "CS", entry["CS#"] ) ;
else if ( "cs#" in entry )
crew_survival = make_cs_string( "cs", entry["cs#"] ) ;
if ( "CS#" in vo_entry )
crew_survival = make_cs_string( "CS", vo_entry["CS#"] ) ;
else if ( "cs#" in vo_entry )
crew_survival = make_cs_string( "cs", vo_entry["cs#"] ) ;
if ( crew_survival === null )
return null ;
@ -741,8 +743,16 @@ function do_load_scenario_data( params )
vo_id = params[key][i].name ; // nb: we store the name in the ID variable, in case we have to log an error below
vo_entry = find_vo_by_name( vo_type, nat, vo_id ) ;
}
var vo_image_id = null ;
if ( "image_id" in params[key][i] ) {
var matches = params[key][i].image_id.match( /^(\d{3,4})\/(\d)$/ ) ;
if ( matches )
vo_image_id = [ parseInt(matches[1]), parseInt(matches[2]) ] ;
else
warnings.push( "Invalid V/O image ID for '" + params[key][i].name + "': " + params[key][i].image_id ) ;
}
if ( vo_entry )
do_add_vo( vo_type, player_no, vo_entry ) ;
do_add_vo( vo_type, player_no, vo_entry, vo_image_id ) ;
else
unknown_vo.push( vo_id || "(not set)" ) ;
}
@ -847,14 +857,17 @@ function unload_params_for_save()
function extract_vo_entries( key ) {
if ( !(key in params) )
return ;
var vo_entries = [] ;
var entries = [] ;
for ( var i=0 ; i < params[key].length ; ++i ) {
vo_entries.push( {
var entry = {
id: params[key][i].id,
name: params[key][i].name, // nb: not necessary, but convenient
} ) ;
} ;
if ( params[key][i].image_id !== null )
entry.image_id = params[key][i].image_id ;
entries.push( entry ) ;
}
params[key] = vo_entries ;
params[key] = entries ;
}
var params = {} ;
unload_snippet_params( params, false ) ;

@ -195,7 +195,7 @@ $.fn.sortable2 = function( action, args )
$entries.each( function() {
var fixed_height = $(this).data( "sortable2-data" ).fixed_height ;
if ( fixed_height )
$(this).css( "height", fixed_height+"px" ) ;
$(this).css( "height", fixed_height ) ;
else
$(this).css({ "max-height": max_height+"px", "overflow-y": "hidden" }) ;
// check for overflow

@ -143,12 +143,18 @@ function auto_dismiss_dialog( $dlg, evt, btn_text )
// check if the user pressed Ctrl-Enter
if ( evt.keyCode == 13 && evt.ctrlKey ) {
// yup - locate the target button and click it
var $dlg2 = $( ".ui-dialog." + $dlg.dialog("option","dialogClass") ) ;
$( $dlg2.find( ".ui-dialog-buttonpane button:contains('" + btn_text + "')" ) ).click() ;
click_dialog_button( $dlg, btn_text ) ;
evt.preventDefault() ;
}
}
function click_dialog_button( $dlg, btn_text )
{
// locate the target button and click it
var $dlg2 = $( ".ui-dialog." + $dlg.dialog("option","dialogClass") ) ;
$( $dlg2.find( ".ui-dialog-buttonpane button:contains('" + btn_text + "')" ) ).click() ;
}
// --------------------------------------------------------------------
function ask( title, msg, args )

@ -31,13 +31,27 @@ function add_vo( vo_type, player_no )
var div_class = "vo-entry" ;
if ( is_small_vasl_piece( vo_entry ) )
div_class += " small-piece" ;
var buf2 = ["<div class='" + div_class + "'>",
"<img src='" + _get_vo_image_url(vo_entry) + "'>",
var buf2 = [ "<div class='" + div_class + "'>",
"<img src='" + _get_vo_image_url(vo_entry,null) + "' class='vasl-image'>",
"<div class='content'><div>",
vo_entry.name,
vo_entry.type ? "&nbsp;<span class='vo-type'>("+vo_entry.type+")</span>" : "",
"</div></div>",
"</div>"
] ;
return $( buf2.join("") ) ;
$entry = $( buf2.join("") ) ;
$entry.find( "img" ).data( "vo-image-id", null ) ;
var vo_images = get_vo_images( vo_entry ) ;
if ( vo_images.length > 1 ) {
$entry.find( "img" ).data( "vo-images", vo_images ) ;
var $btn = $( "<input type='image' class='select-vo-image' src='" + gImagesBaseUrl + "/select-vo-image.png'>" ) ;
$entry.children( ".content" ).append( $btn ) ;
$btn.click( function() {
$(this).blur() ;
on_select_vo_image( $(this) ) ;
} ) ;
}
return $entry ;
}
var $sel = $( "#select-vo select" ) ;
$sel.html( buf.join("") ).select2( {
@ -90,7 +104,9 @@ function add_vo( vo_type, player_no )
var data = $sel.select2( "data" ) ;
if ( ! data )
return ;
do_add_vo( vo_type, player_no, entries[data[0].id] ) ;
var $img = $( "#"+data[0]._resultId ).find( "img[class='vasl-image']" ) ;
var vo_image_id = $img.data( "vo-image-id" ) ;
do_add_vo( vo_type, player_no, entries[data[0].id], vo_image_id ) ;
$(this).dialog( "close" ) ;
},
Cancel: function() { $(this).dialog( "close" ) ; },
@ -100,22 +116,23 @@ function add_vo( vo_type, player_no )
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
function do_add_vo( vo_type, player_no, vo_entry )
function do_add_vo( vo_type, player_no, vo_entry, vo_image_id )
{
// add the specified vehicle/ordnance
// NOTE: We set a fixed height for the sortable2 entries (based on the CSS settings in tabs-ob.css),
// so that the vehicle/ordnance images won't get truncated if there are a lot of them.
var $sortable2 = $( "#ob_" + vo_type + "-sortable_" + player_no ) ;
var div_tag = "<div" ;
var fixed_height = 3.25 * gEmSize ;
var fixed_height = "3.75em" ;
if ( is_small_vasl_piece( vo_entry ) ) {
div_tag += " class='small-piece'" ;
fixed_height = 2.25 * gEmSize ;
fixed_height = "2.5em" ;
}
div_tag += ">" ;
var url = _get_vo_image_url( vo_entry, vo_image_id ) ;
$sortable2.sortable2( "add", {
content: $( div_tag + "<img src='"+_get_vo_image_url(vo_entry)+"'>" + vo_entry.name + "</div>" ),
data: { caption: vo_entry.name, vo_entry: vo_entry, fixed_height: fixed_height },
content: $( div_tag + "<img src='"+url+"'>" + vo_entry.name + "</div>" ),
data: { caption: vo_entry.name, vo_entry: vo_entry, vo_image_id: vo_image_id, fixed_height: fixed_height },
} ) ;
}
@ -145,12 +162,115 @@ function find_vo_by_name( vo_type, nat, name )
// --------------------------------------------------------------------
function _get_vo_image_url( vo_entry )
function get_vo_images( vo_entry )
{
if ( $.isArray( vo_entry.gpid ) ) // FIXME! if > 1 image available, let the user pick which one
return "/counter/" + vo_entry.gpid[0] + "/front" ;
if ( vo_entry.gpid )
return "/counter/" + vo_entry.gpid + "/front" ;
// NOTE: Mapping Chapter H vehicles/ordnance to VASL images is quite messy :-/ Most map one-to-one,
// but some V/O have multiple GPID's, some GPID's have multiple images. Also, some V/O don't have
// a matching GPID (are they in a VASL extension somewhere?), so we can't show an image for them at all.
// So, we identify VASL images by a GPID plus index (if there are multiple images for that GPID).
var images = [] ;
function add_gpid_images( gpid ) {
if ( ! gpid || !(gpid in gVaslPieceInfo) )
return ;
for ( var i=0 ; i < gVaslPieceInfo[gpid].front_images ; ++i )
images.push( [gpid,i] ) ;
}
if ( $.isArray(vo_entry.gpid) ) {
for ( var i=0 ; i < vo_entry.gpid.length ; ++i )
add_gpid_images( vo_entry.gpid[i] ) ;
} else
add_gpid_images( vo_entry.gpid ) ;
return images ;
}
function on_select_vo_image( $btn ) {
// initialize
var $img = $btn.parent().parent().find( "img.vasl-image" ) ;
var vo_images = $img.data( "vo-images" ) ;
var vo_image_id = $img.data( "vo-image-id" ) ;
// NOTE: We need to do this after the dialog has opened, since we need to wait for all the images
// to finish loading, so that we can figure out how big to make the dialog.
function on_open_dialog() {
// load the vehicle/ordnance images
var $images = $( "#select-vo-image .vo-images" ) ;
var n_images_loaded=0, total_width=0, max_height=0 ;
function on_image_loaded() {
total_width += $(this).width() ;
max_height = Math.max( $(this).height(), max_height ) ;
if ( ++n_images_loaded == vo_images.length ) {
// all images have loaded - resize the dialog
var width = 5 + total_width + 20*vo_images.length + 10*vo_images.length + 5 ;
var height = 5 + 10+max_height+10 + 5 ;
$( ".ui-dialog.select-vo-image" ).width( width ).height( height ) ;
}
}
$images.empty() ;
for ( var i=0 ; i < vo_images.length ; ++i ) {
var $elem = $( "<img data-index='" + i + "'>" )
.bind( "load", on_image_loaded )
.attr( "src", _get_vo_image_url( null, vo_images[i] ) ) ;
$images.append( $elem ) ;
}
// highlight the currently-selected image
var sel_index = (vo_image_id === null) ? 0 : vo_images.indexOf(vo_image_id) ;
if ( sel_index === -1 ) {
console.log( "Couldn't find V/O image ID '" + vo_image_id + "' in V/O images: " + vo_images ) ;
sel_index = 0 ;
}
$images.children( "img:eq("+sel_index+")" ).css( "background", "#5897fb" ) ;
// highlight images on mouse-over
var prev_bgd ;
$images.children( "img" ).on( {
"mouseenter": function() {
prev_bgd = $(this).css( "backgroundColor" ) ;
if ( $(this).data("index") != sel_index )
$(this).css( "background", "#ddd" ) ;
},
"mouseleave": function() { $(this).css( "backgroundColor", prev_bgd ) ; }
} ) ;
// handle image selection
$images.children( "img" ).click( function() {
vo_image_id = vo_images[ $(this).data("index") ] ;
$img.attr( "src", _get_vo_image_url(vo_image_id) ) ;
$img.data( "vo-image-id", vo_image_id ) ;
$dlg.dialog( "close" ) ;
// nb: if the user selected an image, we take that to mean they also want to add that vehicle/ordnance
click_dialog_button( $("#select-vo"), "OK" ) ;
} ) ;
}
// show the dialog
var $dlg = $("#select-vo-image").dialog( {
dialogClass: "select-vo-image",
modal: true,
position: { my: "left top", at: "left-50 bottom+5", of: $btn, "collision": "fit" },
width: 1, height: 1, // nb: to avoid flicker; we set the size when the images have finished loading
minWidth: 200,
minHeight: 100,
resizable: false,
"open": on_open_dialog,
} ) ;
}
function _get_vo_image_url( vo_entry, vo_image_id )
{
if ( vo_image_id )
return "/counter/" + vo_image_id[0] + "/front/" + vo_image_id[1] ;
else {
// no V/O image ID was provided, just use the first available image
if ( $.isArray( vo_entry.gpid ) )
return "/counter/" + vo_entry.gpid[0] + "/front" ;
if ( vo_entry.gpid )
return "/counter/" + vo_entry.gpid + "/front" ;
}
return gImagesBaseUrl + "/missing-image.png" ;
}

@ -281,6 +281,10 @@
<select></select>
</div>
<div id="select-vo-image" style="display:none;">
<div class='vo-images'></div>
</div>
<div id="ask" style="display:none;"></div>
<!-- ----------------------------------------------------------------- -->

@ -0,0 +1,10 @@
{
"PLAYER_1": "german",
"OB_VEHICLES_1": [
{
"id": "ge/v:990",
"name": "a german vehicle",
"image_id": "abc123/0"
}
]
}

@ -0,0 +1,10 @@
{
"PLAYER_1": "german",
"OB_VEHICLES_1": [
{
"id": "ge/v:990",
"name": "a german vehicle",
"image_id": "1234/x"
}
]
}

@ -0,0 +1,10 @@
{
"PLAYER_1": "german",
"OB_VEHICLES_1": [
{
"id": "ge/v:990",
"name": "a german vehicle",
"image_id": ""
}
]
}

@ -0,0 +1,10 @@
{
"PLAYER_1": "german",
"OB_VEHICLES_1": [
{
"id": "ge/v:990",
"name": "a german vehicle",
"image_id": "12345/0"
}
]
}

@ -0,0 +1,10 @@
{
"PLAYER_1": "german",
"OB_VEHICLES_1": [
{
"id": "ge/v:990",
"name": "a german vehicle",
"image_id": "/0"
}
]
}

@ -0,0 +1,10 @@
{
"PLAYER_1": "german",
"OB_VEHICLES_1": [
{
"id": "ge/v:990",
"name": "a german vehicle",
"image_id": "1234/"
}
]
}

@ -0,0 +1,10 @@
{
"PLAYER_1": "german",
"OB_VEHICLES_1": [
{
"id": "ge/v:990",
"name": "a german vehicle",
"image_id": "12/0"
}
]
}

@ -8,10 +8,9 @@ import urllib.request
import pytest
import tabulate
from vasl_templates.webapp.file_server.vasl_mod import VaslMod
from vasl_templates.webapp.file_server.utils import get_vo_gpids
from vasl_templates.webapp.config.constants import DATA_DIR
from vasl_templates.webapp import files as webapp_files
from vasl_templates.webapp.tests.utils import load_vasl_mod
# ---------------------------------------------------------------------
@ -44,7 +43,7 @@ def test_counter_images( webapp, monkeypatch ):
assert locals()["check_"+side]( resp_code, resp_data )
# test counter images when no VASL module has been configured
monkeypatch.setattr( webapp_files, "vasl_mod", None )
load_vasl_mod( None, monkeypatch )
fname = os.path.join( os.path.split(__file__)[0], "../static/images/missing-image.png" )
missing_image_data = open( fname, "rb" ).read()
check_images(
@ -59,8 +58,7 @@ def test_counter_images( webapp, monkeypatch ):
for fname in glob.glob(fspec):
# install the VASL module file
vasl_mod = VaslMod( fname, DATA_DIR )
monkeypatch.setattr( webapp_files, "vasl_mod", vasl_mod )
vasl_mod = load_vasl_mod( DATA_DIR, monkeypatch )
# check the pieces loaded
buf = io.StringIO()

@ -110,7 +110,7 @@ def test_scenario_persistence( webapp, webdriver ): #pylint: disable=too-many-st
assert lhs == rhs
# save the scenario and check the results
saved_scenario = _save_scenario()
saved_scenario = save_scenario()
expected = {
k: v for tab in SCENARIO_PARAMS.values() for k,v in tab.items()
}
@ -136,7 +136,7 @@ def test_scenario_persistence( webapp, webdriver ): #pylint: disable=too-many-st
wait_for( 2, lambda: get_stored_msg("_last-info_") == "The scenario was reset." )
check_window_title( "" )
check_ob_tabs( "german", "russian" )
data = _save_scenario()
data = save_scenario()
data2 = { k: v for k,v in data.items() if v }
assert data2 == {
"SCENARIO_THEATER": "ETO",
@ -156,7 +156,7 @@ def test_scenario_persistence( webapp, webdriver ): #pylint: disable=too-many-st
}
# load a scenario and make sure it was loaded into the UI correctly
_load_scenario( saved_scenario )
load_scenario( saved_scenario )
check_window_title( "my test scenario" )
check_ob_tabs( "russian", "german" )
for tab_id in SCENARIO_PARAMS:
@ -201,7 +201,7 @@ def test_loading_ssrs( webapp, webdriver ):
select_tab( "scenario" )
sortable = find_child( "#ssr-sortable" )
def do_test( ssrs ): # pylint: disable=missing-docstring
_load_scenario( { "SSR": ssrs } )
load_scenario( { "SSR": ssrs } )
assert get_sortable_entry_text(sortable) == ssrs
# load a scenario that has SSR's into a UI with no SSR's
@ -240,7 +240,7 @@ def test_unknown_vo( webapp, webdriver ):
"OB_ORDNANCE_2": [ { "name": "unknown ordnance 2" } ],
}
_ = set_stored_msg_marker( "_last-warning_" )
_load_scenario( SCENARIO_PARAMS )
load_scenario( SCENARIO_PARAMS )
last_warning = get_stored_msg( "_last-warning_" )
assert last_warning.startswith( "Unknown vehicles/ordnance:" )
for key,vals in SCENARIO_PARAMS.items():
@ -250,14 +250,14 @@ def test_unknown_vo( webapp, webdriver ):
# ---------------------------------------------------------------------
def _load_scenario( scenario ):
def load_scenario( scenario ):
"""Load a scenario into the UI."""
set_stored_msg( "_scenario-persistence_", json.dumps(scenario) )
_ = set_stored_msg_marker( "_last-info_" )
select_menu_option( "load_scenario" )
wait_for( 2, lambda: get_stored_msg("_last-info_") == "The scenario was loaded." )
def _save_scenario():
def save_scenario():
"""Save the scenario."""
marker = set_stored_msg_marker( "_scenario-persistence_" )
select_menu_option( "save_scenario" )

@ -1,13 +1,18 @@
""" Test generating vehicle/ordnance snippets. """
import os
import re
import json
from selenium.webdriver.common.action_chains import ActionChains
from selenium.webdriver.support.ui import Select
from selenium.webdriver.common.keys import Keys
from vasl_templates.webapp.tests.test_scenario_persistence import load_scenario, save_scenario
from vasl_templates.webapp.tests.utils import \
init_webapp, select_tab, set_template_params, find_child, find_children, \
wait_for_clipboard, click_dialog_button
init_webapp, load_vasl_mod, select_tab, set_template_params, find_child, find_children, \
wait_for_clipboard, click_dialog_button, select_menu_option, select_droplist_val, \
set_stored_msg_marker, get_stored_msg
from vasl_templates.webapp.config.constants import DATA_DIR as REAL_DATA_DIR
# ---------------------------------------------------------------------
@ -218,6 +223,7 @@ def test_html_names( webapp, webdriver, monkeypatch ):
# initialize
monkeypatch.setitem( webapp.config, "DATA_DIR", REAL_DATA_DIR )
load_vasl_mod( REAL_DATA_DIR, monkeypatch )
init_webapp( webapp, webdriver )
def get_available_ivfs():
@ -230,7 +236,7 @@ def test_html_names( webapp, webdriver, monkeypatch ):
select_tab( "ob{}".format( 1 ) )
add_vehicle_btn = find_child( "#ob_vehicles-add_1" )
add_vehicle_btn.click()
assert get_available_ivfs() == [ "PzKpfw IVF\n1\n (MT)", "PzKpfw IVF\n2\n (MT)" ]
assert get_available_ivfs() == [ "PzKpfw IVF1 (MT)", "PzKpfw IVF2 (MT)" ]
# add the PzKw IVF2
elem = find_child( ".ui-dialog .select2-search__field" )
@ -244,7 +250,7 @@ def test_html_names( webapp, webdriver, monkeypatch ):
# start to add another vehicle - make sure only the PzKw IVF1 is present
add_vehicle_btn.click()
assert get_available_ivfs() == [ "PzKpfw IVF\n1\n (MT)" ]
assert get_available_ivfs() == [ "PzKpfw IVF1 (MT)" ]
# add the PzKw IVF1
elem = find_child( ".ui-dialog .select2-search__field" )
@ -267,7 +273,201 @@ def test_html_names( webapp, webdriver, monkeypatch ):
# start to add another vehicle - make sure the PzKw IVF2 is available again
add_vehicle_btn.click()
assert get_available_ivfs() == [ "PzKpfw IVF\n2\n (MT)" ]
assert get_available_ivfs() == [ "PzKpfw IVF2 (MT)" ]
# ---------------------------------------------------------------------
def test_vo_images( webapp, webdriver, monkeypatch ): #pylint: disable=too-many-statements
"""Test handling of vehicles/ordnance that have multiple images."""
# initialize
monkeypatch.setitem( webapp.config, "DATA_DIR", REAL_DATA_DIR )
load_vasl_mod( REAL_DATA_DIR, monkeypatch )
init_webapp( webapp, webdriver, scenario_persistence=1 )
def check_sortable2_entries( player_no, expected ):
"""Check the settings on the player's vehicles."""
entries = find_children( "#ob_vehicles-sortable_{} li".format( player_no ) )
for i,entry in enumerate(entries):
# check the displayed image
elem = find_child( "img", entry )
assert elem.get_attribute( "src" ).endswith( expected[i][0] )
# check the attached data
data = webdriver.execute_script( "return $(arguments[0]).data('sortable2-data')", entry )
assert data["vo_entry"]["id"] == expected[i][1]
assert data["vo_image_id"] == expected[i][2]
def check_save_scenario( player_no, expected ):
"""Check the vo_entry and vo_image_id fields are saved correctly."""
data = save_scenario()
assert data[ "OB_VEHICLES_{}".format(player_no) ] == expected
return data
# start to add a PzKw VIB
select_tab( "ob{}".format( 1 ) )
add_vehicle_btn = find_child( "#ob_vehicles-add_1" )
add_vehicle_btn.click()
search_field = find_child( ".ui-dialog .select2-search__field" )
search_field.send_keys( "VIB" )
# make sure there is only 1 image available
elem = find_child( "#select-vo .select2-results li img[class='vasl-image']" )
assert elem.get_attribute( "src" ).endswith( "/counter/2602/front" )
vo_images = webdriver.execute_script( "return $(arguments[0]).data('vo-images')", elem )
assert vo_images is None
assert not find_child( "#select-vo .select2-results li input.select-vo-image" )
# add the PzKw VIB, make sure the sortable2 entry has its data set correctly
search_field.send_keys( Keys.RETURN )
check_sortable2_entries( 1, [
( "/counter/2602/front", "ge/v:035", None )
] )
# check that the vehicles are saved correctly
check_save_scenario( 1, [
{ "id": "ge/v:035", "name": "PzKpfw VIB" },
] )
# start to add a PzKw IVH (this has multiple GPID's)
add_vehicle_btn.click()
search_field = find_child( ".ui-dialog .select2-search__field" )
search_field.send_keys( "IVH" )
# make sure multiple images are available
elem = find_child( "#select-vo .select2-results li img[class='vasl-image']" )
assert elem.get_attribute( "src" ).endswith( "/counter/2584/front" )
vo_images = webdriver.execute_script( "return $(arguments[0]).data('vo-images')", elem )
assert vo_images == [ [2584,0], [2586,0], [2807,0], [2809,0] ]
assert find_child( "#select-vo .select2-results li input.select-vo-image" )
# add the PzKw IVH, make sure the sortable2 entry has its data set correctly
search_field.send_keys( Keys.RETURN )
check_sortable2_entries( 1, [
( "/counter/2602/front", "ge/v:035", None ),
( "/counter/2584/front", "ge/v:027", None ) # nb: there is no V/O image ID if it's not necessary
] )
# check that the vehicles are saved correctly
check_save_scenario( 1, [
{ "id": "ge/v:035", "name": "PzKpfw VIB" },
{ "id": "ge/v:027", "name": "PzKpfw IVH" }, # nb: there is no V/O image ID if it's not necessary
] )
# delete the PzKw IVH
delete_vo( "vehicles", 1, "PzKpfw IVH", webdriver )
# add the PzKw IVH, with a different image, make sure the sortable2 entry has its data set correctly
add_vehicle_btn.click()
search_field = find_child( ".ui-dialog .select2-search__field" )
search_field.send_keys( "IVH" )
elem = find_child( "#select-vo .select2-results li img[class='vasl-image']" )
assert elem.get_attribute( "src" ).endswith( "/counter/2584/front" )
btn = find_child( "#select-vo .select2-results li input.select-vo-image" )
btn.click()
images = find_children( ".ui-dialog.select-vo-image .vo-images img" )
assert len(images) == 4
images[2].click()
check_sortable2_entries( 1, [
( "/counter/2602/front", "ge/v:035", None ),
( "/counter/2807/front/0", "ge/v:027", [2807,0] )
] )
# check that the vehicles are saved correctly
check_save_scenario( 1, [
{ "id": "ge/v:035", "name": "PzKpfw VIB" },
{ "id": "ge/v:027", "image_id": "2807/0", "name": "PzKpfw IVH" },
] )
# set the British as player 2
select_tab("scenario" )
player2_sel = Select( find_child( "select[name='PLAYER_2']" ) )
select_droplist_val( player2_sel, "british" )
# start to add a 2pdr Portee (this has multiple images for a single GPID)
select_tab( "ob{}".format( 2 ) )
add_vehicle_btn = find_child( "#ob_vehicles-add_2" )
add_vehicle_btn.click()
search_field = find_child( ".ui-dialog .select2-search__field" )
search_field.send_keys( "2pdr" )
# make sure multiple images are available
elem = find_child( "#select-vo .select2-results li img[class='vasl-image']" )
assert elem.get_attribute( "src" ).endswith( "/counter/1555/front" )
vo_images = webdriver.execute_script( "return $(arguments[0]).data('vo-images')", elem )
assert vo_images == [ [1555,0], [1555,1] ]
assert find_child( "#select-vo .select2-results li input.select-vo-image" )
# add the 2pdr Portee, make sure the sortable2 entry has its data set correctly
search_field.send_keys( Keys.RETURN )
check_sortable2_entries( 2, [
( "/counter/1555/front", "br/v:115", None ) # nb: there is no V/O image ID if it's not necessary
] )
# check that the vehicles are saved correctly
check_save_scenario( 2, [
{ "id": "br/v:115", "name": "2pdr Portee" }, # nb: there is no V/O image ID if it's not necessary
] )
# delete the 2pdr Portee
delete_vo( "vehicles", 2, "2pdr Portee", webdriver )
# add the 2pdr Portee, with a different image, make sure the sortable2 entry has its data set correctly
add_vehicle_btn.click()
search_field = find_child( ".ui-dialog .select2-search__field" )
search_field.send_keys( "2pdr" )
elem = find_child( "#select-vo .select2-results li img[class='vasl-image']" )
assert elem.get_attribute( "src" ).endswith( "/counter/1555/front" )
btn = find_child( "#select-vo .select2-results li input.select-vo-image" )
btn.click()
images = find_children( ".ui-dialog.select-vo-image .vo-images img" )
assert len(images) == 2
images[1].click()
check_sortable2_entries( 2, [
( "/counter/1555/front/1", "br/v:115", [1555,1] )
] )
# check that the vehicles are saved correctly
saved_scenario = check_save_scenario( 2, [
{ "id": "br/v:115", "image_id": "1555/1", "name": "2pdr Portee" },
] )
# reset the scenario
select_menu_option( "new_scenario" )
check_sortable2_entries( 1, [] )
check_sortable2_entries( 2, [] )
# load the last saved scenario, make sure the correct images are displayed
load_scenario( saved_scenario )
check_sortable2_entries( 1, [
( "/counter/2602/front", "ge/v:035", None ),
( "/counter/2807/front/0", "ge/v:027", [2807,0] )
] )
check_sortable2_entries( 2, [
( "/counter/1555/front/1", "br/v:115", [1555,1] )
] )
# ---------------------------------------------------------------------
def test_invalid_vo_image_ids( webapp, webdriver ):
"""Test loading scenarios that contain invalid V/O image ID's."""
# initialize
init_webapp( webapp, webdriver, scenario_persistence=1 )
# test each save file
dname = os.path.join( os.path.split(__file__)[0], "fixtures/invalid-vo-image-ids" )
for root,_,fnames in os.walk(dname):
for fname in fnames:
fname = os.path.join( root, fname )
if os.path.splitext( fname )[1] != ".json":
continue
# load the next scenario, make sure a warning was issued for the V/O image ID
data = json.load( open(fname,"r") )
set_stored_msg_marker( "_last-warning_" )
load_scenario( data )
last_warning = get_stored_msg( "_last-warning_" )
assert "Invalid V/O image ID" in last_warning
# ---------------------------------------------------------------------

@ -6,6 +6,7 @@ import json
import time
import re
import uuid
import glob
import pytest
from PyQt5.QtWidgets import QApplication
@ -14,6 +15,9 @@ from selenium.webdriver.common.keys import Keys
from selenium.webdriver.common.action_chains import ActionChains
from selenium.common.exceptions import NoSuchElementException, StaleElementReferenceException
from vasl_templates.webapp.file_server.vasl_mod import VaslMod
from vasl_templates.webapp import files as webapp_files
# standard templates
_STD_TEMPLATES = {
"scenario": [ "scenario", "players", "victory_conditions", "scenario_notes", "ssr" ],
@ -42,6 +46,23 @@ def init_webapp( webapp, webdriver, **options ):
webdriver.get( webapp.url_for( "main", **options ) )
wait_for( 5, lambda: find_child("#_page-loaded_") is not None )
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
def load_vasl_mod( data_dir, monkeypatch ):
"""Load a VASL module."""
if data_dir:
# NOTE: Some tests require a VASL module to be loaded, and since they should all
# should behave in the same way, it doesn't matter which one we load.
fspec = os.path.join( pytest.config.option.vasl_mods, "*.vmod" ) #pylint: disable=no-member
fname = glob.glob( fspec )[0]
vasl_mod = VaslMod( fname, data_dir )
else:
vasl_mod = None
monkeypatch.setattr( webapp_files, "vasl_mod", vasl_mod )
return vasl_mod
# ---------------------------------------------------------------------
def for_each_template( func ): #pylint: disable=too-many-branches
@ -332,6 +353,7 @@ def _do_select_droplist( sel, val ):
if e.text == val
]
assert len(elems) == 1
_webdriver.execute_script( "arguments[0].scrollIntoView()", elems[0] )
ActionChains(_webdriver).click( elems[0] ).perform()
def get_droplist_vals_index( sel ):

Loading…
Cancel
Save