Merge branch 'release/1.1.6'

This commit is contained in:
Daniel Schick 2024-03-13 18:15:59 +01:00
commit f9c664fbc0
19 changed files with 153 additions and 79 deletions

View File

@ -1 +1 @@
1.1.5.0
1.1.6.0

View File

@ -2,7 +2,7 @@
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:BreCalClient"
StartupUri="MainWindow.xaml" Exit="Application_Exit">
StartupUri="MainWindow.xaml" Exit="Application_Exit" Startup="Application_Startup" >
<Application.Resources>
<ResourceDictionary>

View File

@ -1,4 +1,5 @@
using BreCalClient.misc.Model;
using BreCalClient.Properties;
using System.Windows;
namespace BreCalClient
@ -14,5 +15,20 @@ namespace BreCalClient
{
BreCalClient.Properties.Settings.Default.Save();
}
private void Application_Startup(object sender, StartupEventArgs e)
{
// Window size sanity check
if(Settings.Default.Width == 0)
{
Settings.Default.Width = 800;
Settings.Default.Save();
}
if(Settings.Default.Height == 0)
{
Settings.Default.Height = 450;
Settings.Default.Save();
}
}
}
}

View File

@ -8,8 +8,8 @@
<SignAssembly>True</SignAssembly>
<StartupObject>BreCalClient.App</StartupObject>
<AssemblyOriginatorKeyFile>..\..\misc\brecal.snk</AssemblyOriginatorKeyFile>
<AssemblyVersion>1.1.5.0</AssemblyVersion>
<FileVersion>1.1.5.0</FileVersion>
<AssemblyVersion>1.1.6.0</AssemblyVersion>
<FileVersion>1.1.6.0</FileVersion>
<Title>Bremen calling client</Title>
<Description>A Windows WPF client for the Bremen calling API.</Description>
<ApplicationIcon>containership.ico</ApplicationIcon>

View File

@ -46,7 +46,7 @@
<Label Content="ETA" Grid.Column="2" Grid.Row="2" HorizontalContentAlignment="Right"/>
<Label Content="ETD" Grid.Column="2" Grid.Row="3" HorizontalContentAlignment="Right"/>
<ComboBox x:Name="comboBoxCategories" Grid.Column="3" Margin="2" Grid.Row="0" SelectedValuePath="Key" SelectionChanged="comboBoxCategories_SelectionChanged"/>
<ComboBox x:Name="comboBoxCategories" Grid.Column="3" Margin="2" Grid.Row="0" SelectionChanged="comboBoxCategories_SelectionChanged"/>
<Label Content="{x:Static p:Resources.textBerth}" Grid.Column="2" Grid.Row="1" HorizontalContentAlignment="Right"/>
<Grid Grid.Row="1" Grid.Column="3">

View File

