Added the turn track template.

master
Pacman Ghost 2 years ago
parent 3a5e472b36
commit 0966c08dd4
  1. BIN
      examples/Hill 621 (Scenario E) (online).vsav
  2. 12
      examples/Hill 621 (Scenario E).json
  3. BIN
      examples/Hill 621 (Scenario E).png
  4. BIN
      examples/Hill 621 (Scenario E).small.jpg
  5. BIN
      examples/Hill 621 (Scenario E).vsav
  6. BIN
      examples/Hube's Pocket (Scenario G) (online).vsav
  7. 12
      examples/Hube's Pocket (Scenario G).json
  8. BIN
      examples/Hube's Pocket (Scenario G).png
  9. BIN
      examples/Hube's Pocket (Scenario G).small.jpg
  10. BIN
      examples/Hube's Pocket (Scenario G).vsav
  11. BIN
      examples/The Streets Of Stalingrad (Scenario C) (online).vsav
  12. 12
      examples/The Streets Of Stalingrad (Scenario C).json
  13. BIN
      examples/The Streets Of Stalingrad (Scenario C).png
  14. BIN
      examples/The Streets Of Stalingrad (Scenario C).small.jpg
  15. BIN
      examples/The Streets Of Stalingrad (Scenario C).vsav
  16. 93
      vasl_templates/webapp/data/default-template-pack/turn_track.j2
  17. 5
      vasl_templates/webapp/scenarios.py
  18. 7
      vasl_templates/webapp/static/css/tabs-scenario.css
  19. 2
      vasl_templates/webapp/static/css/tabs.css
  20. 27
      vasl_templates/webapp/static/css/turn-track-dialog.css
  21. BIN
      vasl_templates/webapp/static/images/turn-track-half-turn.png
  22. 44
      vasl_templates/webapp/static/main.js
  23. 18
      vasl_templates/webapp/static/scenarios.js
  24. 116
      vasl_templates/webapp/static/snippets.js
  25. 309
      vasl_templates/webapp/static/turn_track.js
  26. 14
      vasl_templates/webapp/static/utils.js
  27. 6
      vasl_templates/webapp/static/vassal.js
  28. 3
      vasl_templates/webapp/templates/index.html
  29. 19
      vasl_templates/webapp/templates/tabs-scenario.html
  30. 21
      vasl_templates/webapp/templates/turn-track-dialog.html
  31. 2
      vasl_templates/webapp/tests/fixtures/asl-scenario-archive.json
  32. 4
      vasl_templates/webapp/tests/fixtures/data/default-template-pack/turn_track.j2
  33. 1
      vasl_templates/webapp/tests/fixtures/template-packs/full/turn_track.j2
  34. 1
      vasl_templates/webapp/tests/fixtures/template-packs/new-default/turn_track.j2
  35. 12
      vasl_templates/webapp/tests/test_help.py
  36. 6
      vasl_templates/webapp/tests/test_lfa.py
  37. 9
      vasl_templates/webapp/tests/test_national_capabilities.py
  38. 7
      vasl_templates/webapp/tests/test_scenario_search.py
  39. 3
      vasl_templates/webapp/tests/test_snippets.py
  40. 6
      vasl_templates/webapp/tests/test_template_packs.py
  41. 343
      vasl_templates/webapp/tests/test_turn_track.py
  42. 56
      vasl_templates/webapp/tests/utils.py

@ -27,6 +27,14 @@
"PLAYER_2": "german",
"PLAYER_2_ELR": "3",
"PLAYER_2_SAN": "4",
"TURN_TRACK": {
"NTURNS": "10",
"WIDTH": "",
"VERTICAL": false,
"REINFORCEMENTS_1": "2,5",
"REINFORCEMENTS_2": "1,2,4,5,8",
"SWAP_PLAYERS": false
},
"SSR": [
"EC are Moderate, with no wind at start.",
"After \"At Start\" placement, each German infantry unit must take a TC. The only possible consequence of failure is that the unit must begin the scenario broken. Those units which break during this pre-game TC are not subject to DM in the initial German RPh.",
@ -173,7 +181,7 @@
"id": 2
}
],
"_app_version": "v1.3",
"_last_update_time": "2020-09-27T04:13:08.719Z",
"_app_version": "v1.10",
"_last_update_time": "2022-06-23T08:15:50.772Z",
"_creation_time": "2020-09-27T03:46:56.089Z"
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 MiB

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 105 KiB

After

Width:  |  Height:  |  Size: 93 KiB

@ -27,6 +27,14 @@
"PLAYER_2": "russian",
"PLAYER_2_ELR": "3",
"PLAYER_2_SAN": "2",
"TURN_TRACK": {
"NTURNS": "14",
"WIDTH": "5",
"VERTICAL": false,
"REINFORCEMENTS_1": "1,5",
"REINFORCEMENTS_2": "1",
"SWAP_PLAYERS": true
},
"SSR": [
"The SPW 251/sMG inherent HS is a 3-4-8.",
"German inherent crews have a morale of 9.",
@ -110,7 +118,7 @@
],
"OB_NOTES_1": [],
"OB_NOTES_2": [],
"_app_version": "v1.3",
"_last_update_time": "2020-09-27T04:33:18.643Z",
"_app_version": "v1.10",
"_last_update_time": "2022-06-23T08:33:33.484Z",
"_creation_time": "2020-09-27T04:11:07.200Z"
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 MiB

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 98 KiB

After

Width:  |  Height:  |  Size: 88 KiB

@ -27,6 +27,14 @@
"PLAYER_2": "german",
"PLAYER_2_ELR": "4",
"PLAYER_2_SAN": "6",
"TURN_TRACK": {
"NTURNS": "7",
"WIDTH": "",
"VERTICAL": false,
"REINFORCEMENTS_1": "2",
"REINFORCEMENTS_2": "3",
"SWAP_PLAYERS": false
},
"SSR": [
"Roll a die to determine who moves first.",
"Set up the forces of Scenario A prior to placing the units of Scenario B.",
@ -157,7 +165,7 @@
],
"OB_NOTES_1": [],
"OB_NOTES_2": [],
"_app_version": "v1.3",
"_last_update_time": "2020-09-27T04:46:21.803Z",
"_app_version": "v1.10",
"_last_update_time": "2022-06-23T08:37:11.811Z",
"_creation_time": "2020-09-27T04:44:48.473Z"
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 MiB

After

Width:  |  Height:  |  Size: 811 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 29 KiB

