Allow images of snippets to be created.

master
Pacman Ghost 5 years ago
parent 0f85f9dfca
commit ffed68d236
  1. 6
      vasl_templates/main_window.py
  2. 11
      vasl_templates/web_channel.py
  3. 30
      vasl_templates/webapp/snippets.py
  4. 4
      vasl_templates/webapp/static/css/snippets.css
  5. 9
      vasl_templates/webapp/static/main.js
  6. 7
      vasl_templates/webapp/static/simple_notes.js
  7. 96
      vasl_templates/webapp/static/snippets.js
  8. 2
      vasl_templates/webapp/static/vassal.js
  9. 7
      vasl_templates/webapp/static/vo.js
  10. 3
      vasl_templates/webapp/templates/index.html
  11. 4
      vasl_templates/webapp/templates/snippets.html
  12. 1
      vasl_templates/webapp/templates/testing.html
  13. 14
      vasl_templates/webapp/tests/remote.py
  14. 88
      vasl_templates/webapp/tests/test_snippets.py
  15. 10
      vasl_templates/webapp/webdriver.py

@ -294,3 +294,9 @@ class MainWindow( QWidget ):
def on_scenario_name_change( self, val ):
"""Update the main window title to show the scenario name."""
self._web_channel_handler.on_scenario_name_change( val )
@pyqtSlot( str )
@catch_exceptions( caption="SLOT EXCEPTION" )
def on_snippet_image( self, img_data ):
"""Called when a snippet image has been generated."""
self._web_channel_handler.on_snippet_image( img_data )

@ -1,6 +1,10 @@
""" Web channel handler. """
import os
import base64
from PyQt5.QtWidgets import QApplication
from PyQt5.QtGui import QImage
from vasl_templates.webapp.config.constants import APP_NAME
from vasl_templates.file_dialog import FileDialog
@ -43,6 +47,13 @@ class WebChannelHandler:
"{} - {}".format( APP_NAME, val ) if val else APP_NAME
)
def on_snippet_image( self, img_data ): #pylint: disable=no-self-use
"""Called when a snippet image has been generated."""
# NOTE: We could maybe add an HTML object to the clipboard as well, but having two formats on the clipboard
# simultaneously might confuse some programs, causing problems for no real benefit :shrug:
img = QImage.fromData( base64.b64decode( img_data ) )
QApplication.clipboard().setImage( img )
def load_vsav( self ):
"""Called when the user wants to load a VASL scenario to update."""
data = self.updated_vsav_file_dialog.load_file( True )

@ -5,12 +5,15 @@ import json
import re
import zipfile
import io
import base64
from flask import request, jsonify, send_file, abort
from PIL import Image
from vasl_templates.webapp import app
from vasl_templates.webapp.utils import SimpleError
from vasl_templates.webapp.config.constants import DATA_DIR
from vasl_templates.webapp.webdriver import WebDriver
default_template_pack = None
@ -107,6 +110,33 @@ def _do_get_template_pack( dname ):
# ---------------------------------------------------------------------
last_snippet_image = None # nb: for the test suite
@app.route( "/snippet-image", methods=["POST"] )
def make_snippet_image():
"""Generate an image for a snippet."""
# Kathmandu, Nepal (DEC/18)
# generate an image for the snippet
snippet = request.data.decode( "utf-8" )
try:
with WebDriver() as webdriver:
img = webdriver.get_snippet_screenshot( None, snippet )
except SimpleError as ex:
return "ERROR: {}".format( ex )
# get the image data
buf = io.BytesIO()
img.save( buf, format="PNG" )
buf.seek( 0 )
img_data = buf.read()
global last_snippet_image
last_snippet_image = img_data
return base64.b64encode( img_data )
# ---------------------------------------------------------------------
@app.route( "/flags/<nat>" )
def get_flag( nat ):
"""Get a flag image."""

@ -0,0 +1,4 @@
.ui-dialog.make-snippet-image .ui-dialog-titlebar { display: none ; }
#make-snippet-image { display: flex ; align-items: center ; }
#make-snippet-image img { margin-right: 1em ; }