@ -19,6 +19,10 @@ using static BreCalClient.Extensions;
using System.Collections.Concurrent;
using Newtonsoft.Json;
using System.Security.Principal;
using Polly;
using System.Net.Http;
using System.Net;
using Polly.Retry;
namespace BreCalClient
{
@ -34,8 +38,7 @@ namespace BreCalClient
#region Fields
private static Int32 _uiUpdateRunning = 0;
private Timer? _timer;
private Credentials? _credentials;
private readonly ConcurrentDictionary<int, ShipcallControlModel> _allShipcallsDict = new();
@ -73,7 +76,29 @@ namespace BreCalClient
{
InitializeComponent();
_api = new DefaultApi(Properties.Settings.Default.API_URL);
_api.Configuration.ApiKeyPrefix["Authorization"] = "Bearer";
_api.Configuration.ApiKeyPrefix["Authorization"] = "Bearer";
const int maxDelayInMilliseconds = 32 * 1000;
var jitterer = new Random();
var retryPolicy =
Policy.Handle<HttpRequestException>()
.OrResult<RestSharp.RestResponse>(resp => resp.StatusCode == HttpStatusCode.Unauthorized)
.WaitAndRetryAsync(3,
retryAttempt =>
{
var calculatedDelayInMilliseconds = Math.Pow(2, retryAttempt) * 1000;
var jitterInMilliseconds = jitterer.Next(0, 1000);
var actualDelay = Math.Min(calculatedDelayInMilliseconds + jitterInMilliseconds, maxDelayInMilliseconds);
return TimeSpan.FromMilliseconds(actualDelay);
},
onRetry: (resp, timespan, context) =>
{
this.RefreshToken(null);
Trace.WriteLine("token refreshed");
});
RetryConfiguration.AsyncRetryPolicy = retryPolicy;
}
#endregion
@ -123,8 +148,7 @@ namespace BreCalClient
this.busyIndicator.IsBusy = false;
this._api.Configuration.ApiKey["Authorization"] = _loginResult.Token;
this.LoadStaticLists();
this.labelUsername.Text = $"{_loginResult.FirstName} {_loginResult.LastName}";
_timer = new Timer(RefreshToken, null, 4000000, Timeout.Infinite);
this.labelUsername.Text = $"{_loginResult.FirstName} {_loginResult.LastName}";
}
}
labelGeneralStatus.Text = $"Connection {ConnectionStatus.SUCCESSFUL}";
@ -150,8 +174,9 @@ namespace BreCalClient
}
}
private void RefreshToken(object? state)
private bool RefreshToken(object? state)
{
bool result = false;
try
{
_loginResult = _api.LoginPost(_credentials);
@ -159,7 +184,8 @@ namespace BreCalClient
{
if (_loginResult.Id > 0)
{
this._api.Configuration.ApiKey["Authorization"] = _loginResult.Token;
this._api.Configuration.ApiKey["Authorization"] = _loginResult.Token;
result = true;
}
}
else
@ -171,6 +197,7 @@ namespace BreCalClient
{
_log.ErrorFormat("Error refreshing token: {0}", ex.Message);
}
return result;
}
private void buttonExit_Click(object sender, RoutedEventArgs e)
@ -805,10 +832,13 @@ namespace BreCalClient
private void ShowErrorDialog(string message, string caption)
{
_log.ErrorFormat("{0} - {1}", caption, message);
/*
Dispatcher.Invoke(new Action(() =>
{
MessageBox.Show(message, caption, MessageBoxButton.OK, MessageBoxImage.Error);
}));
}));
*/
}
private void EnableControlsForParticipant()

View File

@ -5,7 +5,7 @@ https://go.microsoft.com/fwlink/?LinkID=208121.
<Project>
<PropertyGroup>
<ApplicationRevision>0</ApplicationRevision>
<ApplicationVersion>1.1.5.*</ApplicationVersion>
<ApplicationVersion>1.1.6.0</ApplicationVersion>
<BootstrapperEnabled>False</BootstrapperEnabled>
<Configuration>Release</Configuration>
<CreateWebPageOnPublish>True</CreateWebPageOnPublish>
@ -39,7 +39,6 @@ https://go.microsoft.com/fwlink/?LinkID=208121.
<SuiteName>Bremen calling client</SuiteName>
<SupportUrl>https://www.bsmd-emswe.eu/</SupportUrl>
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
<SkipPublishVerification>false</SkipPublishVerification>
</PropertyGroup>
<ItemGroup>
<PublishFile Include="containership.ico">

View File

@ -5,7 +5,7 @@ https://go.microsoft.com/fwlink/?LinkID=208121.
<Project>
<PropertyGroup>
<ApplicationRevision>0</ApplicationRevision>
<ApplicationVersion>1.1.5.*</ApplicationVersion>
<ApplicationVersion>1.1.6.*</ApplicationVersion>
<BootstrapperEnabled>True</BootstrapperEnabled>
<Configuration>Debug</Configuration>
<CreateDesktopShortcut>True</CreateDesktopShortcut>

View File

@ -1,17 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
@ -26,36 +26,36 @@
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
@ -284,10 +284,10 @@
<value>Neues Passwort wiederholen</value>
</data>
<data name="textReplenishingLock" xml:space="preserve">
<value>Versorgungsaufnahme Schleuse</value>
<value>Güterübernahme in der Schleuse geplant</value>
</data>
<data name="textReplenishingTerminal" xml:space="preserve">
<value>Versorgungsaufnahme Terminal</value>
<value>Güterübernahme am Terminal geplant</value>
</data>
<data name="textRotated" xml:space="preserve">
<value>Gedreht</value>

View File

@ -8,7 +8,7 @@
<applicationSettings>
<RoleEditor.Properties.Settings>
<setting name="ConnectionString" serializeAs="String">
<value>Server=localhost;User ID=ds;Password=HalloWach_2323XXL!!;Database=bremen_calling_devel;Port=33306</value>
<value>Server=localhost;User ID=ds;Password=HalloWach_2323XXL!!;Database=bremen_calling;Port=33306</value>
</setting>
</RoleEditor.Properties.Settings>
</applicationSettings>

View File

@ -17,7 +17,7 @@ def GetShipcalls():
token = request.headers.get('Authorization')
options = {}
options["participant_id"] = request.args.get("participant_id")
options["past_days"] = request.args.get("past_days", default=2, type=int)
options["past_days"] = request.args.get("past_days", default=1, type=int)
return impl.shipcalls.GetShipcalls(options)
else:

View File

@ -16,7 +16,7 @@ def GetBerths(token):
pooledConnection = local_db.getPoolConnection()
commands = pydapper.using(pooledConnection)
data = commands.query("SELECT id, name, `lock`, owner_id, authority_id, created, modified, deleted FROM berth WHERE deleted = 0 ORDER BY name", model=model.Berth)
pooledConnection.close()
return json.dumps(data, default=model.obj_dict), 200, {'Content-Type': 'application/json; charset=utf-8'}
except Exception as ex:
logging.error(ex)
@ -25,7 +25,9 @@ def GetBerths(token):
result["message"] = "call failed"
return json.dumps(result), 500
return json.dumps(data, default=model.obj_dict), 200, {'Content-Type': 'application/json; charset=utf-8'}
finally:
if pooledConnection is not None:
pooledConnection.close()

View File

@ -16,7 +16,6 @@ def GetUser(options):
commands = pydapper.using(pooledConnection)
data = commands.query("SELECT id, participant_id, first_name, last_name, user_name, user_email, user_phone, password_hash, api_key, created, modified FROM user WHERE user_name = ?username? OR user_email = ?username?",
model=model.User, param={"username" : options["username"]})
pooledConnection.close()
# print(data)
if len(data) == 1:
if bcrypt.checkpw(options["password"].encode("utf-8"), bytes(data[0].password_hash, "utf-8")):
@ -49,6 +48,10 @@ def GetUser(options):
result["message"] = "call failed: " + str(ex)
return json.dumps(result), 500, {'Content-Type': 'application/json; charset=utf-8'}
finally:
if pooledConnection is not None:
pooledConnection.close()
# $2b$12$uWLE0r32IrtCV30WkMbVwOdltgeibymZyYAf4ZnQb2Bip8hrkGGwG
# $2b$12$.vEapj9xU8z0RK0IpIGeYuRIl0ktdMt4XdJQBhVn.3K2hmvm7qD3y
# $2b$12$yL3PiseU70ciwEuMVM4OtuMwR6tNuIT9vvBiBG/uyMrPxa16E2Zqu

View File

@ -11,7 +11,6 @@ def GetParticipant(options):
options["user_id"]: **Id of user**. *Example: 2*. User id returned by login call.
"""
# TODO: validate token
try:
pooledConnection = local_db.getPoolConnection()
@ -20,7 +19,8 @@ def GetParticipant(options):
data = commands.query("SELECT p.id as id, p.name as name, p.street as street, p.postal_code as postal_code, p.city as city, p.type as type, p.flags as flags, p.created as created, p.modified as modified, p.deleted as deleted FROM participant p INNER JOIN user u WHERE u.participant_id = p.id and u.id = ?userid?", model=model.Participant, param={"userid" : options["user_id"]})
else:
data = commands.query("SELECT id, name, street, postal_code, city, type, flags, created, modified, deleted FROM participant p ORDER BY p.name", model=model.Participant)
pooledConnection.close()
return json.dumps(data, default=model.obj_dict), 200, {'Content-Type': 'application/json; charset=utf-8'}
except Exception as ex:
logging.error(ex)
@ -29,5 +29,7 @@ def GetParticipant(options):
result["message"] = "call failed"
return json.dumps("call failed"), 500
return json.dumps(data, default=model.obj_dict), 200, {'Content-Type': 'application/json; charset=utf-8'}
finally:
if pooledConnection is not None:
pooledConnection.close()

View File

@ -13,17 +13,24 @@ def GetShipcalls(options):
No parameters, gets all entries
"""
# TODO: validate token
try:
pooledConnection = local_db.getPoolConnection()
commands = pydapper.using(pooledConnection)
query = ("SELECT id, ship_id, type, eta, voyage, etd, arrival_berth_id, departure_berth_id, tug_required, pilot_required, "
"flags, pier_side, bunkering, replenishing_terminal, replenishing_lock, draft, tidal_window_from, tidal_window_to, rain_sensitive_cargo, recommended_tugs, "
"anchored, moored_lock, canceled, evaluation, evaluation_message, created, modified FROM shipcall WHERE ((type = 1 OR type = 3) AND eta >= DATE(NOW() - INTERVAL %d DAY)"
"OR (type = 2 AND etd >= DATE(NOW() - INTERVAL %d DAY))) "
"ORDER BY eta") % (options["past_days"], options["past_days"])
query = ("SELECT s.id as id, ship_id, type, eta, voyage, etd, arrival_berth_id, departure_berth_id, tug_required, pilot_required, " +
"flags, s.pier_side, bunkering, replenishing_terminal, replenishing_lock, draft, tidal_window_from, " +
"tidal_window_to, rain_sensitive_cargo, recommended_tugs, anchored, moored_lock, canceled, evaluation, " +
"evaluation_message, s.created as created, s.modified as modified " +
"FROM shipcall s " +
"LEFT JOIN times t ON t.shipcall_id = s.id AND t.participant_type = 8 " +
"WHERE " +
"(type = 1 AND " +
"((t.id IS NOT NULL AND t.eta_berth >= DATE(NOW() - INTERVAL %d DAY)) OR " +
"(eta >= DATE(NOW() - INTERVAL %d DAY)))) OR " +
"((type = 2 OR type = 3) AND " +
"((t.id IS NOT NULL AND t.etd_berth >= DATE(NOW() - INTERVAL %d DAY)) OR " +
"(etd >= DATE(NOW() - INTERVAL %d DAY)))) " +
"ORDER BY eta") % (options["past_days"], options["past_days"], options["past_days"], options["past_days"])
data = commands.query(query, model=model.Shipcall)
for shipcall in data:
@ -33,7 +40,7 @@ def GetShipcalls(options):
pa = model.Participant_Assignment(record["participant_id"], record["type"])
shipcall.participants.append(pa)
pooledConnection.close()
return json.dumps(data, default=model.obj_dict), 200, {'Content-Type': 'application/json; charset=utf-8'}
except Exception as ex:
logging.error(traceback.format_exc())
@ -43,7 +50,9 @@ def GetShipcalls(options):
result["message"] = "call failed"
return json.dumps(result), 500, {'Content-Type': 'application/json; charset=utf-8'}
return json.dumps(data, default=model.obj_dict), 200, {'Content-Type': 'application/json; charset=utf-8'}
finally:
if pooledConnection is not None:
pooledConnection.close()
def PostShipcalls(schemaModel):
@ -111,8 +120,6 @@ def PostShipcalls(schemaModel):
# apply 'Traffic Light' evaluation to obtain 'GREEN', 'YELLOW' or 'RED' evaluation state. The function internally updates the mysql database
evaluate_shipcall_state(mysql_connector_instance=pooledConnection, shipcall_id=new_id) # new_id (last insert id) refers to the shipcall id
pooledConnection.close()
return json.dumps({"id" : new_id}), 201, {'Content-Type': 'application/json; charset=utf-8'}
except Exception as ex:
@ -123,6 +130,10 @@ def PostShipcalls(schemaModel):
result["message"] = "call failed"
return json.dumps(result), 500, {'Content-Type': 'application/json; charset=utf-8'}
finally:
if pooledConnection is not None:
pooledConnection.close()
def PutShipcalls(schemaModel):
"""
@ -194,9 +205,6 @@ def PutShipcalls(schemaModel):
commands.execute(dquery, param={"existing_id" : elem["id"]})
# apply 'Traffic Light' evaluation to obtain 'GREEN', 'YELLOW' or 'RED' evaluation state. The function internally updates the mysql database
evaluate_shipcall_state(mysql_connector_instance=pooledConnection, shipcall_id=schemaModel["id"]) # schemaModel["id"] refers to the shipcall id
pooledConnection.close()
return json.dumps({"id" : schemaModel["id"]}), 200
@ -208,5 +216,7 @@ def PutShipcalls(schemaModel):
result["message"] = "call failed"
return json.dumps(result), 500, {'Content-Type': 'application/json; charset=utf-8'}
finally:
if pooledConnection is not None:
pooledConnection.close()

View File

@ -17,7 +17,8 @@ def GetShips(token):
pooledConnection = local_db.getPoolConnection()
commands = pydapper.using(pooledConnection)
data = commands.query("SELECT id, name, imo, callsign, participant_id, length, width, is_tug, bollard_pull, eni, created, modified, deleted FROM ship ORDER BY name", model=model.Ship)
pooledConnection.close()
return json.dumps(data, default=model.obj_dict), 200, {'Content-Type': 'application/json; charset=utf-8'}
except Exception as ex:
logging.error(ex)
@ -26,7 +27,10 @@ def GetShips(token):
result["message"] = "call failed"
return json.dumps(result), 500, {'Content-Type': 'application/json; charset=utf-8'}
return json.dumps(data, default=model.obj_dict), 200, {'Content-Type': 'application/json; charset=utf-8'}
finally:
if pooledConnection is not None:
pooledConnection.close()

View File

@ -85,8 +85,6 @@ def PostTimes(schemaModel):
# apply 'Traffic Light' evaluation to obtain 'GREEN', 'YELLOW' or 'RED' evaluation state. The function internally updates the mysql database 'shipcall'
evaluate_shipcall_state(mysql_connector_instance=pooledConnection, shipcall_id=schemaModel["shipcall_id"]) # every times data object refers to the 'shipcall_id'
pooledConnection.close()
return json.dumps({"id" : new_id}), 201, {'Content-Type': 'application/json; charset=utf-8'}
except Exception as ex:
@ -96,6 +94,10 @@ def PostTimes(schemaModel):
result["message"] = "call failed"
return json.dumps(result), 500, {'Content-Type': 'application/json; charset=utf-8'}
finally:
if pooledConnection is not None:
pooledConnection.close()
def PutTimes(schemaModel):
"""
@ -130,8 +132,6 @@ def PutTimes(schemaModel):
# apply 'Traffic Light' evaluation to obtain 'GREEN', 'YELLOW' or 'RED' evaluation state. The function internally updates the mysql database 'shipcall'
evaluate_shipcall_state(mysql_connector_instance=pooledConnection, shipcall_id=schemaModel["shipcall_id"]) # every times data object refers to the 'shipcall_id'
pooledConnection.close()
# if affected_rows == 1: # this doesn't work as expected
return json.dumps({"id" : schemaModel["id"]}), 200, {'Content-Type': 'application/json; charset=utf-8'}
@ -145,6 +145,10 @@ def PutTimes(schemaModel):
result["message"] = "call failed"
return json.dumps(result), 500, {'Content-Type': 'application/json; charset=utf-8'}
finally:
if pooledConnection is not None:
pooledConnection.close()
def DeleteTimes(options):
"""
:param options: A dictionary containing all the paramters for the Operations
@ -156,7 +160,6 @@ def DeleteTimes(options):
pooledConnection = local_db.getPoolConnection()
commands = pydapper.using(pooledConnection)
affected_rows = commands.execute("DELETE FROM times WHERE id = ?id?", param={"id" : options["id"]})
pooledConnection.close()
if affected_rows == 1:
return json.dumps({"id" : options["id"]}), 200, {'Content-Type': 'application/json; charset=utf-8'}
@ -170,4 +173,8 @@ def DeleteTimes(options):
print(ex)
result = {}
result["message"] = "call failed"
return json.dumps(result), 500, {'Content-Type': 'application/json; charset=utf-8'}
return json.dumps(result), 500, {'Content-Type': 'application/json; charset=utf-8'}
finally:
if pooledConnection is not None:
pooledConnection.close()

View File

@ -56,8 +56,6 @@ def PutUser(schemaModel):
result["message"] = "old password invalid"
return json.dumps(result), 400, {'Content-Type': 'application/json; charset=utf-8'}
pooledConnection.close()
return json.dumps({"id" : schemaModel["id"]}), 200
except Exception as ex:
@ -67,5 +65,8 @@ def PutUser(schemaModel):
result["message"] = "call failed"
return json.dumps(result), 500, {'Content-Type': 'application/json; charset=utf-8'}
finally:
if pooledConnection is not None:
pooledConnection.close()

View File

@ -2,7 +2,7 @@ from setuptools import find_packages, setup
setup(
name='BreCal',
version='1.1.5',
version='1.1.6',
packages=find_packages(),
include_package_data=True,
zip_safe=False,