@ -0,0 +1,93 @@
<html> <!-- vasl-templates:id {{SNIPPET_ID}} -->
<head>
<meta charset="utf-8">
<style> {{CSS:common}} </style>
<style>
td {
width: 50px ; min-width: 50px ;
height: {%if TURN_TRACK_PREVIEW_MODE%} 50px {%else%} 43px {%endif%} ;
padding: 2px ;
border: 1px solid black ;
}
{% set RESET_TD = "min-width: unset ; height: unset ; padding: 0 ; border: none" %}
td.turn-no {
{{RESET_TD}} ; width: unset ;
text-align: center ; vertical-align: center ; font-size: 18px ; font-weight: bold ;
}
td.no-reinforce { {{RESET_TD}} ; width: 13px ; }
{# NOTE: We do the reinforcement flags as CSS backgrounds, since VASSAL is incredibly slow downloading normal images. #}
td.reinforce1 { {{RESET_TD}} ; width: 13px ; background: url("{{TURN_TRACK_FLAG_1}}") top left no-repeat ; vertical-align: top ; }
td.reinforce2 { {{RESET_TD}} ; width: 13px ; background: url("{{TURN_TRACK_FLAG_2}}") bottom right no-repeat ; vertical-align: bottom ; }
td.half-turn {
background: url("{{IMAGES_BASE_URL}}/turn-track-half-turn.png") bottom right no-repeat ;
background-size: contain ; {# nb: doesn't work in VASSAL, the image file needs to be the correct size :-/ #}
}
{% if TURN_TRACK_PREVIEW_MODE %}
body { margin: 0 ; }
.reinforce1, .reinforce2 { opacity: 0 ; }
.click { width: 13px ; height: 13px ; cursor: pointer ; }
{%endif%}
</style>
</head>
{% if TURN_TRACK_PREVIEW_MODE %}
<script>
// notify the parent window of a click on a reinforcement flag
function onFlagClick( turnNo, playerNo ) {
window.parent.postMessage( {
type: "FlagClick",
turnNo: turnNo, uiPlayerNo: playerNo
}, "*" ) ;
}
</script>
{%endif%}
<table class="turn-track">
{% for row in TURN_TRACK_SQUARES %}
<tr>
{% for turnSquare in row %}
<td {%if turnSquare[0] == TURN_TRACK_HALF_TURN%} class="half-turn" {%endif%} >
<table style="width:100%;height:100%;"> <tr>
<td id="flag-{{turnSquare[0]}}_1" width="100%"
class = {% if turnSquare[1] %} "reinforce1" {%else%} "no-reinforce" {%endif%}
>
{% if TURN_TRACK_PREVIEW_MODE %}
<div class="click"
onclick = "onFlagClick( {{turnSquare[0]}}, 1 )"
> </div>
{%endif%}
</td>
<td class="turn-no"> {{turnSquare[0]}} </td>
<td id="flag-{{turnSquare[0]}}_2" width="100%"
class = {% if turnSquare[2] and turnSquare[0] != TURN_TRACK_HALF_TURN %} "reinforce2" {%else%} "no-reinforce" {%endif%}
>
{% if TURN_TRACK_PREVIEW_MODE and turnSquare[0] != TURN_TRACK_HALF_TURN %}
<div class="click"
onclick = "onFlagClick( {{turnSquare[0]}}, 2 )"
> </div>
{%endif%}
</td>
</tr> </table>
</td>
{%endfor%}
</tr>
{%endfor%}
</table>
</html>

@ -177,6 +177,11 @@ def get_scenario( scenario_id ): #pylint: disable=too-many-locals
args[ "attacker_name" ] = scenario.get( "attacker" )
args = { k.lower(): v for k,v in args.items() }
# get the number of turns
nturns = scenario.get( "max_turns" ) or scenario.get( "min_turns" )
if nturns:
args[ "scenario_turns" ] = nturns
def get_win_score( key ):
"""Get a player's win percentage."""
nWins = parse_int( playings.get( key+"_wins" ), -1 )

@ -9,6 +9,9 @@
#panel-scenario .scenario-search img { margin-top: 2px ; width: 16px ; }
#panel-scenario input[name='SCENARIO_DATE'] { width: 6em ; flex-grow: 0 ; }
#panel-scenario label[for='TURN_TRACK_NTURNS'] { margin-top: 2px ; }
#panel-scenario button#turn-track-settings { width: 25px ; height: 24px ; margin-right: 5px ; padding: 3px 2px 2px 2px ; }
#panel-scenario label[for='PLAYER_1'] { margin-top: 2px ; }
#panel-scenario .select2[name="PLAYER_1"] { flex: 1 ; }
#panel-scenario label[for='PLAYER_2'] { margin-top: 2px ; }
@ -16,14 +19,14 @@
#panel-scenario input[name="PLAYER_1_DESCRIPTION"],
#panel-scenario input[name="PLAYER_2_DESCRIPTION"] {
margin-left: calc(6.35em + 2px) ; margin-top: 1px ; font-size: 75% ;
margin-left: 6.35em ; margin-top: 1px ; font-size: 75% ;
}
#panel-scenario label { font-weight: bold ; width: 4.75em ; }
#panel-scenario label.header { font-weight: bold ; width: 3em ; text-align: center ; }
#panel-scenario input { margin-bottom: 0.25em ; }
#panel-scenario .select2-container { margin: 2px ; }
#panel-scenario .select2-container { margin-right: 2px ; }
#panel-scenario .select2-selection__rendered { height: 23px ; line-height: 23px ; }
#panel-scenario .select2-selection__arrow { margin-top: -2px ; }
#panel-scenario .select2-selection { height: 24px !important ; border-radius: 0 !important ; }

@ -19,7 +19,7 @@
/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
#tabs-scenario { display: flex ; }
#tabs-scenario .left { width: 30em ; min-width: 30em ; }
#tabs-scenario .left { width: 31em ; min-width: 31em ; }
#tabs-scenario .right { flex-grow: 1 ; min-width: 25em ; }
#tabs-scenario .left { display: flex ; flex-direction: column ; }

@ -0,0 +1,27 @@
#turn-track { display: flex ; overflow: hidden ; }
.ui-dialog.turn-track .ui-dialog-titlebar { background: #80d0ff ; }
.ui-dialog.turn-track .ui-dialog-buttonpane { border: none ; margin-top: 0 !important ; padding-top: 0 !important ; }
.ui-dialog.turn-track .controls { display: flex ; }
.ui-dialog.turn-track .controls .row { display: flex ; align-items: center ; }
.ui-dialog.turn-track .controls .select2 { margin: -1px 0 0 0.5em ; }
.ui-dialog.turn-track .controls input { margin-top: 0.25em ; }
.ui-dialog.turn-track .controls button.reset {
height: 26px ; width: 4.5em ;
margin-top: 1em ; padding: 2px 10px 2px 5px ;
display: none ;
}
.ui-dialog.turn-track iframe { margin: -4px ; border: 0 ; overflow: scroll ; }
/* special layout settings for a horizontal turn track */
#turn-track.horz { flex-direction: column ; }
#turn-track.horz .controls { flex-direction: row ; }
#turn-track.horz .controls>div { margin: 0 1em 0 0 ; }
#turn-track.horz .preview { height: auto ; width: 100% ; margin: 10px 0 0 0 ; padding-bottom: 15px ; }
/* special layout settings for a vertical turn track */
#turn-track.vert { flex-direction: row ; }
#turn-track.vert .controls { flex-direction: column ; }
#turn-track.vert .controls>div { margin: 0 0 1em 0 ; }
#turn-track.vert .preview { height: 100% ; width: auto ; margin: 0 0 0 10px ; padding-right: 15px ; }

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

@ -151,6 +151,28 @@ $(document).ready( function () {
onClose: on_scenario_date_change,
} ) ;
// initialize the turn track controls
var $turnCountSel = $( "select[name='TURN_TRACK_NTURNS']" ) ;
init_select2(
$turnCountSel, "4em", false, formatTurnTrackOption
).on( "select2:open", function() {
restrict_droplist_height( $(this) ) ;
} ).on( "change", function() {
if ( $(this).val() === "(show-dialog)" ) {
$(this).val( DEFAULT_TURN_TRACK_TURNS_MIN ).trigger( "change" ) ;
editTurnTrackSettings() ;
} else {
updateTurnTrackNTurns( $(this).val() ) ;
$( "#panel-scenario .turn-track-controls" ).css( {
display: $(this).val() !== "" ? "flex" : "none"
} ) ;
}
} ) ;
$turnCountSel.append( $( "<option value=''>-</option>" ) ) ;
for ( var nTurns=DEFAULT_TURN_TRACK_TURNS_MIN ; nTurns <= DEFAULT_TURN_TRACK_TURNS_MAX ; nTurns += 0.5 )
$turnCountSel.append( $( "<option value='" + nTurns + "'>" + nTurns + "</option>" ) ) ;
$( "button#turn-track-settings" ).button().click( editTurnTrackSettings ) ;
// initialize the SSR's
$("#ssr-sortable").sortable2( "init", {
add: add_ssr, edit: edit_ssr
@ -735,6 +757,11 @@ function install_template_pack( data )
}
$( "button.generate" ).each( function() { update_button( $(this) ) ; } ) ;
$( "button.edit-template" ).each( function() { update_button( $(this) ) ; } ) ;
// update the turn track controls
enable = is_template_available( "turn_track" ) ;
$( "select[name='TURN_TRACK_NTURNS']" ).css("color","red").prop( "disabled", !enable ) ;
$( "button#turn-track-settings" ).button( enable ? "enable" : "disable" ) ;
}
// --------------------------------------------------------------------
@ -960,6 +987,17 @@ function handle_escape( evt )
} ) ;
// find the top-most dialog (if any) and close it
var $topmost = findTopmostDialog() ;
if ( $topmost ) {
var $dlg = $topmost.children( ".ui-dialog-content" ) ;
if ( ["please-wait","ask","lfa"].indexOf( $dlg.attr("id") ) === -1 )
$topmost.children( ".ui-dialog-content" ).dialog( "close" ) ;
}
}
function findTopmostDialog()
{
// find the top-most dialog
var $topmost = null ;
$( ".ui-dialog" ).each( function() {
if ( $(this).css( "display" ) != "block" )
@ -967,11 +1005,7 @@ function handle_escape( evt )
if ( $topmost === null || $(this).css("z-index") > $topmost.css("z-index") )
$topmost = $(this) ;
} ) ;
if ( $topmost ) {
var $dlg = $topmost.children( ".ui-dialog-content" ) ;
if ( ["please-wait","ask","lfa"].indexOf( $dlg.attr("id") ) === -1 )
$topmost.children( ".ui-dialog-content" ).dialog( "close" ) ;
}
return $topmost ;
}
// --------------------------------------------------------------------