@ -17,6 +17,8 @@ var NATIONALITY_SPECIFIC_BUTTONS = {
"british": [ "piat" ],
} ;
GENERATE_SNIPPET_HINT = "Generate an HTML snippet (shift-click to get an image)." ;
// --------------------------------------------------------------------
$(document).ready( function () {
@ -405,9 +407,10 @@ function init_snippet_button( $btn )
var $newBtn = $( buf.join("") ) ;
$newBtn.find( "button" ).prepend(
$( "<img src='" + gImagesBaseUrl + "/snippet.png'>" )
).click( function() {
generate_snippet( $(this), null ) ;
} ).attr( "title", "Generate a snippet." ) ;
).click( function( evt ) {
generate_snippet( $(this), evt, null ) ;
return false ;
} ).attr( "title", GENERATE_SNIPPET_HINT ) ;
// add in the droplist
$newBtn.controlgroup() ;

@ -127,7 +127,7 @@ function _make_simple_note( note_type, caption )
var note_type0 = note_type.substring( 0, note_type.length-1 ) ;
buf.push(
"<img src='" + gImagesBaseUrl + "/snippet.png'",
" class='snippet' data-id='" + note_type0 + "' title='Generate a snippet.'>"
" class='snippet' data-id='" + note_type0 + "' title='" + GENERATE_SNIPPET_HINT + "'>"
) ;
}
buf.push( caption, "</div>" ) ;
@ -138,9 +138,10 @@ function _make_simple_note( note_type, caption )
) ;
// add a handler for the snippet button
$content.children("img.snippet").click( function() {
$content.children("img.snippet").click( function( evt ) {
var extra_params = get_simple_note_snippet_extra_params( $(this) ) ;
generate_snippet( $(this), extra_params ) ;
generate_snippet( $(this), evt, extra_params ) ;
return false ;
} ) ;
return $content ;

@ -21,16 +21,62 @@ var gScenarioCreatedTime = null ;
// --------------------------------------------------------------------
function generate_snippet( $btn, extra_params )
function generate_snippet( $btn, evt, extra_params )
{
// generate the snippet
var template_id = $btn.data( "id" ) ;
var params = unload_snippet_params( true, template_id ) ;
var snippet = make_snippet( $btn, params, extra_params, true ) ;
// check if the user is requesting the snippet as an image
if ( evt.shiftKey ) {
// yup - send the snippet to the backend to generate the image
var $dlg = $( "#make-snippet-image" ).dialog( {
dialogClass: "make-snippet-image",
modal: true,
width: 300,
height: 60,
resizable: false,
closeOnEscape: false,
} ) ;
$.ajax( {
url: gMakeSnippetImageUrl,
type: "POST",
data: snippet.content,
contentType: "text/html",
} ).done( function( resp ) {
$dlg.dialog( "close" ) ;
if ( resp.substr( 0, 6 ) === "ERROR:" ) {
showErrorMsg( resp.substr(7) ) ;
return ;
}
if ( getUrlParam( "snippet_image_persistence" ) ) {
// FOR TESTING PORPOISES! We can't control a file download from Selenium (since
// the browser will use native controls), so we store the result in a <textarea>
// and the test suite will collect it from there).
var fname = _make_snippet_image_filename( snippet ) ;
$("#_snippet-image-persistence_").val( fname + "|" + resp ) ;
return ;
}
if ( gWebChannelHandler ) {
// if we are running inside the PyQt wrapper, let it copy the image to the clipbaord
gWebChannelHandler.on_snippet_image( resp, function() {
showInfoMsg( "The snippet image was copied to the clipboard." ) ;
} ) ;
} else {
// otherwise let the user download the generated image
download( atob(resp), _make_snippet_image_filename(snippet), "image/png" ) ;
}
} ).fail( function( xhr, status, errorMsg ) {
$dlg.dialog( "close" ) ;
showErrorMsg( "Can't get the snippet image:<div class='pre'>" + escapeHTML(errorMsg) + "</div>" ) ;
} ) ;
return ;
}
// copy the snippet to the clipboard
try {
copyToClipboard( snippet ) ;
copyToClipboard( snippet.content ) ;
}
catch( ex ) {
showErrorMsg( "Can't copy to the clipboard:<div class'pre'>" + escapeHTML(ex) + "</div>" ) ;
@ -43,6 +89,7 @@ function make_snippet( $btn, params, extra_params, show_date_warnings )
{
// initialize
var template_id = $btn.data( "id" ) ;
var snippet_save_name = null ;
// set player-specific parameters
var player_no = get_player_no_for_element( $btn ) ;
@ -71,19 +118,23 @@ function make_snippet( $btn, params, extra_params, show_date_warnings )
template_id = "ob_vehicles" ;
params.OB_VEHICLES = params.OB_VEHICLES_1 ;
params.OB_VEHICLES_WIDTH = params.OB_VEHICLES_WIDTH_1 ;
snippet_save_name = params.PLAYER_1 + " vehicles" ;
} else if ( template_id === "ob_vehicles_2" ) {
template_id = "ob_vehicles" ;
params.OB_VEHICLES = params.OB_VEHICLES_2 ;
params.OB_VEHICLES_WIDTH = params.OB_VEHICLES_WIDTH_2 ;
snippet_save_name = params.PLAYER_2 + " vehicles" ;
}
if ( template_id === "ob_ordnance_1" ) {
template_id = "ob_ordnance" ;
params.OB_ORDNANCE = params.OB_ORDNANCE_1 ;
params.OB_ORDNANCE_WIDTH = params.OB_ORDNANCE_WIDTH_1 ;
snippet_save_name = params.PLAYER_1 + " ordnance" ;
} else if ( template_id === "ob_ordnance_2" ) {
template_id = "ob_ordnance" ;
params.OB_ORDNANCE = params.OB_ORDNANCE_2 ;
params.OB_ORDNANCE_WIDTH = params.OB_ORDNANCE_WIDTH_2 ;
snippet_save_name = params.PLAYER_2 + " ordnance" ;
}
// set vehicle/ordnance note parameters
@ -92,6 +143,7 @@ function make_snippet( $btn, params, extra_params, show_date_warnings )
var key = (vo_type === "vehicles") ? "VEHICLE" : "ORDNANCE" ;
params[ key + "_NAME" ] = data.vo_entry.name ;
params[ key + "_NOTE_URL" ] = data.vo_note_url ;
snippet_save_name = data.vo_entry.name ;
}
if ( template_id === "ob_vehicle_note" )
set_vo_note( "vehicles" ) ;
@ -144,7 +196,7 @@ function make_snippet( $btn, params, extra_params, show_date_warnings )
get_ma_notes( "ordnance", 1, "OB_ORDNANCE_MA_NOTES_1" ) ;
get_ma_notes( "vehicles", 2, "OB_VEHICLES_MA_NOTES_2" ) ;
get_ma_notes( "ordnance", 2, "OB_ORDNANCE_MA_NOTES_2" ) ;
function set_params( vo_type, player_no ) {
function set_ma_notes_params( vo_type, player_no ) {
template_id = "ob_" + vo_type + "_ma_notes" ;
var vo_type_uc = vo_type.toUpperCase() ;
var postfixes = [ "MA_NOTES", "MA_NOTES_WIDTH", "EXTRA_MA_NOTES", "EXTRA_MA_NOTES_CAPTION" ] ;
@ -152,15 +204,16 @@ function make_snippet( $btn, params, extra_params, show_date_warnings )
var stem = "OB_" + vo_type_uc + "_" + postfixes[i] ;
params[ stem ] = params[ stem + "_" + player_no ] ;
}
snippet_save_name = params["PLAYER_"+player_no] + (vo_type === "vehicles" ? " vehicle notes" : " ordnance notes") ;
}
if ( template_id === "ob_vehicles_ma_notes_1" )
set_params( "vehicles", 1 ) ;
set_ma_notes_params( "vehicles", 1 ) ;
else if ( template_id === "ob_ordnance_ma_notes_1" )
set_params( "ordnance", 1 ) ;
set_ma_notes_params( "ordnance", 1 ) ;
else if ( template_id === "ob_vehicles_ma_notes_2" )
set_params( "vehicles", 2 ) ;
set_ma_notes_params( "vehicles", 2 ) ;
else if ( template_id === "ob_ordnance_ma_notes_2" )
set_params( "ordnance", 2 ) ;
set_ma_notes_params( "ordnance", 2 ) ;
// include the player display names and flags
params.PLAYER_1_NAME = get_nationality_display_name( params.PLAYER_1 ) ;
@ -252,14 +305,14 @@ function make_snippet( $btn, params, extra_params, show_date_warnings )
// get the template to generate the snippet from
var templ = get_template( template_id, true ) ;
if ( templ === null )
return "" ;
return { content: "[error: can't find template]" } ;
var func ;
try {
func = jinja.compile( templ ).render ;
}
catch( ex ) {
showErrorMsg( "Can't compile template:<div class='pre'>" + escapeHTML(ex) + "</div>" ) ;
return "[error: can't compile template]" ;
return { content: "[error: can't compile template]" } ;
}
// process the template
@ -280,13 +333,18 @@ function make_snippet( $btn, params, extra_params, show_date_warnings )
}
catch( ex ) {
showErrorMsg( "Can't process template: <span class='pre'>" + template_id + "</span><div class='pre'>" + escapeHTML(ex) + "</div>" ) ;
return "[error: can't process template'" ;
return { content: "[error: can't process template'" } ;
}
// fixup any user file URL's
snippet = snippet.replace( "{{USER_FILES}}", APP_URL_BASE + "/user" ) ;
return snippet ;
return {
content: snippet,
template_id: template_id,
snippet_id: params.SNIPPET_ID,
save_name: snippet_save_name,
} ;
}
function get_vo_note_key( vo_entry )
@ -414,6 +472,22 @@ function sort_ma_notes_keys( nat, keys )
return keys ;
}
function _make_snippet_image_filename( snippet )
{
// generate the save filename for a generated snippet image
var fname = snippet.save_name ;
if ( ! snippet.save_name ) {
// no save filename was specified, generate one automatically
fname = snippet.snippet_id ;
if ( fname.substr( 0, 7 ) === "extras/" )
fname = fname.substr( 7 ) ;
fname = fname.replace( /_|-/g, " " ) ;
// handle characters that are not allowed in filenames
fname = fname.replace( /:|\||\//g, "-" ) ;
}
return fname + ".png" ;
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
function unload_snippet_params( unpack_scenario_date, template_id )

@ -206,7 +206,7 @@ function _generate_snippets()
}
}
snippets[snippet_id] = {
content: make_snippet( $btn, params, extra_params, false ),
content: make_snippet( $btn, params, extra_params, false ).content,
auto_create: ! no_autocreate[template_id] && ! inactive,
} ;
if ( raw_content !== true )

