Merged current state of develop

This commit is contained in:
Daniel Schick 2024-02-14 10:56:35 +01:00
commit 898cfdf07d
12 changed files with 145 additions and 70 deletions

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

@ -47,7 +47,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

@ -18,9 +18,12 @@ using BreCalClient.misc.Model;
using static BreCalClient.Extensions;
using System.Collections.Concurrent;
using Newtonsoft.Json;
using System.Security.Principal;
using Polly;
using System.Net.Http;
using System.Net;
namespace BreCalClient
{
/// <summary>
@ -35,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();
@ -87,7 +89,29 @@ namespace BreCalClient
_staticApi = new StaticApi(Properties.Settings.Default.API_URL);
_staticApi.Configuration.ApiKeyPrefix["Authorization"] = "Bearer";
_shipApi = new ShipApi(Properties.Settings.Default.API_URL);
_shipApi.Configuration.ApiKeyPrefix["Authorization"] = "Bearer";
_shipApi.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
@ -141,8 +165,7 @@ namespace BreCalClient
this._staticApi.Configuration.ApiKey["Authorization"] = _loginResult.Token;
this._shipApi.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}";
@ -168,8 +191,9 @@ namespace BreCalClient
}
}
private void RefreshToken(object? state)
private bool RefreshToken(object? state)
{
bool result = false;
try
{
_loginResult = _userApi.Login(_credentials);
@ -181,8 +205,9 @@ namespace BreCalClient
this._timesApi.Configuration.ApiKey["Authorization"] = _loginResult.Token;
this._shipcallApi.Configuration.ApiKey["Authorization"] = _loginResult.Token;
this._staticApi.Configuration.ApiKey["Authorization"] = _loginResult.Token;
this._shipApi.Configuration.ApiKey["Authorization"] = _loginResult.Token;
}
this._shipApi.Configuration.ApiKey["Authorization"] = _loginResult.Token;
result = true;
}
}
else
{
@ -193,6 +218,7 @@ namespace BreCalClient
{
_log.ErrorFormat("Error refreshing token: {0}", ex.Message);
}
return result;
}
private void buttonExit_Click(object sender, RoutedEventArgs e)
@ -835,10 +861,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

@ -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

@ -14,7 +14,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)
@ -23,7 +23,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

@ -19,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)
@ -28,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

@ -18,11 +18,20 @@ def GetShipcalls(options):
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, evaluation_time, evaluation_notifications_sent, 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, evaluation_time, evaluation_notifications_sent, 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:
@ -32,7 +41,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())
@ -42,7 +51,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):
@ -116,8 +127,6 @@ def PostShipcalls(schemaModel):
query = "INSERT INTO history (participant_id, shipcall_id, user_id, timestamp, eta, type, operation) VALUES (?pid?, ?scid?, ?uid?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, 1, 1)"
commands.execute_scalar(query, {"scid" : new_id, "pid" : user_data["participant_id"], "uid" : user_data["id"]})
pooledConnection.close()
return json.dumps({"id" : new_id}), 201, {'Content-Type': 'application/json; charset=utf-8'}
except Exception as ex:
@ -128,6 +137,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):
"""
@ -207,8 +220,6 @@ def PutShipcalls(schemaModel):
query = "INSERT INTO history (participant_id, shipcall_id, user_id, timestamp, eta, type, operation) VALUES (?pid?, ?scid?, ?uid?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, 1, 2)"
commands.execute(query, {"scid" : schemaModel["id"], "pid" : user_data["participant_id"], "uid" : user_data["id"]})
pooledConnection.close()
return json.dumps({"id" : schemaModel["id"]}), 200
except Exception as ex:
@ -219,5 +230,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

@ -15,7 +15,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)
@ -24,7 +25,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

@ -90,8 +90,6 @@ def PostTimes(schemaModel):
query = "INSERT INTO history (participant_id, shipcall_id, user_id, timestamp, eta, type, operation) VALUES (?pid?, ?scid?, ?uid?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, 2, 1)"
commands.execute_scalar(query, {"scid" : schemaModel["shipcall_id"], "pid" : user_data["participant_id"], "uid" : user_data["id"]})
pooledConnection.close()
return json.dumps({"id" : new_id}), 201, {'Content-Type': 'application/json; charset=utf-8'}
except Exception as ex:
@ -101,6 +99,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):
"""
@ -141,14 +143,10 @@ def PutTimes(schemaModel):
query = "INSERT INTO history (participant_id, shipcall_id, user_id, timestamp, eta, type, operation) VALUES (?pid?, ?scid?, ?uid?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, 2, 2)"
commands.execute_scalar(query, {"scid" : schemaModel["shipcall_id"], "pid" : user_data["participant_id"], "uid" : user_data["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'}
# return json.dumps("no such record"), 404, {'Content-Type': 'application/json; charset=utf-8'}
except Exception as ex:
logging.error(ex)
print(ex)
@ -156,6 +154,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
@ -176,8 +178,6 @@ def DeleteTimes(options):
query = "INSERT INTO history (participant_id, shipcall_id, user_id, timestamp, eta, type, operation) VALUES (?pid?, 0, ?uid?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, 2, 3)"
commands.execute_scalar(query, {"pid" : user_data["participant_id"], "uid" : user_data["id"]})
pooledConnection.close()
if affected_rows == 1:
return json.dumps({"id" : options["id"]}), 200, {'Content-Type': 'application/json; charset=utf-8'}
@ -190,4 +190,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()