@ -592,6 +592,7 @@ const IMPORT_FIELDS = [
{ key: "scenario_display_id", name: "scenario ID", paramName: "SCENARIO_ID", type: "text" },
{ key: "scenario_location", name: "location", paramName: "SCENARIO_LOCATION", type: "text" },
{ key: "scenario_date_iso", name: "scenario date", paramName: "SCENARIO_DATE", type: "date" },
{ key: "scenario_turns", name: "scenario_turns", paramName: "TURN_TRACK_NTURNS", type: "select2" },
{ key: "theater", name: "theater", paramName: "SCENARIO_THEATER", type: "select2" },
{ key: "defender_name", name: "defender", paramName: "PLAYER_1", type: "player" },
{ key: "defender_desc", name: "defender description", paramName: "PLAYER_1_DESCRIPTION", type: "text" },
@ -696,6 +697,11 @@ function onImportScenario()
function doImportScenario( scenario )
{
// NOTE: We could reset the ELR/SAN here, but if the user is importing on top of an existing setup,
// the most likely reason is because they want to connect it to an ASA scenario, not because
// they want to import a whole set of new details, so clearing the ELR/SAN wouldn't make sense.
// Ditto for the scenario turn count, and the associated turn track settings.
// import each field
IMPORT_FIELDS.forEach( function( importField ) {
var $elem = $gDialog.find( ".import-control .warnings input[name='" + importField.key + "']" ) ;
@ -706,9 +712,6 @@ function doImportScenario( scenario )
} ) ;
// update for the newly-connected scenario
// NOTE: We could reset the ELR/SAN here, but if the user is importing on top of an existing setup,
// the most likely reason is because they want to connect it to an ASA scenario, not because
// they want to import a whole set of new details, so clearing the ELR/SAN wouldn't make sense.
updateForConnectedScenario(
scenario.scenario_id,
scenario.roar ? scenario.roar.scenario_id : null
@ -779,6 +782,8 @@ function getImportFieldCurrVal_select2( importField ) {
// get the current field value
if ( importField.paramName == "SCENARIO_THEATER" )
return null ; // nb: this will always be updated without warning
if ( importField.paramName == "TURN_TRACK_NTURNS" )
return null ; // nb: this will always be updated without warning
return $( "select[name='" + importField.paramName + "']" ).val().trim() ;
}
@ -790,9 +795,14 @@ function doImportField_select2( importField, newVal ) {
if ( ! newVal )
newVal = "other" ;
}
else
newVal = "ETO" ;
} else if ( importField.paramName === "TURN_TRACK_NTURNS" ) {
// ensure the turn count we are about to import is in the droplist
updateTurnTrackNTurns( newVal ) ;
}
var $elem = $( "select[name='" + importField.paramName + "']" ) ;
$elem.val( newVal || "ETO" ).trigger( "change" ) ;
$elem.val( newVal ).trigger( "change" ) ;
}
function getEffectiveTheater( theater ) {

@ -465,6 +465,9 @@ function make_snippet( $btn, params, extra_params, show_date_warnings )
return { content: "[error: can't compile template]" } ;
}
// generate the turn track parameters
make_turn_track_params( params ) ;
// process the template
var snippet ;
try {
@ -507,6 +510,86 @@ function make_snippet( $btn, params, extra_params, show_date_warnings )
} ;
}
function make_turn_track_params( params )
{
// initialize
if ( ! params.TURN_TRACK || ! params.TURN_TRACK.NTURNS )
return ;
var args = parseTurnTrackParams( params ) ;
// generate the data for each turn track square
var turnTrackSquares=[], nTurnTrackSquares=0 ;
var nRows = Math.ceil( args.nTurns / args.width ) ;
for ( var row=0 ; row < nRows ; ++row ) {
turnTrackSquares.push( [] ) ;
for ( var col=0 ; col < args.width ; ++col ) {
var turnNo ;
if ( args.vertical )
turnNo = 1 + col * nRows + row ;
else
turnNo = 1 + row * args.width + col ;
if ( turnNo > args.nTurns )
break ;
var val = [ turnNo, args.reinforce2[turnNo]?true:false, args.reinforce1[turnNo]?true:false ] ;
if ( params.TURN_TRACK.SWAP_PLAYERS )
val = [ val[0], val[2], val[1] ] ;
turnTrackSquares[ turnTrackSquares.length-1 ].push( val ) ;
nTurnTrackSquares += 1 ;
}
}
// update the snippet params
params.TURN_TRACK_SQUARES = turnTrackSquares ;
if ( args.halfTurn )
params.TURN_TRACK_HALF_TURN = nTurnTrackSquares ;
// NOTE: The convention is that player 1 sets up first, player 2 moves first,
// so swapping players actually maps turn track player 1 to the real player 1.
// NOTE: We generate the player flag URL's instead of using params.PLAYER_FLAG_1/2
// so that flags will work even if the user has disabled player flags in snippets.
var forceLocalImages = params.TURN_TRACK_PREVIEW_MODE ;
params.TURN_TRACK_FLAG_1 = make_player_flag_url(
get_player_nat( params.TURN_TRACK.SWAP_PLAYERS ? 1 : 2 ),
true, forceLocalImages
) ;
params.TURN_TRACK_FLAG_2 = make_player_flag_url(
get_player_nat( params.TURN_TRACK.SWAP_PLAYERS ? 2 : 1 ),
true, forceLocalImages
) ;
}
function parseTurnTrackParams( params )
{
function parseReinforcements( reinf ) {
var turnFlags = {} ;
reinf.split( "," ).forEach( function( turnNo ) {
turnNo = parseInt( turnNo.trim() ) ;
if ( ! isNaN( turnNo ) )
turnFlags[ turnNo ] = true ;
} ) ;
return turnFlags ;
}
// parse the turn track parameters
var nTurns = params.TURN_TRACK.NTURNS ;
var halfTurn = false ;
if ( nTurns.substr( nTurns.length-2 ) === ".5" ) {
nTurns = parseInt( nTurns.substr( 0, nTurns.length-2 ) ) + 1 ;
halfTurn = true ;
}
var vertical = params.TURN_TRACK.VERTICAL ;
var width = params.TURN_TRACK.WIDTH ;
if ( width === "" )
width = vertical ? 1 : nTurns ;
var reinforce1 = parseReinforcements( params.TURN_TRACK.REINFORCEMENTS_1 ) ;
var reinforce2 = parseReinforcements( params.TURN_TRACK.REINFORCEMENTS_2 ) ;
return {
nTurns: nTurns, halfTurn: halfTurn,
vertical: vertical, width: width,
reinforce1: reinforce1, reinforce2: reinforce2
} ;
}
function adjust_vo_comments( params )
{
// NOTE: I tried replacing things like "(11)" and "(12)" here (for breakdown numbers),
@ -868,6 +951,24 @@ function unload_snippet_params( unpack_scenario_date, template_id )
$("textarea.param").each( function() { add_param( $(this) ) ; } ) ;
$("select.param").each( function() { add_param( $(this) ) ; } ) ;
// fix up the turn track parameters
var nTurns = params.TURN_TRACK_NTURNS ;
if ( nTurns !== "" ) {
var width = $( "input[name='TURN_TRACK_WIDTH']" ).val() ;
params.TURN_TRACK = {
"NTURNS": nTurns,
"WIDTH": isNaN( parseInt( width ) ) ? "" : width,
"VERTICAL": $( "input[name='TURN_TRACK_VERTICAL']" ).prop( "checked" ),
"REINFORCEMENTS_1": $( "input[name='TURN_TRACK_REINFORCEMENTS_1']" ).val().trim(),
"REINFORCEMENTS_2": $( "input[name='TURN_TRACK_REINFORCEMENTS_2']" ).val().trim(),
"SWAP_PLAYERS": $( "input[name='TURN_TRACK_SWAP_PLAYERS']" ).prop( "checked" ),
} ;
}
Object.keys( params ).forEach( function( key ) {
if ( key.substr(0,11) === "TURN_TRACK_" )
delete params[key] ;
} ) ;
// collect the SSR's
params.SSR = [] ;
var data = $("#ssr-sortable").sortable2( "get-entry-data" ) ;
@ -1651,6 +1752,16 @@ function do_load_scenario_data( params )
var i ;
for ( var key in params ) {
var player_no, $sortable2 ;
if ( key === "TURN_TRACK" ) {
setTurnTrackNTurns( params[key].NTURNS ) ;
$( "input[name='TURN_TRACK_VERTICAL']" ).prop( "checked", params[key].VERTICAL ) ;
$( "input[name='TURN_TRACK_WIDTH']" ).val( params[key].WIDTH ) ;
$( "input[name='TURN_TRACK_REINFORCEMENTS_1']" ).val( params[key].REINFORCEMENTS_1 ) ;
$( "input[name='TURN_TRACK_REINFORCEMENTS_2']" ).val( params[key].REINFORCEMENTS_2 ) ;
$( "input[name='TURN_TRACK_SWAP_PLAYERS']" ).prop( "checked", params[key].SWAP_PLAYERS ) ;
params_loaded[key] = true ;
continue ;
}
if ( key === "SSR" ) {
$sortable2 = $( "#ssr-sortable" ) ;
for ( i=0 ; i < params[key].length ; ++i )
@ -1984,9 +2095,10 @@ function reset_scenario()
$(this).val( "" ) ;
} ) ;
$("textarea.param").each( function() { $(this).val("") ; } ) ;
$("input[type='checkbox']").prop( "checked", false ) ;
$( "select[name='TURN_TRACK_NTURNS'].param" ).val( "" ).trigger( "change" ) ;
// reset all the template parameters
// nb: there's no way to reset the player droplist's
// reset the player droplist's
var player_no ;
for ( player_no=1 ; player_no <= 2 ; ++player_no ) {
on_player_change( player_no ) ;

@ -0,0 +1,309 @@
DEFAULT_TURN_TRACK_TURNS_MIN = 6 ;
DEFAULT_TURN_TRACK_TURNS_MAX = 10 ;
// NOTE: Reinforcement flags get clipped on turn 100, but this is unlikely to be an issue :-/
_MAX_TURN_TRACK_TURNS = 100 ;
gTurnTrackReinforcements = null ;
// --------------------------------------------------------------------
function editTurnTrackSettings()
{
// initialize
var $dlg, $iframe, iframeSeqNo=0 ;
// FUDGE! This should work as a local variable, but causes a weird problem where it doesn't get reset properly :-/
gTurnTrackReinforcements = null ;
function loadControls() {
// load the dialog controls
$dlg.find( "select[name='nturns']" ).val(
$panel.find( "select[name='TURN_TRACK_NTURNS'].param" ).val()
).trigger("change" ) ;
var width = $panel.find( "input[name='TURN_TRACK_WIDTH']" ).val() ;
$dlg.find( "select[name='width']" ).val(
isNaN( parseInt( width ) ) ? "" : width
).trigger( "change" ) ;
$dlg.find( "input[name='vertical']" ).prop( "checked",
$panel.find( "input[name='TURN_TRACK_VERTICAL']" ).prop( "checked" )
) ;
$dlg.find( "input[name='swap-players']" ).prop( "checked",
$panel.find( "input[name='TURN_TRACK_SWAP_PLAYERS']" ).prop( "checked" )
) ;
// load the reinforcements
var params = updatePreview( false ) ;
var args = parseTurnTrackParams( params ) ;
gTurnTrackReinforcements = { 1: args.reinforce1, 2: args.reinforce2 } ;
// update the UI
updateUI() ;
}
function onResetControls() {
// reset all the controls
ask( "Reset turn track", "Do you want to reset the turn track?", {
ok: function() {
setTurnTrackNTurns( DEFAULT_TURN_TRACK_TURNS_MIN ) ;
$panel.find( "input[name='TURN_TRACK_WIDTH']" ).val( "" ) ;
$panel.find( "input[name='TURN_TRACK_VERTICAL']" ).prop( "checked", false ) ;
$panel.find( "input[name='TURN_TRACK_SWAP_PLAYERS']" ).prop( "checked", false ) ;
$panel.find( "input[name='TURN_TRACK_REINFORCEMENTS_1']" ).val( "" ) ;
$panel.find( "input[name='TURN_TRACK_REINFORCEMENTS_2']" ).val( "" ) ;
gTurnTrackReinforcements = null ;
loadControls() ;
}
} ) ;
}
function initTurnCountSelect2( $sel ) {
// initialize the TURN COUNT droplist
init_select2(
$sel, "4em", false, formatTurnTrackOption
).on( "select2:open", function() {
restrict_droplist_height( $(this) ) ;
} ).on( "change", function() {
setTurnTrackNTurns( $(this).val() ) ;
if ( $dlg )
updateUI() ;
} ) ;
for ( var nTurns=1 ; nTurns <= _MAX_TURN_TRACK_TURNS ; nTurns += 0.5 )
$sel.append( $( "<option value='" + nTurns + "'>" + nTurns + "</option>" ) ) ;
}
function initWidthSelect2( $sel ) {
// initialize the WIDTH droplist
init_select2(
$sel, "3.5em", false, null
).on( "select2:open", function() {
restrict_droplist_height( $(this) ) ;
} ).on( "change", function() {
$panel.find( "input[name='TURN_TRACK_WIDTH']" ).val( $(this).val() ) ;
if ( $dlg )
updateUI() ;
} ) ;
$sel.append( $( "<option value=''>-</option>" ) ) ;
for ( var i=1 ; i <= 30 ; ++i )
$sel.append( $( "<option value='" + i + "'>" + i + "</option>" ) ) ;
}
function syncCheckbox( $elem, $target ) {
// sync the target checkbox in the SCENARIO panel with the one in this dialog
$target.prop( "checked", $elem.prop("checked") ) ;
updateUI() ;
}
function updatePreview( showAllFlags ) {
// generate the turn track snippet
var params = unload_snippet_params( true, "turn_track" ) ;
if ( showAllFlags )
params.TURN_TRACK.REINFORCEMENTS_1 = params.TURN_TRACK.REINFORCEMENTS_2 = makeCommaList( _MAX_TURN_TRACK_TURNS ) ;
params.TURN_TRACK_PREVIEW_MODE = true ;
var $btn = $( "button.generate[data-id='turn_track']" ) ;
var snippet = make_snippet( $btn, params, {}, false ).content ;
// update the preview
// NOTE: To minimize flickering, we load the snippet into a hidden <iframe>,
// then replace the preview <iframe> with it.
// FUDGE! We should be able to wait until the new iframe has finished loading before removing the old one,
// but for some inexplicable reason, the remove doesn't work on Windows, and the iframe's just build up :-/
// Instead, we remove the old iframe here, but by fiddling with opacity, we can avoid flicker. Sigh...
var style = $iframe.attr( "style" ) ;
style.opacity = 0 ;
var $newFrame = $( "<iframe></iframe>", { style: style, "data-seqno": ++iframeSeqNo } ) ;
$iframe.after( $newFrame ) ;
$iframe.remove() ;
$iframe = $newFrame ;
$newFrame.attr( "srcdoc", snippet ).on( "load", function() {
// update the state of each reinforcement flag
for ( var turnNo=1 ; turnNo <= _MAX_TURN_TRACK_TURNS ; ++turnNo ) {
for ( var playerNo=1 ; playerNo <= 2 ; ++playerNo )
updateFlag( turnNo, playerNo ) ;
}
// install the new <iframe>
$newFrame.attr( "id", "turn-track-preview" ).css( "opacity", 1 ) ;
// FUDGE! This works around a weird problem when we load a scenario with a vertical turn track
// and show it in a turn track dialog that was previously showing a horizontal turn track :-/
updateLayout() ;
} ) ;
return params ;
}
function onFlagClick( turnNo, playerNo ) {
// NOTE: This method gets called by a click handler in the snippet HTML.
// toggle the player turn reinforcements
if ( gTurnTrackReinforcements[playerNo][turnNo] )
delete gTurnTrackReinforcements[playerNo][turnNo] ;
else
gTurnTrackReinforcements[playerNo][turnNo] = true ;
$panel.find( "input[name='TURN_TRACK_REINFORCEMENTS_" + playerNo + "']" ).val(
Object.keys( gTurnTrackReinforcements[playerNo] ).join( "," )
) ;
updateFlag( turnNo, playerNo ) ;
}
function updateFlag( turnNo, playerNo ) {
// update the specified reinforcement flag
$iframe.contents().find( "#flag-" + turnNo + "_" + flipPlayerNo2(playerNo) ).css( {
opacity: gTurnTrackReinforcements && gTurnTrackReinforcements[playerNo][turnNo] ? 1 : 0.4,
} ) ;
}
function updateUI() {
// update the UI
updateLayout() ;
updatePreview( true ) ;
}
function updateLayout() {
// update the layout based on the direction of the turn track
if ( $dlg.find( "input[name='vertical']" ).prop( "checked" ) ) {
// vertical layout
$iframe.css( { position: "absolute",
top: "10px", height: "calc(100% - 20px)", left: "150px", width: "calc(100% - 160px)"
} ) ;
$dlg.addClass( "vert" ).removeClass( "horz" ) ;
$dlg.find( ".controls" ).addClass( "vert" ).removeClass( "horz" ) ;
$dlg.find( ".reset1" ).hide() ;
$dlg.find( ".reset2" ).show() ;
} else {
// horizontal layout
$iframe.css( { position: "absolute",
top: "95px", height: "calc(100% - 105px)", left: "10px", width: "calc(100% - 20px)"
} ) ;
$dlg.addClass( "horz" ).removeClass( "vert" ) ;
$dlg.find( ".controls" ).addClass( "horz" ).removeClass( "vert" ) ;
$dlg.find( ".reset1" ).show() ;
$dlg.find( ".reset2" ).hide() ;
}
}
// NOTE: Since Player 1 in the UI is Player 2 in the Turn Track template (by default),
// and vice versa, we often need to flip the player numbers.
function flipPlayerNo( playerNo ) { return parseInt(playerNo) === 1 ? 2 : 1 ; }
function flipPlayerNo2( playerNo ) {
if ( ! $panel.find("input[name='TURN_TRACK_SWAP_PLAYERS']").prop( "checked" ) )
playerNo = flipPlayerNo( playerNo ) ;
return playerNo ;
}
function makeCommaList( nVals ) {
// generate a comma-separated list of values
var vals = [] ;
for ( var i=1 ; i <= nVals ; ++i )
vals.push( i ) ;
return vals.join( "," ) ;
}
// show the TURN TRACK dialog
var $panel = $( "#panel-scenario" ) ;
$( "#turn-track" ).dialog( {
"title": "Turn track",
dialogClass: "turn-track",
modal: true,
minWidth: 500, minHeight: 250,
resizable: true,
create: function() {
// initialize the dialog
init_dialog( $(this), "OK", true ) ;
initTurnCountSelect2( $(this).find( "select[name='nturns']" ) ) ;
initWidthSelect2( $(this).find( "select[name='width']" ) ) ;
$(this).find( "button.reset" ).button().on(
"click", onResetControls
) ;
// keep the settings in the SCENARIO panel in sync with the dialog
$(this).find( "input[name='vertical']" ).on( "change", function() {
syncCheckbox( $(this), $panel.find("input[name='TURN_TRACK_VERTICAL']") ) ;
} ) ;
$(this).find( "input[name='swap-players']" ).on( "change", function() {
syncCheckbox( $(this), $panel.find("input[name='TURN_TRACK_SWAP_PLAYERS']") ) ;
} ) ;
// update the UI when the direction of the turn track is changed
$(this).find( "input[name='vertical']" ).on( "change", function() {
updateUI() ;
} ) ;
// handle clicks on reinforcement flags in the turn track preview
window.addEventListener( "message", function( evt ) {
if ( evt.data.type === "FlagClick" )
onFlagClick( evt.data.turnNo, flipPlayerNo2(evt.data.uiPlayerNo) ) ;
} ) ;
},
open: function() {
// initialize the dialog
var $btnPane = $( ".ui-dialog.turn-track .ui-dialog-buttonpane" ) ;
var $btn = $btnPane.find( "button.snippet" ) ;
$btn.prepend(
$( "<img src='" + gImagesBaseUrl+"/snippet.png" + "' style='height:0.9em;margin:0 0.35em -1px 0;'>" )
) ;
$btn.css( { position: "absolute", left: 5 } ) ;
// load the dialog
$dlg = $(this) ;
$iframe = $dlg.find( "iframe#turn-track-preview" ) ;
loadControls() ;
},
buttons: {
Snippet: { text:" Snippet", class: "snippet", click: function( evt ) {
var $btn = $( "button.generate[data-id='turn_track']" ) ;
generate_snippet( $btn, evt.shiftKey, null ) ;
} },
Close: function() { $(this).dialog( "close" ) ; },
},
} ) ;
}
// --------------------------------------------------------------------
function setTurnTrackNTurns( nTurns )
{
// select the specified number of turns
updateTurnTrackNTurns( nTurns ) ;
$( "select[name='TURN_TRACK_NTURNS']" ).val(
nTurns
).trigger( "change" ) ;
}
function updateTurnTrackNTurns( nTurns )
{
function makeExtraOption( val, caption ) {
return $( "<option class='extra' value='" + val + "'>" + caption + "</option>" ) ;
}
// initialize
var $sel = $( "select[name='TURN_TRACK_NTURNS']" ) ;
var $extra = $sel.find( "option.extra" ) ;
// check if the specified number of turns is already in the droplist
var $opt = $sel.find( "option[value='" + nTurns + "']" ) ;
if ( $opt.length > 0 ) {
// yup - check if it's the special extra entry
if ( ! $opt.hasClass( "extra" ) ) {
// nope - we don't need it any more
$extra.remove() ;
}
// check if the turn track has been disabled
if ( nTurns === "" ) {
// yup - add a special extra entry to open the turn track dialog
$sel.append( makeExtraOption( "(show-dialog)", "(more)" ) ) ;
}
} else {
// nope - add it as a special extra entry
if ( $extra.length > 0 ) {
// FUDGE! If the special entry is already there, we delete and re-create it to get the select2 to work :-/
$extra.remove() ;
}
var $opt2 = makeExtraOption( nTurns, nTurns ) ;
if ( nTurns < DEFAULT_TURN_TRACK_TURNS_MIN )
$sel.find( "option[value='']" ).after( $opt2 ) ;
else
$sel.append( $opt2 ) ;
}
}
function formatTurnTrackOption( opt ) {
// format the turn track <option> element
if ( opt.id === "(show-dialog)" )
return $( "<span style='font-size:80%;font-style:italic;color:#666;'>" + opt.text + "</span>" ) ;
if ( opt.text.substr( opt.text.length-2 ) === ".5" )
return $( "<span>" + opt.text.substr( 0, opt.text.length-2 ) + "&half;" + "</span>" ) ;
return opt.text ;
}

@ -63,10 +63,10 @@ function get_player_colors_for_element( $elem )
return get_player_colors( player_no ) ;
}
function make_player_flag_url( nat, for_snippet ) {
function make_player_flag_url( nat, for_snippet, force_local_image ) {
if ( ! gTemplatePack.nationalities )
return null ;
if ( for_snippet && gUserSettings["scenario-images-source"] == SCENARIO_IMAGES_SOURCE_INTERNET )
if ( for_snippet && gUserSettings["scenario-images-source"] == SCENARIO_IMAGES_SOURCE_INTERNET && !force_local_image )
return gAppConfig.ONLINE_IMAGES_URL_BASE + "/flags/" + nat + ".png" ;
else {
var url = "/flags/" + nat ;
@ -127,11 +127,17 @@ function copyToClipboard( val )
return ;
}
// FUDGE! If a dialog is open, the overlay will stop the copy command from working,
// so we attach the <textarea> to the dialog instead. Setting the z-index to something
// large is also supposed to work, but apparently not... :-/
var $topmost = findTopmostDialog() ;
var target = $topmost ? $topmost[0] : document.body ;
if ( document.queryCommandSupported && document.queryCommandSupported("copy") ) {
// create a textarea to hold the content
var textarea = document.createElement( "textarea" ) ;
textarea.style.position = "fixed" ; // prevent scrolling to bottom in MS Edge
document.body.appendChild( textarea ) ;
target.appendChild( textarea ) ;
textarea.textContent = val ;
// copy the textarea content to the clipboard
textarea.select() ;
@ -144,7 +150,7 @@ function copyToClipboard( val )
showErrorMsg( "Can't copy to the clipboard:<div class='pre'>" + escapeHTML(ex) + "</div>" ) ;
}
finally {
document.body.removeChild( textarea ) ;
target.removeChild( textarea ) ;
}
}
}

@ -160,6 +160,10 @@ function _generate_snippets()
vo_index[ data.vo_entry.id ] = true ;
snippet_id = template_id + "_" + player_no + "." + data.id ;
}
if ( template_id === "turn_track" ) {
if ( $( "select[name='TURN_TRACK_NTURNS']" ).val() === "" )
return ;
}
var raw_content = _get_raw_content( snippet_id, $btn, params ) ;
if ( ["scenario","players","victory_conditions"].indexOf( snippet_id ) === -1 ) {
// NOTE: We don't pass through a snippet for things that have no content,
@ -211,6 +215,8 @@ function _get_raw_content( snippet_id, $btn, params )
return get_values([ "SCENARIO_NAME", "SCENARIO_ID", "SCENARIO_LOCATION" ]) ;
if ( snippet_id === "victory_conditions" )
return get_values([ "VICTORY_CONDITIONS" ]) ;
if ( snippet_id === "turn_track" )
return true ;
if ( snippet_id === "players" ) {
return [
"ELR:", "SAN:",

@ -33,6 +33,7 @@
<link rel="stylesheet" type="text/css" href="{{url_for('static',filename='css/scenario-card.css')}}" />
<link rel="stylesheet" type="text/css" href="{{url_for('static',filename='css/scenario-upload-dialog.css')}}" />
<link rel="stylesheet" type="text/css" href="{{url_for('static',filename='css/scenario-downloads-dialog.css')}}" />
<link rel="stylesheet" type="text/css" href="{{url_for('static',filename='css/turn-track-dialog.css')}}" />
<link rel="stylesheet" type="text/css" href="{{url_for('static',filename='css/user-settings-dialog.css')}}" />
<link rel="stylesheet" type="text/css" href="{{url_for('static',filename='css/program-info.css')}}" />
<link rel="stylesheet" type="text/css" href="{{url_for('static',filename='css/please-wait.css')}}" />
@ -90,6 +91,7 @@
{%include "lfa.html"%}
{%include "lfa-upload.html"%}
{%include "turn-track-dialog.html"%}
{%include "user-settings-dialog.html"%}
{%include "program-info-dialog.html"%}
@ -162,6 +164,7 @@ gHelpUrl = "{{url_for('show_help')}}" ;
<script src="{{url_for('static',filename='lfa.js')}}"></script>
<script src="{{url_for('static',filename='lfa-upload.js')}}"></script>
<script src="{{url_for('static',filename='LogFileAnalysis.js')}}"></script>
<script src="{{url_for('static',filename='turn_track.js')}}"></script>
<script src="{{url_for('static',filename='roar.js')}}"></script>
<script src="{{url_for('static',filename='sortable.js')}}"></script>
<script src="{{url_for('static',filename='user_settings.js')}}"></script>

@ -24,9 +24,24 @@
<button class="generate" data-id="scenario">Snippet</button>
</span>
</div>
<div class="row" style="margin-top:0.5em;">
<div class="row" style="margin-top:0.25em;">
<label for="TURN_TRACK_NTURNS">Turns:</label>
<select name="TURN_TRACK_NTURNS" class="param"></select>
<div class="turn-track-controls small" style="display:none;align-items:center;">
<button id="turn-track-settings" title="Turn track settings"><img src="{{url_for('static',filename='images/menu/settings.png')}}"></button>
<button class="generate" data-id="turn_track">Snippet</button>
</div>
</div>
<div class="small" style="display:none;">
Width: <input type="text" name="TURN_TRACK_WIDTH" class="param" size="1">
<input type="checkbox" name="TURN_TRACK_VERTICAL" class="param"> Vertical </input> &nbsp;
R<sub>1</sub>: <input type="text" name="TURN_TRACK_REINFORCEMENTS_1" class="param" size=8">
R<sub>2</sub>: <input type="text" name="TURN_TRACK_REINFORCEMENTS_2" class="param" size=8">
<input type="checkbox" name="TURN_TRACK_SWAP_PLAYERS" class="param"> Swap players </input>
</div>
<div class="row" style="margin-top:-0.75em;">
<label></label>
<span style='width:12.9em'></span>
<span style='width:13.9em'></span>
<label class="header" for="ELR">ELR</label>
<span style='width:0.2em'></span>
<label class="header" for="SAN">SAN</label>

@ -0,0 +1,21 @@
<div id="turn-track" style="display:none;">
<div class="controls">
<div style="display:flex;flex-direction:column;">
<div class="row"> Turns: <select name="nturns"></select> </div>
<button class="reset reset1"> Reset </button>
</div>
<div style="display:flex;flex-direction:column;">
<div class="row"> Width: <select name="width"></select> </div>
<div> <input type="checkbox" name="vertical"> Vertical </input> </div>
<div> <input type="checkbox" name="swap-players"> Swap players </input> </div>
<button class="reset reset2"> Reset </button>
</div>
</div>
<iframe id="turn-track-preview"></iframe>
</div>

@ -12,7 +12,7 @@
"scen_date": "1945-12-31 00:00:00",
"theatre": "PTO",
"time_to_play": 1.25,
"min_turns": "6", "max_turns": "6",
"min_turns": "4", "max_turns": "5",
"oba": "B", "night": "1", "deluxe": "1", "aslsk": "1",
"author": "Joe Author",
"pub_name": "ASL Journal",

@ -0,0 +1,4 @@
TURN TRACK:
{% for row in TURN_TRACK_SQUARES %}
{% for turnSquare in row %}[{{turnSquare}}] {%endfor%}
{%endfor%}

@ -1,7 +1,7 @@
""" Test the help page. """
from vasl_templates.webapp.tests.utils import \
init_webapp, select_menu_option, find_child, find_children, wait_for, wait_for_elem
init_webapp, select_menu_option, find_children, wait_for, wait_for_elem, SwitchFrame
# ---------------------------------------------------------------------
@ -28,10 +28,7 @@ def test_help( webapp, webdriver ):
assert "tabs-help" in get_tabs()
# check what's in the help iframe
try:
# switch to the frame
webdriver.switch_to.frame( find_child( "#tabs-help iframe" ) )
with SwitchFrame( webdriver, "#tabs-help iframe" ):
# check that the content loaded OK
assert "everyone's favorite scenario" in webdriver.page_source
@ -41,8 +38,3 @@ def test_help( webapp, webdriver ):
assert elem.is_displayed()
wait_for( 2, lambda: "GNU AFFERO GENERAL PUBLIC LICENSE" in webdriver.page_source )
assert "Version 3" in webdriver.page_source
finally:
# switch back to the main window
webdriver.switch_to.default_content()

@ -381,7 +381,8 @@ def test_hotness_report( webapp, webdriver ):
report = {}
for key in ( "2s", "12s", "snipers" ):
report[ key ] = unload_table(
"//div[@class='hotness-popup']//table[@class='{}']//tr".format( key )
"//div[@class='hotness-popup']//table[@class='{}']/tbody".format( key ),
unload=True
)
return report
@ -692,5 +693,6 @@ def _select_log_file( fname ):
def _unload_table( sel ):
"""Unload chart data from an HTML table."""
return unload_table(
"//*[@class='{}']//table[@class='chart-data']//tr".format( sel )
"//*[@class='{}']//table[@class='chart-data']/tbody".format( sel ),
unload=True
)

@ -6,7 +6,7 @@ import io
import lxml.html
from vasl_templates.webapp.tests.utils import init_webapp, get_nationalities, wait_for, find_child
from vasl_templates.webapp.tests.utils import init_webapp, get_nationalities, SwitchFrame
# ---------------------------------------------------------------------
@ -83,13 +83,8 @@ def _get_nat_caps( webapp, webdriver, nat, theater, year, month ): #pylint: disa
# get the snippet
url = webapp.url_for( "get_national_capabilities", nat=nat, theater=theater, year=year, month=month )
webdriver.get( url )
iframe = find_child( "#results" )
wait_for( 2, iframe.is_displayed )
try:
webdriver.switch_to.frame( iframe )
with SwitchFrame( webdriver, "#results" ):
buf = webdriver.page_source
finally:
webdriver.switch_to.default_content()
# check if there is anything
if "Not available." in buf:

@ -14,6 +14,7 @@ from vasl_templates.webapp.tests.test_scenario_persistence import save_scenario,
from vasl_templates.webapp.tests.test_vassal import run_vassal_tests
from vasl_templates.webapp.tests.utils import init_webapp, select_tab, new_scenario, \
set_player, set_template_params, set_scenario_date, get_player_nat, get_theater, set_theater, \
get_turn_track_nturns, \
wait_for, wait_for_elem, find_child, find_children, get_css_classes, set_stored_msg, click_dialog_button
# ---------------------------------------------------------------------
@ -31,7 +32,7 @@ def test_scenario_cards( webapp, webdriver ):
"scenario_name": "Full content scenario", "scenario_id": "FCS-1",
"scenario_url": "https://aslscenarioarchive.com/scenario.php?id=1",
"scenario_location": "Some place", "scenario_date": "31st December, 1945",
"theater": "PTO", "turn_count": "6", "playing_time": "1\u00bc hours",
"theater": "PTO", "turn_count": "4-5", "playing_time": "1\u00bc hours",
"icons": [ "aslsk.png", "deluxe.png", "night.png", "oba.png" ],
"designer": "Joe Author",
"publication": "ASL Journal",
@ -87,6 +88,7 @@ def test_import_scenario( webapp, webdriver ):
wait_for( 2, lambda: _check_scenario(
SCENARIO_NAME="Full content scenario", SCENARIO_ID="FCS-1",
SCENARIO_LOCATION="Some place",
SCENARIO_TURNS="5",
PLAYER_1="dutch", PLAYER_1_DESCRIPTION="1st Dutch Army",
PLAYER_2="romanian", PLAYER_2_DESCRIPTION="1st Romanian Army",
THEATER="PTO"
@ -100,6 +102,7 @@ def test_import_scenario( webapp, webdriver ):
wait_for( 2, lambda: _check_scenario(
SCENARIO_NAME="Untitled scenario (#no-content)", SCENARIO_ID="",
SCENARIO_LOCATION="",
SCENARIO_TURNS="",
PLAYER_1="dutch", PLAYER_1_DESCRIPTION="",
PLAYER_2="romanian", PLAYER_2_DESCRIPTION="",
THEATER="ETO"
@ -115,6 +118,8 @@ def _check_scenario( **kwargs ):
return False
if get_theater() != kwargs[ "THEATER" ]:
return False
if get_turn_track_nturns() != kwargs["SCENARIO_TURNS"]:
return False
return True
# ---------------------------------------------------------------------

@ -12,7 +12,7 @@ from vasl_templates.webapp.tests.utils import \
get_stored_msg, set_stored_msg_marker, find_child, find_children, adjust_html, \
for_each_template, add_simple_note, edit_simple_note, \
get_sortable_entry_count, generate_sortable_entry_snippet, drag_sortable_entry_to_trash, \
new_scenario, set_scenario_date
new_scenario, set_scenario_date, set_turn_track_nturns
from vasl_templates.webapp.tests.test_scenario_persistence import load_scenario, load_scenario_params
# ---------------------------------------------------------------------
@ -232,6 +232,7 @@ def test_edit_templates( webapp, webdriver ):
# initialize
webapp.control_tests.set_vo_notes_dir( "{TEST}" )
init_webapp( webapp, webdriver, edit_template_links=1 )
set_turn_track_nturns( "3" )
ob_setups = {
1: find_child( "#ob_setups-sortable_1" ),
2: find_child( "#ob_setups-sortable_2" )

@ -11,7 +11,7 @@ from selenium.webdriver.common.by import By
from vasl_templates.webapp.utils import TempFile
from vasl_templates.webapp.tests.test_vehicles_ordnance import add_vo
from vasl_templates.webapp.tests.utils import \
select_tab, select_menu_option, set_player, \
select_tab, select_menu_option, set_player, set_turn_track_nturns, \
wait_for_clipboard, get_stored_msg, set_stored_msg, set_stored_msg_marker,\
add_simple_note, for_each_template, find_child, find_children, wait_for, \
get_player_nat, get_droplist_vals_index, init_webapp, get_css_classes
@ -26,6 +26,7 @@ def test_individual_files( webapp, webdriver ):
init_webapp( webapp, webdriver, template_pack_persistence=1 )
set_player( 1, "german" )
set_player( 2, "russian" )
set_turn_track_nturns( "3" )
# try uploading a customized version of each template
def test_template( template_id, orig_template_id ):
@ -55,6 +56,7 @@ def test_zip_files( webapp, webdriver ):
init_webapp( webapp, webdriver, template_pack_persistence=1 )
set_player( 1, "german" )
set_player( 2, "russian" )
set_turn_track_nturns( "3" )
# upload a template pack that contains a full set of templates
zip_data = make_zip_from_files( "full" )
@ -89,6 +91,7 @@ def test_new_default_template_pack( webapp, webdriver ):
.set_default_template_pack( "new-default/" ) \
.set_vo_notes_dir( "{TEST}" )
init_webapp( webapp, webdriver )
set_turn_track_nturns( "3" )
# check that the new templates are being used
_do_test_default_template_pack( webdriver )
@ -104,6 +107,7 @@ def test_new_default_template_pack_zip( webapp, webdriver ):
.set_default_template_pack( zip_data ) \
.set_vo_notes_dir( "{TEST}" )
init_webapp( webapp, webdriver )
set_turn_track_nturns( "3" )
# check that the new templates are being used
_do_test_default_template_pack( webdriver )

@ -0,0 +1,343 @@
""" Test the turn track functionality. """
from selenium.webdriver.support.ui import Select
from selenium.webdriver.common.keys import Keys
from vasl_templates.webapp.tests.utils import \
init_webapp, get_turn_track_nturns, set_turn_track_nturns, select_droplist_val, get_droplist_vals, \
SwitchFrame, unload_table, wait_for, wait_for_elem, wait_for_clipboard, find_child
from vasl_templates.webapp.tests.test_scenario_persistence import load_scenario, save_scenario
# ---------------------------------------------------------------------
def test_turn_track_basic( webapp, webdriver ):
"""Test basic turn track functionality."""
# initialize
webapp.control_tests.set_data_dir( "{REAL}" )
init_webapp( webapp, webdriver )
# check the initial state of the UI
assert get_turn_track_nturns() == ""
assert not find_child( "button#turn-track-settings" ).is_displayed()
assert not find_child( ".snippet-control[data-id='turn_track']" ).is_displayed()
# configure a number of turns
set_turn_track_nturns( 6 )
assert find_child( "button#turn-track-settings" ).is_displayed()
assert find_child( ".snippet-control[data-id='turn_track']" ).is_displayed()
# generate a snippet
assert _generate_turn_track_snippet( None ) == [
[ (1,None,None), (2,None,None), (3,None,None), (4,None,None), (5,None,None), (6,None,None) ]
]
# ---------------------------------------------------------------------
def test_turn_track_controls( webapp, webdriver ):
"""Test the basic controls for configuring the turn track."""
# initialize
webapp.control_tests.set_data_dir( "{REAL}" )
init_webapp( webapp, webdriver )
# show the turn track dialog
dlg = _show_turn_track_dialog( 6 )
with SwitchFrame( webdriver, "#turn-track-preview" ):
_click_reinf_flag( 1, 1 )
_click_reinf_flag( 2, 1 )
_click_reinf_flag( 2, 2 )
_click_reinf_flag( 3, 2 )
# change the width
_change_turn_track_width( dlg, 3 )
def check_for_width():
return _generate_turn_track_snippet( dlg ) == [
[ (1,"player1",None), (2,"player1","player2"), (3,None,"player2") ],
[ (4,None,None), (5,None,None), (6,None,None) ]
]
wait_for( 2, check_for_width )
# swap the players
_swap_turn_track_players( dlg )
def check_for_swap_players():
return _generate_turn_track_snippet( dlg ) == [
[ (1,None,"player2"), (2,"player1","player2"), (3,"player1",None) ],
[ (4,None,None), (5,None,None), (6,None,None) ]
]
wait_for( 2, check_for_swap_players )
# make the turn track vertical
_change_turn_track_direction( dlg )
def check_for_vertical():
return _generate_turn_track_snippet( dlg ) == [
[ (1,None,"player2"), (3,"player1",None), (5,None,None) ],
[ (2,"player1","player2"), (4,None,None), (6,None,None) ]
]
wait_for( 2, check_for_vertical )
# reset the controls
find_child( "button.reset2" ).click()
ask = wait_for_elem( 2, ".ui-dialog.ask" )
find_child( "button.ok", ask ).click()
def check_for_reset():
return _generate_turn_track_snippet( dlg ) == [
[ (1,None,None), (2,None,None), (3,None,None), (4,None,None), (5,None,None), (6,None,None) ]
]
wait_for( 2, check_for_reset )
# ---------------------------------------------------------------------
def test_turn_track_reinforcements( webapp, webdriver ):
"""Test configuring reinforcements on the turn track."""
# initialize
webapp.control_tests.set_data_dir( "{REAL}" )
init_webapp( webapp, webdriver )
# show the turn track dialog
dlg = _show_turn_track_dialog( 6.5 )
# turn on some reinforcements, then check the snippet
with SwitchFrame( webdriver, "#turn-track-preview" ):
_click_reinf_flag( 2, 1 )
_click_reinf_flag( 3, 1 )
_click_reinf_flag( 3, 2 )
_click_reinf_flag( 7, 1 )
assert _generate_turn_track_snippet( dlg ) == [
[ (1,None,None), (2,"player1",None), (3,"player1","player2"),
(4,None,None), (5,None,None) , (6,None,None), (7,"player1",None)
]
]
# turn off some reinforcements, turn some on, then check the snippet
with SwitchFrame( webdriver, "#turn-track-preview" ):
_click_reinf_flag( 2, 2 )
_click_reinf_flag( 3, 1 )
_click_reinf_flag( 3, 2 )
_click_reinf_flag( 5, 2 )
_click_reinf_flag( 7, 1 )
assert _generate_turn_track_snippet( dlg ) == [
[ (1,None,None), (2,"player1","player2"), (3,None,None),
(4,None,None), (5,None,"player2") , (6,None,None), (7,None,None)
]
]
# ---------------------------------------------------------------------
def test_turn_track_persistence( webapp, webdriver ):
"""Test saving and loading turn track settings."""
# initialize
webapp.control_tests.set_data_dir( "{REAL}" )
init_webapp( webapp, webdriver, scenario_persistence=1 )
# show the turn track dialog
load_scenario( {
"PLAYER_1": "japanese", "PLAYER_2": "american",
"TURN_TRACK": { "NTURNS": 6.5 },
} )
dlg = _show_turn_track_dialog( None )
# configure the turn track
with SwitchFrame( webdriver, "#turn-track-preview" ):
_click_reinf_flag( 1, 1 )
_click_reinf_flag( 2, 2 )
_click_reinf_flag( 3, 1 )
_click_reinf_flag( 3, 2 )
_change_turn_track_width( dlg, 4 )
_swap_turn_track_players( dlg )
_change_turn_track_direction( dlg )
# check the snippet
expected = [
[ (1,None,"player2"), (3,"player1","player2"), (5,None,None), (7,None,None) ],
[ (2,"player1",None) , (4,None,None), (6,None,None) ]
]
wait_for( 2,
lambda: _generate_turn_track_snippet( dlg ) == expected
)
# save the scenario
dlg.send_keys( Keys.ESCAPE )
saved_scenario = save_scenario()
assert saved_scenario["TURN_TRACK"] == {
"NTURNS": "6.5",
"WIDTH": "4", "VERTICAL": True, "SWAP_PLAYERS": True,
"REINFORCEMENTS_1": "2,3", "REINFORCEMENTS_2": "1,3",
}
assert _generate_turn_track_snippet( None ) == expected
# reset the scenario
webdriver.refresh()
assert not find_child( "button.generate[data-id='turn_track']" ).is_displayed()
# load the scenario and generate the snippet
load_scenario( saved_scenario )
assert _generate_turn_track_snippet( None ) == expected
# open the turn track dialog and check that the controls were loaded correctly
dlg = _show_turn_track_dialog( None )
assert get_turn_track_nturns() == "6.5"
sel = Select( find_child( "select[name='width']", dlg ) )
assert sel.first_selected_option.get_attribute( "value" ) == "4"
assert find_child( "input[name='vertical']" ).is_selected()
assert find_child( "input[name='swap-players']" ).is_selected()
assert _generate_turn_track_snippet( dlg ) == expected
# ---------------------------------------------------------------------
def test_turn_track_droplist( webapp, webdriver ):
"""Test updating entries in the turn track droplist (#turns)."""
# initialize
webapp.control_tests.set_data_dir( "{REAL}" )
init_webapp( webapp, webdriver, scenario_persistence=1 )
# check the initial state of the droplist
assert _unload_turn_track_droplist() == [
"", "6", "6.5", "7", "7.5", "8", "8.5", "9", "9.5", "10", "(show-dialog)"
]
# configure a number of turns that is not in the default list (small)
sel = Select( find_child( "select[name='TURN_TRACK_NTURNS']" ) )
select_droplist_val( sel, "(show-dialog)" )
sel2 = Select( find_child( "#turn-track select[name='nturns']" ) )
select_droplist_val( sel2, "2" )
assert _unload_turn_track_droplist() == [
"", "2", "6", "6.5", "7", "7.5", "8", "8.5", "9", "9.5", "10"
]
# configure a number of turns that is not in the default list (large)
select_droplist_val( sel2, "14.5" )
expected = [ "", "6", "6.5", "7", "7.5", "8", "8.5", "9", "9.5", "10", "14.5" ]
assert _unload_turn_track_droplist() == expected
# save and reload the scenario
find_child( ".ui-dialog.turn-track" ).send_keys( Keys.ESCAPE )
saved_scenario = save_scenario()
webdriver.refresh()
load_scenario( saved_scenario )
assert _unload_turn_track_droplist() == expected
# configure a number of turns that is in the default list
sel = Select( find_child( "select[name='TURN_TRACK_NTURNS']" ) )
select_droplist_val( sel, "8" )
assert _unload_turn_track_droplist() == [
"", "6", "6.5", "7", "7.5", "8", "8.5", "9", "9.5", "10"
]
# disable the turn track
select_droplist_val( sel, "" )
assert _unload_turn_track_droplist() == [
"", "6", "6.5", "7", "7.5", "8", "8.5", "9", "9.5", "10", "(show-dialog)"
]
# ---------------------------------------------------------------------
def _show_turn_track_dialog( nturns ):
"""Show the TURN TRACK dialog."""
if nturns:
set_turn_track_nturns( nturns )
btn = wait_for_elem( 2, "button#turn-track-settings" )
btn.click()
dlg = wait_for_elem( 2, ".ui-dialog.turn-track" )
return dlg
def _generate_turn_track_snippet( dlg ):
"""Generate a turn track snippet."""
# generate the snippet
btn = find_child( "button.snippet", dlg ) if dlg else find_child( "button.generate[data-id='turn_track']" )
assert btn.is_displayed()
btn.click()
clipboard = wait_for_clipboard( 2, "<!-- vasl-templates:id", contains=True )
def get_reinforce_class( cell ):
cell_class = str( cell.xpath( ".//@class" )[0] )
if cell_class == "reinforce1":
return "player1"
elif cell_class == "reinforce2":
return "player2"
else:
assert cell_class == "no-reinforce"
return None
def unload_square( square ):
cells = square.xpath( ".//td" )
assert len(cells) == 3
return (
int( cells[1].text ),
get_reinforce_class( cells[0] ),
get_reinforce_class( cells[2] ),
)
# unload the snippet contents
squares = unload_table( "//table[@class='turn-track']", html=clipboard, unload=False )
for row_no, row in enumerate(squares):
for col_no, square in enumerate(row):
squares[row_no][col_no] = unload_square( square )
return squares
def _unload_turn_track_droplist():
"""Get the available options in the turn track droplist."""
keys = []
prev_key = None
options = get_droplist_vals( Select(
find_child( "select[name='TURN_TRACK_NTURNS']" )
) )
for key, caption in options:
if key == "":
assert caption == "-"
elif key == "(show-dialog)":
assert caption == "(more)"
else:
assert key == caption
key2 = float( key )
if prev_key:
assert key2 > prev_key
assert int( 10 * key2 ) % 10 in (0,5)
prev_key = key2
keys.append( key )
return keys
def _click_reinf_flag( turn_no, player_no ):
"""Click on a reinforcement flag."""
find_child(
"#flag-{}_{} .click".format( turn_no, player_no )
).click()
# ---------------------------------------------------------------------
def _change_turn_track_width( dlg, width ):
"""Change the turn track width."""
sel = Select( find_child( "select[name='width']", dlg ) )
_wait_for_preview( dlg,
lambda: select_droplist_val( sel, width )
)
def _change_turn_track_direction( dlg ):
"""Toggle the direction of the turn track."""
_wait_for_preview( dlg,
lambda: find_child( "input[name='vertical']", dlg ).click()
)
def _swap_turn_track_players( dlg ):
"""Swap the turn track players."""
_wait_for_preview( dlg,
lambda: find_child( "input[name='swap-players']", dlg ).click()
)
def _wait_for_preview( dlg, func ):
"""Make a change to the preview, and wait for the UI to update."""
# NOTE: The preview <iframe> is replaced with a new one, so we need to be ready for the old one to disappear.
iframe = find_child( "iframe", dlg )
seqno = iframe.get_attribute( "data-seqno" )
func()
def check_seqno():
iframe = find_child( "iframe", dlg )
if not iframe:
return False
return iframe.get_attribute( "data-seqno" ) != seqno
wait_for( 20, check_seqno )

@ -24,6 +24,7 @@ _STD_TEMPLATES = {
"scenario": [
"scenario", "players", "victory_conditions", "scenario_notes", "ssr",
"nat_caps_1", # nb: "nat_caps_2" is functionally the same as this
"turn_track", # nb: this will only be used if a turn count has been specified
],
"ob1": [ "ob_setup_1", "ob_note_1",
"ob_vehicles_1", "ob_vehicle_note_1", "ob_vehicles_ma_notes_1",
@ -83,6 +84,10 @@ def _wait_for_webapp():
def for_each_template( func ): #pylint: disable=too-many-branches
"""Test each template."""
# initialize
sel = Select( find_child( "#tabs-scenario select[name='TURN_TRACK_NTURNS']" ) )
has_turn_track = sel.first_selected_option.get_attribute( "value" ) != ""
# generate a list of all the templates we need to test
templates_to_test = set()
dname = os.path.join( os.path.split(__file__)[0], "../data/default-template-pack" )
@ -90,6 +95,8 @@ def for_each_template( func ): #pylint: disable=too-many-branches
fname,extn = os.path.splitext( fname )
if extn != ".j2":
continue
if fname == "turn_track" and not has_turn_track:
continue
if fname == "ob_vo":
templates_to_test.update( [ "ob_vehicles", "ob_ordnance" ] )
elif fname == "ob_vo_note":
@ -102,6 +109,8 @@ def for_each_template( func ): #pylint: disable=too-many-branches
# test the standard templates
for tab_id,template_ids in _STD_TEMPLATES.items():
for template_id in template_ids:
if template_id == "turn_track" and not has_turn_track:
continue
select_tab( tab_id )
orig_template_id = template_id
if template_id == "scenario_notes":
@ -315,6 +324,16 @@ def get_theater():
sel = Select( find_child( "select[name='SCENARIO_THEATER']" ) )
return sel.first_selected_option.get_attribute( "value" )
def set_turn_track_nturns( nturns ):
"""Set the number of turns in the scenario (to enable the turn track)."""
elem = find_child( "#tabs-scenario select[name='TURN_TRACK_NTURNS']" )
select_droplist_val( Select(elem), nturns )
def get_turn_track_nturns():
"""Get the number of turns in the scenario."""
sel = Select( find_child( "#tabs-scenario select[name='TURN_TRACK_NTURNS']" ) )
return sel.first_selected_option.get_attribute( "value" )
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
_nationalities = None
@ -600,28 +619,53 @@ def wait_for_clipboard( timeout, expected, contains=None, transform=None ):
# ---------------------------------------------------------------------
def unload_table( xpath ):
class SwitchFrame:
"""Helper class to switch frames in the browser window."""
def __init__( self, webdriver, iframe ):
self.webdriver = webdriver
self.iframe = iframe
def __enter__( self ):
if isinstance( self.iframe, str ):
self.iframe = wait_for( 5, lambda: find_child( self.iframe ) )
wait_for( 2, self.iframe.is_displayed )
self.webdriver.switch_to.frame( self.iframe )
return self
def __exit__( self, *args ):
self.webdriver.switch_to.default_content()
# ---------------------------------------------------------------------
def unload_table( xpath, unload, html=None ):
"""Unload data from an HTML table."""
# NOTE: Extracting table data using Selenium is extremely slow, we use lxml for the win!
# initialize
doc = lxml.html.fromstring( html or _webdriver.page_source )
elems = doc.xpath( xpath )
if not elems:
return []
assert len(elems) == 1
table = elems[0]
def unload_cells( cells ):
"""Unload cell data from a table row."""
return [ "" if c.text is None else c.text.strip() for c in cells ]
# unload the table data
results = []
doc = lxml.html.fromstring( _webdriver.page_source )
for row in doc.xpath( xpath ):
for row in table.xpath( "./tr" ):
if not results:
# we check for <th> in the first row only
cells = list( row.xpath( ".//th" ) )
cells = list( row.xpath( "./th" ) )
if cells:
results.append( unload_cells( cells ) )
continue
# extract the next row
cells = row.xpath( ".//td" )
results.append( unload_cells( cells ) )
cells = row.xpath( "./td" )
if unload:
cells = unload_cells( cells )
results.append( cells )
return results

Loading…
Cancel
Save