@ -175,7 +175,7 @@ function do_add_vo( vo_type, player_no, vo_entry, vo_image_id, custom_capabiliti
var template_id = (vo_type === "vehicles") ? "ob_vehicle_note" : "ob_ordnance_note" ;
buf.push(
"<img src='" + gImagesBaseUrl + "/snippet.png'",
" class='snippet' data-id='" + template_id + "' title='Generate a snippet.'>"
" class='snippet' data-id='" + template_id + "' title='" + GENERATE_SNIPPET_HINT + "'>"
) ;
data.vo_note_url = APP_URL_BASE + "/" + vo_type + "/" + vo_nat + "/note/" + vo_note_key ;
}
@ -188,8 +188,9 @@ function do_add_vo( vo_type, player_no, vo_entry, vo_image_id, custom_capabiliti
update_vo_sortable2_entry( $entry ) ;
// add a handler for the snippet button
$content.children("img.snippet").click( function() {
generate_snippet( $(this), {} ) ;
$content.children("img.snippet").click( function( evt ) {
generate_snippet( $(this), evt, {} ) ;
return false ;
} ) ;
}

@ -23,6 +23,7 @@
<link rel="stylesheet" type="text/css" href="{{url_for('static',filename='css/select-vo-dialog.css')}}" />
<link rel="stylesheet" type="text/css" href="{{url_for('static',filename='css/edit-vo-dialog.css')}}" />
<link rel="stylesheet" type="text/css" href="{{url_for('static',filename='css/vassal.css')}}" />
<link rel="stylesheet" type="text/css" href="{{url_for('static',filename='css/snippets.css')}}" />
<link rel="stylesheet" type="text/css" href="{{url_for('static',filename='css/user-settings-dialog.css')}}" />
</head>
@ -64,6 +65,7 @@
{%include "edit-vo-dialog.html"%}
{%include "vassal.html"%}
{%include "snippets.html"%}
{%include "user-settings-dialog.html"%}
@ -97,6 +99,7 @@ gVehicleNotesUrl = "{{url_for('get_vehicle_notes')}}" ;
gOrdnanceNotesUrl = "{{url_for('get_ordnance_notes')}}" ;
gGetVaslPieceInfoUrl = "{{url_for('get_vasl_piece_info')}}" ;
gUpdateVsavUrl = "{{url_for('update_vsav')}}" ;
gMakeSnippetImageUrl = "{{url_for('make_snippet_image')}}" ;
gHelpUrl = "{{url_for('show_help')}}" ;
</script>

@ -0,0 +1,4 @@
<div id="make-snippet-image" style="display:none;">
<img src="{{url_for('static',filename='images/loader.gif')}}">
Generating the snippet image...
</div>

@ -6,3 +6,4 @@
<textarea id="_template-pack-persistence_" style="display:none;"></textarea>
<textarea id="_scenario-persistence_" style="display:none;"></textarea>
<textarea id="_vsav-persistence_" style="display:none;"></textarea>
<textarea id="_snippet-image-persistence_" style="display:none;"></textarea>

@ -72,6 +72,13 @@ class ControlTests:
else:
return json.loads( resp.decode( "utf-8" ) )
def _get_app_config( self ): #pylint: disable=no-self-use
"""Get the webapp config."""
return {
k: v for k,v in app.config.items()
if isinstance( v, (str,int,bool,list,dict) )
}
def _set_data_dir( self, dtype=None ):
"""Set the webapp's data directory."""
if dtype == "real":
@ -257,3 +264,10 @@ class ControlTests:
_logger.info( "Setting user files: %s", dname )
app.config["USER_FILES_DIR"] = dname
return self
def _get_last_snippet_image( self ): #pylint: disable=no-self-use
"""Get the last snippet image generated."""
from vasl_templates.webapp.snippets import last_snippet_image
assert last_snippet_image
_logger.info( "Returning the last snippet image: #bytes=%d", len(last_snippet_image) )
return base64.b64encode( last_snippet_image ).decode( "utf-8" )

@ -1,9 +1,10 @@
""" Test HTML snippet generation. """
from selenium.webdriver.common.action_chains import ActionChains
from selenium.webdriver.common.keys import Keys
from vasl_templates.webapp.tests.utils import \
init_webapp, select_tab, select_tab_for_elem, set_template_params, wait_for_clipboard, \
init_webapp, select_tab, select_tab_for_elem, set_template_params, wait_for, wait_for_clipboard, \
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, \
@ -307,6 +308,91 @@ def test_edit_templates( webapp, webdriver ):
# ---------------------------------------------------------------------
def test_snippet_images( webapp, webdriver ):
"""Test generating snippet images."""
# initialize
control_tests = init_webapp( webapp, webdriver, scenario_persistence=1, snippet_image_persistence=1,
reset = lambda ct: ct.set_vo_notes_dir( dtype="test" )
)
# check if there is a webdriver configured
if "WEBDRIVER_PATH" not in control_tests.get_app_config():
return
# load a test scenario
load_scenario( {
"PLAYER_1": "german", "PLAYER_2": "russian",
"SCENARIO_NAME": "test scenario", "SCENARIO_DATE": "1940-01-01", "SCENARIO_LOCATION": "somewhere",
"SCENARIO_NOTES": [ { "caption": "Scenario note #1" } ],
"VICTORY_CONDITIONS": "win at all costs!",
"SSR": [ "a test ssr" ],
"OB_SETUPS_1": [ { "caption": "OB setup note #1" } ],
"OB_NOTES_1": [ { "caption": "OB note #1" } ],
"OB_VEHICLES_1": [ { "name": "a german vehicle" } ],
"OB_ORDNANCE_1": [ { "name": "a german ordnance" } ],
"OB_SETUPS_2": [ { "caption": "OB setup note #2" } ],
"OB_NOTES_2": [ { "caption": "OB note #2" } ],
"OB_VEHICLES_2": [ { "name": "a russian vehicle" } ],
"OB_ORDNANCE_2": [ { "name": "a russian ordnance" } ],
} )
def do_test( snippet_btn, expected_fname ): #pylint: disable=missing-docstring
# clear the return buffer
ret_buffer = find_child( "#_snippet-image-persistence_" )
assert ret_buffer.tag_name == "textarea"
webdriver.execute_script( "arguments[0].value = arguments[1]", ret_buffer, "" )
# shift-click the snippet button
ActionChains( webdriver ) \
.key_down( Keys.SHIFT ) \
.click( snippet_btn ) \
.key_up( Keys.SHIFT ) \
.perform()
# wait for the snippet image to be generated
wait_for( 20, lambda: ret_buffer.get_attribute( "value" ) )
fname, img_data = ret_buffer.get_attribute( "value" ).split( "|", 1 )
# check the results
assert fname == expected_fname
last_snippet_image = control_tests.get_last_snippet_image()
assert img_data == last_snippet_image
def do_simple_test( template_id, expected_fname ): #pylint: disable=missing-docstring
btn = find_child( "button.generate[data-id='{}']".format( template_id ) )
do_test( btn, expected_fname )
def do_sortable_test( sortable_id, expected_fname ): #pylint: disable=missing-docstring
entries = find_children( "#{} li".format( sortable_id ) )
assert len(entries) == 1
btn = find_child( "img.snippet", entries[0] )
do_test( btn, expected_fname )
# do the tests
do_simple_test( "scenario", "scenario.png" )
do_simple_test( "players", "players.png" )
do_sortable_test( "scenario_notes-sortable", "scenario note.1.png" )
do_simple_test( "victory_conditions", "victory conditions.png" )
do_simple_test( "ssr", "ssr.png" )
# do the tests
select_tab( "ob1" )
do_sortable_test( "ob_setups-sortable_1", "ob setup 1.1.png" )
do_sortable_test( "ob_notes-sortable_1", "ob note 1.1.png" )
do_sortable_test( "ob_vehicles-sortable_1", "a german vehicle.png" )
do_sortable_test( "ob_ordnance-sortable_1", "a german ordnance.png" )
# do the tests
select_tab( "ob2" )
do_sortable_test( "ob_setups-sortable_2", "ob setup 2.1.png" )
do_sortable_test( "ob_notes-sortable_2", "ob note 2.1.png" )
do_sortable_test( "ob_vehicles-sortable_2", "a russian vehicle.png" )
do_sortable_test( "ob_ordnance-sortable_2", "a russian ordnance.png" )
# ---------------------------------------------------------------------
def _test_snippet( btn, params, expected, expected2 ):
"""Do a single test."""

@ -17,7 +17,7 @@ class WebDriver:
def __init__( self ):
self.driver = None
def start_webdriver( self ):
def start( self ):
"""Start the webdriver."""
# initialize
@ -60,7 +60,7 @@ class WebDriver:
return self
def stop_webdriver( self ):
def stop( self ):
"""Stop the webdriver."""
assert self.driver
self.driver.quit()
@ -111,7 +111,7 @@ class WebDriver:
# have really large labels, it just affects the positioning of auto-created labels.
window_size, window_size2 = (500,500), (1500,1500)
if snippet_id.startswith(
if snippet_id and snippet_id.startswith(
("ob_vehicles_ma_notes_","ob_vehicle_note_","ob_ordnance_ma_notes_","ob_ordnance_note_")
):
# nb: these tend to be large, don't bother with a smaller window
@ -119,8 +119,8 @@ class WebDriver:
return self.get_screenshot( snippet, window_size, window_size2 )
def __enter__( self ):
self.start_webdriver()
self.start()
return self
def __exit__( self, *args ):
self.stop_webdriver()
self.stop()

Loading…
Cancel
Save