Compare commits

...

19 Commits

Author SHA1 Message Date
529872b590 Allow new shipcalls up to 1 day in the past for serverside validation 2024-11-07 09:26:23 +01:00
f1e1355986 more validation input fixes 2024-10-18 11:49:18 +02:00
e911da20ef Fixed tidal window validation and description output 2024-10-18 09:51:29 +02:00
2ee9af4b9d Version bump to 1.5.0.7 2024-10-17 15:41:50 +02:00
5e2cb3f745 Fixed some more small bugs in validation when only a partial times dataset is put 2024-10-17 14:49:51 +02:00
704c58222c Fix text filter if there is whitespace in the text, simplified some events 2024-10-17 07:42:36 +02:00
401e0d4ae8 bugfix for shipcall PUT validation 2024-10-16 16:16:54 +02:00
scopesorting
c0902c65ee
regardless of the BSMD flag, BSMD users are now able to perform shipc… (#51)
* regardless of the BSMD flag, BSMD users are now able to perform shipcall PUT-requests

* regardless of the BSMD flag, BSMD users are now able to perform shipcall PUT-requests

* docstrings and BSMD-flag handling
2024-10-15 15:19:08 +02:00
8fe2a9ebca Fixed error in validation when times data was updated for operations 2024-09-26 15:00:41 +02:00
d62250fb4f Limit shifting number to 127 to avoid int -> uint overflow on insert 2024-09-25 07:57:58 +02:00
28404fb8b6 Version bump to 1.5.0.6 2024-09-23 08:56:55 +02:00
40e1c91755 Merge branch 'feature/extra_warnings' into release/1.5.0 2024-09-23 08:52:39 +02:00
7921a138d4 fixed bug in ship create 2024-09-23 08:39:25 +02:00
0c8a5cfc2c fixed site module include path 2024-09-23 08:24:39 +02:00
27179da2a2 fixed some missing client warnings 2024-09-22 14:43:52 +02:00
df050cb83b
Merge pull request #50 from puls200/hotfix/20240912
Hotfix/20240912
2024-09-18 08:27:36 +02:00
Max Metz
5e50e09966 when a ship is deleted, the IMO is no longer considered to exist 2024-09-17 15:33:48 +02:00
0bd526e08e Version bump to 1.5.0.5 2024-09-17 10:12:11 +02:00
Max Metz
8df9034574 BSMD-flag check was executed on the wrong ID. Now, it correctly uses the assigned agency's ID to determine the presence of a BSMD flag 2024-09-12 11:07:45 +02:00
23 changed files with 740 additions and 531 deletions

View File

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

View File

@ -85,7 +85,7 @@
<Label x:Name="labelBSMDGranted" Grid.Row="7" Grid.Column="3" Grid.ColumnSpan="1" Content="{x:Static p:Resources.textBSMDGranted}" Visibility="Hidden" FontWeight="DemiBold" />
<Label x:Name="labelShiftingCount" Grid.Row="6" Grid.Column="2" HorizontalAlignment="Right" Content="{x:Static p:Resources.textShiftingSequence}" />
<xctk:IntegerUpDown x:Name="integerUpDownShiftingCount" Grid.Row="6" Grid.Column="3" Margin="2" Minimum="0" Maximum="255" />
<xctk:IntegerUpDown x:Name="integerUpDownShiftingCount" Grid.Row="6" Grid.Column="3" Margin="2" Minimum="0" Maximum="127" />
<StackPanel Grid.Row="8" Grid.Column="3" Orientation="Horizontal" HorizontalAlignment="Right">
<Button Width= "80" Margin="2" Content="{x:Static p:Resources.textOK}" x:Name="buttonOK" Click="buttonOK_Click" IsEnabled="False" />

View File

@ -73,7 +73,7 @@ namespace BreCalClient
{
if (!CheckValues(out string message))
{
MessageBox.Show(message, "Warning", MessageBoxButton.OK, MessageBoxImage.Warning);
MessageBox.Show(message, BreCalClient.Resources.Resources.textWarning, MessageBoxButton.OK, MessageBoxImage.Warning);
}
else
{
@ -145,6 +145,13 @@ namespace BreCalClient
return false;
}
if((this.datePickerETA_End.Value.HasValue && !this.datePickerETA.Value.HasValue) ||
(this.datePickerTidalWindowTo.Value.HasValue && !this.datePickerTidalWindowFrom.Value.HasValue))
{
message = BreCalClient.Resources.Resources.textStartTimeMissing;
return false;
}
return true;
}

View File

@ -82,7 +82,7 @@ namespace BreCalClient
{
if (!CheckValues(out string message))
{
System.Windows.MessageBox.Show(message, "Warning", MessageBoxButton.OK, MessageBoxImage.Warning);
System.Windows.MessageBox.Show(message, BreCalClient.Resources.Resources.textWarning, MessageBoxButton.OK, MessageBoxImage.Warning);
}
else
{
@ -155,6 +155,13 @@ namespace BreCalClient
return false;
}
if((this.datePickerETD_End.Value.HasValue && !this.datePickerETD.Value.HasValue) ||
(this.datePickerTidalWindowTo.Value.HasValue && !this.datePickerTidalWindowFrom.Value.HasValue))
{
message = BreCalClient.Resources.Resources.textStartTimeMissing;
return false;
}
return true;
}

View File

@ -73,7 +73,7 @@ namespace BreCalClient
{
if (!CheckValues(out string message))
{
System.Windows.MessageBox.Show(message, "Warning", MessageBoxButton.OK, MessageBoxImage.Warning);
System.Windows.MessageBox.Show(message, BreCalClient.Resources.Resources.textWarning, MessageBoxButton.OK, MessageBoxImage.Warning);
}
else
{
@ -164,6 +164,14 @@ namespace BreCalClient
return false;
}
if((this.datePickerETA_End.Value.HasValue && !this.datePickerETA.Value.HasValue) ||
(this.datePickerETD_End.Value.HasValue && !this.datePickerETD.Value.HasValue) ||
(this.datePickerTidalWindowTo.Value.HasValue && !this.datePickerTidalWindowFrom.Value.HasValue))
{
message = BreCalClient.Resources.Resources.textStartTimeMissing;
return false;
}
return true;
}

View File

@ -49,7 +49,8 @@ namespace BreCalClient
{
if (!CheckValues(out string message))
{
System.Windows.MessageBox.Show(message, "Warning", MessageBoxButton.OK, MessageBoxImage.Warning);
System.Windows.MessageBox.Show(message, BreCalClient.Resources.Resources.textWarning,
MessageBoxButton.OK, MessageBoxImage.Warning);
}
else
{
@ -116,6 +117,24 @@ namespace BreCalClient
return false;
}
if (this.datePickerETDBerth.Value.HasValue && (this.datePickerETDBerth.Value.Value < DateTime.Now) && (this.datePickerETABerth_End.Value == null))
{
message = BreCalClient.Resources.Resources.textETDInThePast;
return false;
}
if (this.datePickerETDBerth_End.Value.HasValue && this.datePickerETDBerth_End.Value < DateTime.Now)
{
message = BreCalClient.Resources.Resources.textETDInThePast;
return false;
}
if (this.datePickerETDBerth.Value.HasValue && this.datePickerETDBerth_End.Value.HasValue && this.datePickerETDBerth.Value > this.datePickerETDBerth_End.Value)
{
message = BreCalClient.Resources.Resources.textEndValueBeforeStartValue;
return false;
}
if (this.datePickerLockTime.Value.HasValue && (this.datePickerLockTime.Value.Value < DateTime.Now))
{
message = BreCalClient.Resources.Resources.textLockTimeInThePast;
@ -135,6 +154,13 @@ namespace BreCalClient
return false;
}
if((this.datePickerETABerth_End.Value.HasValue && !this.datePickerETABerth.Value.HasValue) ||
(this.datePickerETDBerth_End.Value.HasValue && !this.datePickerETDBerth.Value.HasValue))
{
message = BreCalClient.Resources.Resources.textStartTimeMissing;
return false;
}
return true;
}

View File

@ -64,7 +64,7 @@ namespace BreCalClient
{
if (!CheckValues(out string message))
{
System.Windows.MessageBox.Show(message, "Warning", MessageBoxButton.OK, MessageBoxImage.Warning);
System.Windows.MessageBox.Show(message, BreCalClient.Resources.Resources.textWarning, MessageBoxButton.OK, MessageBoxImage.Warning);
}
else
{
@ -150,6 +150,13 @@ namespace BreCalClient
return false;
}
if((this.datePickerOperationEnd_End.Value.HasValue && !this.datePickerOperationEnd.Value.HasValue) ||
(this.datePickerOperationStart_End.Value.HasValue && !this.datePickerOperationStart.Value.HasValue))
{
message = BreCalClient.Resources.Resources.textStartTimeMissing;
return false;
}
return true;
}

View File

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

View File

@ -119,6 +119,15 @@ namespace BreCalClient.Resources {
}
}
/// <summary>
/// Looks up a localized string similar to .
/// </summary>
public static string arrow_right_blue {
get {
return ResourceManager.GetString("arrow_right_blue", resourceCulture);
}
}
/// <summary>
/// Looks up a localized resource of type System.Byte[].
/// </summary>
@ -139,6 +148,15 @@ namespace BreCalClient.Resources {
}
}
/// <summary>
/// Looks up a localized string similar to .
/// </summary>
public static string arrow_up_green {
get {
return ResourceManager.GetString("arrow_up_green", resourceCulture);
}
}
/// <summary>
/// Looks up a localized resource of type System.Byte[].
/// </summary>
@ -1199,6 +1217,15 @@ namespace BreCalClient.Resources {
}
}
/// <summary>
/// Looks up a localized string similar to If an end time is set, a start time is also required.
/// </summary>
public static string textStartTimeMissing {
get {
return ResourceManager.GetString("textStartTimeMissing", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Terminal.
/// </summary>

View File

@ -117,16 +117,13 @@
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="Arrival" xml:space="preserve">
<value>Einkommend</value>
</data>
<assembly alias="System.Windows.Forms" name="System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" />
<data name="arrow_down_red" type="System.Resources.ResXFileRef, System.Windows.Forms">
<value>arrow_down_red.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</data>
<data name="arrow_right_blue" type="System.Resources.ResXFileRef, System.Windows.Forms">
<value>arrow_right_blue.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</data>
<data name="arrow_up_green" type="System.Resources.ResXFileRef, System.Windows.Forms">
<value>arrow_up_green.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</data>
<data name="clipboard" type="System.Resources.ResXFileRef, System.Windows.Forms">
<value>clipboard.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</data>
@ -136,15 +133,24 @@
<data name="containership" type="System.Resources.ResXFileRef, System.Windows.Forms">
<value>containership.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</data>
<data name="Departure" xml:space="preserve">
<value>Ausgehend</value>
</data>
<data name="emergency_stop_button" type="System.Resources.ResXFileRef, System.Windows.Forms">
<value>emergency_stop_button.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</data>
<data name="logo_bremen_calling" type="System.Resources.ResXFileRef, System.Windows.Forms">
<value>logo_bremen_calling.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</data>
<data name="Shifting" xml:space="preserve">
<value>Verholung</value>
</data>
<data name="ship2" type="System.Resources.ResXFileRef, System.Windows.Forms">
<value>ship2.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</data>
<data name="textAdd" xml:space="preserve">
<value>Hinzufügen</value>
</data>
<data name="textAgencies" xml:space="preserve">
<value>Agenturen</value>
</data>
@ -160,9 +166,18 @@
<data name="textBerth" xml:space="preserve">
<value>Liegeplatz</value>
</data>
<data name="textBerthRemarks" xml:space="preserve">
<value>Liegeplatz Informationen</value>
</data>
<data name="textBerths" xml:space="preserve">
<value>Liegeplätze</value>
</data>
<data name="textBothTideTimesNecessary" xml:space="preserve">
<value>Beide Tidenzeiten sollten angegeben werden (von - bis)</value>
</data>
<data name="textBSMDGranted" xml:space="preserve">
<value>Freigabe zur Bearb. f. BSMD erteilt</value>
</data>
<data name="textBunkering" xml:space="preserve">
<value>Bunkeraufnahme</value>
</data>
@ -181,59 +196,122 @@
<data name="textChange" xml:space="preserve">
<value>Ändern</value>
</data>
<data name="textChangeContactInfo" xml:space="preserve">
<value>Kontaktdaten bearbeiten</value>
</data>
<data name="textChangeHistory" xml:space="preserve">
<value>Verlauf</value>
</data>
<data name="textChangePassword" xml:space="preserve">
<value>Passwort ändern</value>
</data>
<data name="textClearAll" xml:space="preserve">
<value>Alle Eintragungen zurücksetzen?</value>
</data>
<data name="textClearAssignment" xml:space="preserve">
<value>Zuordnung entfernen</value>
</data>
<data name="textClearFilters" xml:space="preserve">
<value>Filter löschen</value>
</data>
<data name="textClearValue" xml:space="preserve">
<value>Eingabe löschen</value>
</data>
<data name="textClose" xml:space="preserve">
<value>Schliessen</value>
</data>
<data name="textConfirmation" xml:space="preserve">
<value>Bestätigung</value>
</data>
<data name="textDelete" xml:space="preserve">
<value>Löschen</value>
</data>
<data name="textDeleted" xml:space="preserve">
<value>Gelöscht</value>
</data>
<data name="textDepartureTerminal" xml:space="preserve">
<value>Terminal Abfahrt</value>
</data>
<data name="textDraft" xml:space="preserve">
<value>Tiefgang (m)</value>
</data>
<data name="textEdit" xml:space="preserve">
<value>Bearbeiten</value>
</data>
<data name="textEditShip" xml:space="preserve">
<value>Schiff bearbeiten</value>
</data>
<data name="textEditShipcall" xml:space="preserve">
<value>Schiffsanlauf bearbeiten</value>
</data>
<data name="textEditShips" xml:space="preserve">
<value>Schiffe anlegen / bearbeiten</value>
</data>
<data name="textEditTimes" xml:space="preserve">
<value>Zeiten bearbeiten</value>
</data>
<data name="textETABerth" xml:space="preserve">
<value>ETA Liegeplatz</value>
<data name="textEmail" xml:space="preserve">
<value>E-Mail</value>
</data>
<data name="textETDBerth" xml:space="preserve">
<value>ETD Liegeplatz</value>
<data name="textEndValueBeforeStartValue" xml:space="preserve">
<value>Endzeit liegt vor Startzeit</value>
</data>
<data name="textEnterKeyword" xml:space="preserve">
<value>Schiff / Bemerkung</value>
</data>
<data name="textError" xml:space="preserve">
<value>Error</value>
</data>
<data name="textETABerth" xml:space="preserve">
<value>ETA Liegeplatz</value>
</data>
<data name="textETAInThePast" xml:space="preserve">
<value>Zeitpunkt ETA liegt in der Vergangenheit</value>
</data>
<data name="textETDBerth" xml:space="preserve">
<value>ETD Liegeplatz</value>
</data>
<data name="textETDInThePast" xml:space="preserve">
<value>Zeitpunkt ETD liegt in der Vergangenheit</value>
</data>
<data name="textExit" xml:space="preserve">
<value>Verlassen</value>
</data>
<data name="textFrom" xml:space="preserve">
<value>von</value>
</data>
<data name="textInterval" xml:space="preserve">
<value>Zeitraum</value>
</data>
<data name="textLengthWidth" xml:space="preserve">
<value>L/B (m)</value>
</data>
<data name="textLogin" xml:space="preserve">
<value>Anmelden</value>
</data>
<data name="textFixed" xml:space="preserve">
<value>Fest</value>
</data>
<data name="textFixedOrder" xml:space="preserve">
<value>Als feste Bestellung vermerkt</value>
</data>
<data name="textFrom" xml:space="preserve">
<value>von</value>
</data>
<data name="textIncoming" xml:space="preserve">
<value>Einkommend</value>
</data>
<data name="textInfoChangePW" xml:space="preserve">
<value>App Info anzeigen und Passwort ändern</value>
</data>
<data name="textInterval" xml:space="preserve">
<value>Zeitraum</value>
</data>
<data name="textLength" xml:space="preserve">
<value>Länge</value>
</data>
<data name="textLengthWidth" xml:space="preserve">
<value>L/B (m)</value>
</data>
<data name="textLockTime" xml:space="preserve">
<value>Zeit Schleuse</value>
</data>
<data name="textOperationsEnd" xml:space="preserve">
<value>Operation Ende</value>
<data name="textLockTimeInThePast" xml:space="preserve">
<value>Schleusenzeit liegt in der Vergangenheit</value>
</data>
<data name="textOperationsStart" xml:space="preserve">
<value>Operation Start</value>
<data name="textLogin" xml:space="preserve">
<value>Anmelden</value>
</data>
<data name="textMineOnly" xml:space="preserve">
<value>nur eigene</value>
</data>
<data name="textMooredLock" xml:space="preserve">
<value>auch in Schleuse</value>
@ -250,21 +328,45 @@
<data name="textNotRotated" xml:space="preserve">
<value>Ungedreht</value>
</data>
<data name="textZoneEntryTime" xml:space="preserve">
<value>Reviereintritt</value>
</data>
<data name="textOK" xml:space="preserve">
<value>OK</value>
</data>
<data name="textOldPassword" xml:space="preserve">
<value>Altes Passwort</value>
</data>
<data name="textOperation" xml:space="preserve">
<value>Vorgang</value>
</data>
<data name="textOperationEndInThePast" xml:space="preserve">
<value>Operation Endzeit liegt in der Vergangenheit</value>
</data>
<data name="textOperationsEnd" xml:space="preserve">
<value>Operation Ende</value>
</data>
<data name="textOperationsStart" xml:space="preserve">
<value>Operation Start</value>
</data>
<data name="textOperationStartInThePast" xml:space="preserve">
<value>Operation Startzeit liegt in der Vergangenheit</value>
</data>
<data name="textOutgoing" xml:space="preserve">
<value>Ausgehend</value>
</data>
<data name="textParticipant" xml:space="preserve">
<value>Teilnehmer</value>
</data>
<data name="textParticipants" xml:space="preserve">
<value>Teilnehmer</value>
</data>
<data name="textPassword" xml:space="preserve">
<value>Passwort</value>
</data>
<data name="textPasswordChanged" xml:space="preserve">
<value>Passwort geändert.</value>
</data>
<data name="textPhone" xml:space="preserve">
<value>Telefon</value>
</data>
<data name="textPierside" xml:space="preserve">
<value>Anlegeseite</value>
</data>
@ -274,12 +376,24 @@
<data name="textPilotRequired" xml:space="preserve">
<value>Lotsorder</value>
</data>
<data name="textPilots" xml:space="preserve">
<value>Flusslotsen</value>
</data>
<data name="textPort" xml:space="preserve">
<value>Backbord</value>
</data>
<data name="textPortAuthority" xml:space="preserve">
<value>Hafenamt</value>
</data>
<data name="textRainSensitiveCargo" xml:space="preserve">
<value>Regensensitive Ladung</value>
</data>
<data name="textRecommendedTugs" xml:space="preserve">
<value>Anzahl Schlepper</value>
</data>
<data name="textRemarks" xml:space="preserve">
<value>Info</value>
</data>
<data name="textRepeatNewPassword" xml:space="preserve">
<value>Neues Passwort wiederholen</value>
</data>
@ -295,27 +409,75 @@
<data name="textSearch" xml:space="preserve">
<value>Suche</value>
</data>
<data name="textShifting" xml:space="preserve">
<value>Verholung</value>
</data>
<data name="textShiftingFrom" xml:space="preserve">
<value>Verholung von</value>
</data>
<data name="textShiftingSequence" xml:space="preserve">
<value>Verhol. Nr.</value>
</data>
<data name="textShiftingTo" xml:space="preserve">
<value>Verholung nach</value>
</data>
<data name="textShip" xml:space="preserve">
<value>Schiff</value>
</data>
<data name="textShipLength" xml:space="preserve">
<value>Schiffslänge</value>
</data>
<data name="textShips" xml:space="preserve">
<value>Schiffe</value>
</data>
<data name="textShowCancelledShipcalls" xml:space="preserve">
<value>Stornierte anzeigen</value>
</data>
<data name="textShowHistory" xml:space="preserve">
<value>Änderungshistorie der Anläufe anzeigen</value>
</data>
<data name="textSortOrder" xml:space="preserve">
<value>Sortierung</value>
</data>
<data name="textStarboard" xml:space="preserve">
<value>Steuerbord</value>
</data>
<data name="textTerminal" xml:space="preserve">
<value>Terminal</value>
</data>
<data name="textTidalBothValues" xml:space="preserve">
<value>Für das Tidenfenster müssen beide Zeiten angegeben werden</value>
</data>
<data name="textTidalWindow" xml:space="preserve">
<value>Tidenfenster</value>
</data>
<data name="textTideTimesInThePast" xml:space="preserve">
<value>Tidenzeit liegt in der Vergangenheit</value>
</data>
<data name="textTimestamp" xml:space="preserve">
<value>Zeitpunkt</value>
</data>
<data name="textTo" xml:space="preserve">
<value>bis</value>
</data>
<data name="textTooFarInTheFuture" xml:space="preserve">
<value>Eine Zeiteingabe ist zu weit in der Zukunft</value>
</data>
<data name="textTooltipSetFixedOrder" xml:space="preserve">
<value>Als feste Bestellung vermerken</value>
</data>
<data name="textTooltipUnSetFixedOrder" xml:space="preserve">
<value>Feste Bestellung zurücknehmen</value>
</data>
<data name="textTriggerManualRefresh" xml:space="preserve">
<value>Manuelle Aktualisierung der Anläufe auslösen</value>
</data>
<data name="textTug" xml:space="preserve">
<value>Schlepper</value>
</data>
<data name="textTugCompany" xml:space="preserve">
<value>Schlepper-Reederei</value>
</data>
<data name="textTugRequired" xml:space="preserve">
<value>Schlepperorder</value>
</data>
@ -328,9 +490,27 @@
<data name="textUsername" xml:space="preserve">
<value>Benutzername</value>
</data>
<data name="textUserNamePasswordEmpty" xml:space="preserve">
<value>Benutzername / Passwort leer!</value>
</data>
<data name="textVoyage" xml:space="preserve">
<value>Reise</value>
</data>
<data name="textWarning" xml:space="preserve">
<value>Warnung</value>
</data>
<data name="textWidth" xml:space="preserve">
<value>Breite</value>
</data>
<data name="textWrongCredentials" xml:space="preserve">
<value>Benutzername / Passwort falsch</value>
</data>
<data name="textZoneEntryInThePast" xml:space="preserve">
<value>Zeit Reviereintritt liegt in der Vergangenheit</value>
</data>
<data name="textZoneEntryTime" xml:space="preserve">
<value>Reviereintritt</value>
</data>
<data name="trafficlight_green" type="System.Resources.ResXFileRef, System.Windows.Forms">
<value>trafficlight_green.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</data>
@ -355,196 +535,19 @@
<data name="umbrella_open" type="System.Resources.ResXFileRef, System.Windows.Forms">
<value>umbrella_open.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</data>
<data name="worker2" type="System.Resources.ResXFileRef, System.Windows.Forms">
<value>worker2.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</data>
<data name="textClearAssignment" xml:space="preserve">
<value>Zuordnung entfernen</value>
</data>
<data name="textClearValue" xml:space="preserve">
<value>Eingabe löschen</value>
</data>
<data name="textConfirmation" xml:space="preserve">
<value>Bestätigung</value>
</data>
<data name="textPasswordChanged" xml:space="preserve">
<value>Passwort geändert.</value>
</data>
<data name="textClearFilters" xml:space="preserve">
<value>Filter löschen</value>
</data>
<data name="textShowCancelledShipcalls" xml:space="preserve">
<value>Stornierte anzeigen</value>
</data>
<data name="textBerthRemarks" xml:space="preserve">
<value>Liegeplatz Informationen</value>
</data>
<data name="textBSMDGranted" xml:space="preserve">
<value>Freigabe zur Bearb. f. BSMD erteilt</value>
</data>
<data name="textRemarks" xml:space="preserve">
<value>Info</value>
</data>
<data name="textIncoming" xml:space="preserve">
<value>Einkommend</value>
</data>
<data name="textOutgoing" xml:space="preserve">
<value>Ausgehend</value>
</data>
<data name="textShifting" xml:space="preserve">
<value>Verholung</value>
</data>
<data name="textLength" xml:space="preserve">
<value>Länge</value>
</data>
<data name="textShiftingFrom" xml:space="preserve">
<value>Verholung von</value>
</data>
<data name="textShiftingTo" xml:space="preserve">
<value>Verholung nach</value>
</data>
<data name="textWidth" xml:space="preserve">
<value>Breite</value>
</data>
<data name="textChangeContactInfo" xml:space="preserve">
<value>Kontaktdaten bearbeiten</value>
</data>
<data name="textEmail" xml:space="preserve">
<value>E-Mail</value>
</data>
<data name="textPhone" xml:space="preserve">
<value>Telefon</value>
</data>
<data name="textWrongCredentials" xml:space="preserve">
<value>Benutzername / Passwort falsch</value>
</data>
<data name="textUserNamePasswordEmpty" xml:space="preserve">
<value>Benutzername / Passwort leer!</value>
</data>
<data name="textPort" xml:space="preserve">
<value>Backbord</value>
</data>
<data name="textStarboard" xml:space="preserve">
<value>Steuerbord</value>
</data>
<data name="textAdd" xml:space="preserve">
<value>Hinzufügen</value>
</data>
<data name="textDelete" xml:space="preserve">
<value>Löschen</value>
</data>
<data name="textEdit" xml:space="preserve">
<value>Bearbeiten</value>
</data>
<data name="textEditShips" xml:space="preserve">
<value>Schiffe anlegen / bearbeiten</value>
</data>
<data name="textDeleted" xml:space="preserve">
<value>Gelöscht</value>
</data>
<data name="textEditShip" xml:space="preserve">
<value>Schiff bearbeiten</value>
</data>
<data name="textTugCompany" xml:space="preserve">
<value>Schlepper-Reederei</value>
</data>
<data name="textChangeHistory" xml:space="preserve">
<value>Verlauf</value>
</data>
<data name="textInfoChangePW" xml:space="preserve">
<value>App Info anzeigen und Passwort ändern</value>
</data>
<data name="textShowHistory" xml:space="preserve">
<value>Änderungshistorie der Anläufe anzeigen</value>
</data>
<data name="textMineOnly" xml:space="preserve">
<value>nur eigene</value>
</data>
<data name="textOperation" xml:space="preserve">
<value>Vorgang</value>
</data>
<data name="textParticipant" xml:space="preserve">
<value>Teilnehmer</value>
</data>
<data name="textTimestamp" xml:space="preserve">
<value>Zeitpunkt</value>
</data>
<data name="textFixedOrder" xml:space="preserve">
<value>Als feste Bestellung vermerkt</value>
</data>
<data name="textTooltipSetFixedOrder" xml:space="preserve">
<value>Als feste Bestellung vermerken</value>
</data>
<data name="textTooltipUnSetFixedOrder" xml:space="preserve">
<value>Feste Bestellung zurücknehmen</value>
</data>
<data name="textTriggerManualRefresh" xml:space="preserve">
<value>Manuelle Aktualisierung der Anläufe auslösen</value>
</data>
<data name="Arrival" xml:space="preserve">
<value>Einkommend</value>
</data>
<data name="Departure" xml:space="preserve">
<value>Ausgehend</value>
</data>
<data name="Shifting" xml:space="preserve">
<value>Verholung</value>
</data>
<data name="Undefined" xml:space="preserve">
<value>Unbekannt</value>
</data>
<data name="textShips" xml:space="preserve">
<value>Schiffe</value>
<data name="worker2" type="System.Resources.ResXFileRef, System.Windows.Forms">
<value>worker2.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</data>
<data name="textPilots" xml:space="preserve">
<value>Flusslotsen</value>
<data name="arrow_right_blue" type="System.Resources.ResXFileRef, System.Windows.Forms">
<value>arrow_right_blue.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</data>
<data name="textPortAuthority" xml:space="preserve">
<value>Hafenamt</value>
<data name="arrow_up_green" type="System.Resources.ResXFileRef, System.Windows.Forms">
<value>arrow_up_green.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</data>
<data name="textShiftingSequence" xml:space="preserve">
<value>Verhol. Nr.</value>
</data>
<data name="textClearAll" xml:space="preserve">
<value>Alle Eintragungen zurücksetzen?</value>
</data>
<data name="textBothTideTimesNecessary" xml:space="preserve">
<value>Beide Tidenzeiten sollten angegeben werden (von - bis)</value>
</data>
<data name="textEndValueBeforeStartValue" xml:space="preserve">
<value>Endzeit liegt vor Startzeit</value>
</data>
<data name="textError" xml:space="preserve">
<value>Error</value>
</data>
<data name="textETAInThePast" xml:space="preserve">
<value>Zeitpunkt ETA liegt in der Vergangenheit</value>
</data>
<data name="textETDInThePast" xml:space="preserve">
<value>Zeitpunkt ETD liegt in der Vergangenheit</value>
</data>
<data name="textLockTimeInThePast" xml:space="preserve">
<value>Schleusenzeit liegt in der Vergangenheit</value>
</data>
<data name="textOperationEndInThePast" xml:space="preserve">
<value>Operation Endzeit liegt in der Vergangenheit</value>
</data>
<data name="textOperationStartInThePast" xml:space="preserve">
<value>Operation Startzeit liegt in der Vergangenheit</value>
</data>
<data name="textTideTimesInThePast" xml:space="preserve">
<value>Tidenzeit liegt in der Vergangenheit</value>
</data>
<data name="textWarning" xml:space="preserve">
<value>Warnung</value>
</data>
<data name="textZoneEntryInThePast" xml:space="preserve">
<value>Zeit Reviereintritt liegt in der Vergangenheit</value>
</data>
<data name="textTidalBothValues" xml:space="preserve">
<value>Für das Tidenfenster müssen beide Zeiten angegeben werden</value>
</data>
<data name="textTooFarInTheFuture" xml:space="preserve">
<value>Eine Zeiteingabe ist zu weit in der Zukunft</value>
<data name="textStartTimeMissing" xml:space="preserve">
<value>Wenn eine Ende-Zeit angegeben wird, muss auch eine Start-Zeit angegeben werden</value>
</data>
</root>

View File

@ -589,4 +589,13 @@
<data name="_lock" type="System.Resources.ResXFileRef, System.Windows.Forms">
<value>lock.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</data>
<data name="arrow_right_blue" xml:space="preserve">
<value />
</data>
<data name="arrow_up_green" xml:space="preserve">
<value />
</data>
<data name="textStartTimeMissing" xml:space="preserve">
<value>If an end time is set, a start time is also required</value>
</data>
</root>

View File

@ -89,8 +89,7 @@
<ColumnDefinition Width="30" />
<ColumnDefinition Width=".5*" />
</Grid.ColumnDefinitions>
<xctk:WatermarkTextBox x:Name="textBoxSearch" Grid.Column="0" Margin="2" Watermark="{x:Static p:Resources.textEnterKeyword}" PreviewTextInput="textBoxSearch_PreviewTextInput"
DataObject.Pasting="textBoxSearch_Pasting" TextChanged="textBoxSearch_TextChanged" />
<xctk:WatermarkTextBox x:Name="textBoxSearch" Grid.Column="0" Margin="2" Watermark="{x:Static p:Resources.textEnterKeyword}" TextChanged="textBoxSearch_TextChanged" />
<CheckBox x:Name="checkBoxOwn" VerticalAlignment="Center" Grid.Column="1" HorizontalAlignment="Right" Margin="2" Checked="checkBoxOwn_Checked" Unchecked="checkBoxOwn_Checked" />
<Label Content="{x:Static p:Resources.textMineOnly}" Grid.Column="2" />
</Grid>

View File

@ -183,23 +183,11 @@ namespace BreCalClient
foreach (Participant agency in comboBoxAgencies.SelectedItems)
_model.Agencies.Add(agency.Id);
SearchFilterChanged?.Invoke();
}
private void textBoxSearch_Pasting(object sender, System.Windows.DataObjectPastingEventArgs e)
{
this.SearchFilter.SearchString = e.SourceDataObject.ToString();
SearchFilterChanged?.Invoke();
}
private void textBoxSearch_PreviewTextInput(object sender, TextCompositionEventArgs e)
{
this.SearchFilter.SearchString = this.textBoxSearch.Text + e.Text;
SearchFilterChanged?.Invoke();
}
}
private void textBoxSearch_TextChanged(object sender, TextChangedEventArgs e)
{
this.SearchFilter.SearchString = this.textBoxSearch.Text;
this.SearchFilter.SearchString = this.textBoxSearch.Text.Trim();
SearchFilterChanged?.Invoke();
}

View File

@ -51,7 +51,8 @@ def create_app(test_config=None, instance_path=None):
try:
import os
print(f'Instance path = {app.instance_path}')
os.makedirs(app.instance_path)
if not os.path.exists(app.instance_path):
os.makedirs(app.instance_path)
except OSError:
pass

View File

@ -6,9 +6,12 @@ import typing
from BreCal.schemas.model import Shipcall, Ship, Participant, Berth, User, Times, ShipcallParticipantMap
from BreCal.database.enums import ParticipantType
from BreCal.local_db import getPoolConnection
from BreCal.database.sql_queries import SQLQuery
from BreCal.schemas import model
def pandas_series_to_data_model():
return
return
def set_participant_type(x, participant_df)->int:
"""
@ -20,12 +23,12 @@ def set_participant_type(x, participant_df)->int:
participant_id = x["participant_id"]
participant_type = participant_df.loc[participant_id, "type"]
return participant_type
def get_synchronous_shipcall_times_standalone(query_time:pd.Timestamp, all_df_times:pd.DataFrame, delta_threshold=900)->int:
"""
This function counts all entries in {all_df_times}, which have the same timestamp as {query_time}.
It does so by:
1.) selecting all eta_berth & etd_berth entries
1.) selecting all eta_berth & etd_berth entries
2.) measuring the timedelta towards {query_time}
3.) converting the timedelta to total absolute seconds (positive or negative time differences do not matter)
4.) applying a {delta_threshold} to identify, whether two times are too closely together
@ -57,8 +60,8 @@ def get_synchronous_shipcall_times_standalone(query_time:pd.Timestamp, all_df_ti
def execute_sql_query_standalone(query, param={}, pooledConnection=None, model=None, command_type="query"):
"""
execute an arbitrary query with a set of parameters, return the output and convert it to a list.
when the pooled connection is rebuilt, it will be closed at the end of the function.
execute an arbitrary query with a set of parameters, return the output and convert it to a list.
when the pooled connection is rebuilt, it will be closed at the end of the function.
"""
rebuild_pooled_connection = pooledConnection is None
@ -89,18 +92,45 @@ def execute_sql_query_standalone(query, param={}, pooledConnection=None, model=N
schemas = commands.query_single_or_default(query, sentinel, param=param) if model is None else commands.query_single_or_default(query, sentinel, param=param, model=model)
if schemas is sentinel:
raise Exception("no such record")
elif command_type=="single_or_none":
sentinel = object()
# pulls a *single* row from the query. Typically, these queries require an ID within the param dictionary.
# when providing a model, such as model.Shipcall, the dataset is immediately translated into a data model.
schemas = commands.query_single_or_default(query, sentinel, param=param) if model is None else commands.query_single_or_default(query, sentinel, param=param, model=model)
schemas = None if schemas is sentinel else schemas
elif command_type=="execute_scalar":
schemas = commands.execute_scalar(query)
else:
raise ValueError(command_type)
finally: # if needed, ensure that the pooled connection is closed.
if rebuild_pooled_connection:
pooledConnection.close()
return schemas
def get_assigned_participant_of_type(shipcall_id:int, participant_type:typing.Union[int,model.ParticipantType])->typing.Optional[model.Participant]:
"""obtains the ShipcallParticipantMap of a given shipcall and finds the participant id of a desired type. Finally, returns the respective Participant"""
spm_shipcall_data = execute_sql_query_standalone(
query=SQLQuery.get_shipcall_participant_map_by_shipcall_id_and_type(),
param={"id":shipcall_id, "type":int(participant_type)},
command_type="query") # returns a list of matches
if len(spm_shipcall_data)==0:
return None
query = 'SELECT * FROM participant WHERE id=?participant_id?'
assigned_participant = execute_sql_query_standalone(
query=query,
param={"participant_id":spm_shipcall_data[0]["participant_id"]},
model=model.Participant,
command_type="single_or_none"
) # returns a list of matches
return assigned_participant
class SQLHandler():
"""
An object that reads SQL queries from the sql_connection and stores it in pandas DataFrames. The object can read all available tables
@ -131,7 +161,7 @@ class SQLHandler():
schema = cursor.fetchall()
all_schemas = [schem[0] for schem in schema]
return all_schemas
def build_str_to_model_dict(self):
"""
creates a simple dictionary, which maps a string to a data object
@ -151,7 +181,7 @@ class SQLHandler():
cursor.execute(f"DESCRIBE {table_name}")
cols = cursor.fetchall()
column_names = [col_name[0] for col_name in cols]
# 2.) get the data tuples
cursor.execute(f"SELECT * FROM {table_name}")
data = cursor.fetchall()
@ -162,14 +192,14 @@ class SQLHandler():
# 4.) build a dataframe from the respective data models (which ensures the correct data type)
df = self.build_df_from_data_and_name(data, table_name)
return df
def build_df_from_data_and_name(self, data, table_name):
data_model = self.str_to_model_dict.get(table_name)
if data_model is not None:
df = pd.DataFrame([data_model(**dat) for dat in data], columns=list(data_model.__annotations__.keys()))
else:
df = pd.DataFrame([dat for dat in data])
return df
return df
def mysql_to_df(self, query, table_name):
"""provide an arbitrary sql query that should be read from a mysql server {sql_connection}. returns a pandas DataFrame with the obtained data"""
@ -192,7 +222,7 @@ class SQLHandler():
if 'id' in df.columns:
df = df.set_index('id', inplace=False) # avoid inplace updates, so the raw sql remains unchanged
return df
def read_all(self, all_schemas):
# create a dictionary, which maps every mysql schema to pandas DataFrames
self.df_dict = self.build_full_mysql_df_dict(all_schemas)
@ -212,7 +242,7 @@ class SQLHandler():
query = f"SELECT * FROM {schem}"
mysql_df_dict[schem] = self.mysql_to_df(query, table_name=schem)
return mysql_df_dict
def initialize_shipcall_participant_list(self):
"""
iteratively applies the .get_participants method to each shipcall.
@ -226,10 +256,10 @@ class SQLHandler():
# if the shipcall_id exists, the list contains ids
# otherwise, return a blank list
df['participants'] = df.apply(
lambda x: self.get_participants(x.name),
lambda x: self.get_participants(x.name),
axis=1)
return
def add_participant_type_to_map(self):
"""
applies a lambda function, where the 'type'-column in the shipcall_participant_map is updated by reading the
@ -242,14 +272,14 @@ class SQLHandler():
#spm.loc[:,"type"] = spm.loc[:].apply(lambda x: set_participant_type(x, participant_df=participant_df),axis=1)
#self.df_dict["shipcall_participant_map"] = spm
return
def get_assigned_participants(self, shipcall)->pd.DataFrame:
"""return each participant of a respective shipcall, filtered by the shipcall id"""
# get the shipcall_participant_map
spm = self.df_dict["shipcall_participant_map"]
assigned_participants = spm.loc[spm["shipcall_id"]==shipcall.id]
return assigned_participants
def get_assigned_participants_by_type(self, assigned_participants:pd.DataFrame, participant_type:ParticipantType):
"""filters a dataframe of assigned_participants by the provided type enumerator"""
if isinstance(participant_type,int):
@ -258,7 +288,7 @@ class SQLHandler():
assigned_participants_of_type = assigned_participants.loc[[participant_type in ParticipantType(int(pt_)) for pt_ in list(assigned_participants["type"].values)]]
#assigned_participants_of_type = assigned_participants.loc[assigned_participants["type"]==participant_type.value]
return assigned_participants_of_type
def check_if_any_participant_of_type_is_unassigned(self, shipcall, *args:list[ParticipantType])->bool:
"""
given a list of input arguments, where item is a participant type, the function determines, whether at least one participant
@ -275,13 +305,13 @@ class SQLHandler():
unassignment = len(assignments_of_type)==0 # a participant type does not exist, when there is no match
unassigned.append(unassignment)
return any(unassigned) # returns a single boolean, whether ANY of the types is not assigned
def standardize_model_str(self, model_str:str)->str:
"""check if the 'model_str' is valid and apply lowercasing to the string"""
model_str = model_str.lower()
assert model_str in list(self.df_dict.keys()), f"cannot find the requested 'model_str' in mysql: {model_str}"
return model_str
def get_data(self, id:int, model_str:str):
"""
obtains {id} from the respective mysql database and builds a data model from that.
@ -293,11 +323,11 @@ class SQLHandler():
returns a Shipcall object
"""
model_str = self.standardize_model_str(model_str)
df = self.df_dict.get(model_str)
data = self.df_loc_to_data_model(df, id, model_str)
return data
def get_all(self, model_str:str)->list:
"""
given a model string (e.g., 'shipcall'), return a list of all
@ -311,13 +341,13 @@ class SQLHandler():
for _aid in all_ids
]
return all_data
def df_loc_to_data_model(self, df, id, model_str, loc_type:str="loc"):
if not len(df)>0:
import warnings
warnings.warn(f"empty dataframe in SQLHandler.df_loc_to_data_model for model type: {model_str}\n")
return df
# get a pandas series from the dataframe
series = df.loc[id] if loc_type=="loc" else df.iloc[id]
@ -330,7 +360,7 @@ class SQLHandler():
data = {**{'id':int(id)}, **series.to_dict()} # 'id' must be added manually, as .to_dict does not contain the index, which was set with .set_index
data = data_model(**data)
return data
def filter_df_by_participant_type(self, df, participant_type:typing.Union[int, ParticipantType])->pd.DataFrame:
"""
As ParticipantTypes are Flag objects, a dataframe's integer might resemble multiple participant types simultaneously.
@ -346,7 +376,7 @@ class SQLHandler():
participant_type = ParticipantType(participant_type)
filtered_df = df.loc[[participant_type in ParticipantType(df_pt) for df_pt in list(df["participant_type"].values)]]
return filtered_df
def get_times_for_participant_type(self, df_times, participant_type:int):
filtered_series = self.filter_df_by_participant_type(df_times, participant_type)
#filtered_series = df_times.loc[df_times["participant_type"]==participant_type]
@ -355,14 +385,14 @@ class SQLHandler():
return None
if not len(filtered_series)<=1:
# correcting the error: ERROR:root:found multiple results
# correcting the error: ERROR:root:found multiple results
# however, a warning will still be issued
import warnings
warnings.warn(f"found multiple results in function SQLHandler.get_times_for_participant_type\nConsidering only the first match!\nAffected Times Indexes: {filtered_series.index}")
times = self.df_loc_to_data_model(filtered_series, id=0, model_str='times', loc_type="iloc") # use iloc! to retrieve the first result
return times
def dataframe_to_data_model_list(self, df, model_str)->list:
model_str = self.standardize_model_str(model_str)
@ -383,22 +413,22 @@ class SQLHandler():
df = self.df_dict.get("shipcall_participant_map")
if 'shipcall_id' in list(df.columns):
df = df.set_index('shipcall_id', inplace=False)
# the 'if' call is needed to ensure, that no Exception is raised, when the shipcall_id is not present in the df
participant_id_list = df.loc[shipcall_id, "participant_id"].tolist() if shipcall_id in list(df.index) else []
if not isinstance(participant_id_list,list):
participant_id_list = [participant_id_list]
return participant_id_list
def get_times_of_shipcall(self, shipcall)->pd.DataFrame:
df_times = self.df_dict.get('times') # -> pd.DataFrame
df_times = df_times.loc[df_times["shipcall_id"]==shipcall.id]
return df_times
def get_times_for_agency(self, non_null_column=None)->pd.DataFrame:
"""
options:
non_null_column:
non_null_column:
None or str. If provided, the 'non_null_column'-column of the dataframe will be filtered,
so only entries with provided values are returned (filters all NaN and NaT entries)
"""
@ -407,8 +437,8 @@ class SQLHandler():
# filter out all NaN and NaT entries
if non_null_column is not None:
# in the Pandas documentation, it says for .isnull():
# "This function takes a scalar or array-like object and indicates whether values are missing
# in the Pandas documentation, it says for .isnull():
# "This function takes a scalar or array-like object and indicates whether values are missing
# (NaN in numeric arrays, None or NaN in object arrays, NaT in datetimelike)."
df_times = df_times.loc[~df_times[non_null_column].isnull()] # NOT null filter
@ -416,10 +446,10 @@ class SQLHandler():
times_agency = self.filter_df_by_participant_type(df_times, ParticipantType.AGENCY.value)
#times_agency = df_times.loc[df_times["participant_type"]==ParticipantType.AGENCY.value]
return times_agency
def filter_df_by_key_value(self, df, key, value)->pd.DataFrame:
return df.loc[df[key]==value]
def get_unique_ship_counts(self, all_df_times:pd.DataFrame, times_agency:pd.DataFrame, query:str, rounding:str="min", maximum_threshold=3):
"""given a dataframe of all agency times, get all unique ship counts, their values (datetime) and the string tags. returns a tuple (values,unique,counts)"""
# #deprecated!

View File

@ -12,3 +12,11 @@ def get_user_data_for_id(user_id:int, expiration_time:int=90):
user_data["exp"] = (datetime.datetime.now()+datetime.timedelta(minutes=expiration_time)).timestamp()
return user_data
def get_times_data_for_id(times_id:int):
"""helper function to load previous times data from the database"""
query = "SELECT * FROM times where id = ?id?"
pdata = execute_sql_query_standalone(query=query, param={"id":times_id})
pdata = pdata[0] if len(pdata)>0 else None
assert pdata is not None, f"could not find times with id {times_id}"
return pdata

View File

@ -28,7 +28,7 @@ def validation_error_default_asserts(response):
def validate_posted_shipcall_data(user_data:dict, loadedModel:dict, content:dict):
"""this function applies more complex validation functions to data, which is sent to a post-request of shipcalls"""
# DEPRECATED: this function has been refactored into InputValidationShipcall (see methods for POST and PUT evaluation)
# #TODO_refactor: this function is pretty complex. One may instead build an object, which calls the methods separately.
# #TODO_refactor: this function is pretty complex. One may instead build an object, which calls the methods separately.
##### Section 1: check user_data #####
# check, whether the user belongs to a participant, which is of type ParticipantType.BSMD
@ -36,23 +36,23 @@ def validate_posted_shipcall_data(user_data:dict, loadedModel:dict, content:dict
is_bsmd = check_if_user_is_bsmd_type(user_data)
if not is_bsmd:
raise ValidationError({"user_participant_type":f"current user does not belong to BSMD. Cannot post shipcalls. Found user data: {user_data}"})
##### Section 2: check loadedModel #####
valid_ship_id = check_if_ship_id_is_valid(ship_id=loadedModel.get("ship_id", None))
if not valid_ship_id:
raise ValidationError({"ship_id":f"provided an invalid ship id, which is not found in the database: {loadedModel.get('ship_id', None)}"})
valid_arrival_berth_id = check_if_berth_id_is_valid(berth_id=loadedModel.get("arrival_berth_id", None))
if not valid_arrival_berth_id:
raise ValidationError({"arrival_berth_id":f"provided an invalid arrival berth id, which is not found in the database: {loadedModel.get('arrival_berth_id', None)}"})
valid_departure_berth_id = check_if_berth_id_is_valid(berth_id=loadedModel.get("departure_berth_id", None))
if not valid_departure_berth_id:
raise ValidationError({"departure_berth_id":f"provided an invalid departure berth id, which is not found in the database: {loadedModel.get('departure_berth_id', None)}"})
valid_participant_ids = check_if_participant_ids_are_valid(participants=loadedModel.get("participants",[]))
if not valid_participant_ids:
raise ValidationError({"participants":f"one of the provided participant ids is invalid. Could not find one of these in the database: {loadedModel.get('participants', None)}"})
raise ValidationError({"participants":f"one of the provided participant ids is invalid. Could not find one of these in the database: {loadedModel.get('participants', None)}"})
##### Section 3: check content #####
@ -63,18 +63,18 @@ def validate_posted_shipcall_data(user_data:dict, loadedModel:dict, content:dict
value = content.get(forbidden_key, None)
if value is not None:
raise ValidationError({"forbidden_key":f"'{forbidden_key}' may not be set on POST. Found: {value}"})
voyage_str_is_invalid = check_if_string_has_special_characters(text=content.get("voyage",""))
if voyage_str_is_invalid:
raise ValidationError({"voyage":f"there are invalid characters in the 'voyage'-string. Please use only digits and ASCII letters. Allowed: {ascii_letters+digits}. Found: {content.get('voyage')}"})
raise ValidationError({"voyage":f"there are invalid characters in the 'voyage'-string. Please use only digits and ASCII letters. Allowed: {ascii_letters+digits}. Found: {content.get('voyage')}"})
##### Section 4: check loadedModel & content #####
# #TODO_refactor: these methods should be placed in separate locations
# existance checks in content
# datetime checks in loadedModel (datetime.datetime objects). Dates should be in the future.
time_now = datetime.datetime.now()
# datetime checks in loadedModel (datetime.datetime objects). Dates should be in the future.
time_now = datetime.datetime.now() - datetime.timedelta(days=1)
type_ = loadedModel.get("type", int(ShipcallType.undefined))
if int(type_)==int(ShipcallType.undefined):
raise ValidationError({"type":f"providing 'type' is mandatory. Missing key!"})
@ -85,7 +85,7 @@ def validate_posted_shipcall_data(user_data:dict, loadedModel:dict, content:dict
if content.get("arrival_berth_id", None) is None:
raise ValidationError({"arrival_berth_id":f"providing 'arrival_berth_id' is mandatory. Missing key!"})
if not eta >= time_now:
raise ValidationError({"eta":f"'eta' must be in the future. Incorrect datetime provided."})
raise ValidationError({"eta":f"'eta' is too far in the past. Incorrect datetime provided."})
elif int(type_)==int(ShipcallType.departure):
etd = loadedModel.get("etd")
if (content.get("etd", None) is None):
@ -93,7 +93,7 @@ def validate_posted_shipcall_data(user_data:dict, loadedModel:dict, content:dict
if content.get("departure_berth_id", None) is None:
raise ValidationError({"departure_berth_id":f"providing 'departure_berth_id' is mandatory. Missing key!"})
if not etd >= time_now:
raise ValidationError({"etd":f"'etd' must be in the future. Incorrect datetime provided."})
raise ValidationError({"etd":f"'etd' is too far in the past. Incorrect datetime provided."})
elif int(type_)==int(ShipcallType.shifting):
eta = loadedModel.get("eta")
etd = loadedModel.get("etd")
@ -103,20 +103,20 @@ def validate_posted_shipcall_data(user_data:dict, loadedModel:dict, content:dict
if (content.get("arrival_berth_id", None) is None) or (content.get("departure_berth_id", None) is None):
raise ValidationError({"arrival_berth_id_or_departure_berth_id":f"providing 'arrival_berth_id' & 'departure_berth_id' is mandatory. Missing key!"})
if (not eta >= time_now) or (not etd >= time_now) or (not eta >= etd):
raise ValidationError({"eta_or_etd":f"'eta' and 'etd' must be in the future. Incorrect datetime provided."})
raise ValidationError({"eta_or_etd":f"'eta' and 'etd' are too far in the past. Incorrect datetime provided."})
tidal_window_from = loadedModel.get("tidal_window_from", None)
tidal_window_to = loadedModel.get("tidal_window_to", None)
if tidal_window_to is not None:
if not tidal_window_to >= time_now:
raise ValidationError({"tidal_window_to":f"'tidal_window_to' must be in the future. Incorrect datetime provided."})
raise ValidationError({"tidal_window_to":f"'tidal_window_to' is too far in the past. Incorrect datetime provided."})
if tidal_window_from is not None:
if not tidal_window_from >= time_now:
raise ValidationError({"tidal_window_from":f"'tidal_window_from' must be in the future. Incorrect datetime provided."})
raise ValidationError({"tidal_window_from":f"'tidal_window_from' is too far in the past. Incorrect datetime provided."})
# #TODO: assert tidal_window_from > tidal_window_to
# #TODO: len of participants > 0, if agency
# * assigned participant for agency
return
@ -126,21 +126,21 @@ class InputValidation():
def __init__(self):
self.build_supported_models_dictionary()
return
def build_supported_models_dictionary(self):
self.supported_models = {
Ship:ShipValidation(),
Shipcall:ShipcallValidation(),
Berth:BerthValidation(),
User:UserValidation(),
Berth:BerthValidation(),
User:UserValidation(),
Participant:ParticipantValidation(),
}
return
def assert_if_not_supported(self, dataclass_object):
assert type(dataclass_object) in self.supported_models, f"unsupported model. Found: {type(dataclass_object)}"
return
def verify(self, dataclass_object):
self.assert_if_not_supported(dataclass_object)
@ -157,17 +157,17 @@ class DataclassValidation(ABC):
"""parent class of dataclas validators, which determines the outline of every object"""
def __init__(self):
return
def check(self, dataclass_object) -> (list, bool):
"""
the 'check' method provides a default style, how each dataclass object is validated. It returns a list of violations
the 'check' method provides a default style, how each dataclass object is validated. It returns a list of violations
and a boolean, which determines, whether the check is passed successfully
"""
all_rules = self.apply_all_rules(dataclass_object)
violations = self.filter_violations(all_rules)
input_validation_state = self.evaluate(violations)
return (violations, input_validation_state)
@abstractmethod
def apply_all_rules(self, dataclass_object) -> list:
"""
@ -176,13 +176,13 @@ class DataclassValidation(ABC):
"""
all_rules = [(True, 'blank_validation_rule')]
return all_rules
def filter_violations(self, all_rules):
"""input: all_rules, a list of tuples, where each element is (output, validation_name), which are (bool, str). """
# if output is False, a violation is observed
violations = [result[1] for result in all_rules if not result[0]]
return violations
def evaluate(self, violations) -> bool:
input_validation_state = len(violations)==0
return input_validation_state
@ -194,7 +194,7 @@ class ShipcallValidation(DataclassValidation):
def __init__(self):
super().__init__()
return
def apply_all_rules(self, dataclass_object) -> list:
"""apply all input validation rules to determine, whether there are violations. returns a list of tuples (output, validation_name)"""
raise NotImplementedError()
@ -202,16 +202,16 @@ class ShipcallValidation(DataclassValidation):
from BreCal.validators.schema_validation import ship_bollard_pull_is_defined_or_is_not_tug, ship_bollard_pull_is_none_or_in_range, ship_callsign_len_is_seven_at_maximum, ship_eni_len_is_eight, ship_imo_len_is_seven, ship_length_in_range, ship_participant_id_is_defined_or_is_not_tug, ship_participant_id_is_none_or_int, ship_width_in_range
# skip: ship_max_draft_is_defined_or_is_not_tug, ship_max_draft_is_none_or_in_range,
# skip: ship_max_draft_is_defined_or_is_not_tug, ship_max_draft_is_none_or_in_range,
class ShipValidation(DataclassValidation):
"""an object that validates a Ship dataclass object"""
def __init__(self):
super().__init__()
return
def apply_all_rules(self, dataclass_object) -> list:
"""apply all input validation rules to determine, whether there are violations. returns a list of tuples (output, validation_name)"""
# skip: ship_max_draft_is_defined_or_is_not_tug, ship_max_draft_is_none_or_in_range,
# skip: ship_max_draft_is_defined_or_is_not_tug, ship_max_draft_is_none_or_in_range,
"""
#TODO_ship_max_draft
with pytest.raises(AttributeError, match="'Ship' object has no attribute 'max_draft'"):
@ -225,14 +225,14 @@ class ShipValidation(DataclassValidation):
check_rule(dataclass_object)
for check_rule in [
ship_bollard_pull_is_defined_or_is_not_tug,
ship_bollard_pull_is_none_or_in_range,
ship_callsign_len_is_seven_at_maximum,
ship_eni_len_is_eight,
ship_imo_len_is_seven,
ship_length_in_range,
ship_participant_id_is_defined_or_is_not_tug,
ship_participant_id_is_none_or_int,
ship_bollard_pull_is_defined_or_is_not_tug,
ship_bollard_pull_is_none_or_in_range,
ship_callsign_len_is_seven_at_maximum,
ship_eni_len_is_eight,
ship_imo_len_is_seven,
ship_length_in_range,
ship_participant_id_is_defined_or_is_not_tug,
ship_participant_id_is_none_or_int,
ship_width_in_range
]
]
@ -243,7 +243,7 @@ class BerthValidation(DataclassValidation):
def __init__(self):
super().__init__()
return
def apply_all_rules(self, dataclass_object) -> list:
"""apply all input validation rules to determine, whether there are violations. returns a list of tuples (output, validation_name)"""
raise NotImplementedError()
@ -254,7 +254,7 @@ class UserValidation(DataclassValidation):
def __init__(self):
super().__init__()
return
def apply_all_rules(self, dataclass_object) -> list:
"""apply all input validation rules to determine, whether there are violations. returns a list of tuples (output, validation_name)"""
raise NotImplementedError()
@ -266,7 +266,7 @@ class ParticipantValidation(DataclassValidation):
def __init__(self):
super().__init__()
return
def apply_all_rules(self, dataclass_object) -> list:
"""apply all input validation rules to determine, whether there are violations. returns a list of tuples (output, validation_name)"""

View File

@ -6,7 +6,7 @@ from marshmallow import ValidationError
from string import ascii_letters, digits
from BreCal.schemas.model import Ship, Shipcall, Berth, User, Participant, ShipcallType
from BreCal.database.sql_handler import execute_sql_query_standalone
from BreCal.database.sql_handler import execute_sql_query_standalone
from BreCal.database.sql_queries import SQLQuery
from BreCal.impl.participant import GetParticipant
from BreCal.impl.ships import GetShips
@ -27,7 +27,7 @@ class InputValidationShip():
Example:
InputValidationShip.evaluate(user_data, loadedModel, content)
When the data violates one of the rules, a marshmallow.ValidationError is raised, which details the issues.
When the data violates one of the rules, a marshmallow.ValidationError is raised, which details the issues.
"""
def __init__(self) -> None:
pass
@ -37,13 +37,13 @@ class InputValidationShip():
# 1.) Only users of type BSMD are allowed to POST
InputValidationShip.check_user_is_bsmd_type(user_data)
# 2.) The ship IMOs are used as matching keys. They must be unique in the database.
# 2.) The ship IMOs are used as matching keys. They must be unique in the database.
InputValidationShip.check_ship_imo_already_exists(loadedModel)
# 3.) Check for reasonable Values (see BreCal.schemas.model.ShipSchema)
InputValidationShip.optionally_evaluate_bollard_pull_value(content)
return
@staticmethod
def evaluate_put_data(user_data:dict, loadedModel:dict, content:dict):
# 1.) Only users of type BSMD are allowed to PUT
@ -58,20 +58,20 @@ class InputValidationShip():
# 4.) Check for reasonable Values (see BreCal.schemas.model.ShipSchema)
InputValidationShip.optionally_evaluate_bollard_pull_value(content)
return
@staticmethod
def evaluate_delete_data(user_data:dict, ship_id:typing.Optional[int]):
if ship_id is None:
raise ValidationError({"id":f"The ship id must be provided."})
ship_id = int(ship_id)
# 1.) Only users of type BSMD are allowed to PUT
InputValidationShip.check_user_is_bsmd_type(user_data)
# 2.) The dataset entry may not be deleted already
InputValidationShip.check_if_entry_is_already_deleted(ship_id)
return
@staticmethod
def optionally_evaluate_bollard_pull_value(content:dict):
bollard_pull = content.get("bollard_pull",None)
@ -89,7 +89,7 @@ class InputValidationShip():
is_bsmd = check_if_user_is_bsmd_type(user_data)
if not is_bsmd:
raise ValidationError({"participant_type":f"current user does not belong to BSMD. Cannot post, put or delete ships. Found user data: {user_data}"})
@staticmethod
def check_ship_imo_already_exists(loadedModel:dict):
# get the ships, convert them to a list of JSON dictionaries
@ -97,35 +97,35 @@ class InputValidationShip():
ships = json.loads(response)
# extract only the 'imo' values
ship_imos = [ship.get("imo") for ship in ships]
ship_imos = [ship.get("imo") for ship in ships if not ship.get("deleted")]
# check, if the imo in the POST-request already exists in the list
imo_already_exists = loadedModel.get("imo") in ship_imos
if imo_already_exists:
raise ValidationError({"imo":f"the provided ship IMO {loadedModel.get('imo')} already exists. A ship may only be added, if there is no other ship with the same IMO number."})
return
@staticmethod
def put_content_may_not_contain_imo_number(content:dict):
# IMO is a required field, so it will never be None outside of tests. If so, there is no violation
put_data_ship_imo = content.get("imo",None)
if put_data_ship_imo is None:
return
# grab the ship by its ID and compare, whether the IMO is unchanged
ship = execute_sql_query_standalone(SQLQuery.get_ship_by_id(), param={"id":content.get("id")}, command_type="single", model=Ship)
if put_data_ship_imo != ship.imo:
raise ValidationError({"imo":f"The IMO number field may not be changed since it serves the purpose of a primary (matching) key."})
return
@staticmethod
def content_contains_ship_id(content:dict):
put_data_ship_id = content.get('id',None)
if put_data_ship_id is None:
raise ValidationError({"id":f"The id field is required."})
return
@staticmethod
def check_if_entry_is_already_deleted(ship_id:typing.Optional[int]):
"""
@ -147,4 +147,4 @@ class InputValidationShip():
return

View File

@ -12,6 +12,7 @@ from BreCal.impl.berths import GetBerths
from BreCal.database.enums import ParticipantType, ParticipantFlag
from BreCal.validators.input_validation_utils import check_if_user_is_bsmd_type, check_if_ship_id_is_valid, check_if_berth_id_is_valid, check_if_participant_ids_are_valid, check_if_participant_ids_and_types_are_valid, get_shipcall_id_dictionary, get_participant_type_from_user_data
from BreCal.database.sql_handler import get_assigned_participant_of_type
from BreCal.database.sql_handler import execute_sql_query_standalone
from BreCal.validators.validation_base_utils import check_if_int_is_valid_flag
from BreCal.validators.validation_base_utils import check_if_string_has_special_characters
@ -28,7 +29,7 @@ class InputValidationShipcall():
Example:
InputValidationShipcall.evaluate(user_data, loadedModel, content)
When the data violates one of the rules, a marshmallow.ValidationError is raised, which details the issues.
When the data violates one of the rules, a marshmallow.ValidationError is raised, which details the issues.
"""
def __init__(self) -> None:
@ -38,7 +39,7 @@ class InputValidationShipcall():
def evaluate_post_data(user_data:dict, loadedModel:dict, content:dict):
"""
this function combines multiple validation functions to verify data, which is sent to the API as a shipcall's POST-request
checks:
1. permission: only participants that belong to the BSMD or AGENCY groups are allowed to POST shipcalls
2. reference checks: all refered objects within the Shipcall must exist
@ -60,16 +61,16 @@ class InputValidationShipcall():
# check for reasonable values in the shipcall fields
InputValidationShipcall.check_shipcall_values(loadedModel, content, forbidden_keys=["evaluation", "evaluation_message"]) # "canceled"
return
@staticmethod
def evaluate_put_data(user_data:dict, loadedModel:dict, content:dict):
"""
this function combines multiple validation functions to verify data, which is sent to the API as a shipcall's PUT-request
checks:
1. user's authority:
a) whether the user's participant is assigned to the shipcall (via shipcall-participant-map)
b) whether the user is either an AGENCY (assigned) or the BSMD, in case the AGENCY allows the BSMD to edit their shipcalls
b) whether the user is either an AGENCY (assigned) or the BSMD, in case the AGENCY allows the BSMD to edit their shipcalls
2. existance of required fields
3. all value-rules of the POST evaluation
4. a canceled shipcall may not be changed
@ -89,24 +90,24 @@ class InputValidationShipcall():
# check the referenced IDs
InputValidationShipcall.check_referenced_ids(loadedModel)
# check for reasonable values in the shipcall fields and checks for forbidden keys.
# check for reasonable values in the shipcall fields and checks for forbidden keys.
InputValidationShipcall.check_shipcall_values(loadedModel, content, forbidden_keys=["evaluation", "evaluation_message"], is_put_data=True)
# a canceled shipcall cannot be selected
# Note: 'canceled' is allowed in PUT-requests, if it is not already set (which is checked by InputValidationShipcall.check_shipcall_is_cancel)
InputValidationShipcall.check_shipcall_is_canceled(loadedModel, content)
return
@staticmethod
def check_shipcall_values(loadedModel:dict, content:dict, forbidden_keys:list=["evaluation", "evaluation_message"], is_put_data:bool=False):
"""
individually checks each value provided in the loadedModel/content.
individually checks each value provided in the loadedModel/content.
This function validates, whether the values are reasonable.
Also, some data may not be set in a POST-request.
options:
is_put_data: bool. Some validation rules do not apply to POST data, but apply to PUT data. This flag separates the two.
is_put_data: bool. Some validation rules do not apply to POST data, but apply to PUT data. This flag separates the two.
"""
# Note: BreCal.schemas.model.ShipcallSchema has an internal validation, which the marshmallow library provides. This is used
# to verify values individually, when the schema is loaded with data.
@ -116,24 +117,23 @@ class InputValidationShipcall():
# voyage shall not contain special characters
voyage_str_is_invalid = check_if_string_has_special_characters(text=content.get("voyage",""))
if voyage_str_is_invalid:
raise ValidationError({"voyage":f"there are invalid characters in the 'voyage'-string. Please use only digits and ASCII letters. Allowed: {ascii_letters+digits}. Found: {content.get('voyage')}"})
raise ValidationError({"voyage":f"there are invalid characters in the 'voyage'-string. Please use only digits and ASCII letters. Allowed: {ascii_letters+digits}. Found: {content.get('voyage')}"})
# the 'flags' integer must be valid
flags_value = content.get("flags", 0)
if check_if_int_is_valid_flag(flags_value, enum_object=ParticipantFlag):
raise ValidationError({"flags":f"incorrect value provided for 'flags'. Must be a valid combination of the flags."})
if is_put_data:
# the type of a shipcall may not be changed. It can only be set with the initial POST-request.
InputValidationShipcall.check_shipcall_type_is_unchanged(loadedModel)
else:
# time values must use future-dates
InputValidationShipcall.check_times_are_in_future(loadedModel, content)
InputValidationShipcall.check_times_are_in_future(loadedModel, content)
# some arguments must not be provided
InputValidationShipcall.check_forbidden_arguments(content, forbidden_keys=forbidden_keys)
return
@staticmethod
def check_agency_in_shipcall_participant_map(user_data:dict, loadedModel:dict, content:dict, spm_shipcall_data:typing.Optional[list]=None):
"""
@ -144,11 +144,11 @@ class InputValidationShipcall():
Upon violation, this method issues 'Forbidden'-Exceptions with HTTP status code 403. There are four reasons for violations:
a) an agency tries to self-assign for a shipcall
b) there is no assigned agency for the current shipcall
c) an agency is assigned, but the current agency-user belongs to a different participant_id
c) an agency is assigned, but the current agency-user belongs to a different participant_id
d) the user must be of ParticipantType BSMD or AGENCY
args:
spm_shipcall_data:
spm_shipcall_data:
a list of entries obtained from the ShipcallParticipantMap. These are deserialized dictionaries.
e.g., [{'participant_id': 136, 'type': 8}, ]
"""
@ -157,11 +157,11 @@ class InputValidationShipcall():
# read the ShipcallParticipantMap entry of the current shipcall_id. This is used within the input validation of a PUT request
spm_shipcall_data = execute_sql_query_standalone(
# #TODO_refactor: place this within the SQLQuery object
query = "SELECT participant_id, type FROM shipcall_participant_map WHERE shipcall_id=?shipcall_id?",
query = "SELECT participant_id, type FROM shipcall_participant_map WHERE shipcall_id=?shipcall_id?",
param={"shipcall_id":loadedModel["id"]},
pooledConnection=None
)
# which role should be set by the PUT request? If the agency is about to be set, an error will be created
# read the user data from the JWT token (set when login is performed)
user_type = get_participant_type_from_user_data(user_data) # decode JWT -> get 'type' value (guarantees to convert user type into an IntFlag)
@ -179,7 +179,7 @@ class InputValidationShipcall():
# user not AGENCY or BSMD
raise werkzeug.exceptions.Forbidden(f"PUT Requests for shipcalls can only be issued by AGENCY or BSMD users.") # Forbidden: 403
# Placeholder: when a user is an AGENCY,
# Placeholder: when a user is an AGENCY,
if (ParticipantType.AGENCY in user_type) & (any_type_is_agency):
# self-assignment: agency sets agency participant
@ -188,7 +188,7 @@ class InputValidationShipcall():
if len(agency_entries)>0:
# agency participant exists: participant id must be the same as shipcall participant map entry
matching_spm_entry = [spm_entry for spm_entry in spm_shipcall_data if (spm_entry.get("participant_id")==user_data["id"]) & (int(spm_entry.get("type"))==int(ParticipantType.AGENCY))]
if len(matching_spm_entry)==0:
# An AGENCY was found, but a different participant_id is assigned to that AGENCY
raise werkzeug.exceptions.Forbidden(f"A different participant_id is assigned as the AGENCY of this shipcall. Provided ID: {user_data.get('id')}, Assigned ShipcallParticipantMap: {agency_entries}") # Forbidden: 403
@ -200,7 +200,7 @@ class InputValidationShipcall():
# agency participant does not exist: there is no assigned agency role for the shipcall {shipcall_id}
raise werkzeug.exceptions.Forbidden(f"There is no assigned agency for this shipcall. Shipcall ID: {loadedModel['id']}") # Forbidden: 403
return
@staticmethod
def check_user_is_bsmd_or_agent_type(user_data):
"""
@ -225,7 +225,7 @@ class InputValidationShipcall():
check, whether the referenced entries exist (e.g., when a Ship ID is referenced, but does not exist, the validation fails)
"""
# #TODO: arrival and departure berth id should be coupled with the shipcall type. One shall not provide
# arrival berth id when the shipcall type is departure or vise versa.
# arrival berth id when the shipcall type is departure or vise versa.
# a similar logic has already been implemented to the eta/etd or for the operation windows
# get all IDs from the loadedModel
@ -237,23 +237,23 @@ class InputValidationShipcall():
valid_ship_id = check_if_ship_id_is_valid(ship_id=ship_id)
if not valid_ship_id:
raise ValidationError({"ship_id":f"provided an invalid ship id, which is not found in the database: {ship_id}"})
valid_arrival_berth_id = check_if_berth_id_is_valid(berth_id=arrival_berth_id)
if not valid_arrival_berth_id:
raise ValidationError({"arrival_berth_id":f"provided an invalid arrival berth id, which is not found in the database: {arrival_berth_id}"})
valid_departure_berth_id = check_if_berth_id_is_valid(berth_id=departure_berth_id)
if not valid_departure_berth_id:
raise ValidationError({"departure_berth_id":f"provided an invalid departure berth id, which is not found in the database: {departure_berth_id}"})
valid_participant_ids = check_if_participant_ids_are_valid(participants=participants)
if not valid_participant_ids:
raise ValidationError({"participants":f"one of the provided participant ids is invalid. Could not find one of these in the database: {participants}"})
valid_participant_types = check_if_participant_ids_and_types_are_valid(participants=participants)
if not valid_participant_types: # #TODO: according to Daniel, there may eventually be multi-assignment of participants for the same role
raise ValidationError({"participants":f"every participant id and type should be listed only once. Found multiple entries for one of the participants."})
@staticmethod
def check_shipcall_type_is_unchanged(loadedModel:dict):
# the type of a shipcall may only be set on POST requests. Afterwards, shipcall types may not be changed.
@ -263,20 +263,20 @@ class InputValidationShipcall():
if int(loadedModel["type"]) != int(shipcall.type):
raise ValidationError({"type":f"The shipcall type may only be set in the initial POST-request. Afterwards, changing the shipcall type is not allowed."}) # @pytest.raises
return
@staticmethod
def check_forbidden_arguments(content:dict, forbidden_keys=["evaluation", "evaluation_message"]):
"""
a post-request must not contain the arguments 'canceled', 'evaluation', 'evaluation_message'.
a put-request must not contain the arguments 'evaluation', 'evaluation_message'
"""
# the following keys should not be set in a POST-request.
for forbidden_key in forbidden_keys:
value = content.get(forbidden_key, None)
if value is not None:
raise ValidationError({"forbidden_key":f"'{forbidden_key}' may not be set on POST. Found: {value}"})
return
return
@staticmethod
def check_required_fields_exist_based_on_type(loadedModel:dict, content:dict):
@ -295,30 +295,30 @@ class InputValidationShipcall():
if int(type_)==int(ShipcallType.undefined):
raise ValidationError({"type":f"providing 'type' is mandatory. Missing key!"})
# arrival: arrival_berth_id & eta must exist
elif int(type_)==int(ShipcallType.arrival):
if eta is None:
raise ValidationError({"eta":f"providing 'eta' is mandatory. Missing key!"})
if arrival_berth_id is None:
raise ValidationError({"arrival_berth_id":f"providing 'arrival_berth_id' is mandatory. Missing key!"})
# departure: departive_berth_id and etd must exist
elif int(type_)==int(ShipcallType.departure):
if etd is None:
raise ValidationError({"etd":f"providing 'etd' is mandatory. Missing key!"})
if departure_berth_id is None:
raise ValidationError({"departure_berth_id":f"providing 'departure_berth_id' is mandatory. Missing key!"})
# shifting: arrival_berth_id, departure_berth_id, eta and etd must exist
elif int(type_)==int(ShipcallType.shifting):
if (eta is None) or (etd is None):
raise ValidationError({"eta_or_etd":f"providing 'eta' and 'etd' is mandatory. Missing one of those keys!"})
if (arrival_berth_id is None) or (departure_berth_id is None):
raise ValidationError({"arrival_berth_id_or_departure_berth_id":f"providing 'arrival_berth_id' & 'departure_berth_id' is mandatory. Missing key!"})
else:
raise ValidationError({"type":f"incorrect 'type' provided!"})
return
@ -327,10 +327,10 @@ class InputValidationShipcall():
def check_times_are_in_future(loadedModel:dict, content:dict):
"""
Dates should be in the future. Depending on the ShipcallType, specific values should be checked
Perfornms datetime checks in the loadedModel (datetime.datetime objects).
Performs datetime checks in the loadedModel (datetime.datetime objects).
"""
# obtain the current datetime to check, whether the provided values are in the future
time_now = datetime.datetime.now()
# obtain the current datetime to check, whether the provided values are after ref time
time_ref = datetime.datetime.now() - datetime.timedelta(days=1)
type_ = loadedModel.get("type", ShipcallType.undefined.name)
if isinstance(type_, str): # convert the name string to a ShipcallType data model
@ -348,14 +348,14 @@ class InputValidationShipcall():
tidal_window_to = loadedModel.get("tidal_window_to", None)
# Estimated arrival or departure times
InputValidationShipcall.check_times_in_future_based_on_type(type_, time_now, eta, etd)
InputValidationShipcall.check_times_in_future_based_on_type(type_, time_ref, eta, etd)
# Tidal Window
InputValidationShipcall.check_tidal_window_in_future(type_, time_now, tidal_window_from, tidal_window_to)
InputValidationShipcall.check_tidal_window_in_future(type_, time_ref, tidal_window_from, tidal_window_to)
return
@staticmethod
def check_times_in_future_based_on_type(type_, time_now, eta, etd):
def check_times_in_future_based_on_type(type_, time_ref, eta, etd):
"""
checks, whether the ETA & ETD times are in the future.
based on the type, this function checks:
@ -365,7 +365,9 @@ class InputValidationShipcall():
"""
if (eta is None) and (etd is None):
return
time_in_a_year = datetime.datetime.now().replace(datetime.datetime.now().year + 1)
if type_ is None:
raise ValidationError({"type":f"when providing 'eta' or 'etd', one must provide the type of the shipcall, so the datetimes can be verified."})
@ -379,20 +381,24 @@ class InputValidationShipcall():
if eta is None: # null values -> no violation
return
if not eta > time_now:
raise ValidationError({"eta":f"'eta' must be in the future. Incorrect datetime provided. Current Time: {time_now}. ETA: {eta}."})
if not eta > time_ref:
raise ValidationError({"eta":f"'eta' is too far in the past. Incorrect datetime provided. Current Time: {time_ref}. ETA: {eta}."})
if etd is not None:
raise ValidationError({"etd":f"'etd' should not be set when the shipcall type is 'arrival'."})
if eta > time_in_a_year:
raise ValidationError({"eta":f"'eta' is more than a year in the future. ETA: {eta}."})
elif int(type_)==int(ShipcallType.departure):
if etd is None: # null values -> no violation
return
if not etd > time_now:
raise ValidationError({"etd":f"'etd' must be in the future. Incorrect datetime provided. Current Time: {time_now}. ETD: {etd}."})
if not etd > time_ref:
raise ValidationError({"etd":f"'etd' is too far in the past. Incorrect datetime provided. Current Time: {time_ref}. ETD: {etd}."})
if eta is not None:
raise ValidationError({"eta":f"'eta' should not be set when the shipcall type is 'departure'."})
if etd > time_in_a_year:
raise ValidationError({"etd":f"'etd' is more than a year in the future. ETD: {etd}."})
elif int(type_)==int(ShipcallType.shifting):
if (eta is None) and (etd is None): # null values -> no violation
@ -403,30 +409,45 @@ class InputValidationShipcall():
# rules, a user is only allowed to provide *both* values.
raise ValidationError({"eta_or_etd":f"For shifting shipcalls one should always provide, both, eta and etd."})
if (not eta > time_now) or (not etd > time_now):
raise ValidationError({"eta_or_etd":f"'eta' and 'etd' must be in the future. Incorrect datetime provided. Current Time: {time_now}. ETA: {eta}. ETD: {etd}"})
if (not eta > time_ref) or (not etd > time_ref):
raise ValidationError({"eta_or_etd":f"'eta' and 'etd' is too far in the past. Incorrect datetime provided. Current Time: {time_ref}. ETA: {eta}. ETD: {etd}"})
if (not etd < eta):
raise ValidationError({"eta_or_etd":f"The estimated time of departure ('etd') must take place *before the estimated time of arrival ('eta'). The ship cannot arrive, before it has departed. Found: ETD: {etd}, ETA: {eta}"})
if (eta is not None and etd is None) or (eta is None and etd is not None):
raise ValidationError({"eta_or_etd":f"'eta' and 'etd' must both be provided when the shipcall type is 'shifting'."})
if eta > time_in_a_year:
raise ValidationError({"eta":f"'eta' is more than a year in the future. ETA: {eta}."})
if etd > time_in_a_year:
raise ValidationError({"etd":f"'etd' is more than a year in the future. ETD: {etd}."})
return
@staticmethod
def check_tidal_window_in_future(type_, time_now, tidal_window_from, tidal_window_to):
def check_tidal_window_in_future(type_, time_ref, tidal_window_from, tidal_window_to):
time_in_a_year = datetime.datetime.now().replace(datetime.datetime.now().year + 1)
if tidal_window_to is not None:
if not tidal_window_to >= time_now:
raise ValidationError({"tidal_window_to":f"'tidal_window_to' must be in the future. Incorrect datetime provided."})
if not tidal_window_to >= time_ref:
raise ValidationError({"tidal_window_to":f"'tidal_window_to' is too far in the past. Incorrect datetime provided."})
if tidal_window_to > time_in_a_year:
raise ValidationError({"tidal_window_to":f"'tidal_window_to' is more than a year in the future. Found: {tidal_window_to}."})
if tidal_window_from is not None:
if not tidal_window_from >= time_now:
raise ValidationError({"tidal_window_from":f"'tidal_window_from' must be in the future. Incorrect datetime provided."})
if not tidal_window_from >= time_ref:
raise ValidationError({"tidal_window_from":f"'tidal_window_from' is too far in the past. Incorrect datetime provided."})
if tidal_window_from > time_in_a_year:
raise ValidationError({"tidal_window_from":f"'tidal_window_from' is more than a year in the future. Found: {tidal_window_from}."})
if (tidal_window_to is not None) and (tidal_window_from is not None):
if tidal_window_to < tidal_window_from:
raise ValidationError({"tidal_window_to_or_tidal_window_from":f"'tidal_window_to' must take place after 'tidal_window_from'. Incorrect datetime provided. Found 'tidal_window_to': {tidal_window_to}, 'tidal_window_from': {tidal_window_to}."})
if (tidal_window_to is not None and tidal_window_from is None) or (tidal_window_to is None and tidal_window_from is not None):
raise ValidationError({"tidal_window_to_or_tidal_window_from":f"'tidal_window_to' and 'tidal_window_from' must both be provided."})
return
@staticmethod
def check_participant_list_not_empty_when_user_is_agency(loadedModel):
"""
@ -435,10 +456,10 @@ class InputValidationShipcall():
participants = loadedModel.get("participants", [])
is_agency_participant = [ParticipantType.AGENCY in ParticipantType(participant.get("type")) for participant in participants]
if not any(is_agency_participant):
if not any(is_agency_participant):
raise ValidationError({"participants":f"One of the assigned participants *must* be of type 'ParticipantType.AGENCY'. Found list of participants: {participants}"})
return
@staticmethod
def check_shipcall_is_canceled(loadedModel, content):
# read the shipcall_id from the PUT data
@ -454,17 +475,20 @@ class InputValidationShipcall():
if shipcall.get("canceled", False):
raise ValidationError({"canceled":f"The shipcall with id 'shipcall_id' is canceled. A canceled shipcall may not be changed."})
return
@staticmethod
def check_required_fields_of_put_request(content:dict):
shipcall_id = content.get("id", None)
if shipcall_id is None:
raise ValidationError({"id":f"A PUT request requires an 'id' to refer to."})
@staticmethod
def check_shipcall_id_exists(loadedModel):
"""simply checks, whether the defined shipcall ID exists in the database. Otherwise, a PUT-request must fail."""
shipcall_id = loadedModel.get("id")
if shipcall_id is None:
raise ValidationError({"id":"a shipcall id must be provided"})
query = 'SELECT * FROM shipcall where (id = ?shipcall_id?)'
shipcalls = execute_sql_query_standalone(query=query, model=Shipcall, param={"shipcall_id" : shipcall_id})
if len(shipcalls)==0:
@ -479,12 +503,12 @@ class InputValidationShipcall():
a) belong to the ASSIGNED agency participant group
b) belong to a BSMD participant, if the assigned agency has enabled the bit flag
When there is not yet an assigned agency for the respective shipcall, the request fails, and the user is considered as not authorized.
This mechanism prevents self-assignment of an agency to arbitrary shipcalls.
When there is not yet an assigned agency for the respective shipcall, only BSMD users are authorized.
This mechanism prevents self-assignment of an agency to arbitrary shipcalls.
"""
### preparation ###
# use the decoded JWT token and extract the participant type & participant id
participant_id = user_data.get("participant_id")
user_participant_id = user_data.get("participant_id")
participant_type = get_participant_type_from_user_data(user_data)
user_is_bsmd = (ParticipantType.BSMD in participant_type)
@ -494,44 +518,42 @@ class InputValidationShipcall():
### AGENCY in SPM ###
# determine, who is assigned as the agency for the shipcall
if shipcall_participant_map is None:
query = 'SELECT * FROM shipcall_participant_map where (shipcall_id = ?shipcall_id? AND type=?participant_type?)'
assigned_agency = execute_sql_query_standalone(query=query, model=ShipcallParticipantMap, param={"shipcall_id" : shipcall_id, "participant_type":int(ParticipantType.AGENCY)})
# query = 'SELECT * FROM shipcall_participant_map where (shipcall_id = ?shipcall_id? AND type=?participant_type?)'
# assigned_agency = execute_sql_query_standalone(query=query, model=ShipcallParticipantMap, param={"shipcall_id" : shipcall_id, "participant_type":int(ParticipantType.AGENCY)})
assigned_agency = get_assigned_participant_of_type(shipcall_id, participant_type=ParticipantType.AGENCY)
an_agency_is_assigned = True if assigned_agency is not None else False
else:
assigned_agency = [spm for spm in shipcall_participant_map if int(spm.type) == int(ParticipantType.AGENCY)]
an_agency_is_assigned = len(assigned_agency)==1
if len(assigned_agency)>1:
raise ValidationError({"internal_error":f"Internal error? Found more than one assigned agency for the shipcall with ID {shipcall_id}. Found: {assigned_agency}"})
if an_agency_is_assigned:
# Agency assigned? User must belong to the assigned agency or be a BSMD user, in case the flag is set
assigned_agency = [spm for spm in shipcall_participant_map if int(spm.type) == int(ParticipantType.AGENCY)]
an_agency_is_assigned = len(assigned_agency)==1
if len(assigned_agency)>1:
raise ValidationError({"internal_error":f"Internal error? Found more than one assigned agency for the shipcall with ID {shipcall_id}. Found: {assigned_agency}"})
assigned_agency = assigned_agency[0]
# determine, whether the assigned agency has set the BSMD-flag to allow BSMD users to edit their assigned shipcalls
query = 'SELECT * FROM participant where (id = ?participant_id?)'
agency_participant = execute_sql_query_standalone(query=query, param={"participant_id" : participant_id}, command_type="single", model=Participant)
if an_agency_is_assigned:
assert isinstance(assigned_agency, Participant), f"expecting the assigency agency to be a Participant object. Found: {type(assigned_agency)}"
assert isinstance(assigned_agency.flags, int), f"this method has currently only been developed with 'flags' being set as an integer. Found: {type(assigned_agency.flags)}"
assert isinstance(agency_participant.flags, int), f"this method has currently only been developed with 'flags' being set as an integer. Found: {type(agency_participant.flags)}"
agency_has_bsmd_flag = agency_participant.flags==1 # once the flags are an IntFlag, change the boolean check to: (ParticipantFlag.BSMD in agency_participant.flags)
# determine, whether the assigned agency has set the BSMD-flag to allow BSMD users to edit their assigned shipcalls
agency_has_bsmd_flag = assigned_agency.flags==1 # once the flags are an IntFlag, change the boolean check to: (ParticipantFlag.BSMD in agency_participant.flags)
### USER authority ###
# determine, whether the user is a) the assigned agency or b) a BSMD participant
user_is_assigned_agency = (participant_id == assigned_agency.participant_id)
user_is_assigned_agency = (user_participant_id == assigned_agency.id)
# when the BSMD flag is set: the user must be either BSMD or the assigned agency
# when the BSMD flag is not set: the user must be the assigned agency
user_is_authorized = (user_is_bsmd or user_is_assigned_agency) if agency_has_bsmd_flag else user_is_assigned_agency
user_is_authorized = (user_is_bsmd or user_is_assigned_agency) #if agency_has_bsmd_flag else user_is_assigned_agency
if not user_is_authorized:
raise werkzeug.exceptions.Forbidden(f"PUT Requests for shipcalls can only be issued by an assigned AGENCY or BSMD users (if the special-flag is enabled). Assigned Agency: {assigned_agency} with Flags: {agency_participant.flags}") # Forbidden: 403
raise werkzeug.exceptions.Forbidden(f"PUT Requests for shipcalls can only be issued by an assigned AGENCY or BSMD users (if the special-flag is enabled). Assigned Agency: {assigned_agency} with Flags: {assigned_agency.flags}") # Forbidden: 403
else:
# when there is no assigned agency, only BSMD users can update the shipcall
user_is_authorized = user_is_bsmd
if not user_is_authorized:
if not user_is_bsmd:
raise werkzeug.exceptions.Forbidden(f"PUT Requests for shipcalls can only be issued by an assigned AGENCY or BSMD users (if the special-flag is enabled). There is no assigned agency yet, so only BSMD users can change datasets.") # part of a pytest.raises. Forbidden: 403
return

View File

@ -15,6 +15,8 @@ from BreCal.database.enums import ParticipantType, ParticipantFlag
from BreCal.validators.input_validation_utils import check_if_user_is_bsmd_type, check_if_ship_id_is_valid, check_if_berth_id_is_valid, check_if_participant_ids_are_valid, check_if_participant_ids_and_types_are_valid, check_if_shipcall_id_is_valid, get_shipcall_id_dictionary, get_participant_type_from_user_data, get_participant_id_dictionary, check_if_participant_id_is_valid_standalone
from BreCal.database.sql_queries import SQLQuery
from BreCal.database.sql_handler import execute_sql_query_standalone
from BreCal.database.sql_handler import get_assigned_participant_of_type
from BreCal.database.sql_utils import get_times_data_for_id
from BreCal.validators.validation_base_utils import check_if_int_is_valid_flag, check_if_string_has_special_characters
import werkzeug
@ -23,7 +25,7 @@ def build_post_data_type_dependent_required_fields_dict()->dict[ShipcallType,dic
The required fields of a POST-request depend on ShipcallType and ParticipantType. This function creates
a dictionary, which maps those types to a list of required fields.
The participant types 'undefined' and 'bsmd' should not be used in POST-requests. They return 'None'.
The participant types 'undefined' and 'bsmd' should not be used in POST-requests. They return 'None'.
"""
post_data_type_dependent_required_fields_dict = {
ShipcallType.arrival:{
@ -69,7 +71,7 @@ class InputValidationTimes():
Example:
InputValidationTimes.evaluate(user_data, loadedModel, content)
When the data violates one of the rules, a marshmallow.ValidationError is raised, which details the issues.
When the data violates one of the rules, a marshmallow.ValidationError is raised, which details the issues.
"""
def __init__(self) -> None:
pass
@ -91,12 +93,16 @@ class InputValidationTimes():
# 4.) Value checking
InputValidationTimes.check_dataset_values(user_data, loadedModel, content)
return
@staticmethod
def evaluate_put_data(user_data:dict, loadedModel:dict, content:dict):
# 1.) Check for the presence of required fields
InputValidationTimes.check_times_required_fields_put_data(content)
# 1.5) Load model from database and set existing fields from dict to model
old_times = get_times_data_for_id(content["id"])
loadedModel = {**old_times, **loadedModel}
# 2.) Only users of the same participant_id, which the times dataset refers to, can update the entry
InputValidationTimes.check_user_belongs_to_same_group_as_dataset_determines(user_data, loadedModel=loadedModel, times_id=None)
@ -106,7 +112,7 @@ class InputValidationTimes():
# 4.) Value checking
InputValidationTimes.check_dataset_values(user_data, loadedModel, content)
return
@staticmethod
def evaluate_delete_data(user_data:dict, times_id:typing.Optional[int]):
# 0.) an ID reference must be provided and will be converted to int
@ -118,15 +124,16 @@ class InputValidationTimes():
InputValidationTimes.check_if_entry_is_already_deleted(times_id)
# 2.) Only users of the same participant_id, which the times dataset refers to, can delete the entry
InputValidationTimes.check_user_belongs_to_same_group_as_dataset_determines(user_data, loadedModel=None, times_id=times_id)
if not check_if_user_is_bsmd_type(user_data):
InputValidationTimes.check_user_belongs_to_same_group_as_dataset_determines(user_data, loadedModel=None, times_id=times_id)
return
@staticmethod
def check_if_entry_is_already_deleted(times_id:int):
"""
When calling a delete request for times, the dataset may not be deleted already. This method
makes sure, that the request contains and ID, has a matching entry in the database.
When a times dataset is deleted, it is directly removed from the database.
makes sure, that the request contains and ID, has a matching entry in the database.
When a times dataset is deleted, it is directly removed from the database.
To identify deleted entries, query from the database and check, whether there is a match for the times id.
@ -146,19 +153,19 @@ class InputValidationTimes():
if is_bsmd:
raise ValidationError({"participant_type":f"current user belongs to BSMD. Cannot post 'times' datasets. Found user data: {user_data}"})
return
@staticmethod
def check_dataset_values(user_data:dict, loadedModel:dict, content:dict):
"""
this method validates POST and PUT data. Most of the dataset arguments are validated directly in the
BreCal.schemas.model.TimesSchema, using @validates. This is exclusive for 'simple' validation rules.
this method validates POST and PUT data. Most of the dataset arguments are validated directly in the
BreCal.schemas.model.TimesSchema, using @validates. This is exclusive for 'simple' validation rules.
This applies to:
"remarks" & "berth_info"
"eta_berth", "etd_berth", "lock_time", "zone_entry", "operations_start", "operations_end"
"""
# while InputValidationTimes.check_user_is_not_bsmd_type already validates a user, this method
# validates the times dataset.
# validates the times dataset.
# ensure loadedModel["participant_type"] is of type ParticipantType
if not isinstance(loadedModel["participant_type"], ParticipantType):
@ -166,18 +173,18 @@ class InputValidationTimes():
if ParticipantType.BSMD in loadedModel["participant_type"]:
raise ValidationError({"participant_type":f"current user belongs to BSMD. Cannot post times datasets. Found user data: {user_data}"})
if (loadedModel["etd_interval_end"] is not None) and (loadedModel["etd_berth"] is not None):
if ("etd_interval_end" in loadedModel and loadedModel["etd_interval_end"] is not None) and ("etd_berth" in loadedModel and loadedModel["etd_berth"] is not None):
time_end_after_time_start = loadedModel["etd_interval_end"] >= loadedModel["etd_berth"]
if not time_end_after_time_start:
raise ValidationError({"etd":f"The provided time interval for the estimated departure time is invalid. The interval end takes place before the interval start. Found interval data: {loadedModel['etd_berth']} to {loadedModel['etd_interval_end']}"})
if (loadedModel["eta_interval_end"] is not None) and (loadedModel["eta_berth"] is not None):
if ("eta_interval_end" in loadedModel and loadedModel["eta_interval_end"] is not None) and ("eta_berth" in loadedModel and loadedModel["eta_berth"] is not None):
time_end_after_time_start = loadedModel["eta_interval_end"] >= loadedModel["eta_berth"]
if not time_end_after_time_start:
raise ValidationError({"eta":f"The provided time interval for the estimated arrival time is invalid. The interval begin takes place after the interval end. Found interval data: {loadedModel['eta_berth']} to {loadedModel['eta_interval_end']}"})
return
@staticmethod
def check_dataset_references(content:dict):
"""
@ -192,25 +199,25 @@ class InputValidationTimes():
valid_berth_id_reference = check_if_berth_id_is_valid(berth_id)
if not valid_berth_id_reference:
raise ValidationError({"berth_id":f"The referenced berth_id '{berth_id}' does not exist in the database."})
valid_shipcall_id_reference = check_if_shipcall_id_is_valid(shipcall_id)
if not valid_shipcall_id_reference:
raise ValidationError({"shipcall_id":f"The referenced shipcall_id '{shipcall_id}' does not exist in the database."})
valid_participant_id_reference = check_if_participant_id_is_valid_standalone(participant_id, participant_type=None)
if not valid_participant_id_reference:
raise ValidationError({"participant_id":f"The referenced participant_id '{participant_id}' does not exist in the database."})
return
@staticmethod
def check_times_required_fields_post_data(loadedModel:dict, content:dict):
"""
Depending on ShipcallType and ParticipantType, there is a rather complex set of required fields.
Independent of those types, any POST request for times should always include the default fields.
Independent of those types, any POST request for times should always include the default fields.
The dependent and independent fields are validated by checking, whether the respective value in 'content'
is undefined (returns None). When any of these fields is undefined, a ValidationError is raised.
is undefined (returns None). When any of these fields is undefined, a ValidationError is raised.
"""
participant_type = loadedModel["participant_type"]
shipcall_id = loadedModel["shipcall_id"]
@ -240,14 +247,14 @@ class InputValidationTimes():
verbosity_tuple = [(field, missing) for field, missing in zip(required_fields, missing_required_fields) if missing]
raise ValidationError({"required_fields":f"At least one of the required fields is missing. Missing: {verbosity_tuple}"})
return
@staticmethod
def check_times_required_fields_put_data(content:dict):
"""in a PUT request, only the 'id' is a required field. All other fields are simply ignored, when they are not provided."""
if content.get("id") is None:
raise ValidationError({"id":f"A PUT-request requires an 'id' reference, which was not found."})
return
@staticmethod
def get_post_data_type_independent_fields()->list[str]:
"""
@ -257,16 +264,16 @@ class InputValidationTimes():
"shipcall_id", "participant_id", "participant_type"
]
return independent_required_fields
@staticmethod
def get_post_data_type_dependent_fields(shipcall_type:typing.Union[int, ShipcallType], participant_type:typing.Union[int, ParticipantType]):
"""
Depending on ShipcallType and ParticipantType, there is a rather complex set of required fields.
Arriving shipcalls need arrival times (e.g., 'eta'), Departing shipcalls need departure times (e.g., 'etd') and
Shifting shipcalls need both times (e.g., 'eta' and 'etd').
Arriving shipcalls need arrival times (e.g., 'eta'), Departing shipcalls need departure times (e.g., 'etd') and
Shifting shipcalls need both times (e.g., 'eta' and 'etd').
Further, the ParticipantType determines the set of relevant times. In particular, the terminal uses
Further, the ParticipantType determines the set of relevant times. In particular, the terminal uses
'operations_start' and 'operations_end', while other users use 'eta_berth' or 'etd_berth'.
"""
# ensure that both, shipcall_type and participant_type, refer to the enumerators, as opposed to integers.
@ -282,7 +289,7 @@ class InputValidationTimes():
dependent_required_fields = dependent_required_fields_dict.get(shipcall_type,{}).get(participant_type,[])
dependent_required_fields = dependent_required_fields if dependent_required_fields is not None else []
return dependent_required_fields
@staticmethod
def check_if_user_fits_shipcall_participant_map(user_data:dict, loadedModel:dict, content:dict, spm_shipcall_data=None):
"""
@ -290,15 +297,15 @@ class InputValidationTimes():
which is assigned to the shipcall within the ShipcallParticipantMap
This method does not validate, what the POST-request contains, but it validates, whether the *user* is
authorized to send the request.
authorized to send the request.
This method also checks for a special case: when an assigned AGENCY participant has the .BSMD flag enabled,
a user of type BSMD may also post the times dataset.
options:
spm_shipcall_data:
spm_shipcall_data:
data from the ShipcallParticipantMap, which refers to the respective shipcall ID. The SPM can be
an optional argument to allow for much easier unit testing.
an optional argument to allow for much easier unit testing.
"""
### TIMES DATASET (ShipcallParticipantMap) ###
# identify shipcall_id
@ -310,13 +317,13 @@ class InputValidationTimes():
# read the ShipcallParticipantMap entry of the current shipcall_id. This is used within the input validation of a PUT request
# creates a list of {'participant_id: ..., 'type': ...} elements
spm_shipcall_data = execute_sql_query_standalone(
query = "SELECT participant_id, type FROM shipcall_participant_map WHERE (shipcall_id=?shipcall_id? AND type=?type?)",
query = "SELECT participant_id, type FROM shipcall_participant_map WHERE (shipcall_id=?shipcall_id? AND type=?type?)",
param={"shipcall_id":shipcall_id, "type":int(DATASET_participant_type)},
pooledConnection=None
)
DATASET_participant_id = InputValidationTimes.get_participant_id_from_shipcall_participant_map(shipcall_id, participant_type=DATASET_participant_type, spm_shipcall_data=spm_shipcall_data)
### USER DATA (token) ###
# identify user's participant_id & type (get all participants; then filter these for the {participant_id})
user_participant_id = user_data["participant_id"] #participants = get_participant_id_dictionary() #participant_type = ParticipantType(participants.get(participant_id,{}).get("type"))
@ -326,7 +333,7 @@ class InputValidationTimes():
if (special_case__bsmd_may_edit_agency_dataset):
# when a BSMD user posts a dataset of an AGENCY with BSMD-flag, there is no violation
return
# check, if participant_id is assigned to the ShipcallParticipantMap
matching_spm = [
spm
@ -337,7 +344,7 @@ class InputValidationTimes():
if not len(matching_spm)>0:
raise ValidationError({"participant_id":f'The participant group with id {user_participant_id} is not assigned to the shipcall. Found ShipcallParticipantMap: {spm_shipcall_data}'}) # part of a pytest.raises
return
@staticmethod
def check_if_entry_already_exists_for_participant_type(user_data:dict, loadedModel:dict, content:dict):
"""determines, whether a dataset for the participant type is already present"""
@ -356,7 +363,7 @@ class InputValidationTimes():
if participant_type_exists_already:
raise ValidationError({"participant_type":f"A dataset for the participant type is already present. Participant Type: {participant_type}. Times Datasets: {times}"})
return
@staticmethod
def check_user_belongs_to_same_group_as_dataset_determines(user_data:dict, loadedModel:typing.Optional[dict]=None, times_id:typing.Optional[int]=None, pdata:typing.Optional[list[dict]]=None):
"""
@ -371,7 +378,7 @@ class InputValidationTimes():
times_id is used to directly identify the matching times entry
A special exception takes place, when a participant of type AGENCY is involved. In those times-entries, users with the
IS_BSMD-Flag may also edit the entry.
IS_BSMD-Flag may also edit the entry.
"""
assert not ((loadedModel is None) and (times_id is None)), f"must provide either loadedModel OR times_id. Both are 'None'"
assert (loadedModel is None) or (times_id is None), f"must provide either loadedModel OR times_id. Both are defined."
@ -379,62 +386,86 @@ class InputValidationTimes():
# identify the user's participant id
user_participant_id = user_data["participant_id"]
""" # #TODO:
First of all, this method is shared for PUT and DELETE requests.
PUT) is based on the loadedModel
DELETE) is based on the times_id
Both of them share the {user_data}-argument
These arguments are used to obtain shipcall_id, participant_type (of the times entry) and times_assigned_participant
there should be the following authorization approaches
a) the user has the participant ID of the assigned entry for a given role
for this, we need:
1) user_participant_id
2) times_participant_type
3) SPM: assigned participant of the respective type (times_assigned_participant)
_ = get_assigned_participant_of_type(shipcall_id, participant_type=ParticipantType.WHATTYPE)
b) the user is the assigned agency (or the BSMD if allowed)
for this, we need:
1) assigned_agency
assigned_agency = get_assigned_participant_of_type(shipcall_id, participant_type=ParticipantType.AGENCY)
2) agency's flag
assigned_agency.flags
3) user_is_bsmd boolean
"""
# commonly used in the PUT-request
if loadedModel is not None:
shipcall_id = loadedModel["shipcall_id"]
participant_type = loadedModel["participant_type"]
# get the matching entry from the shipcall participant map. Raise an error, when there is no match.
participant_id_of_times_dataset = InputValidationTimes.get_participant_id_from_shipcall_participant_map(shipcall_id, participant_type)
(shipcall_id, times_assigned_participant) = InputValidationTimes.prepare_authority_check_for_put_request(loadedModel)
# commonly used in the DELETE-request
if times_id is not None:
if pdata is None: # regular behavior. pdata is only defined in unit tests.
# perform an SQL query. Creates a pooled connection internally, queries the database, then closes the connection.
query = "SELECT participant_id, participant_type, shipcall_id FROM times WHERE id = ?id?"
pdata = execute_sql_query_standalone(query=query, param={"id":times_id}, pooledConnection=None)
# #TODO_refactor:
(shipcall_id, times_assigned_participant) = InputValidationTimes.prepare_authority_check_for_delete_request(times_id, pdata)
# extracts the participant_id from the first matching entry, if applicable
if not len(pdata)>0:
# this case is usually covered by the InputValidationTimes.check_if_entry_is_already_deleted method already
raise ValidationError({"times_id":f"Unknown times_id. Could not find a matching entry for ID: {times_id}"})
else:
participant_type = pdata[0].get("participant_type")
shipcall_id = pdata[0].get("shipcall_id")
# get the matching entry from the shipcall participant map, where the role matches. Raise an error, when there is no match.
assigned_agency = get_assigned_participant_of_type(shipcall_id, participant_type=ParticipantType.AGENCY)
# get the matching entry from the shipcall participant map. Raise an error, when there is no match.
participant_id_of_times_dataset = pdata[0].get("participant_id")
# participant_id_of_times_dataset = InputValidationTimes.get_participant_id_from_shipcall_participant_map(shipcall_id, participant_type)
# a) the user has the participant ID of the assigned entry for a given role
user_is_assigned_role = user_participant_id == times_assigned_participant.id
# b) the user is the assigned agency
user_is_assigned_agency = user_participant_id == assigned_agency.id
# c) the user is BSMD, if the assigned agency allows that
assigned_agency_has_bsmd_flag = assigned_agency.flags == 1
user_is_bsmd_type = check_if_user_is_bsmd_type(user_data={"participant_id":user_participant_id})
user_is_bsmd_and_assigned_agency_has_flag = assigned_agency_has_bsmd_flag & user_is_bsmd_type
if user_is_assigned_role:
return
elif user_is_assigned_agency:
return
elif user_is_bsmd_and_assigned_agency_has_flag:
return
else:
times_participant_id = loadedModel.get("participant_id")
raise ValidationError({"user_participant_type": f"The dataset may only be changed by a user belonging to the same participant group as the times dataset is referring to. Alternatively, the assigned agency may edit and delete the dataset. As a special case, BSMD users may edit and delete times datasets, when the assigned agency allows that. User participant_id: {user_participant_id}; Dataset participant_id: {times_participant_id}"})
# when the user's participant id is different from the times dataset, an exception is raised
if user_participant_id != participant_id_of_times_dataset:
# for some AGENCY participants, users with the BSMD flag may also edit the datasets
special_case__bsmd_may_edit_agency_dataset = InputValidationTimes.check_if_bsmd_may_edit_agency_dataset(user_participant_id, participant_id_of_times_dataset, participant_type)
if special_case__bsmd_may_edit_agency_dataset:
return
else:
raise ValidationError({"user_participant_type":f"The dataset may only be changed by a user belonging to the same participant group as the times dataset is referring to. User participant_id: {user_participant_id}; Dataset participant_id: {participant_id_of_times_dataset}"})
return
@staticmethod
def get_participant_id_from_shipcall_participant_map(shipcall_id:int, participant_type:int, spm_shipcall_data=None)->int:
def get_participant_id_from_shipcall_participant_map(shipcall_id:typing.Optional[int], participant_type:int, spm_shipcall_data=None)->int:
"""use shipcall_id and participant_type to identify the matching participant_id"""
if shipcall_id is None:
raise ValidationError({"shipcall_id":f"Could not find a referenced shipcall_id within the request."})
if spm_shipcall_data is None:
spm_shipcall_data = execute_sql_query_standalone(
query=SQLQuery.get_shipcall_participant_map_by_shipcall_id_and_type(),
param={"id":shipcall_id, "type":participant_type},
query=SQLQuery.get_shipcall_participant_map_by_shipcall_id_and_type(),
param={"id":shipcall_id, "type":participant_type},
command_type="query") # returns a list of matches
# raise an error when there are no matches
if len(spm_shipcall_data)==0:
raise ValidationError({"participant_type":f"Could not find a matching time dataset for the provided participant_type: {participant_type} at shipcall with id {shipcall_id}."})
participant_id_of_times_dataset = spm_shipcall_data[0].get("participant_id")
return participant_id_of_times_dataset
@staticmethod
def check_if_bsmd_may_edit_agency_dataset(user_participant_id:int, participant_id_of_times_dataset:int, participant_type:ParticipantType)->bool:
"""
@ -448,45 +479,81 @@ class InputValidationTimes():
args:
user_participant_id: ID of the user, obtained from the jwt-token
participant_id_of_times_dataset: assigned participant of the shipcall, obtained from the ShipcallParticipantMap
participant_id_of_times_dataset: assigned participant of the shipcall, obtained from the ShipcallParticipantMap
"""
# when the participant type of the dataset is not an AGENCY, this exception rule does not take place
# when the participant type of the dataset is not an AGENCY, this exception rule does not take place
dataset_participant_type_is_agency = int(participant_type)==int(ParticipantType.AGENCY)
if not dataset_participant_type_is_agency:
return False
### TIMES ENTRY (ShipcallParticipantMap) ###
# identify, whether the dataset's assigned participant has the BSMD flag
agency_has_bsmd_flag = InputValidationTimes.check_if_participant_has_bsmd_flag(participant_id=participant_id_of_times_dataset)
### USER DATA (token) ###
# determine, whether the user is of participant_type BSMD
user_is_bsmd_type = check_if_user_is_bsmd_type(user_data={"participant_id":user_participant_id})
return (agency_has_bsmd_flag) & (user_is_bsmd_type)
@staticmethod
def check_if_participant_has_bsmd_flag(participant_id:int)->bool:
"""
Given a participant_id, this method checks, whether the participant with {participant_id}
has the .BSMD flag in the .flags field.
has the .BSMD flag in the .flags field.
"""
# get the dataset's assigned Participant, which matches the SPM entry
participant = execute_sql_query_standalone(
SQLQuery.get_participant_from_id(),
param={"participant_id":participant_id},
command_type="single",
param={"participant_id":participant_id},
command_type="single",
pooledConnection=None)
has_bsmd_flag = ParticipantFlag.BSMD in [ParticipantFlag(participant.get("flags"))]
return has_bsmd_flag
@staticmethod
def prepare_authority_check_for_put_request(loadedModel)->typing.Tuple[int,Participant]:
"""extracts the loadedModel to obtain relevant arguments"""
shipcall_id = loadedModel["shipcall_id"]
participant_type = loadedModel["participant_type"]
# get the matching entry from the shipcall participant map, where the role matches. Raise an error, when there is no match.
times_assigned_participant = get_assigned_participant_of_type(shipcall_id, participant_type=participant_type)
if times_assigned_participant is None:
raise ValidationError({"participant_type":"the requested participant type is not assigned to the shipcall."})
return (shipcall_id, times_assigned_participant)
@staticmethod
def prepare_authority_check_for_delete_request(times_id, pdata=None)->typing.Tuple[int,Participant]:
if pdata is None: # regular behavior. pdata is only defined in unit tests.
# perform an SQL query. Creates a pooled connection internally, queries the database, then closes the connection.
query = "SELECT participant_id, participant_type, shipcall_id FROM times WHERE id = ?id?"
pdata = execute_sql_query_standalone(query=query, param={"id":times_id}, pooledConnection=None)
# extracts the participant_id from the first matching entry, if applicable
if not len(pdata)>0:
# this case is usually covered by the InputValidationTimes.check_if_entry_is_already_deleted method already
raise ValidationError({"times_id":f"Unknown times_id. Could not find a matching entry for ID: {times_id}"})
else:
participant_type = pdata[0].get("participant_type")
shipcall_id = pdata[0].get("shipcall_id")
# get the matching entry from the shipcall participant map, where the role matches. Raise an error, when there is no match.
times_assigned_participant = get_assigned_participant_of_type(shipcall_id, participant_type=participant_type)
if times_assigned_participant is None:
raise ValidationError({"participant_type":"the requested participant type is not assigned to the shipcall."})
return (shipcall_id, times_assigned_participant)
def deprecated_build_post_data_type_dependent_required_fields_dict()->dict[ShipcallType,dict[ParticipantType,typing.Optional[list[str]]]]:
"""
The required fields of a POST-request depend on ShipcallType and ParticipantType. This function creates
a dictionary, which maps those types to a list of required fields.
The participant types 'undefined' and 'bsmd' should not be used in POST-requests. They return 'None'.
The participant types 'undefined' and 'bsmd' should not be used in POST-requests. They return 'None'.
"""
post_data_type_dependent_required_fields_dict = {
ShipcallType.arrival:{

View File

@ -41,7 +41,7 @@ def unbundle_validation_error_message(message):
unbundle_(message, unbundled=unbundled)
if len(unbundled)>0:
error_field = "ValidationError in the following field(s): " + " & ".join([unb["error_field"] for unb in unbundled])
error_description = "Error Description(s): " + " & ".join([unb["error_description"] for unb in unbundled])
error_description = " " . join([unb["error_description"] for unb in unbundled])
else:
error_field = "ValidationError"
error_description = "unknown validation error"

View File

@ -3,7 +3,7 @@ import sys
import logging
sys.path.insert(0, '/var/www/brecal_test/src/server')
sys.path.insert(0, '/var/www/venv/lib/python3.10/site-packages/')
sys.path.insert(0, '/var/www/venv/lib/python3.12/site-packages/')
import schedule

View File

@ -207,7 +207,7 @@ def test_shipcall_post_request_fails_when_voyage_string_is_invalid(get_stub_toke
with pytest.raises(ValidationError, match="Longer than maximum length 16"):
assert response.status_code==400
raise ValidationError(response.json())
# Fail: special characters
post_data = original_post_data.copy()
post_data["voyage"] = '👽'
@ -226,17 +226,17 @@ def test_shipcall_post_request_fails_when_type_arrival_and_not_in_future(get_stu
# accept
post_data = original_post_data.copy()
post_data["type"] = ShipcallType.arrival.name
post_data["eta"] = (datetime.datetime.now() + datetime.timedelta(hours=3)).isoformat()
post_data["eta"] = (datetime.datetime.now() + datetime.timedelta(days=2)).isoformat()
response = requests.post(f"{url}/shipcalls", headers={"Content-Type":"text", "Authorization":f"Bearer {token}"}, json=post_data)
assert response.status_code == 201
# error
post_data = original_post_data.copy()
post_data["type"] = ShipcallType.arrival.name
post_data["eta"] = (datetime.datetime.now() - datetime.timedelta(hours=3)).isoformat()
post_data["eta"] = (datetime.datetime.now() - datetime.timedelta(days=2)).isoformat()
response = requests.post(f"{url}/shipcalls", headers={"Content-Type":"text", "Authorization":f"Bearer {token}"}, json=post_data)
with pytest.raises(ValidationError, match="must be in the future. Incorrect datetime provided"):
with pytest.raises(ValidationError, match="is too far in the past. Incorrect datetime provided"):
assert response.status_code==400
raise ValidationError(response.json())
return
@ -256,10 +256,10 @@ def test_shipcall_post_request_fails_when_type_departure_and_not_in_future(get_s
# error
post_data = original_post_data.copy()
post_data["type"] = ShipcallType.departure.name
post_data["etd"] = (datetime.datetime.now() - datetime.timedelta(hours=3)).isoformat()
post_data["etd"] = (datetime.datetime.now() - datetime.timedelta(days=3)).isoformat()
response = requests.post(f"{url}/shipcalls", headers={"Content-Type":"text", "Authorization":f"Bearer {token}"}, json=post_data)
with pytest.raises(ValidationError, match="must be in the future. Incorrect datetime provided"):
with pytest.raises(ValidationError, match="is too far in the past. Incorrect datetime provided"):
assert response.status_code==400
raise ValidationError(response.json())
return
@ -280,11 +280,11 @@ def test_shipcall_post_request_fails_when_type_shifting_and_not_in_future(get_st
# error
post_data = original_post_data.copy()
post_data["type"] = ShipcallType.shifting.name
post_data["etd"] = (datetime.datetime.now() - datetime.timedelta(hours=3)).isoformat()
post_data["etd"] = (datetime.datetime.now() - datetime.timedelta(days=3)).isoformat()
post_data["eta"] = (datetime.datetime.now() + datetime.timedelta(hours=3,minutes=1)).isoformat()
response = requests.post(f"{url}/shipcalls", headers={"Content-Type":"text", "Authorization":f"Bearer {token}"}, json=post_data)
with pytest.raises(ValidationError, match="must be in the future. Incorrect datetime provided"):
with pytest.raises(ValidationError, match="is too far in the past. Incorrect datetime provided"):
assert response.status_code==400
raise ValidationError(response.json())
return
@ -629,7 +629,7 @@ def test_shipcall_put_request_fails_when_different_participant_id_is_assigned(ge
{"id":99115, 'participant_id': 5, 'type': 8}]
spm_shipcall_data = [
{**{"created":created, "modified":modified, "shipcall_id":shipcall_id}, **spm}
for spm in
for spm in
spm_shipcall_data
]
spm_shipcall_data = [ShipcallParticipantMap(**spm) for spm in spm_shipcall_data]
@ -652,7 +652,7 @@ def test_shipcall_put_request_success(get_shipcall_id_after_stub_post_request):
user_data = {'id':6, 'participant_id':1}
loadedModel = post_data
content = post_data
created = datetime.datetime.now()+datetime.timedelta(minutes=1)
modified = datetime.datetime.now()+datetime.timedelta(minutes=2)
@ -662,7 +662,7 @@ def test_shipcall_put_request_success(get_shipcall_id_after_stub_post_request):
{"id":99115, 'participant_id': 5, 'type': 8}]
spm_shipcall_data = [
{**{"created":created, "modified":modified, "shipcall_id":shipcall_id}, **spm}
for spm in
for spm in
spm_shipcall_data
]
spm_shipcall_data = [ShipcallParticipantMap(**spm) for spm in spm_shipcall_data]
@ -687,7 +687,7 @@ def test_shipcall_put_request_fails_when_no_agency_is_assigned(get_shipcall_id_a
user_data = {'id':6, 'participant_id':2} # participant_id 2 is not BSMD and is not authorized.
loadedModel = post_data
content = post_data
created = datetime.datetime.now()+datetime.timedelta(minutes=1)
modified = datetime.datetime.now()+datetime.timedelta(minutes=2)
@ -697,7 +697,7 @@ def test_shipcall_put_request_fails_when_no_agency_is_assigned(get_shipcall_id_a
{"id":99115, 'participant_id': 5, 'type': 4}]
spm_shipcall_data = [
{**{"created":created, "modified":modified, "shipcall_id":shipcall_id}, **spm}
for spm in
for spm in
spm_shipcall_data
]
spm_shipcall_data = [ShipcallParticipantMap(**spm) for spm in spm_shipcall_data]
@ -705,7 +705,7 @@ def test_shipcall_put_request_fails_when_no_agency_is_assigned(get_shipcall_id_a
# no agency assigned
ivs = InputValidationShipcall()
with pytest.raises(werkzeug.exceptions.Forbidden, match=re.escape(f"PUT Requests for shipcalls can only be issued by an assigned AGENCY or BSMD users (if the special-flag is enabled). There is no assigned agency yet, so only BSMD users can change datasets.")):
ivs.check_user_is_authorized_for_put_request(user_data, loadedModel, content, spm_shipcall_data)
return
@ -730,7 +730,7 @@ def test_shipcall_put_request_fails_when_user_is_not_authorized(get_shipcall_id_
{"id":99115, 'participant_id': 5, 'type': 4}]
spm_shipcall_data = [
{**{"created":created, "modified":modified, "shipcall_id":shipcall_id}, **spm}
for spm in
for spm in
spm_shipcall_data
]
spm_shipcall_data = [ShipcallParticipantMap(**spm) for spm in spm_shipcall_data]
@ -754,20 +754,20 @@ def test_shipcall_put_request_fails_when_user_tries_self_assignment(get_shipcall
created = datetime.datetime.now()+datetime.timedelta(minutes=1)
modified = datetime.datetime.now()+datetime.timedelta(minutes=2)
spm_shipcall_data = [
{"id":99113, 'participant_id': 3, 'type': 1},
{"id":99114, 'participant_id': 4, 'type': 2},
{"id":99115, 'participant_id': 5, 'type': 4}]
spm_shipcall_data = [
{**{"created":created, "modified":modified, "shipcall_id":shipcall_id}, **spm}
for spm in
for spm in
spm_shipcall_data
]
spm_shipcall_data = [ShipcallParticipantMap(**spm) for spm in spm_shipcall_data]
# self-assignment. User is participant 6, and wants to assign participant 6.
# self-assignment. User is participant 6, and wants to assign participant 6.
ivs = InputValidationShipcall()
with pytest.raises(werkzeug.exceptions.Forbidden, match=re.escape("PUT Requests for shipcalls can only be issued by an assigned AGENCY or BSMD users (if the special-flag is enabled). There is no assigned agency yet, so only BSMD users can change datasets.")):
# previous error message: An agency cannot self-register for a shipcall. The request is issued by an agency-user and tries to assign an AGENCY as the participant of the shipcall.""
@ -813,7 +813,7 @@ def test_shipcall_put_request_works_if_most_values_are_null():
InputValidationShipcall.evaluate_put_data(user_data, loadedModel, content)
return
@ -860,10 +860,10 @@ def test_shipcall_put_request_fails_input_validation_shipcall_when_shipcall_is_c
return
def test_post_data_with_valid_data(get_stub_token):
"""This unit test uses the input data from
"""This unit test uses the input data from
# https://trello.com/c/VXVSLTF4/267-shipcall-anlegen-shifting-erh%C3%A4lt-fehler-aufgrund-fr%C3%BCherem-etd-als-eta
to make sure, the failure case no longer appears.
to make sure, the failure case no longer appears.
"""
url, token = get_stub_token["url"], get_stub_token["token"]