Merge branch 'release/1.5.0'
This commit is contained in:
commit
c49953bf8e
@ -10,7 +10,8 @@ ___
|
||||
|
||||
|Date|Edit|Author|
|
||||
|--|--|--|
|
||||
|2.2.24 | Document created, first draft | Daniel Schick |
|
||||
| 2.2.24 | Document created, first draft | Daniel Schick |
|
||||
| 25.7.24 | Update for BC 1.4 and other changes | Daniel Schick |
|
||||
|
||||
## Global constants and definitions
|
||||
|
||||
@ -50,7 +51,7 @@ The shipcall type which is set in the shipcall record may have the following val
|
||||
|
||||
### Token evaluation
|
||||
|
||||
The identity of the caller can be retrieved from the token. This contains an id (="user id") and more importantly, the "participant_id". Every call is only allow if the user is properly authorized to perform the call or modify the dataset.
|
||||
The identity of the caller can be retrieved from the token. This contains an id (="user id") and more importantly, the "participant_id". Every call is only allow if the user is properly authorized to perform the call or modify the dataset.
|
||||
|
||||
At this time, authorization is performed on a participant level. This means that all users that belong to a particular participant have the same rights.
|
||||
|
||||
@ -60,11 +61,12 @@ PUT / DELETE calls referencing entities that are not found in the database will
|
||||
|
||||
### Return value
|
||||
|
||||
If a validation rule fails the call should return 400 (Bad request) including an error message in the format already in use:
|
||||
If a validation rule fails the call should return 400 (Bad request) including an error message in the following format:
|
||||
|
||||
```json
|
||||
{
|
||||
"message" : "reason why this call failed"
|
||||
"error_field" : "A reference to the respective field(s) which have caused the error",
|
||||
"error_description" : "Reason why this call failed"
|
||||
}
|
||||
```
|
||||
|
||||
@ -78,21 +80,24 @@ Date and date+time values are specified as text formatted in [RFC 3339](https://
|
||||
}
|
||||
```
|
||||
|
||||
Usually the "Z" is missing at the end indicating local time.
|
||||
Usually the "Z" is missing at the end indicating local time.
|
||||
|
||||
Generally, times may not be updated to a value in the past. There are exception (see SHIPCALL PUT below).
|
||||
|
||||
Times should also not be set to a value more than 1 year in the future. The reasoning is to prevent shipcalls to stick to the top of the list by implausible (or for the workings of Bremen calling irrelevant) values too far in the future.
|
||||
|
||||
## /shipcall POST
|
||||
|
||||
1. The call may only be performed by a user belonging to participant group type BSMD.
|
||||
2. Reference checking: The dataset includes multiple fields referring other tables. The validation must make sure the referenced entities exist. This includes the following fields:
|
||||
| Field | Referenced table |
|
||||
|-------|------------------|
|
||||
| ship_id | ship |
|
||||
| arrival_berth_id | berth |
|
||||
| departure_berth_id | berth |
|
||||
| participants | 1. participant_id values may appear more once 2. types may only appear once and must not include type "BSMD".|
|
||||
|
||||
| Field | Referenced table | Notes |
|
||||
|-------|------------------| ----- |
|
||||
| ship_id | ship | |
|
||||
| arrival_berth_id | berth | required if type is "arrival" (1) |
|
||||
| departure_berth_id | berth | required if type is "departure" or "shifting" (2 or 3) |
|
||||
| participants | participant | This is a list containing participant_id and type pairs with the following rules: 1. participant_id values may appear more once 2. types may only appear once, therefore there are a maximum of 7 elements in this list. |
|
||||
|
||||
3. Check for reasonable values for the following fields:
|
||||
3. Check for reasonable values for the following fields:
|
||||
|
||||
| Field | Validation |
|
||||
|-------|------------|
|
||||
@ -105,12 +110,14 @@ Usually the "Z" is missing at the end indicating local time.
|
||||
| tidal_window_from | value must be in the future |
|
||||
| tidal_window_to | value must be in the future, value must be > tidal_window_from |
|
||||
| recommended_tugs | 0 < value < 10 |
|
||||
| canceled | may not be set on POST |
|
||||
| canceled | optional on POST |
|
||||
| evaluation | may not be set |
|
||||
| evaluation_message | may not be set |
|
||||
| evaluation_message | may not be set |
|
||||
| created / modified | may not be set |
|
||||
|
||||
#### Required fields
|
||||
* eta / etd (depending on value of type: 1: eta, 2: etd, 3: both)
|
||||
|
||||
* eta / etd (depending on value of type: 1: eta, 2: etd, 3: etd)
|
||||
* type
|
||||
* ship_id
|
||||
* arrival_berth_id / departure_berth_id (depending on type, see above)
|
||||
@ -119,9 +126,13 @@ Usually the "Z" is missing at the end indicating local time.
|
||||
## /shipcall PUT
|
||||
|
||||
1. The call may only be performed by a user belonging to participant group type BSMD.
|
||||
2. If a agency is selected via the shipcall_participant_map entry, users of this agency may also edit the shipcall. Care has to be taken: The agency must have been set _before_ a member of the group may edit the record.
|
||||
2. If a agency is selected via the shipcall_participant_map entry, users of this agency may also edit the shipcall. Care has to be taken: The agency must have been set _before_ a member of the group may edit the record.
|
||||
In other words: no setting the agency and editing the record by a member of the agency within the same call.
|
||||
3. See value rules in /shipcall POST. Exception: Canceled may be set but only if not already set.
|
||||
3. See value rules in /shipcall POST.
|
||||
Exceptions:
|
||||
a) Canceled may be set but only if not already set.
|
||||
b) ETA/ETD may be in the past. This can happen if an agency has entered an ETA/ETD (times) in the future but
|
||||
wants to edit fields of the shipcall record (e.g. the draft) but the shipcall was originally created with an ETA/ETD in the past
|
||||
4. A cancelled shipcall may not be changed (is logical delete)
|
||||
|
||||
#### Required fields
|
||||
@ -140,11 +151,12 @@ The id field is required, missing fields will not be updated.
|
||||
| participant_id | participant |
|
||||
| berth_id | berth |
|
||||
|
||||
5. Check for reasonable values for the following fields:
|
||||
5. Check for reasonable values for the following fields:
|
||||
|
||||
| Field | Validation |
|
||||
|-------|------------|
|
||||
| eta_berth, etd_berth, lock_time, zone_entry, operations_start, operations_end | if set these values must be in the future|
|
||||
| eta_interval_end, etd_interval_end | if set these values must be in the future. They must be larger than their ETA/ETD counterparts. |
|
||||
| remarks, berth_info | must be <= 512 chars |
|
||||
| participant_type | must not be BSMD |
|
||||
|
||||
@ -156,41 +168,41 @@ This depends on the shipcall and participant type:
|
||||
|
||||
AGENCY, PILOT, PORT_AUTHORITY, MOORING, TUG:
|
||||
|
||||
eta_berth, shipcall_id, participant_id, participant_type
|
||||
shipcall_id, participant_id, participant_type
|
||||
|
||||
TERMINAL:
|
||||
|
||||
operations_start, shipcall_id, participant_id, participant_type
|
||||
shipcall_id, participant_id, participant_type
|
||||
|
||||
##### Outgoing
|
||||
|
||||
AGENCY, PILOT, PORT_AUTHORITY, MOORING, TUG:
|
||||
|
||||
etd_berth, shipcall_id, participant_id, participant_type
|
||||
shipcall_id, participant_id, participant_type
|
||||
|
||||
TERMINAL:
|
||||
|
||||
operations_end, shipcall_id, participant_id, participant_type
|
||||
shipcall_id, participant_id, participant_type
|
||||
|
||||
##### Shifting
|
||||
|
||||
AGENCY, PILOT, PORT_AUTHORITY, MOORING, TUG:
|
||||
|
||||
eta_berth, etd_berth, shipcall_id, participant_id, participant_type
|
||||
shipcall_id, participant_id, participant_type
|
||||
|
||||
TERMINAL:
|
||||
|
||||
operations_start, operations_end, shipcall_id, participant_id, participant_type
|
||||
|
||||
shipcall_id, participant_id, participant_type
|
||||
|
||||
## /times PUT
|
||||
|
||||
1. A dataset may only be changed by a user belonging to the same participant as the times dataset is referring to.
|
||||
2. See reference and value checking as specified in /times POST.
|
||||
3. The shipcall type may not be changed.
|
||||
|
||||
#### Required fields
|
||||
|
||||
The id field is required, missing fields will not be updated.
|
||||
The id field is required, missing fields will not be updated
|
||||
|
||||
## /times DELETE
|
||||
|
||||
@ -201,7 +213,7 @@ The id field is required, missing fields will not be updated.
|
||||
|
||||
1. The call may only be performed by a user belonging to participant group type BSMD.
|
||||
2. A ship may only be added if there is no other ship with the same IMO number already present in the database.
|
||||
3. Check for reasonable values for the following fields:
|
||||
3. Check for reasonable values for the following fields:
|
||||
|
||||
| Field | Validation |
|
||||
|-------|------------|
|
||||
@ -219,7 +231,6 @@ The id field is required, missing fields will not be updated.
|
||||
3. See value rules in /ship POST
|
||||
4. The id field is required, missing fields will not be updated
|
||||
|
||||
|
||||
## /ship DELETE
|
||||
|
||||
1. The call may only be performed by a user belonging to participant group type BSMD.
|
||||
|
||||
@ -32,7 +32,7 @@ ___
|
||||
| 0002 | Zeiten für einen Eintrag weichen voneinander ab | Bedingungen:<br/> - Header der Zeile ist zugeordnet (Agentur, Festmacher usw. - außer BSMD-Spalte)<br/> - Zeiten ungleich (leere Einträge nicht berücksichtigen => 0001) | |
|
||||
| 0002 - A | Agentur + Festmacher + Hafenamt + Lotsen + Schlepper / einkommend | Schnittmenge aus:<br/>times_agency:<br/> - ETA Berth <br/>____und____<br/>times_mooring:<br/> - ETA Berth <br/>____und____<br/>times_portauthority:<br/>- ETA Berth <br/>____und____<br/>times_pilot:<br/> - ETA Berth <br/>____und____<br/>times_tug:<br/> - ETA Berth | rot |
|
||||
| 0002 - B | Agentur + Festmacher + Hafenamt + Lotsen + Schlepper / ausgehend | Schnittmenge aus:<br/>times_agency:<br/> - ETD Berth <br/>____und____<br/>times_mooring:<br/> - ETD Berth <br/>____und____<br/>times_portauthority:<br/>- ETD Berth <br/>____und____<br/>times_pilot:<br/> - ETD Berth <br/>____und____<br/>times_tug:<br/> - ETD Berth | rot |
|
||||
| 0002 - C | Agentur + Festmacher + Hafenamt + Lotsen + Schlepper / Verholung | Schnittmenge aus:<br />times_agency:<br/> - ETA Berth <br/> - ETD Berth <br/>____und____<br/>times_mooring:<br/> - ETA Berth <br/> - ETD Berth <br/>____und____<br/>times_portauthority:<br/> - ETA Berth <br/>- ETD Berth <br/>____und____<br/>times_pilot:<br/> - ETA Berth <br/> - ETD Berth <br/>____und____<br/>times_tug:<br/> - ETA Berth <br/> - ETD Berth | rot |
|
||||
| 0002 - C | Agentur + Festmacher + Hafenamt + Lotsen + Schlepper / Verholung | Schnittmenge aus:<br/>times_agency:<br/> - ETD Berth <br/>____und____<br/>times_mooring:<br/> - ETD Berth <br/>____und____<br/>times_portauthority:<br/>- ETD Berth <br/>____und____<br/>times_pilot:<br/> - ETD Berth <br/>____und____<br/>times_tug:<br/> - ETD Berth | rot |
|
||||
| 0003 | Arbeitszeit überschneidet sich mit Fahrtzeit | Bedingungen:<br/> - Header der Zeile ist zugeordnet (Terminal)<br/> - Zeiten passt nicht zu Ankunft / Abfahrt (leere Einträge nicht berücksichtigen => 0001) | |
|
||||
| 0003 - A | Terminal / einkommend | times_terminal:<br/> - Operation Start<br/>___vor (kleiner als)____<br/> times_agency:<br/> - ETA Berth | rot, aktuell __deaktiviert__! |
|
||||
| 0003 - B | Terminal / ausgehend + Verholung | times_terminal:<br/> - Operation Ende<br/>___nach (größer als)____<br/> times_agency:<br/> - ETD Berth | rot, aktuell __deaktiviert__! |
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -2,7 +2,7 @@ openapi: 3.0.0
|
||||
x-stoplight:
|
||||
id: mwv4y8vcnopwr
|
||||
info:
|
||||
version: 1.4.0
|
||||
version: 1.5.0
|
||||
title: Bremen calling API
|
||||
description: 'Administer DEBRE ship calls, times and notifications'
|
||||
termsOfService: 'https://www.bsmd.de/'
|
||||
@ -15,7 +15,7 @@ info:
|
||||
url: 'https://www.bsmd.de/license'
|
||||
servers:
|
||||
- url: 'https://brecal.bsmd-emswe.eu'
|
||||
description: Prod server hosted on vcup
|
||||
description: Development server hosted on vcup
|
||||
tags:
|
||||
- name: user
|
||||
- name: shipcall
|
||||
@ -566,6 +566,7 @@ components:
|
||||
evaluation:
|
||||
$ref: '#/components/schemas/EvaluationType'
|
||||
evaluation_message:
|
||||
readOnly: true
|
||||
maxLength: 512
|
||||
type: string
|
||||
nullable: true
|
||||
@ -942,12 +943,21 @@ components:
|
||||
type: integer
|
||||
Error:
|
||||
type: object
|
||||
required:
|
||||
- message
|
||||
description: Structure returned when invalid data (bad request) is created.
|
||||
properties:
|
||||
message:
|
||||
description: A human readable error message
|
||||
type: string
|
||||
errors:
|
||||
description: A list of errors
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
valid_data:
|
||||
description: A dictionary of valid data
|
||||
type: object
|
||||
required:
|
||||
- message
|
||||
ShipcallType:
|
||||
type: string
|
||||
enum:
|
||||
@ -964,8 +974,9 @@ components:
|
||||
- email
|
||||
- push
|
||||
EvaluationType:
|
||||
type: string
|
||||
readOnly: true
|
||||
nullable: true
|
||||
type: string
|
||||
enum:
|
||||
- undefined
|
||||
- green
|
||||
|
||||
19
misc/Readme_yaml.md
Normal file
19
misc/Readme_yaml.md
Normal file
@ -0,0 +1,19 @@
|
||||
# Bremen Calling Open API spec
|
||||
|
||||
## Infos zur Generierung der CS Wrapper / Mapping Datei aus YAML
|
||||
|
||||
Verwendung von "OpenAPIGenerator" aus dem [Rest API Client Code Generator 2022](https://marketplace.visualstudio.com/items?itemName=ChristianResmaHelle.ApiClientCodeGenerator2022).
|
||||
Die automatisch generierte Datei muss leider noch nachgearbeitet werden:
|
||||
|
||||
1) #pragma warning disable CS8073 (direkt nach der NS declaration).
|
||||
2) #pragma warning restore CS8073 // The result of the expression is always the same since a value of this type is never equal to 'null' (am Schluss vor der schließenden Klammer des NS)
|
||||
3) Für readOnly Properties wird bei einem Enum kein korrekter Code emittiert. Daher muss aktuell folgende Funktion ergänzt werden, damit beim Speichern das "evaluation" Flag nicht mitgesendet wird:
|
||||
|
||||
```C++
|
||||
public bool ShouldSerializeEvaluation()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
```
|
||||
|
||||
Witziger(!)weise funktioniert es für das Property EvaluationMessage korrekt.
|
||||
@ -1 +1 @@
|
||||
1.4.0.0
|
||||
1.4.1.0
|
||||
@ -8,8 +8,8 @@
|
||||
<SignAssembly>True</SignAssembly>
|
||||
<StartupObject>BreCalClient.App</StartupObject>
|
||||
<AssemblyOriginatorKeyFile>..\..\misc\brecal.snk</AssemblyOriginatorKeyFile>
|
||||
<AssemblyVersion>1.4.0.0</AssemblyVersion>
|
||||
<FileVersion>1.4.0.0</FileVersion>
|
||||
<AssemblyVersion>1.5.0.12</AssemblyVersion>
|
||||
<FileVersion>1.5.0.12</FileVersion>
|
||||
<Title>Bremen calling client</Title>
|
||||
<Description>A Windows WPF client for the Bremen calling API.</Description>
|
||||
<ApplicationIcon>containership.ico</ApplicationIcon>
|
||||
@ -116,12 +116,12 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Extended.Wpf.Toolkit" Version="4.6.0" />
|
||||
<PackageReference Include="Extended.Wpf.Toolkit" Version="4.6.1" />
|
||||
<PackageReference Include="JsonSubTypes" Version="2.0.1" />
|
||||
<PackageReference Include="log4net" Version="2.0.17" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
<PackageReference Include="Polly" Version="8.3.1" />
|
||||
<PackageReference Include="RestSharp" Version="110.2.0" />
|
||||
<PackageReference Include="Polly" Version="8.4.1" />
|
||||
<PackageReference Include="RestSharp" Version="112.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@ -41,8 +41,8 @@ namespace BreCalClient
|
||||
}
|
||||
this.Ship.Imo = this.integerUpDownIMO.Value;
|
||||
this.Ship.Callsign = this.textBoxCallsign.Text.ToUpper().Trim();
|
||||
this.Ship.Length = (float?) this.doubleUpDownLength.Value;
|
||||
this.Ship.Width = (float?) this.doubleUpDownWidth.Value;
|
||||
this.Ship.Length = this.doubleUpDownLength.Value;
|
||||
this.Ship.Width = this.doubleUpDownWidth.Value;
|
||||
this.DialogResult = true;
|
||||
this.Close();
|
||||
|
||||
|
||||
@ -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" />
|
||||
|
||||
@ -204,16 +204,26 @@ namespace BreCalClient
|
||||
{
|
||||
case ShipcallType.Departure:
|
||||
isEnabled &= this.comboBoxDepartureBerth.SelectedItem != null;
|
||||
isEnabled &= this.datePickerETD.Value.HasValue;
|
||||
isEnabled &= this.datePickerETD.Value.HasValue;
|
||||
isEnabled &= !(this.datePickerETD.Value.IsTooOld() && this.datePickerETD.Value != this.ShipcallModel.Shipcall?.Etd);
|
||||
isEnabled &= !this.datePickerETD.Value.IsTooFar();
|
||||
break;
|
||||
case ShipcallType.Arrival:
|
||||
isEnabled &= this.comboBoxArrivalBerth.SelectedItem != null;
|
||||
isEnabled &= this.datePickerETA.Value.HasValue;
|
||||
isEnabled &= this.datePickerETA.Value.HasValue;
|
||||
isEnabled &= !(this.datePickerETA.Value.IsTooOld() && this.datePickerETA.Value != this.ShipcallModel.Shipcall?.Eta);
|
||||
isEnabled &= !this.datePickerETA.Value.IsTooFar();
|
||||
break;
|
||||
case ShipcallType.Shifting:
|
||||
isEnabled &= ((this.comboBoxDepartureBerth.SelectedItem != null) && (this.comboBoxArrivalBerth.SelectedItem != null));
|
||||
isEnabled &= this.datePickerETD.Value.HasValue;
|
||||
isEnabled &= this.datePickerETA.Value.HasValue;
|
||||
isEnabled &= this.datePickerETA.Value.HasValue;
|
||||
isEnabled &= !(this.datePickerETD.Value.IsTooOld() && this.datePickerETD.Value != this.ShipcallModel.Shipcall?.Etd);
|
||||
isEnabled &= !(this.datePickerETA.Value.IsTooOld() && this.datePickerETA.Value != this.ShipcallModel.Shipcall?.Eta);
|
||||
if (this.datePickerETA.Value.HasValue && this.datePickerETD.Value.HasValue)
|
||||
isEnabled &= (this.datePickerETA.Value.Value > this.datePickerETD.Value.Value);
|
||||
isEnabled &= !this.datePickerETD.Value.IsTooFar();
|
||||
isEnabled &= !this.datePickerETA.Value.IsTooFar();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@ -70,9 +70,16 @@ namespace BreCalClient
|
||||
|
||||
private void buttonOK_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
this.CopyToModel();
|
||||
this.DialogResult = true;
|
||||
this.Close();
|
||||
if (!CheckValues(out string message))
|
||||
{
|
||||
MessageBox.Show(message, BreCalClient.Resources.Resources.textWarning, MessageBoxButton.OK, MessageBoxImage.Warning);
|
||||
}
|
||||
else
|
||||
{
|
||||
this.CopyToModel();
|
||||
this.DialogResult = true;
|
||||
this.Close();
|
||||
}
|
||||
}
|
||||
|
||||
private void buttonCancel_Click(object sender, RoutedEventArgs e)
|
||||
@ -85,6 +92,61 @@ namespace BreCalClient
|
||||
|
||||
#region private methods
|
||||
|
||||
private bool CheckValues(out string message)
|
||||
{
|
||||
message = "";
|
||||
|
||||
if ((this.datePickerETA.Value != this.Times.EtaBerth) || (this.datePickerETA_End.Value != this.Times.EtaIntervalEnd)) // something has changed
|
||||
{
|
||||
if (datePickerETA.Value.IsTooOld() || datePickerETA_End.Value.IsTooOld())
|
||||
{
|
||||
message = BreCalClient.Resources.Resources.textETAInThePast;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.datePickerETA.Value.HasValue && this.datePickerETA_End.Value.HasValue && this.datePickerETA.Value > this.datePickerETA_End.Value)
|
||||
{
|
||||
message = BreCalClient.Resources.Resources.textEndValueBeforeStartValue;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if((this.datePickerTidalWindowFrom.Value != this.ShipcallModel.Shipcall?.TidalWindowFrom) || (this.datePickerTidalWindowTo.Value != this.ShipcallModel.Shipcall?.TidalWindowTo)) // something has changed
|
||||
{
|
||||
if(datePickerTidalWindowTo.Value.IsTooOld() || this.datePickerTidalWindowFrom.Value.IsTooOld())
|
||||
{
|
||||
message = BreCalClient.Resources.Resources.textTideTimesInThePast;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.datePickerTidalWindowFrom.Value.HasValue && this.datePickerTidalWindowTo.Value.HasValue && this.datePickerTidalWindowFrom.Value > this.datePickerTidalWindowTo.Value)
|
||||
{
|
||||
message = BreCalClient.Resources.Resources.textEndValueBeforeStartValue;
|
||||
return false;
|
||||
}
|
||||
|
||||
if ((this.datePickerTidalWindowFrom.Value.HasValue && !this.datePickerTidalWindowTo.Value.HasValue) || (!this.datePickerTidalWindowFrom.Value.HasValue && this.datePickerTidalWindowTo.Value.HasValue))
|
||||
{
|
||||
message = BreCalClient.Resources.Resources.textTidalBothValues;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if(this.datePickerETA.Value.IsTooFar() || this.datePickerETA_End.Value.IsTooFar() || this.datePickerTidalWindowFrom.Value.IsTooFar() || this.datePickerTidalWindowTo.Value.IsTooFar())
|
||||
{
|
||||
message = BreCalClient.Resources.Resources.textTooFarInTheFuture;
|
||||
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;
|
||||
}
|
||||
|
||||
private void CopyToModel()
|
||||
{
|
||||
if (this.ShipcallModel.Shipcall != null)
|
||||
|
||||
@ -4,15 +4,8 @@
|
||||
|
||||
using BreCalClient.misc.Model;
|
||||
using System;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls.Primitives;
|
||||
using System.Windows.Controls;
|
||||
using Xceed.Wpf.Toolkit;
|
||||
using static BreCalClient.Extensions;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Windows.Media;
|
||||
|
||||
namespace BreCalClient
|
||||
{
|
||||
@ -80,9 +73,16 @@ namespace BreCalClient
|
||||
|
||||
private void buttonOK_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
this.CopyToModel();
|
||||
this.DialogResult = true;
|
||||
this.Close();
|
||||
if (!CheckValues(out string message))
|
||||
{
|
||||
System.Windows.MessageBox.Show(message, BreCalClient.Resources.Resources.textWarning, MessageBoxButton.OK, MessageBoxImage.Warning);
|
||||
}
|
||||
else
|
||||
{
|
||||
this.CopyToModel();
|
||||
this.DialogResult = true;
|
||||
this.Close();
|
||||
}
|
||||
}
|
||||
|
||||
private void buttonCancel_Click(object sender, RoutedEventArgs e)
|
||||
@ -95,6 +95,62 @@ namespace BreCalClient
|
||||
|
||||
#region private methods
|
||||
|
||||
|
||||
private bool CheckValues(out string message)
|
||||
{
|
||||
message = "";
|
||||
|
||||
if((this.datePickerETD.Value != this.Times.EtdBerth) || (this.datePickerETD_End.Value != this.Times.EtdIntervalEnd))
|
||||
{
|
||||
if (datePickerETD.Value.IsTooOld() || datePickerETD_End.Value.IsTooOld())
|
||||
{
|
||||
message = BreCalClient.Resources.Resources.textETDInThePast;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.datePickerETD.Value.HasValue && this.datePickerETD_End.Value.HasValue && this.datePickerETD.Value > this.datePickerETD_End.Value)
|
||||
{
|
||||
message = BreCalClient.Resources.Resources.textEndValueBeforeStartValue;
|
||||
return false;
|
||||
}
|
||||
|
||||
if((this.datePickerTidalWindowFrom.Value != this.ShipcallModel.Shipcall?.TidalWindowFrom) || (this.datePickerTidalWindowTo.Value != this.ShipcallModel.Shipcall?.TidalWindowTo))
|
||||
{
|
||||
if (this.datePickerTidalWindowTo.Value.IsTooOld() || this.datePickerTidalWindowFrom.Value.IsTooOld())
|
||||
{
|
||||
message = BreCalClient.Resources.Resources.textTideTimesInThePast;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.datePickerTidalWindowFrom.Value.HasValue && this.datePickerTidalWindowTo.Value.HasValue && this.datePickerTidalWindowFrom.Value > this.datePickerTidalWindowTo.Value)
|
||||
{
|
||||
message = BreCalClient.Resources.Resources.textEndValueBeforeStartValue;
|
||||
return false;
|
||||
}
|
||||
|
||||
if ((this.datePickerTidalWindowFrom.Value.HasValue && !this.datePickerTidalWindowTo.Value.HasValue) || (!this.datePickerTidalWindowFrom.Value.HasValue && this.datePickerTidalWindowTo.Value.HasValue))
|
||||
{
|
||||
message = BreCalClient.Resources.Resources.textTidalBothValues;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.datePickerETD.Value.IsTooFar() || this.datePickerETD_End.Value.IsTooFar() || this.datePickerTidalWindowFrom.Value.IsTooFar() || this.datePickerTidalWindowTo.Value.IsTooFar())
|
||||
{
|
||||
message = BreCalClient.Resources.Resources.textTooFarInTheFuture;
|
||||
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;
|
||||
}
|
||||
|
||||
private void CopyToModel()
|
||||
{
|
||||
if (this.ShipcallModel.Shipcall != null)
|
||||
|
||||
@ -71,9 +71,16 @@ namespace BreCalClient
|
||||
|
||||
private void buttonOK_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
this.CopyToModel();
|
||||
this.DialogResult = true;
|
||||
this.Close();
|
||||
if (!CheckValues(out string message))
|
||||
{
|
||||
System.Windows.MessageBox.Show(message, BreCalClient.Resources.Resources.textWarning, MessageBoxButton.OK, MessageBoxImage.Warning);
|
||||
}
|
||||
else
|
||||
{
|
||||
this.CopyToModel();
|
||||
this.DialogResult = true;
|
||||
this.Close();
|
||||
}
|
||||
}
|
||||
|
||||
private void buttonCancel_Click(object sender, RoutedEventArgs e)
|
||||
@ -86,6 +93,80 @@ namespace BreCalClient
|
||||
|
||||
#region private methods
|
||||
|
||||
private bool CheckValues(out string message)
|
||||
{
|
||||
message = "";
|
||||
|
||||
|
||||
if((this.datePickerETA.Value != this.Times.EtaBerth) || (this.datePickerETA_End.Value != this.Times.EtaIntervalEnd))
|
||||
{
|
||||
if (this.datePickerETA.Value.IsTooOld() && this.datePickerETA_End.Value.IsTooOld())
|
||||
{
|
||||
message = BreCalClient.Resources.Resources.textETAInThePast;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.datePickerETA.Value.HasValue && this.datePickerETA_End.Value.HasValue && this.datePickerETA.Value > this.datePickerETA_End.Value)
|
||||
{
|
||||
message = BreCalClient.Resources.Resources.textEndValueBeforeStartValue;
|
||||
return false;
|
||||
}
|
||||
|
||||
if((this.datePickerETD.Value != this.Times.EtdBerth) || (this.datePickerETD_End.Value != this.Times.EtdIntervalEnd))
|
||||
{
|
||||
if (this.datePickerETD.Value.IsTooOld() || this.datePickerETD_End.Value.IsTooOld())
|
||||
{
|
||||
message = BreCalClient.Resources.Resources.textETDInThePast;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.datePickerETD.Value.HasValue && this.datePickerETD_End.Value.HasValue && this.datePickerETD.Value > this.datePickerETD_End.Value)
|
||||
{
|
||||
message = BreCalClient.Resources.Resources.textEndValueBeforeStartValue;
|
||||
return false;
|
||||
}
|
||||
|
||||
if((this.datePickerTidalWindowFrom.Value != this.ShipcallModel.Shipcall?.TidalWindowFrom) || (this.datePickerTidalWindowTo.Value != this.ShipcallModel.Shipcall?.TidalWindowTo))
|
||||
{
|
||||
if (this.datePickerTidalWindowFrom.Value.IsTooOld() && this.datePickerTidalWindowTo.Value.IsTooOld())
|
||||
{
|
||||
message = BreCalClient.Resources.Resources.textTideTimesInThePast;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.datePickerTidalWindowFrom.Value.HasValue && this.datePickerTidalWindowTo.Value.HasValue && this.datePickerTidalWindowFrom.Value > this.datePickerTidalWindowTo.Value)
|
||||
{
|
||||
message = BreCalClient.Resources.Resources.textEndValueBeforeStartValue;
|
||||
return false;
|
||||
}
|
||||
|
||||
if ((this.datePickerTidalWindowFrom.Value.HasValue && !this.datePickerTidalWindowTo.Value.HasValue) || (!this.datePickerTidalWindowFrom.Value.HasValue && this.datePickerTidalWindowTo.Value.HasValue))
|
||||
{
|
||||
message = BreCalClient.Resources.Resources.textTidalBothValues;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.datePickerETA.Value.IsTooFar() || this.datePickerETA_End.Value.IsTooFar() || this.datePickerTidalWindowFrom.Value.IsTooFar() || this.datePickerTidalWindowTo.Value.IsTooFar() ||
|
||||
this.datePickerETD.Value.IsTooFar() || this.datePickerETD_End.Value.IsTooFar())
|
||||
{
|
||||
message = BreCalClient.Resources.Resources.textTooFarInTheFuture;
|
||||
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;
|
||||
}
|
||||
|
||||
private void CopyToModel()
|
||||
{
|
||||
if (this.ShipcallModel.Shipcall != null)
|
||||
|
||||
@ -44,7 +44,7 @@
|
||||
<ColumnDefinition Width=".5*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<local:DateTimePickerExt IsEnabled="False" Grid.Row="0" Grid.Column="0" Margin="4,2,0,2" x:Name="datePickerETABerth" Format="Custom" FormatString="dd.MM. yyyy HH:mm">
|
||||
<local:DateTimePickerExt IsEnabled="False" Grid.Row="0" Grid.Column="0" Margin="2" x:Name="datePickerETABerth" Format="Custom" FormatString="dd.MM. yyyy HH:mm">
|
||||
<xctk:DateTimePicker.ContextMenu>
|
||||
<ContextMenu>
|
||||
<MenuItem Header="{x:Static p:Resources.textClearValue}" Name="contextMenuItemClearETA" Click="contextMenuItemClearETA_Click" >
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
// Copyright (c) 2023 schick Informatik
|
||||
// Description: Single dialog to edit times for all participant types
|
||||
// (we might use different controls at a later time)
|
||||
//
|
||||
//
|
||||
|
||||
using BreCalClient.misc.Model;
|
||||
using System;
|
||||
@ -39,17 +39,25 @@ namespace BreCalClient
|
||||
#region event handler
|
||||
|
||||
private void Window_Loaded(object sender, RoutedEventArgs e)
|
||||
{
|
||||
{
|
||||
this.EnableControls();
|
||||
this.CopyToControls();
|
||||
}
|
||||
|
||||
private void buttonOK_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
this.CopyToModel();
|
||||
this.DialogResult = true;
|
||||
this.Close();
|
||||
}
|
||||
if (!CheckValues(out string message))
|
||||
{
|
||||
System.Windows.MessageBox.Show(message, BreCalClient.Resources.Resources.textWarning,
|
||||
MessageBoxButton.OK, MessageBoxImage.Warning);
|
||||
}
|
||||
else
|
||||
{
|
||||
this.CopyToModel();
|
||||
this.DialogResult = true;
|
||||
this.Close();
|
||||
}
|
||||
}
|
||||
|
||||
private void buttonCancel_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
@ -85,6 +93,69 @@ namespace BreCalClient
|
||||
|
||||
#region private methods
|
||||
|
||||
private bool CheckValues(out string message)
|
||||
{
|
||||
|
||||
message = "";
|
||||
|
||||
if ((this.datePickerETABerth.Value != this.Times.EtaBerth) || (this.datePickerETABerth_End.Value != this.Times.EtaIntervalEnd))
|
||||
{
|
||||
if (this.datePickerETABerth.Value.IsTooOld() || this.datePickerETABerth_End.Value.IsTooOld())
|
||||
{
|
||||
message = BreCalClient.Resources.Resources.textETAInThePast;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.datePickerETABerth.Value.HasValue && this.datePickerETABerth_End.Value.HasValue && this.datePickerETABerth.Value > this.datePickerETABerth_End.Value)
|
||||
{
|
||||
message = BreCalClient.Resources.Resources.textEndValueBeforeStartValue;
|
||||
return false;
|
||||
}
|
||||
|
||||
if((this.datePickerETDBerth.Value != this.Times.EtdBerth) || (this.datePickerETDBerth_End.Value != this.Times.EtdIntervalEnd))
|
||||
{
|
||||
if(this.datePickerETDBerth.Value.IsTooOld() || this.datePickerETDBerth_End.Value.IsTooOld())
|
||||
{
|
||||
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.IsTooOld() && (this.datePickerLockTime.Value != this.Times.LockTime))
|
||||
{
|
||||
message = BreCalClient.Resources.Resources.textLockTimeInThePast;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.datePickerZoneEntry.Value.IsTooOld() && (this.datePickerZoneEntry.Value != this.Times.ZoneEntry))
|
||||
{
|
||||
message = BreCalClient.Resources.Resources.textZoneEntryInThePast;
|
||||
return false;
|
||||
}
|
||||
|
||||
if(this.datePickerATA.Value.IsTooFar() || this.datePickerATD.Value.IsTooFar() || this.datePickerETABerth.Value.IsTooFar() || this.datePickerETABerth_End.Value.IsTooFar() ||
|
||||
this.datePickerETDBerth.Value.IsTooFar() || this.datePickerETDBerth_End.Value.IsTooFar() || this.datePickerLockTime.Value.IsTooFar() || this.datePickerZoneEntry.Value.IsTooFar())
|
||||
{
|
||||
message = BreCalClient.Resources.Resources.textTooFarInTheFuture;
|
||||
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;
|
||||
}
|
||||
|
||||
private void CopyToModel()
|
||||
{
|
||||
this.Times.Remarks = this.textBoxRemarks.Text.Trim().Truncate(512);
|
||||
@ -102,13 +173,13 @@ namespace BreCalClient
|
||||
{
|
||||
this.textBoxRemarks.Text = this.Times.Remarks;
|
||||
this.datePickerETABerth.Value = this.Times.EtaBerth;
|
||||
if(this.datePickerETABerth.IsEnabled && (this.Times.EtaBerth == null) && (this.AgencyTimes?.EtaBerth != null) && (ShipcallModel.Shipcall?.Type == ShipcallType.Arrival))
|
||||
if(this.datePickerETABerth.IsEnabled && (this.Times.EtaBerth == null) && (this.AgencyTimes?.EtaBerth != null) && (ShipcallModel.Shipcall?.Type == ShipcallType.Arrival) && (this.AgencyTimes?.EtaBerth > DateTime.Now))
|
||||
{
|
||||
this.datePickerETABerth.Value = this.AgencyTimes.EtaBerth;
|
||||
if (this.datePickerETABerth.Template.FindName("PART_TextBox", this.datePickerETABerth) is WatermarkTextBox tb) { tb.Focus(); tb.SelectAll(); }
|
||||
}
|
||||
this.datePickerETDBerth.Value = this.Times.EtdBerth;
|
||||
if(this.datePickerETDBerth.IsEnabled && (this.Times.EtdBerth == null) && (this.AgencyTimes?.EtdBerth != null) && ((ShipcallModel.Shipcall?.Type == ShipcallType.Departure) || (ShipcallModel.Shipcall?.Type == ShipcallType.Shifting)))
|
||||
if(this.datePickerETDBerth.IsEnabled && (this.Times.EtdBerth == null) && (this.AgencyTimes?.EtdBerth != null) && ((ShipcallModel.Shipcall?.Type == ShipcallType.Departure) || (ShipcallModel.Shipcall?.Type == ShipcallType.Shifting)) && (this.AgencyTimes?.EtdBerth > DateTime.Now))
|
||||
{
|
||||
this.datePickerETDBerth.Value = this.AgencyTimes.EtdBerth;
|
||||
if (this.datePickerETDBerth.Template.FindName("PART_TextBox", this.datePickerETDBerth) is WatermarkTextBox tb) tb.SelectAll();
|
||||
@ -139,13 +210,13 @@ namespace BreCalClient
|
||||
{
|
||||
this.labelETA.Content = string.Format("ETA {0}", BreCalLists.TimeRefs[displayIndex]);
|
||||
this.labelETD.Content = string.Format("ETD {0}", BreCalLists.TimeRefs[displayIndex]);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
this.labelETA.Content = BreCalClient.Resources.Resources.textETABerth;
|
||||
this.labelETD.Content = BreCalClient.Resources.Resources.textETDBerth;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.SetLockButton(this.Times.EtaBerthFixed ?? false);
|
||||
}
|
||||
@ -191,6 +262,7 @@ namespace BreCalClient
|
||||
if (this.Times.ParticipantId != App.Participant.Id)
|
||||
{
|
||||
this.buttonFixedOrder.IsEnabled = false;
|
||||
this.buttonOK.IsEnabled = false;
|
||||
return; // if this is not "my" entry, there is no editing!
|
||||
}
|
||||
|
||||
@ -210,11 +282,11 @@ namespace BreCalClient
|
||||
case Extensions.ParticipantType.PORT_ADMINISTRATION:
|
||||
this.datePickerLockTime.IsEnabled = true;
|
||||
break;
|
||||
case Extensions.ParticipantType.TUG:
|
||||
case Extensions.ParticipantType.PILOT:
|
||||
this.datePickerZoneEntry.IsEnabled = (ShipcallModel.Shipcall?.Type == ShipcallType.Arrival);
|
||||
case Extensions.ParticipantType.TUG:
|
||||
case Extensions.ParticipantType.PILOT:
|
||||
this.datePickerZoneEntry.IsEnabled = (ShipcallModel.Shipcall?.Type == ShipcallType.Arrival);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void SetLockButton(bool newValue)
|
||||
@ -231,7 +303,7 @@ namespace BreCalClient
|
||||
this.imageFixedOrder.Source = new BitmapImage(new Uri(@"pack://application:,,,/Resources/lock_open.png", UriKind.RelativeOrAbsolute));
|
||||
this.buttonFixedOrder.ToolTip = BreCalClient.Resources.Resources.textTooltipSetFixedOrder;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
@ -255,17 +327,17 @@ namespace BreCalClient
|
||||
private void contextMenuItemClearZoneEntry_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
this.datePickerZoneEntry.Value = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void contextMenuItemClearATA_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
this.datePickerATA.Value = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void contextMenuItemClearATD_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
this.datePickerATD.Value = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void contextMenuItemClearETA_End_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
@ -278,6 +350,6 @@ namespace BreCalClient
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
//
|
||||
|
||||
using BreCalClient.misc.Model;
|
||||
using System;
|
||||
using System.Windows;
|
||||
|
||||
namespace BreCalClient
|
||||
@ -61,9 +62,16 @@ namespace BreCalClient
|
||||
|
||||
private void buttonOK_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
this.CopyToModel();
|
||||
this.DialogResult = true;
|
||||
this.Close();
|
||||
if (!CheckValues(out string message))
|
||||
{
|
||||
System.Windows.MessageBox.Show(message, BreCalClient.Resources.Resources.textWarning, MessageBoxButton.OK, MessageBoxImage.Warning);
|
||||
}
|
||||
else
|
||||
{
|
||||
this.CopyToModel();
|
||||
this.DialogResult = true;
|
||||
this.Close();
|
||||
}
|
||||
}
|
||||
|
||||
private void buttonCancel_Click(object sender, RoutedEventArgs e)
|
||||
@ -95,6 +103,56 @@ namespace BreCalClient
|
||||
|
||||
#region private methods
|
||||
|
||||
private bool CheckValues(out string message)
|
||||
{
|
||||
|
||||
message = "";
|
||||
|
||||
if((this.datePickerOperationStart.Value != this.Times.OperationsStart) || (this.datePickerOperationStart_End.Value != this.Times.EtaIntervalEnd))
|
||||
{
|
||||
if(this.datePickerOperationStart.Value.IsTooOld() || this.datePickerOperationStart_End.Value.IsTooOld())
|
||||
{
|
||||
message = BreCalClient.Resources.Resources.textOperationStartInThePast;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.datePickerOperationStart.Value.HasValue && this.datePickerOperationStart_End.Value.HasValue && this.datePickerOperationStart.Value > this.datePickerOperationStart_End.Value)
|
||||
{
|
||||
message = BreCalClient.Resources.Resources.textEndValueBeforeStartValue;
|
||||
return false;
|
||||
}
|
||||
|
||||
if ((this.datePickerOperationEnd.Value != this.Times.OperationsEnd) || (this.datePickerOperationEnd_End.Value != this.Times.EtdIntervalEnd))
|
||||
{
|
||||
if(this.datePickerOperationEnd.Value.IsTooOld() || this.datePickerOperationEnd_End.Value.IsTooOld())
|
||||
{
|
||||
message = BreCalClient.Resources.Resources.textOperationEndInThePast;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.datePickerOperationEnd.Value.HasValue && this.datePickerOperationEnd_End.Value.HasValue && this.datePickerOperationEnd.Value > this.datePickerOperationEnd_End.Value)
|
||||
{
|
||||
message = BreCalClient.Resources.Resources.textEndValueBeforeStartValue;
|
||||
return false;
|
||||
}
|
||||
|
||||
if(this.datePickerOperationEnd.Value.IsTooFar() || this.datePickerOperationEnd_End.Value.IsTooFar() || this.datePickerOperationStart.Value.IsTooFar() || this.datePickerOperationStart_End.Value.IsTooFar())
|
||||
{
|
||||
message = BreCalClient.Resources.Resources.textTooFarInTheFuture;
|
||||
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;
|
||||
}
|
||||
|
||||
private void CopyToModel()
|
||||
{
|
||||
this.Times.PierSide = this.comboBoxPierside.SelectedIndex switch
|
||||
@ -156,7 +214,11 @@ namespace BreCalClient
|
||||
|
||||
private void EnableControls()
|
||||
{
|
||||
if (this.Times.ParticipantId != App.Participant.Id) return;
|
||||
if (this.Times.ParticipantId != App.Participant.Id)
|
||||
{
|
||||
this.buttonOK.IsEnabled = false;
|
||||
return;
|
||||
}
|
||||
|
||||
this.datePickerOperationStart.IsEnabled = ShipcallModel.Shipcall?.Type == ShipcallType.Arrival;
|
||||
this.datePickerOperationStart_End.IsEnabled = ShipcallModel.Shipcall?.Type == ShipcallType.Arrival;
|
||||
|
||||
@ -3,10 +3,8 @@
|
||||
//
|
||||
|
||||
using BreCalClient.misc.Model;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace BreCalClient
|
||||
{
|
||||
@ -60,6 +58,25 @@ namespace BreCalClient
|
||||
|
||||
#region public helper
|
||||
|
||||
public static bool IsTooFar(this DateTime datetime)
|
||||
{
|
||||
return datetime > DateTime.Now.AddYears(1);
|
||||
}
|
||||
|
||||
public static bool IsTooFar(this DateTime? datetime)
|
||||
{
|
||||
if (datetime == null) return false;
|
||||
return datetime > DateTime.Now.AddYears(1);
|
||||
}
|
||||
|
||||
public static bool IsTooOld(this DateTime? datetime)
|
||||
{
|
||||
if (datetime == null) return false;
|
||||
{
|
||||
return datetime < DateTime.Now.AddDays(-1);
|
||||
}
|
||||
}
|
||||
|
||||
public static bool IsTypeFlagSet(this Participant participant, ParticipantType flag)
|
||||
{
|
||||
return (participant.Type & (uint)flag) != 0;
|
||||
|
||||
@ -23,6 +23,8 @@ using Polly;
|
||||
using System.Net.Http;
|
||||
using System.Net;
|
||||
using System.Windows.Input;
|
||||
using System.Text.RegularExpressions;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
|
||||
namespace BreCalClient
|
||||
@ -335,7 +337,7 @@ namespace BreCalClient
|
||||
{
|
||||
this.Dispatcher.Invoke(new Action(() =>
|
||||
{
|
||||
MessageBox.Show(ex.Message, "Error", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
ShowErrorDialog(ex.Message, "Error saving user information");
|
||||
}));
|
||||
}
|
||||
}
|
||||
@ -897,7 +899,10 @@ namespace BreCalClient
|
||||
try
|
||||
{
|
||||
obj.ShipcallControlModel.Shipcall?.Participants.Clear();
|
||||
obj.ShipcallControlModel.UpdateTimesAssignments(this._timesApi);
|
||||
if(! await obj.ShipcallControlModel.UpdateTimesAssignments(this._timesApi))
|
||||
{
|
||||
ShowErrorDialog(obj.ShipcallControlModel.LastErrorMessage, "Update assignments error");
|
||||
}
|
||||
foreach(ParticipantAssignment pa in obj.ShipcallControlModel.AssignedParticipants.Values)
|
||||
obj.ShipcallControlModel.Shipcall?.Participants.Add(pa);
|
||||
await _shipcallApi.ShipcallUpdateAsync(obj.ShipcallControlModel.Shipcall);
|
||||
@ -1009,7 +1014,13 @@ namespace BreCalClient
|
||||
{
|
||||
try
|
||||
{
|
||||
sc.ShipcallControlModel?.UpdateTimesAssignments(_timesApi); // if the agent changed the assignment of the participant to another
|
||||
if (sc.ShipcallControlModel != null)
|
||||
{
|
||||
if (!await sc.ShipcallControlModel.UpdateTimesAssignments(_timesApi)) // if the agent changed the assignment of the participant to another
|
||||
{
|
||||
ShowErrorDialog(sc.ShipcallControlModel.LastErrorMessage, "Error updating times assignment");
|
||||
}
|
||||
}
|
||||
|
||||
// always try to be the agent, even if we are BSMD
|
||||
if (editControl.ShipcallModel.AssignedParticipants.ContainsKey(ParticipantType.AGENCY))
|
||||
@ -1058,13 +1069,40 @@ namespace BreCalClient
|
||||
|
||||
private void ShowErrorDialog(string message, string caption)
|
||||
{
|
||||
// Example:
|
||||
// Error calling ShipcallUpdate: {\"message\": \"PUT Requests for shipcalls can only be issued by an assigned AGENCY or BSMD users
|
||||
// (if the special-flag is enabled). Assigned Agency: ShipcallParticipantMap(id=628, shipcall_id=115, participant_id=10,
|
||||
// type=8, created=datetime.datetime(2024, 8, 28, 15, 13, 14), modified=None) with Flags: 42\"}
|
||||
|
||||
Match m = Regex.Match(message, "\\{(.*)\\}");
|
||||
if ((m != null) && m.Success)
|
||||
{
|
||||
try
|
||||
{
|
||||
dynamic? msg = JsonConvert.DeserializeObject(m.Value);
|
||||
if (msg != null)
|
||||
{
|
||||
if (msg.error_field != null)
|
||||
{
|
||||
caption = $"{caption}: {msg.error_field}";
|
||||
}
|
||||
|
||||
if(msg.error_description != null)
|
||||
{
|
||||
message = msg.error_description;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception) { }
|
||||
}
|
||||
|
||||
_log.ErrorFormat("{0} - {1}", caption, message);
|
||||
/*
|
||||
|
||||
Dispatcher.Invoke(new Action(() =>
|
||||
{
|
||||
MessageBox.Show(message, caption, MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
}));
|
||||
*/
|
||||
|
||||
}
|
||||
|
||||
private void EnableControlsForParticipant()
|
||||
|
||||
@ -5,7 +5,7 @@ https://go.microsoft.com/fwlink/?LinkID=208121.
|
||||
<Project>
|
||||
<PropertyGroup>
|
||||
<ApplicationRevision>1</ApplicationRevision>
|
||||
<ApplicationVersion>1.4.0.0</ApplicationVersion>
|
||||
<ApplicationVersion>1.4.1.0</ApplicationVersion>
|
||||
<BootstrapperEnabled>True</BootstrapperEnabled>
|
||||
<Configuration>Debug</Configuration>
|
||||
<CreateDesktopShortcut>True</CreateDesktopShortcut>
|
||||
|
||||
@ -4,8 +4,8 @@ https://go.microsoft.com/fwlink/?LinkID=208121.
|
||||
-->
|
||||
<Project>
|
||||
<PropertyGroup>
|
||||
<ApplicationRevision>0</ApplicationRevision>
|
||||
<ApplicationVersion>1.4.0.0</ApplicationVersion>
|
||||
<ApplicationRevision>12</ApplicationRevision>
|
||||
<ApplicationVersion>1.5.0.12</ApplicationVersion>
|
||||
<BootstrapperEnabled>False</BootstrapperEnabled>
|
||||
<Configuration>Release</Configuration>
|
||||
<CreateWebPageOnPublish>True</CreateWebPageOnPublish>
|
||||
|
||||
@ -4,8 +4,8 @@ https://go.microsoft.com/fwlink/?LinkID=208121.
|
||||
-->
|
||||
<Project>
|
||||
<PropertyGroup>
|
||||
<ApplicationRevision>0</ApplicationRevision>
|
||||
<ApplicationVersion>1.4.0.0</ApplicationVersion>
|
||||
<ApplicationRevision>8</ApplicationRevision>
|
||||
<ApplicationVersion>1.5.0.8</ApplicationVersion>
|
||||
<BootstrapperEnabled>True</BootstrapperEnabled>
|
||||
<Configuration>Debug</Configuration>
|
||||
<CreateDesktopShortcut>True</CreateDesktopShortcut>
|
||||
@ -40,6 +40,7 @@ https://go.microsoft.com/fwlink/?LinkID=208121.
|
||||
<SupportUrl>http://www.textbausteine.net/</SupportUrl>
|
||||
<PublishDir>bin\Debug\net6.0-windows\win-x64\app.publish\</PublishDir>
|
||||
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
|
||||
<SkipPublishVerification>false</SkipPublishVerification>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PublishFile Include="containership.ico">
|
||||
|
||||
144
src/BreCalClient/Resources/Resources.Designer.cs
generated
144
src/BreCalClient/Resources/Resources.Designer.cs
generated
@ -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>
|
||||
@ -389,6 +407,15 @@ namespace BreCalClient.Resources {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Both tide times are required.
|
||||
/// </summary>
|
||||
public static string textBothTideTimesNecessary {
|
||||
get {
|
||||
return ResourceManager.GetString("textBothTideTimesNecessary", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to BSMD right to edit granted.
|
||||
/// </summary>
|
||||
@ -623,6 +650,15 @@ namespace BreCalClient.Resources {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Some interval end times are before start times.
|
||||
/// </summary>
|
||||
public static string textEndValueBeforeStartValue {
|
||||
get {
|
||||
return ResourceManager.GetString("textEndValueBeforeStartValue", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Ship / remark.
|
||||
/// </summary>
|
||||
@ -632,6 +668,15 @@ namespace BreCalClient.Resources {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Error.
|
||||
/// </summary>
|
||||
public static string textError {
|
||||
get {
|
||||
return ResourceManager.GetString("textError", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to ETA berth.
|
||||
/// </summary>
|
||||
@ -641,6 +686,15 @@ namespace BreCalClient.Resources {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to ETA value is in the past.
|
||||
/// </summary>
|
||||
public static string textETAInThePast {
|
||||
get {
|
||||
return ResourceManager.GetString("textETAInThePast", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to ETD berth.
|
||||
/// </summary>
|
||||
@ -650,6 +704,15 @@ namespace BreCalClient.Resources {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to ETD value is in the past.
|
||||
/// </summary>
|
||||
public static string textETDInThePast {
|
||||
get {
|
||||
return ResourceManager.GetString("textETDInThePast", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Exit.
|
||||
/// </summary>
|
||||
@ -740,6 +803,15 @@ namespace BreCalClient.Resources {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Lock time is in the past.
|
||||
/// </summary>
|
||||
public static string textLockTimeInThePast {
|
||||
get {
|
||||
return ResourceManager.GetString("textLockTimeInThePast", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Login.
|
||||
/// </summary>
|
||||
@ -830,6 +902,15 @@ namespace BreCalClient.Resources {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Operation end is in the past.
|
||||
/// </summary>
|
||||
public static string textOperationEndInThePast {
|
||||
get {
|
||||
return ResourceManager.GetString("textOperationEndInThePast", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Operations end.
|
||||
/// </summary>
|
||||
@ -848,6 +929,15 @@ namespace BreCalClient.Resources {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Operation start is in the past.
|
||||
/// </summary>
|
||||
public static string textOperationStartInThePast {
|
||||
get {
|
||||
return ResourceManager.GetString("textOperationStartInThePast", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Outgoing.
|
||||
/// </summary>
|
||||
@ -1127,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>
|
||||
@ -1136,6 +1235,15 @@ namespace BreCalClient.Resources {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to For a tidal window both values must be set.
|
||||
/// </summary>
|
||||
public static string textTidalBothValues {
|
||||
get {
|
||||
return ResourceManager.GetString("textTidalBothValues", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Tidal window.
|
||||
/// </summary>
|
||||
@ -1145,6 +1253,15 @@ namespace BreCalClient.Resources {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Times for the tide window are in the past.
|
||||
/// </summary>
|
||||
public static string textTideTimesInThePast {
|
||||
get {
|
||||
return ResourceManager.GetString("textTideTimesInThePast", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Timestamp.
|
||||
/// </summary>
|
||||
@ -1163,6 +1280,15 @@ namespace BreCalClient.Resources {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to A time value is too far in the future.
|
||||
/// </summary>
|
||||
public static string textTooFarInTheFuture {
|
||||
get {
|
||||
return ResourceManager.GetString("textTooFarInTheFuture", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Set as a fixed order.
|
||||
/// </summary>
|
||||
@ -1262,6 +1388,15 @@ namespace BreCalClient.Resources {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Warning.
|
||||
/// </summary>
|
||||
public static string textWarning {
|
||||
get {
|
||||
return ResourceManager.GetString("textWarning", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Width.
|
||||
/// </summary>
|
||||
@ -1280,6 +1415,15 @@ namespace BreCalClient.Resources {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Zone entry time is in the past.
|
||||
/// </summary>
|
||||
public static string textZoneEntryInThePast {
|
||||
get {
|
||||
return ResourceManager.GetString("textZoneEntryInThePast", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Zone entry.
|
||||
/// </summary>
|
||||
|
||||
@ -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,157 +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 name="textStartTimeMissing" xml:space="preserve">
|
||||
<value>Wenn eine Ende-Zeit angegeben wird, muss auch eine Start-Zeit angegeben werden</value>
|
||||
</data>
|
||||
</root>
|
||||
@ -214,6 +214,9 @@
|
||||
<data name="textBerths" xml:space="preserve">
|
||||
<value>Berths</value>
|
||||
</data>
|
||||
<data name="textBothTideTimesNecessary" xml:space="preserve">
|
||||
<value>Both tide times are required</value>
|
||||
</data>
|
||||
<data name="textBSMDGranted" xml:space="preserve">
|
||||
<value>BSMD right to edit granted</value>
|
||||
</data>
|
||||
@ -292,15 +295,27 @@
|
||||
<data name="textEmail" xml:space="preserve">
|
||||
<value>E-mail</value>
|
||||
</data>
|
||||
<data name="textEndValueBeforeStartValue" xml:space="preserve">
|
||||
<value>Some interval end times are before start times</value>
|
||||
</data>
|
||||
<data name="textEnterKeyword" xml:space="preserve">
|
||||
<value>Ship / remark</value>
|
||||
</data>
|
||||
<data name="textError" xml:space="preserve">
|
||||
<value>Error</value>
|
||||
</data>
|
||||
<data name="textETABerth" xml:space="preserve">
|
||||
<value>ETA berth</value>
|
||||
</data>
|
||||
<data name="textETAInThePast" xml:space="preserve">
|
||||
<value>ETA value is in the past</value>
|
||||
</data>
|
||||
<data name="textETDBerth" xml:space="preserve">
|
||||
<value>ETD berth</value>
|
||||
</data>
|
||||
<data name="textETDInThePast" xml:space="preserve">
|
||||
<value>ETD value is in the past</value>
|
||||
</data>
|
||||
<data name="textExit" xml:space="preserve">
|
||||
<value>Exit</value>
|
||||
</data>
|
||||
@ -331,6 +346,9 @@
|
||||
<data name="textLockTime" xml:space="preserve">
|
||||
<value>Lock time</value>
|
||||
</data>
|
||||
<data name="textLockTimeInThePast" xml:space="preserve">
|
||||
<value>Lock time is in the past</value>
|
||||
</data>
|
||||
<data name="textLogin" xml:space="preserve">
|
||||
<value>Login</value>
|
||||
</data>
|
||||
@ -361,12 +379,18 @@
|
||||
<data name="textOperation" xml:space="preserve">
|
||||
<value>Operation</value>
|
||||
</data>
|
||||
<data name="textOperationEndInThePast" xml:space="preserve">
|
||||
<value>Operation end is in the past</value>
|
||||
</data>
|
||||
<data name="textOperationsEnd" xml:space="preserve">
|
||||
<value>Operations end</value>
|
||||
</data>
|
||||
<data name="textOperationsStart" xml:space="preserve">
|
||||
<value>Operations start</value>
|
||||
</data>
|
||||
<data name="textOperationStartInThePast" xml:space="preserve">
|
||||
<value>Operation start is in the past</value>
|
||||
</data>
|
||||
<data name="textOutgoing" xml:space="preserve">
|
||||
<value>Outgoing</value>
|
||||
</data>
|
||||
@ -463,15 +487,24 @@
|
||||
<data name="textTerminal" xml:space="preserve">
|
||||
<value>Terminal</value>
|
||||
</data>
|
||||
<data name="textTidalBothValues" xml:space="preserve">
|
||||
<value>For a tidal window both values must be set</value>
|
||||
</data>
|
||||
<data name="textTidalWindow" xml:space="preserve">
|
||||
<value>Tidal window</value>
|
||||
</data>
|
||||
<data name="textTideTimesInThePast" xml:space="preserve">
|
||||
<value>Times for the tide window are in the past</value>
|
||||
</data>
|
||||
<data name="textTimestamp" xml:space="preserve">
|
||||
<value>Timestamp</value>
|
||||
</data>
|
||||
<data name="textTo" xml:space="preserve">
|
||||
<value>to</value>
|
||||
</data>
|
||||
<data name="textTooFarInTheFuture" xml:space="preserve">
|
||||
<value>A time value is too far in the future</value>
|
||||
</data>
|
||||
<data name="textTooltipSetFixedOrder" xml:space="preserve">
|
||||
<value>Set as a fixed order</value>
|
||||
</data>
|
||||
@ -505,12 +538,18 @@
|
||||
<data name="textVoyage" xml:space="preserve">
|
||||
<value>Voyage</value>
|
||||
</data>
|
||||
<data name="textWarning" xml:space="preserve">
|
||||
<value>Warning</value>
|
||||
</data>
|
||||
<data name="textWidth" xml:space="preserve">
|
||||
<value>Width</value>
|
||||
</data>
|
||||
<data name="textWrongCredentials" xml:space="preserve">
|
||||
<value>Wrong username or password</value>
|
||||
</data>
|
||||
<data name="textZoneEntryInThePast" xml:space="preserve">
|
||||
<value>Zone entry time is in the past</value>
|
||||
</data>
|
||||
<data name="textZoneEntryTime" xml:space="preserve">
|
||||
<value>Zone entry</value>
|
||||
</data>
|
||||
@ -550,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>
|
||||
@ -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>
|
||||
|
||||
@ -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();
|
||||
}
|
||||
|
||||
|
||||
@ -49,8 +49,15 @@ namespace BreCalClient
|
||||
{
|
||||
if (!shipmodel.Ship.Deleted)
|
||||
{
|
||||
if (this.ShipApi != null)
|
||||
await this.ShipApi.ShipDeleteAsync(shipmodel.Ship.Id);
|
||||
try
|
||||
{
|
||||
if (this.ShipApi != null)
|
||||
await this.ShipApi.ShipDeleteAsync(shipmodel.Ship.Id);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
MessageBox.Show(ex.Message, "Error", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
}
|
||||
BreCalLists.Ships.Remove(shipmodel); // remove from "selectable" ships
|
||||
shipmodel.Ship.Deleted = true; // set deleted marker on working instance
|
||||
this.dataGridShips.ItemsSource = null;
|
||||
@ -90,7 +97,9 @@ namespace BreCalClient
|
||||
|
||||
private async void DataGridShips_CreateRequested()
|
||||
{
|
||||
ShipModel shipModel = new(new Ship());
|
||||
|
||||
ShipModel shipModel = new((ShipModel.LastEditShip != null) ? ShipModel.LastEditShip : new Ship());
|
||||
|
||||
EditShipDialog esd = new()
|
||||
{
|
||||
Ship = shipModel.Ship
|
||||
@ -111,14 +120,15 @@ namespace BreCalClient
|
||||
if(!BreCalLists.ShipLookupDict.TryAdd(id.VarId, shipModel))
|
||||
BreCalLists.ShipLookupDict[id.VarId] = shipModel;
|
||||
this.dataGridShips.ItemsSource = BreCalLists.AllShips;
|
||||
ShipModel.LastEditShip = null;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
MessageBox.Show(ex.Message);
|
||||
ShipModel.LastEditShip = shipModel.Ship;
|
||||
MessageBox.Show(ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
@ -16,6 +16,8 @@ namespace BreCalClient
|
||||
|
||||
public Ship Ship { get; private set; }
|
||||
|
||||
public static Ship? LastEditShip { get; set; }
|
||||
|
||||
public string TugCompany
|
||||
{
|
||||
get { if(this.Ship.ParticipantId.HasValue)
|
||||
|
||||
@ -6,6 +6,7 @@ using BreCalClient.misc.Api;
|
||||
using BreCalClient.misc.Model;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BreCalClient
|
||||
{
|
||||
@ -38,6 +39,8 @@ namespace BreCalClient
|
||||
|
||||
public string? Berth { get; set; }
|
||||
|
||||
public string LastErrorMessage { get; private set; } = "";
|
||||
|
||||
internal Dictionary<Extensions.ParticipantType, ParticipantAssignment> AssignedParticipants { get; } = new();
|
||||
|
||||
public List<Times> Times { get; set; } = new();
|
||||
@ -247,8 +250,9 @@ namespace BreCalClient
|
||||
/// This function updates the assignments for existing times records accordingly and saves them.
|
||||
/// </summary>
|
||||
/// <param name="_api">API reference to PUT eidted times</param>
|
||||
internal async void UpdateTimesAssignments(TimesApi api)
|
||||
{
|
||||
internal async Task<bool> UpdateTimesAssignments(TimesApi api)
|
||||
{
|
||||
|
||||
foreach (Extensions.ParticipantType participantType in this.AssignedParticipants.Keys)
|
||||
{
|
||||
Times? times = this.GetTimesForParticipantType(participantType);
|
||||
@ -256,7 +260,15 @@ namespace BreCalClient
|
||||
if(times.ParticipantId != this.AssignedParticipants[participantType].ParticipantId)
|
||||
{
|
||||
times.ParticipantId = this.AssignedParticipants[participantType].ParticipantId;
|
||||
await api.TimesUpdateAsync(times);
|
||||
try
|
||||
{
|
||||
await api.TimesUpdateAsync(times);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LastErrorMessage = ex.Message;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -283,9 +295,18 @@ namespace BreCalClient
|
||||
|
||||
foreach(Times times in deleteTimes)
|
||||
{
|
||||
api.TimesDelete(times.Id);
|
||||
this.Times.Remove(times);
|
||||
try
|
||||
{
|
||||
api.TimesDelete(times.Id);
|
||||
this.Times.Remove(times);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LastErrorMessage = ex.Message;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
@ -8,7 +8,7 @@
|
||||
<applicationSettings>
|
||||
<RoleEditor.Properties.Settings>
|
||||
<setting name="ConnectionString" serializeAs="String">
|
||||
<value>Server=localhost;User ID=ds;Password=HalloWach_2323XXL!!;Database=bremen_calling;Port=33306</value>
|
||||
<value>Server=localhost;User ID=ds;Password=HalloWach_2323XXL!!;Database=bremen_calling_test;Port=33306</value>
|
||||
</setting>
|
||||
</RoleEditor.Properties.Settings>
|
||||
</applicationSettings>
|
||||
|
||||
@ -182,8 +182,8 @@
|
||||
{
|
||||
"Name" = "8:Microsoft Visual Studio"
|
||||
"ProductName" = "8:Bremen calling"
|
||||
"ProductCode" = "8:{79B2385A-EA5E-4008-A190-7374F18BBA5E}"
|
||||
"PackageCode" = "8:{084F8E83-0A68-4410-B23D-614BBA59227E}"
|
||||
"ProductCode" = "8:{35061DB2-0F66-4DEF-B1C8-A9D21A0BCD46}"
|
||||
"PackageCode" = "8:{8E55F30F-A9C8-41E9-BCE8-431770A951ED}"
|
||||
"UpgradeCode" = "8:{1C7FA3E4-BAB9-4911-9348-73094357FC7C}"
|
||||
"AspNetVersion" = "8:"
|
||||
"RestartWWWService" = "11:FALSE"
|
||||
|
||||
@ -34,7 +34,7 @@ from BreCal.stubs.df_times import get_df_times
|
||||
|
||||
from BreCal.services.schedule_routines import setup_schedule, run_schedule_permanently_in_background
|
||||
|
||||
def create_app(test_config=None):
|
||||
def create_app(test_config=None, instance_path=None):
|
||||
|
||||
app = Flask(__name__, instance_relative_config=True)
|
||||
app.config.from_mapping(
|
||||
@ -45,9 +45,14 @@ def create_app(test_config=None):
|
||||
else:
|
||||
app.config.from_mapping(test_config)
|
||||
|
||||
if instance_path is not None:
|
||||
app.instance_path = instance_path
|
||||
|
||||
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
|
||||
|
||||
@ -62,7 +67,7 @@ def create_app(test_config=None):
|
||||
app.register_blueprint(user.bp)
|
||||
app.register_blueprint(history.bp)
|
||||
|
||||
logging.basicConfig(filename='brecaltest.log', level=logging.DEBUG, format='%(asctime)s | %(name)s | %(levelname)s | %(message)s')
|
||||
logging.basicConfig(filename='brecal.log', level=logging.DEBUG, format='%(asctime)s | %(name)s | %(levelname)s | %(message)s')
|
||||
local_db.initPool(os.path.dirname(app.instance_path))
|
||||
logging.info('App started')
|
||||
|
||||
|
||||
@ -0,0 +1,15 @@
|
||||
|
||||
|
||||
import json
|
||||
import logging
|
||||
from flask import request
|
||||
|
||||
def verify_if_request_is_json(request):
|
||||
"""
|
||||
when a request contains invalid JSON data, this function raises a 400 error (bad request) and returns an error description.
|
||||
this function avoids less precise 500 Internal Server Error messages.
|
||||
"""
|
||||
if request.is_json:
|
||||
# when invalid json data is posted, a JSONDecodeError will be raised
|
||||
json.loads(request.data)
|
||||
return
|
||||
@ -1,8 +1,10 @@
|
||||
import logging
|
||||
from flask import Blueprint, request
|
||||
from webargs.flaskparser import parser
|
||||
from .. import impl
|
||||
from ..services.auth_guard import auth_guard
|
||||
import json
|
||||
from BreCal.validators.validation_error import create_dynamic_exception_response
|
||||
|
||||
bp = Blueprint('berths', __name__)
|
||||
|
||||
@ -11,8 +13,12 @@ bp = Blueprint('berths', __name__)
|
||||
@auth_guard() # no restriction by role
|
||||
def GetBerths():
|
||||
|
||||
if 'Authorization' in request.headers:
|
||||
token = request.headers.get('Authorization')
|
||||
return impl.berths.GetBerths(token)
|
||||
else:
|
||||
return json.dumps("not authenticated"), 403
|
||||
try:
|
||||
if 'Authorization' in request.headers:
|
||||
token = request.headers.get('Authorization')
|
||||
return impl.berths.GetBerths(token)
|
||||
else:
|
||||
return create_dynamic_exception_response(ex=None, status_code=403, message="not authenticated")
|
||||
|
||||
except Exception as ex:
|
||||
return create_dynamic_exception_response(ex=ex, status_code=400)
|
||||
|
||||
@ -1,21 +1,26 @@
|
||||
import logging
|
||||
from flask import Blueprint, request
|
||||
from .. import impl
|
||||
from ..services.auth_guard import auth_guard
|
||||
import json
|
||||
from BreCal.validators.validation_error import create_dynamic_exception_response
|
||||
|
||||
bp = Blueprint('history', __name__)
|
||||
|
||||
@bp.route('/history', methods=['get'])
|
||||
@auth_guard() # no restriction by role
|
||||
def GetParticipant():
|
||||
def GetHistory():
|
||||
|
||||
if 'Authorization' in request.headers:
|
||||
token = request.headers.get('Authorization')
|
||||
options = {}
|
||||
if not 'shipcall_id' in request.args:
|
||||
return json.dumps("missing parameter"), 400
|
||||
options["shipcall_id"] = request.args.get("shipcall_id")
|
||||
return impl.history.GetHistory(options)
|
||||
else:
|
||||
return json.dumps("not authenticated"), 403
|
||||
try:
|
||||
if 'Authorization' in request.headers:
|
||||
token = request.headers.get('Authorization')
|
||||
options = {}
|
||||
if not 'shipcall_id' in request.args:
|
||||
return create_dynamic_exception_response(ex=None, status_code=400, message="missing parameter: shipcall_id")
|
||||
options["shipcall_id"] = request.args.get("shipcall_id")
|
||||
return impl.history.GetHistory(options)
|
||||
else:
|
||||
return create_dynamic_exception_response(ex=None, status_code=403, message="not authenticated")
|
||||
|
||||
except Exception as ex:
|
||||
return create_dynamic_exception_response(ex=ex, status_code=400)
|
||||
|
||||
@ -3,6 +3,7 @@ from .. import impl
|
||||
from ..services.auth_guard import auth_guard
|
||||
import logging
|
||||
import json
|
||||
from BreCal.validators.validation_error import create_dynamic_exception_response
|
||||
|
||||
bp = Blueprint('notifications', __name__)
|
||||
|
||||
@ -10,10 +11,13 @@ bp = Blueprint('notifications', __name__)
|
||||
@bp.route('/notifications', methods=['get'])
|
||||
@auth_guard() # no restriction by role
|
||||
def GetNotifications():
|
||||
if 'shipcall_id' in request.args:
|
||||
options = {}
|
||||
options["shipcall_id"] = request.args.get("shipcall_id")
|
||||
return impl.notifications.GetNotifications(options)
|
||||
else:
|
||||
logging.warning("attempt to load notifications without shipcall id")
|
||||
return json.dumps("missing argument"), 400
|
||||
try:
|
||||
if 'shipcall_id' in request.args:
|
||||
options = {}
|
||||
options["shipcall_id"] = request.args.get("shipcall_id")
|
||||
return impl.notifications.GetNotifications(options)
|
||||
else:
|
||||
return create_dynamic_exception_response(ex=None, status_code=400, message="missing argument: shipcall_id")
|
||||
|
||||
except Exception as ex:
|
||||
return create_dynamic_exception_response(ex=ex, status_code=400)
|
||||
|
||||
@ -1,7 +1,9 @@
|
||||
import logging
|
||||
from flask import Blueprint, request
|
||||
from .. import impl
|
||||
from ..services.auth_guard import auth_guard
|
||||
import json
|
||||
from BreCal.validators.validation_error import create_dynamic_exception_response
|
||||
|
||||
bp = Blueprint('participants', __name__)
|
||||
|
||||
@ -9,11 +11,14 @@ bp = Blueprint('participants', __name__)
|
||||
@auth_guard() # no restriction by role
|
||||
def GetParticipant():
|
||||
|
||||
if 'Authorization' in request.headers:
|
||||
token = request.headers.get('Authorization')
|
||||
options = {}
|
||||
options["user_id"] = request.args.get("user_id")
|
||||
return impl.participant.GetParticipant(options)
|
||||
else:
|
||||
return json.dumps("not authenticated"), 403
|
||||
try:
|
||||
if 'Authorization' in request.headers:
|
||||
token = request.headers.get('Authorization')
|
||||
options = {}
|
||||
options["user_id"] = request.args.get("user_id")
|
||||
return impl.participant.GetParticipant(options)
|
||||
else:
|
||||
return create_dynamic_exception_response(ex=None, status_code=403, message="not authenticated")
|
||||
|
||||
except Exception as ex:
|
||||
return create_dynamic_exception_response(ex=ex, status_code=400)
|
||||
|
||||
@ -1,27 +1,47 @@
|
||||
from flask import Blueprint, request
|
||||
from webargs.flaskparser import parser
|
||||
from marshmallow import Schema, fields
|
||||
from marshmallow import Schema, fields, ValidationError
|
||||
from ..schemas import model
|
||||
from .. import impl
|
||||
from ..services.auth_guard import auth_guard
|
||||
from ..services.auth_guard import auth_guard, check_jwt
|
||||
from BreCal.validators.input_validation import validate_posted_shipcall_data, check_if_user_is_bsmd_type
|
||||
from BreCal.validators.input_validation_shipcall import InputValidationShipcall
|
||||
from BreCal.database.sql_handler import execute_sql_query_standalone
|
||||
from BreCal.validators.validation_error import create_validation_error_response, create_werkzeug_error_response, create_dynamic_exception_response
|
||||
from . import verify_if_request_is_json
|
||||
|
||||
import logging
|
||||
import json
|
||||
import traceback
|
||||
import werkzeug
|
||||
|
||||
bp = Blueprint('shipcalls', __name__)
|
||||
|
||||
@bp.route('/shipcalls', methods=['get'])
|
||||
@auth_guard() # no restriction by role
|
||||
def GetShipcalls():
|
||||
if 'Authorization' in request.headers:
|
||||
token = request.headers.get('Authorization')
|
||||
options = {}
|
||||
options["participant_id"] = request.args.get("participant_id")
|
||||
options["past_days"] = request.args.get("past_days", default=1, type=int)
|
||||
try:
|
||||
if 'Authorization' in request.headers:
|
||||
token = request.headers.get('Authorization') # see impl/login to see the token encoding, which is a JWT token.
|
||||
|
||||
return impl.shipcalls.GetShipcalls(options)
|
||||
else:
|
||||
return json.dumps("not authenticated"), 403
|
||||
"""
|
||||
from BreCal.services.jwt_handler import decode_jwt
|
||||
jwt = token.split('Bearer ')[1] # string key
|
||||
payload = decode_jwt(jwt) # dictionary, which includes 'id' (user id) and 'participant_id'
|
||||
|
||||
# oneline:
|
||||
payload = decode_jwt(request.headers.get("Authorization").split("Bearer ")[-1])
|
||||
"""
|
||||
options = {}
|
||||
options["participant_id"] = request.args.get("participant_id")
|
||||
options["past_days"] = request.args.get("past_days", default=1, type=int)
|
||||
|
||||
return impl.shipcalls.GetShipcalls(options)
|
||||
else:
|
||||
return create_dynamic_exception_response(ex=None, status_code=403, message="not authenticated")
|
||||
|
||||
except Exception as ex:
|
||||
return create_dynamic_exception_response(ex=ex, status_code=400)
|
||||
|
||||
|
||||
@bp.route('/shipcalls', methods=['post'])
|
||||
@ -29,15 +49,25 @@ def GetShipcalls():
|
||||
def PostShipcalls():
|
||||
|
||||
try:
|
||||
verify_if_request_is_json(request)
|
||||
|
||||
content = request.get_json(force=True)
|
||||
logging.info(content)
|
||||
loadedModel = model.ShipcallSchema().load(data=content, many=False, partial=True)
|
||||
except Exception as ex:
|
||||
logging.error(ex)
|
||||
print(ex)
|
||||
return json.dumps("bad format"), 400
|
||||
|
||||
return impl.shipcalls.PostShipcalls(loadedModel)
|
||||
# read the user data from the JWT token (set when login is performed)
|
||||
user_data = check_jwt()
|
||||
|
||||
# validate the posted shipcall data & the user's authority
|
||||
InputValidationShipcall.evaluate_post_data(user_data, loadedModel, content)
|
||||
return impl.shipcalls.PostShipcalls(loadedModel)
|
||||
|
||||
except ValidationError as ex:
|
||||
return create_validation_error_response(ex=ex, status_code=400)
|
||||
|
||||
except Exception as ex:
|
||||
logging.error(traceback.format_exc())
|
||||
return create_dynamic_exception_response(ex=ex, status_code=400, message="bad format")
|
||||
|
||||
|
||||
@bp.route('/shipcalls', methods=['put'])
|
||||
@ -45,12 +75,25 @@ def PostShipcalls():
|
||||
def PutShipcalls():
|
||||
|
||||
try:
|
||||
verify_if_request_is_json(request)
|
||||
|
||||
content = request.get_json(force=True)
|
||||
logging.info(content)
|
||||
loadedModel = model.ShipcallSchema().load(data=content, many=False, partial=True)
|
||||
except Exception as ex:
|
||||
logging.error(ex)
|
||||
print(ex)
|
||||
return json.dumps("bad format"), 400
|
||||
|
||||
return impl.shipcalls.PutShipcalls(loadedModel)
|
||||
# read the user data from the JWT token (set when login is performed)
|
||||
user_data = check_jwt()
|
||||
|
||||
# validate the PUT shipcall data and the user's authority
|
||||
InputValidationShipcall.evaluate_put_data(user_data, loadedModel, content)
|
||||
return impl.shipcalls.PutShipcalls(loadedModel)
|
||||
|
||||
except ValidationError as ex:
|
||||
return create_validation_error_response(ex=ex, status_code=400)
|
||||
|
||||
except werkzeug.exceptions.Forbidden as ex:
|
||||
return create_werkzeug_error_response(ex=ex, status_code=403)
|
||||
|
||||
except Exception as ex:
|
||||
logging.error(traceback.format_exc())
|
||||
return create_dynamic_exception_response(ex=None, status_code=400, message="bad format")
|
||||
|
||||
|
||||
@ -1,10 +1,15 @@
|
||||
from flask import Blueprint, request
|
||||
from .. import impl
|
||||
from ..services.auth_guard import auth_guard
|
||||
from marshmallow import EXCLUDE
|
||||
from ..services.auth_guard import auth_guard, check_jwt
|
||||
from marshmallow import EXCLUDE, ValidationError
|
||||
from ..schemas import model
|
||||
import json
|
||||
import logging
|
||||
from . import verify_if_request_is_json
|
||||
from BreCal.validators.validation_error import create_validation_error_response, create_dynamic_exception_response
|
||||
|
||||
from BreCal.validators.input_validation import check_if_user_is_bsmd_type
|
||||
from BreCal.validators.input_validation_ship import InputValidationShip
|
||||
|
||||
bp = Blueprint('ships', __name__)
|
||||
|
||||
@ -12,11 +17,15 @@ bp = Blueprint('ships', __name__)
|
||||
@auth_guard() # no restriction by role
|
||||
def GetShips():
|
||||
|
||||
if 'Authorization' in request.headers:
|
||||
token = request.headers.get('Authorization')
|
||||
return impl.ships.GetShips(token)
|
||||
else:
|
||||
return json.dumps("not authenticated"), 403
|
||||
try:
|
||||
if 'Authorization' in request.headers:
|
||||
token = request.headers.get('Authorization')
|
||||
return impl.ships.GetShips(token)
|
||||
else:
|
||||
return create_dynamic_exception_response(ex=None, status_code=403, message="not authenticated")
|
||||
|
||||
except Exception as ex:
|
||||
return create_dynamic_exception_response(ex=ex, status_code=400)
|
||||
|
||||
|
||||
@bp.route('/ships', methods=['post'])
|
||||
@ -24,14 +33,29 @@ def GetShips():
|
||||
def PostShip():
|
||||
|
||||
try:
|
||||
verify_if_request_is_json(request)
|
||||
|
||||
# read the user data from the JWT token (set when login is performed)
|
||||
user_data = check_jwt()
|
||||
|
||||
# check, whether the user belongs to a participant, which is of type ParticipantType.BSMD
|
||||
# as ParticipantType is an IntFlag, a user belonging to multiple groups is properly evaluated.
|
||||
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 shipcalls. Found user data: {user_data}"})
|
||||
|
||||
content = request.get_json(force=True)
|
||||
loadedModel = model.ShipSchema().load(data=content, many=False, partial=True)
|
||||
except Exception as ex:
|
||||
logging.error(ex)
|
||||
print(ex)
|
||||
return json.dumps("bad format"), 400
|
||||
|
||||
return impl.ships.PostShip(loadedModel)
|
||||
# validate the request data & user permissions
|
||||
InputValidationShip.evaluate_post_data(user_data, loadedModel, content)
|
||||
return impl.ships.PostShip(loadedModel)
|
||||
|
||||
except ValidationError as ex:
|
||||
return create_validation_error_response(ex=ex, status_code=400)
|
||||
|
||||
except Exception as ex:
|
||||
return create_dynamic_exception_response(ex=ex, status_code=400, message=None)
|
||||
|
||||
|
||||
@bp.route('/ships', methods=['put'])
|
||||
@ -39,30 +63,49 @@ def PostShip():
|
||||
def PutShip():
|
||||
|
||||
try:
|
||||
verify_if_request_is_json(request)
|
||||
|
||||
# read the user data from the JWT token (set when login is performed)
|
||||
user_data = check_jwt()
|
||||
|
||||
content = request.get_json(force=True)
|
||||
loadedModel = model.ShipSchema().load(data=content, many=False, partial=True, unknown=EXCLUDE)
|
||||
except Exception as ex:
|
||||
logging.error(ex)
|
||||
print(ex)
|
||||
return json.dumps("bad format"), 400
|
||||
|
||||
return impl.ships.PutShip(loadedModel)
|
||||
# validate the request data & user permissions
|
||||
InputValidationShip.evaluate_put_data(user_data, loadedModel, content)
|
||||
return impl.ships.PutShip(loadedModel)
|
||||
|
||||
except ValidationError as ex:
|
||||
return create_validation_error_response(ex=ex, status_code=400)
|
||||
|
||||
except Exception as ex:
|
||||
return create_dynamic_exception_response(ex=ex, status_code=400)
|
||||
|
||||
|
||||
@bp.route('/ships', methods=['delete'])
|
||||
@auth_guard() # no restriction by role
|
||||
def DeleteShip():
|
||||
|
||||
# TODO check if I am allowed to delete this thing by deriving the participant from the bearer token
|
||||
try:
|
||||
verify_if_request_is_json(request)
|
||||
|
||||
# read the user data from the JWT token (set when login is performed)
|
||||
user_data = check_jwt()
|
||||
|
||||
if 'id' in request.args:
|
||||
options = {}
|
||||
options["id"] = request.args.get("id")
|
||||
else:
|
||||
return json.dumps("no id provided"), 400
|
||||
except Exception as ex:
|
||||
logging.error(ex)
|
||||
print(ex)
|
||||
return json.dumps("bad format"), 400
|
||||
return create_dynamic_exception_response(ex=None, status_code=400, message="no id provided")
|
||||
|
||||
# validate the request data & user permissions
|
||||
ship_id = request.args.get("id")
|
||||
InputValidationShip.evaluate_delete_data(user_data, ship_id)
|
||||
return impl.ships.DeleteShip(options)
|
||||
|
||||
except ValidationError as ex:
|
||||
return create_validation_error_response(ex=ex, status_code=400)
|
||||
|
||||
except Exception as ex:
|
||||
return create_dynamic_exception_response(ex=ex, status_code=400)
|
||||
|
||||
return impl.ships.DeleteShip(options)
|
||||
|
||||
@ -1,9 +1,13 @@
|
||||
from flask import Blueprint, request
|
||||
from ..schemas import model
|
||||
from .. import impl
|
||||
from ..services.auth_guard import auth_guard
|
||||
from ..services.auth_guard import auth_guard, check_jwt
|
||||
import json
|
||||
import logging
|
||||
from marshmallow import ValidationError
|
||||
from BreCal.validators.input_validation_times import InputValidationTimes
|
||||
from . import verify_if_request_is_json
|
||||
from BreCal.validators.validation_error import create_validation_error_response, create_werkzeug_error_response, create_dynamic_exception_response
|
||||
|
||||
bp = Blueprint('times', __name__)
|
||||
|
||||
@ -12,10 +16,13 @@ bp = Blueprint('times', __name__)
|
||||
@auth_guard() # no restriction by role
|
||||
def GetTimes():
|
||||
|
||||
options = {}
|
||||
options["shipcall_id"] = request.args.get("shipcall_id")
|
||||
|
||||
return impl.times.GetTimes(options)
|
||||
try:
|
||||
options = {}
|
||||
options["shipcall_id"] = request.args.get("shipcall_id")
|
||||
return impl.times.GetTimes(options)
|
||||
|
||||
except Exception as ex:
|
||||
return create_dynamic_exception_response(ex=ex, status_code=400)
|
||||
|
||||
|
||||
@bp.route('/times', methods=['post'])
|
||||
@ -23,6 +30,8 @@ def GetTimes():
|
||||
def PostTimes():
|
||||
|
||||
try:
|
||||
verify_if_request_is_json(request)
|
||||
|
||||
# print (request.is_json)
|
||||
content = request.get_json(force=True) # force gets us json even if the content-type was wrong
|
||||
|
||||
@ -30,12 +39,19 @@ def PostTimes():
|
||||
# body = parser.parse(schema, request, location='json')
|
||||
loadedModel = model.TimesSchema().load(data=content, many=False, partial=True)
|
||||
|
||||
except Exception as ex:
|
||||
logging.error(ex)
|
||||
print(ex)
|
||||
return json.dumps("bad format"), 400
|
||||
# read the user data from the JWT token (set when login is performed)
|
||||
user_data = check_jwt()
|
||||
|
||||
# validate the request
|
||||
InputValidationTimes.evaluate_post_data(user_data, loadedModel, content)
|
||||
return impl.times.PostTimes(loadedModel)
|
||||
|
||||
except ValidationError as ex:
|
||||
return create_validation_error_response(ex=ex, status_code=400)
|
||||
|
||||
except Exception as ex:
|
||||
return create_dynamic_exception_response(ex=ex, status_code=400, message="bad format")
|
||||
|
||||
return impl.times.PostTimes(loadedModel)
|
||||
|
||||
|
||||
@bp.route('/times', methods=['put'])
|
||||
@ -43,27 +59,47 @@ def PostTimes():
|
||||
def PutTimes():
|
||||
|
||||
try:
|
||||
verify_if_request_is_json(request)
|
||||
|
||||
content = request.get_json(force=True)
|
||||
loadedModel = model.TimesSchema().load(data=content, many=False, partial=True)
|
||||
|
||||
except Exception as ex:
|
||||
logging.error(ex)
|
||||
print(ex)
|
||||
return json.dumps("bad format"), 400
|
||||
# read the user data from the JWT token (set when login is performed)
|
||||
user_data = check_jwt()
|
||||
|
||||
# validate the request
|
||||
InputValidationTimes.evaluate_put_data(user_data, loadedModel, content)
|
||||
return impl.times.PutTimes(loadedModel)
|
||||
|
||||
except ValidationError as ex:
|
||||
return create_validation_error_response(ex=ex, status_code=400)
|
||||
|
||||
except Exception as ex:
|
||||
return create_dynamic_exception_response(ex=ex, status_code=400, message="bad format")
|
||||
|
||||
return impl.times.PutTimes(loadedModel)
|
||||
|
||||
|
||||
@bp.route('/times', methods=['delete'])
|
||||
@auth_guard() # no restriction by role
|
||||
def DeleteTimes():
|
||||
|
||||
# TODO check if I am allowd to delete this thing by deriving the participant from the bearer token
|
||||
try:
|
||||
if 'id' in request.args:
|
||||
options = {}
|
||||
options["id"] = request.args.get("id")
|
||||
|
||||
if 'id' in request.args:
|
||||
options = {}
|
||||
options["id"] = request.args.get("id")
|
||||
return impl.times.DeleteTimes(options)
|
||||
else:
|
||||
logging.warning("Times delete missing id argument")
|
||||
return json.dumps("missing argument"), 400
|
||||
# read the user data from the JWT token (set when login is performed)
|
||||
user_data = check_jwt()
|
||||
|
||||
# validate the request
|
||||
InputValidationTimes.evaluate_delete_data(user_data, times_id = request.args.get("id"))
|
||||
|
||||
return impl.times.DeleteTimes(options)
|
||||
else:
|
||||
return create_dynamic_exception_response(ex=None, status_code=400, message="Times delete missing argument: id")
|
||||
|
||||
except ValidationError as ex:
|
||||
return create_validation_error_response(ex=ex, status_code=400)
|
||||
|
||||
except Exception as ex:
|
||||
return create_dynamic_exception_response(ex=ex, status_code=400, message="bad format")
|
||||
|
||||
@ -4,6 +4,9 @@ from .. import impl
|
||||
from ..services.auth_guard import auth_guard
|
||||
import json
|
||||
import logging
|
||||
from marshmallow import ValidationError
|
||||
from . import verify_if_request_is_json
|
||||
from BreCal.validators.validation_error import create_dynamic_exception_response, create_validation_error_response
|
||||
|
||||
bp = Blueprint('user', __name__)
|
||||
|
||||
@ -12,13 +15,16 @@ bp = Blueprint('user', __name__)
|
||||
def PutUser():
|
||||
|
||||
try:
|
||||
verify_if_request_is_json(request)
|
||||
|
||||
content = request.get_json(force=True)
|
||||
loadedModel = model.UserSchema().load(data=content, many=False, partial=True)
|
||||
return impl.user.PutUser(loadedModel)
|
||||
|
||||
except ValidationError as ex:
|
||||
return create_validation_error_response(ex=ex, status_code=400)
|
||||
|
||||
except Exception as ex:
|
||||
logging.error(ex)
|
||||
print(ex)
|
||||
return json.dumps("bad format"), 400
|
||||
return create_dynamic_exception_response(ex=None, status_code=400, message="bad format")
|
||||
|
||||
return impl.user.PutUser(loadedModel)
|
||||
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
from enum import Enum, IntFlag
|
||||
from enum import IntEnum, Enum, IntFlag
|
||||
|
||||
class ParticipantType(IntFlag):
|
||||
"""determines the type of a participant"""
|
||||
NONE = 0
|
||||
undefined = 0
|
||||
BSMD = 1
|
||||
TERMINAL = 2
|
||||
PILOT = 4
|
||||
@ -11,12 +11,17 @@ class ParticipantType(IntFlag):
|
||||
PORT_ADMINISTRATION = 32
|
||||
TUG = 64
|
||||
|
||||
class ShipcallType(Enum):
|
||||
class ShipcallType(IntEnum):
|
||||
"""determines the type of a shipcall, as this changes the applicable validation rules"""
|
||||
undefined = 0
|
||||
INCOMING = 1
|
||||
OUTGOING = 2
|
||||
SHIFTING = 3
|
||||
|
||||
@classmethod
|
||||
def _missing_(cls, value):
|
||||
return cls.undefined
|
||||
|
||||
class ParticipantwiseTimeDelta():
|
||||
"""stores the time delta for every participant, which triggers the validation rules in the rule set '0001'"""
|
||||
AGENCY = 1200.0 # 20 h * 60 min/h = 1200 min
|
||||
@ -26,7 +31,9 @@ class ParticipantwiseTimeDelta():
|
||||
TUG = 960.0 # 16 h * 60 min/h = 960 min
|
||||
TERMINAL = 960.0 # 16 h * 60 min/h = 960 min
|
||||
|
||||
class StatusFlags(Enum):
|
||||
NOTIFICATION = 10.0 # after n minutes, an evaluation may rise a notification
|
||||
|
||||
class StatusFlags(IntEnum):
|
||||
"""
|
||||
these enumerators ensure that each traffic light validation rule state corresponds to a value, which will be used in the ValidationRules object to identify
|
||||
the necessity of notifications.
|
||||
@ -36,3 +43,23 @@ class StatusFlags(Enum):
|
||||
YELLOW = 2
|
||||
RED = 3
|
||||
|
||||
class PierSide(IntEnum):
|
||||
"""These enumerators determine the pier side of a shipcall."""
|
||||
PORTSIDE = 0 # Port/Backbord
|
||||
STARBOARD_SIDE = 1 # Starboard / Steuerbord
|
||||
|
||||
class NotificationType(IntFlag):
|
||||
"""determines the method by which a notification is distributed to users. Flagging allows selecting multiple notification types."""
|
||||
UNDEFINED = 0
|
||||
EMAIL = 1
|
||||
POPUP = 2
|
||||
MESSENGER = 4
|
||||
|
||||
class ParticipantFlag(IntFlag):
|
||||
"""
|
||||
| 1 | If this flag is set on a shipcall record with participant type Agency (8),
|
||||
all participants of type BSMD (1) may edit the record.
|
||||
"""
|
||||
undefined = 0
|
||||
BSMD = 1
|
||||
|
||||
|
||||
@ -1,12 +1,17 @@
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
import pydapper
|
||||
import datetime
|
||||
import typing
|
||||
from BreCal.schemas.model import Shipcall, Ship, Participant, Berth, User, Times
|
||||
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:
|
||||
"""
|
||||
@ -18,7 +23,112 @@ 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
|
||||
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
|
||||
5.) counting the times, where the timedelta is below the threshold
|
||||
|
||||
returns: counts
|
||||
"""
|
||||
assert (isinstance(query_time,pd.Timestamp)) or (pd.isnull(query_time)), f"expected a timestamp. Found type: {type(query_time)} with value: {query_time}"
|
||||
if pd.isnull(query_time):
|
||||
return 0
|
||||
|
||||
# get a timedelta for each valid (not Null) time entry
|
||||
time_deltas_eta = [(query_time.to_pydatetime()-time_.to_pydatetime()) for time_ in all_df_times.loc[:,"eta_berth"] if not pd.isnull(time_)]
|
||||
time_deltas_etd = [(query_time.to_pydatetime()-time_.to_pydatetime()) for time_ in all_df_times.loc[:,"etd_berth"] if not pd.isnull(time_)]
|
||||
|
||||
# consider both, eta and etd times
|
||||
time_deltas = time_deltas_eta + time_deltas_etd
|
||||
|
||||
# convert the timedelta to absolute total seconds
|
||||
time_deltas = [abs(delta.total_seconds()) for delta in time_deltas]
|
||||
|
||||
# consider only those time deltas, which are <= the determined threshold
|
||||
# create a list of booleans
|
||||
time_deltas_filtered = [delta <= delta_threshold for delta in time_deltas]
|
||||
|
||||
# booleans can be added/counted in Python by using sum()
|
||||
counts = sum(time_deltas_filtered) # int
|
||||
return counts
|
||||
|
||||
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.
|
||||
"""
|
||||
rebuild_pooled_connection = pooledConnection is None
|
||||
|
||||
if rebuild_pooled_connection:
|
||||
pooledConnection = getPoolConnection()
|
||||
|
||||
commands = pydapper.using(pooledConnection)
|
||||
# participant_query = "SELECT participant_id, type FROM shipcall_participant_map WHERE shipcall_id=?shipcall_id?";
|
||||
|
||||
# creates a generator
|
||||
try:
|
||||
if command_type=="query":
|
||||
if model is None:
|
||||
schemas = commands.query(query, model=dict, param=param, buffered=False)
|
||||
else:
|
||||
schemas = commands.query(query, model=model, param=param, buffered=False)
|
||||
|
||||
# creates a list of results from the generator
|
||||
schemas = [schema for schema in schemas]
|
||||
|
||||
elif command_type=="execute":
|
||||
schemas = commands.execute(query, param=param)
|
||||
elif command_type=="single":
|
||||
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)
|
||||
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():
|
||||
@ -36,13 +146,22 @@ class SQLHandler():
|
||||
if read_all:
|
||||
self.read_all(self.all_schemas)
|
||||
|
||||
def execute_sql_query(self, sql_connection, query, param):
|
||||
"""
|
||||
this method is best used in combination with a python context-manager, such as:
|
||||
with mysql.connector.connect(**mysql_connection_data) as sql_connection:
|
||||
schema = sql_handler.execute_sql_query(sql_connection, query)
|
||||
"""
|
||||
schemas = execute_sql_query_standalone(query, param, pooledConnection=sql_connection)
|
||||
return schemas
|
||||
|
||||
def get_all_schemas_from_mysql(self):
|
||||
with self.sql_connection.cursor(buffered=True) as cursor:
|
||||
cursor.execute("SHOW TABLES")
|
||||
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
|
||||
@ -50,7 +169,8 @@ class SQLHandler():
|
||||
'ship'->BreCal.schemas.model.Ship object
|
||||
"""
|
||||
self.str_to_model_dict = {
|
||||
"shipcall":Shipcall, "ship":Ship, "participant":Participant, "berth":Berth, "user":User, "times":Times
|
||||
"shipcall":Shipcall, "ship":Ship, "participant":Participant, "berth":Berth, "user":User, "times":Times,
|
||||
"shipcall_participant_map":ShipcallParticipantMap
|
||||
}
|
||||
return
|
||||
|
||||
@ -61,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()
|
||||
@ -70,9 +190,13 @@ class SQLHandler():
|
||||
data = [{k:v for k,v in zip(column_names, dat)} for dat in data]
|
||||
|
||||
# 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])
|
||||
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
|
||||
@ -94,15 +218,11 @@ class SQLHandler():
|
||||
|
||||
# 4.) build a dataframe from the respective data models (which ensures the correct data type)
|
||||
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])
|
||||
else:
|
||||
df = pd.DataFrame([dat for dat in data])
|
||||
|
||||
df = self.build_df_from_data_and_name(data, table_name)
|
||||
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)
|
||||
@ -122,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.
|
||||
@ -136,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
|
||||
@ -152,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):
|
||||
@ -168,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
|
||||
@ -185,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.
|
||||
@ -203,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
|
||||
@ -221,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]
|
||||
|
||||
@ -240,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.
|
||||
@ -256,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]
|
||||
@ -265,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)
|
||||
|
||||
@ -293,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)
|
||||
"""
|
||||
@ -317,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
|
||||
|
||||
@ -326,12 +446,16 @@ 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!
|
||||
import warnings
|
||||
warnings.warn(f"SQLHandler.get_unique_ship_counts is deprecated. Instead, please use SQLHandler.count_synchronous_shipcall_times")
|
||||
|
||||
# optional: rounding
|
||||
if rounding is not None:
|
||||
all_df_times.loc[:, query] = pd.to_datetime(all_df_times.loc[:, query]).dt.round(rounding) # e.g., 'min' --- # correcting the error: 'AttributeError: Can only use .dt accessor with datetimelike values'
|
||||
@ -347,3 +471,9 @@ class SQLHandler():
|
||||
# get unique entries and counts
|
||||
counts = len(values) # unique, counts = np.unique(values, return_counts=True)
|
||||
return counts # (values, unique, counts)
|
||||
|
||||
def count_synchronous_shipcall_times(self, query_time:pd.Timestamp, all_df_times:pd.DataFrame, delta_threshold=900)->int:
|
||||
"""count all times entries, which are too close to the query_time. The {delta_threshold} determines the threshold. returns counts (int)"""
|
||||
if all_df_times is None:
|
||||
all_df_times = self.df_dict.get("times")
|
||||
return get_synchronous_shipcall_times_standalone(query_time, all_df_times, delta_threshold)
|
||||
|
||||
433
src/server/BreCal/database/sql_queries.py
Normal file
433
src/server/BreCal/database/sql_queries.py
Normal file
@ -0,0 +1,433 @@
|
||||
import logging
|
||||
|
||||
|
||||
def create_sql_query_shipcall_get(options:dict)->str:
|
||||
"""
|
||||
creates an SQL query, which selects all shipcalls from the mysql database.
|
||||
the agency eta times are used to order the entries.
|
||||
|
||||
args:
|
||||
options : dict. A dictionary, which must contains the 'past_days' key (int). Determines the range
|
||||
by which shipcalls are filtered.
|
||||
"""
|
||||
query = ("SELECT s.id as id, ship_id, type, eta, voyage, etd, arrival_berth_id, departure_berth_id, tug_required, pilot_required, " +
|
||||
"flags, s.pier_side, bunkering, replenishing_terminal, replenishing_lock, draft, tidal_window_from, " +
|
||||
"tidal_window_to, rain_sensitive_cargo, recommended_tugs, anchored, moored_lock, canceled, evaluation, " +
|
||||
"evaluation_message, evaluation_time, evaluation_notifications_sent, s.created as created, s.modified as modified, time_ref_point " +
|
||||
"FROM shipcall s " +
|
||||
"LEFT JOIN times t ON t.shipcall_id = s.id AND t.participant_type = 8 " +
|
||||
"WHERE " +
|
||||
"(type = 1 AND " +
|
||||
"((t.id IS NOT NULL AND t.eta_berth >= DATE(NOW() - INTERVAL %d DAY)) OR " +
|
||||
"(eta >= DATE(NOW() - INTERVAL %d DAY)))) OR " +
|
||||
"((type = 2 OR type = 3) AND " +
|
||||
"((t.id IS NOT NULL AND t.etd_berth >= DATE(NOW() - INTERVAL %d DAY)) OR " +
|
||||
"(etd >= DATE(NOW() - INTERVAL %d DAY)))) " +
|
||||
"ORDER BY eta") % (options["past_days"], options["past_days"], options["past_days"], options["past_days"])
|
||||
|
||||
return query
|
||||
|
||||
|
||||
def create_sql_query_shipcall_post(schemaModel:dict)->str:
|
||||
query = "INSERT INTO shipcall ("
|
||||
isNotFirst = False
|
||||
for key in schemaModel.keys():
|
||||
if key == "id":
|
||||
continue
|
||||
if key == "participants":
|
||||
continue
|
||||
if key == "created":
|
||||
continue
|
||||
if key == "modified":
|
||||
continue
|
||||
if key == "evaluation":
|
||||
continue
|
||||
if key == "evaluation_message":
|
||||
continue
|
||||
if key == "type_value":
|
||||
continue
|
||||
if key == "evaluation_value":
|
||||
continue
|
||||
if isNotFirst:
|
||||
query += ","
|
||||
isNotFirst = True
|
||||
query += key
|
||||
query += ") VALUES ("
|
||||
isNotFirst = False
|
||||
for key in schemaModel.keys():
|
||||
param_key = key
|
||||
if key == "id":
|
||||
continue
|
||||
if key == "participants":
|
||||
continue
|
||||
if key == "created":
|
||||
continue
|
||||
if key == "modified":
|
||||
continue
|
||||
if key == "evaluation":
|
||||
continue
|
||||
if key == "evaluation_message":
|
||||
continue
|
||||
if key == "type":
|
||||
param_key = "type_value"
|
||||
if key == "type_value":
|
||||
continue
|
||||
if key == "evaluation":
|
||||
param_key = "evaluation_value"
|
||||
if key == "evaluation_value":
|
||||
continue
|
||||
if isNotFirst:
|
||||
query += ","
|
||||
isNotFirst = True
|
||||
query += "?" + param_key + "?"
|
||||
query += ")"
|
||||
return query
|
||||
|
||||
def create_sql_query_shipcall_put(schemaModel:dict)->str:
|
||||
query = "UPDATE shipcall SET "
|
||||
isNotFirst = False
|
||||
for key in schemaModel.keys():
|
||||
param_key = key
|
||||
if key == "id":
|
||||
continue
|
||||
if key == "participants":
|
||||
continue
|
||||
if key == "created":
|
||||
continue
|
||||
if key == "modified":
|
||||
continue
|
||||
if key == "evaluation":
|
||||
continue
|
||||
if key == "evaluation_message":
|
||||
continue
|
||||
if key == "type":
|
||||
param_key = "type_value"
|
||||
if key == "type_value":
|
||||
continue
|
||||
if key == "evaluation":
|
||||
param_key = "evaluation_value"
|
||||
if key == "evaluation_value":
|
||||
continue
|
||||
if isNotFirst:
|
||||
query += ", "
|
||||
isNotFirst = True
|
||||
query += key + " = ?" + param_key + "? "
|
||||
|
||||
query += "WHERE id = ?id?"
|
||||
return query
|
||||
|
||||
|
||||
def create_sql_query_history_post()->str:
|
||||
query = "INSERT INTO history (participant_id, shipcall_id, user_id, timestamp, eta, type, operation) VALUES (?pid?, ?scid?, ?uid?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, 1, 1)"
|
||||
return query
|
||||
|
||||
def create_sql_query_history_put()->str:
|
||||
query = "INSERT INTO history (participant_id, shipcall_id, user_id, timestamp, eta, type, operation) VALUES (?pid?, ?scid?, ?uid?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, 1, 2)"
|
||||
return query
|
||||
|
||||
def create_sql_query_user_put(schemaModel:dict):
|
||||
query = "UPDATE user SET "
|
||||
isNotFirst = False
|
||||
for key in schemaModel.keys():
|
||||
if key == "id":
|
||||
continue
|
||||
if key == "old_password":
|
||||
continue
|
||||
if key == "new_password":
|
||||
continue
|
||||
if isNotFirst:
|
||||
query += ", "
|
||||
isNotFirst = True
|
||||
query += key + " = ?" + key + "? "
|
||||
|
||||
query += "WHERE id = ?id?"
|
||||
return query
|
||||
|
||||
def create_sql_query_ship_post(schemaModel:dict):
|
||||
query = "INSERT INTO ship ("
|
||||
isNotFirst = False
|
||||
for key in schemaModel.keys():
|
||||
if key == "id":
|
||||
continue
|
||||
if key == "created":
|
||||
continue
|
||||
if key == "modified":
|
||||
continue
|
||||
if isNotFirst:
|
||||
query += ","
|
||||
isNotFirst = True
|
||||
query += key
|
||||
query += ") VALUES ("
|
||||
isNotFirst = False
|
||||
for key in schemaModel.keys():
|
||||
if key == "id":
|
||||
continue
|
||||
if key == "created":
|
||||
continue
|
||||
if key == "modified":
|
||||
continue
|
||||
if isNotFirst:
|
||||
query += ","
|
||||
isNotFirst = True
|
||||
query += "?" + key + "?"
|
||||
query += ")"
|
||||
return query
|
||||
|
||||
def create_sql_query_ship_put(schemaModel:dict):
|
||||
query = "UPDATE ship SET "
|
||||
isNotFirst = False
|
||||
for key in schemaModel.keys():
|
||||
if key == "id":
|
||||
continue
|
||||
if key == "created":
|
||||
continue
|
||||
if key == "modified":
|
||||
continue
|
||||
if isNotFirst:
|
||||
query += ", "
|
||||
isNotFirst = True
|
||||
query += key + " = ?" + key + "? "
|
||||
|
||||
query += "WHERE id = ?id?"
|
||||
return query
|
||||
|
||||
|
||||
|
||||
class SQLQuery():
|
||||
"""
|
||||
This class provides quick access to different SQL query functions, which creates default queries for the BreCal package.
|
||||
Each method is callable without initializing the SQLQuery object.
|
||||
|
||||
Example:
|
||||
SQLQuery.get_berths()
|
||||
|
||||
When the data violates one of the rules, a marshmallow.ValidationError is raised, which details the issues.
|
||||
"""
|
||||
def __init__(self) -> None:
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def get_berth()->str:
|
||||
query = "SELECT id, name, `lock`, owner_id, authority_id, created, modified, deleted FROM berth WHERE deleted = 0 ORDER BY name"
|
||||
return query
|
||||
|
||||
@staticmethod
|
||||
def get_history()->str:
|
||||
query = "SELECT id, participant_id, shipcall_id, timestamp, eta, type, operation FROM history WHERE shipcall_id = ?shipcallid?"
|
||||
return query
|
||||
|
||||
@staticmethod
|
||||
def get_user()->str:
|
||||
query = "SELECT id, participant_id, first_name, last_name, user_name, user_email, user_phone, password_hash, " +\
|
||||
"api_key, notify_email, notify_whatsapp, notify_signal, notify_popup, created, modified FROM user " +\
|
||||
"WHERE user_name = ?username? OR user_email = ?username?"
|
||||
return query
|
||||
|
||||
@staticmethod
|
||||
def get_notifications()->str:
|
||||
query = "SELECT id, shipcall_id, level, type, message, created, modified FROM notification " + \
|
||||
"WHERE shipcall_id = ?scid?"
|
||||
return query
|
||||
|
||||
@staticmethod
|
||||
def get_participant_by_user_id()->str:
|
||||
query = "SELECT p.id as id, p.name as name, p.street as street, p.postal_code as postal_code, p.city as city, p.type as type, p.flags as flags, p.created as created, p.modified as modified, p.deleted as deleted FROM participant p INNER JOIN user u WHERE u.participant_id = p.id and u.id = ?userid?"
|
||||
return query
|
||||
|
||||
@staticmethod
|
||||
def get_participants()->str:
|
||||
query = "SELECT id, name, street, postal_code, city, type, flags, created, modified, deleted FROM participant p ORDER BY p.name"
|
||||
return query
|
||||
|
||||
@staticmethod
|
||||
def get_shipcalls(options:dict={'past_days':3})->str:
|
||||
# a pytest proves this method to be identical to create_sql_query_shipcall_get(options)
|
||||
assert 'past_days' in list(options.keys()), f"there must be a key 'past_days' in the options, which determines, how recent the returned list of shipcalls shall be." # part of a pytest.raises
|
||||
past_days = options['past_days']
|
||||
|
||||
query = ("SELECT s.id as id, ship_id, type, eta, voyage, etd, arrival_berth_id, departure_berth_id, tug_required, pilot_required, " + \
|
||||
"flags, s.pier_side, bunkering, replenishing_terminal, replenishing_lock, draft, tidal_window_from, " + \
|
||||
"tidal_window_to, rain_sensitive_cargo, recommended_tugs, anchored, moored_lock, canceled, evaluation, " + \
|
||||
"evaluation_message, evaluation_time, evaluation_notifications_sent, s.created as created, s.modified as modified, time_ref_point " + \
|
||||
"FROM shipcall s " + \
|
||||
"LEFT JOIN times t ON t.shipcall_id = s.id AND t.participant_type = 8 " + \
|
||||
"WHERE " + \
|
||||
"(type = 1 AND " + \
|
||||
f"((t.id IS NOT NULL AND t.eta_berth >= DATE(NOW() - INTERVAL {past_days} DAY)) OR " + \
|
||||
f"(eta >= DATE(NOW() - INTERVAL {past_days} DAY)))) OR " + \
|
||||
"((type = 2 OR type = 3) AND " + \
|
||||
f"((t.id IS NOT NULL AND t.etd_berth >= DATE(NOW() - INTERVAL {past_days} DAY)) OR " + \
|
||||
f"(etd >= DATE(NOW() - INTERVAL {past_days} DAY)))) " + \
|
||||
"ORDER BY eta")
|
||||
return query
|
||||
|
||||
@staticmethod
|
||||
def get_ships()->str:
|
||||
query = "SELECT id, name, imo, callsign, participant_id, length, width, is_tug, bollard_pull, eni, created, modified, deleted FROM ship ORDER BY name"
|
||||
return query
|
||||
|
||||
@staticmethod
|
||||
def get_ship_by_id()->str:
|
||||
query = "SELECT * FROM ship where id = ?id?"
|
||||
return query
|
||||
|
||||
@staticmethod
|
||||
def get_times()->str:
|
||||
query = "SELECT id, eta_berth, eta_berth_fixed, etd_berth, etd_berth_fixed, lock_time, lock_time_fixed, " + \
|
||||
"zone_entry, zone_entry_fixed, operations_start, operations_end, remarks, shipcall_id, participant_id, " + \
|
||||
"berth_id, berth_info, pier_side, participant_type, created, modified, ata, atd, eta_interval_end, etd_interval_end FROM times " + \
|
||||
"WHERE times.shipcall_id = ?scid?"
|
||||
return query
|
||||
|
||||
@staticmethod
|
||||
def get_user_by_id():
|
||||
query = "SELECT * FROM user where id = ?id?"
|
||||
return query
|
||||
|
||||
@staticmethod
|
||||
def get_user_put(schemaModel:dict):
|
||||
# a pytest proves this method to be identical to create_sql_query_user_put(schemaModel)
|
||||
prefix = "UPDATE user SET "
|
||||
suffix = "WHERE id = ?id?"
|
||||
center = [f"{key} = ?{key}? " for key in schemaModel.keys() if key not in ["id", "old_password", "new_password"]]
|
||||
|
||||
query = prefix + ", ".join(center) + suffix
|
||||
return query
|
||||
|
||||
@staticmethod
|
||||
def get_update_user_password()->str:
|
||||
query = "UPDATE user SET password_hash = ?password_hash? WHERE id = ?id?"
|
||||
return query
|
||||
|
||||
@staticmethod
|
||||
def get_participants()->str:
|
||||
query = "SELECT participant_id, type FROM shipcall_participant_map WHERE shipcall_id=?shipcall_id?"
|
||||
return query
|
||||
|
||||
@staticmethod
|
||||
def get_participant_from_id()->str:
|
||||
query = "SELECT id, type, flags FROM participant WHERE id=?participant_id?"
|
||||
return query
|
||||
|
||||
@staticmethod
|
||||
def get_shipcall_post(schemaModel:dict)->str:
|
||||
# a pytest proves this method to be identical to create_sql_query_shipcall_post(schemaModel)
|
||||
|
||||
param_keys = {key:key for key in schemaModel.keys()}
|
||||
param_keys["type"] = "type_value"
|
||||
param_keys["evaluation"] = "evaluation_value"
|
||||
|
||||
prefix = "INSERT INTO shipcall ("
|
||||
bridge = ") VALUES ("
|
||||
suffix = ")"
|
||||
|
||||
stage1 = ",".join([key for key in schemaModel.keys() if not key in ["id","participants","created","modified","evaluation","evaluation_message","type_value","evaluation_value"]])
|
||||
stage2 = ",".join([f"?{param_keys.get(key)}?" for key in schemaModel.keys() if not key in ["id","participants","created","modified","evaluation","evaluation_message","type_value","evaluation_value"]])
|
||||
|
||||
query = prefix+stage1+bridge+stage2+suffix
|
||||
return query
|
||||
|
||||
@staticmethod
|
||||
def get_last_insert_id()->str:
|
||||
query = "select last_insert_id()"
|
||||
return query
|
||||
|
||||
@staticmethod
|
||||
def get_shipcall_post_last_insert_id()->str:
|
||||
"""alias function. May be deleted soon"""
|
||||
query = SQLQuery.get_last_insert_id()
|
||||
return query
|
||||
|
||||
@staticmethod
|
||||
def get_shipcall_post_update_shipcall_participant_map()->str:
|
||||
query = "INSERT INTO shipcall_participant_map (shipcall_id, participant_id, type) VALUES (?shipcall_id?, ?participant_id?, ?type?)"
|
||||
return query
|
||||
|
||||
@staticmethod
|
||||
def create_sql_query_history_post()->str:
|
||||
query = create_sql_query_history_post()
|
||||
return query
|
||||
|
||||
@staticmethod
|
||||
def get_shipcall_by_id()->str:
|
||||
query = "SELECT * FROM shipcall where id = ?id?"
|
||||
return query
|
||||
|
||||
@staticmethod
|
||||
def get_shipcall_put(schemaModel:dict)->str:
|
||||
# a pytest proves this method to be identical to create_sql_query_shipcall_put(schemaModel)
|
||||
param_keys = {key:key for key in schemaModel.keys()}
|
||||
param_keys["type"] = "type_value"
|
||||
param_keys["evaluation"] = "evaluation_value"
|
||||
|
||||
prefix = "UPDATE shipcall SET "
|
||||
suffix = "WHERE id = ?id?"
|
||||
body = ", ".join([f"{key} = ?{param_keys.get(key)}? " for key in schemaModel.keys() if key not in ["id", "participants", "created", "modified", "evaluation", "evaluation_message", "type_value", "evaluation_value"]])
|
||||
|
||||
query = prefix + body + suffix
|
||||
return query
|
||||
|
||||
@staticmethod
|
||||
def get_shipcall_participant_map_by_shipcall_id()->str:
|
||||
query = "SELECT id, participant_id, type FROM shipcall_participant_map where shipcall_id = ?id?"
|
||||
return query
|
||||
|
||||
@staticmethod
|
||||
def get_shipcall_participant_map_by_shipcall_id_and_type()->str:
|
||||
query = "SELECT id, participant_id FROM shipcall_participant_map where (shipcall_id = ?id? AND type=?type?)"
|
||||
return query
|
||||
|
||||
@staticmethod
|
||||
def get_shipcall_participant_map_delete_by_id()->str:
|
||||
query = "DELETE FROM shipcall_participant_map WHERE id = ?existing_id?"
|
||||
return query
|
||||
|
||||
@staticmethod
|
||||
def create_sql_query_history_put()->str:
|
||||
query = create_sql_query_history_put()
|
||||
return query
|
||||
|
||||
@staticmethod
|
||||
def get_ship_post(schemaModel:dict)->str:
|
||||
# a pytest proves this method to be identical to create_sql_query_ship_post(schemaModel)
|
||||
prefix = "INSERT INTO ship ("
|
||||
suffix = ")"
|
||||
bridge = ") VALUES ("
|
||||
|
||||
stage1 = ",".join([key for key in schemaModel.keys() if not key in ["id", "created", "modified"]])
|
||||
stage2 = ",".join([f"?{key}?" for key in schemaModel.keys() if not key in ["id", "created", "modified"]])
|
||||
|
||||
query = prefix + stage1 + bridge + stage2 + suffix
|
||||
return query
|
||||
|
||||
@staticmethod
|
||||
def get_ship_put(schemaModel:dict)->str:
|
||||
# a pytest proves this method to be identical to create_sql_query_ship_put(schemaModel)
|
||||
prefix = "UPDATE ship SET "
|
||||
suffix = "WHERE id = ?id?"
|
||||
body = ", ".join([f"{key} = ?{key}? " for key in schemaModel.keys() if not key in ["id","created","modified"]])
|
||||
|
||||
query = prefix + body + suffix
|
||||
return query
|
||||
|
||||
@staticmethod
|
||||
def get_ship_delete_by_id()->str:
|
||||
query = "UPDATE ship SET deleted = 1 WHERE id = ?id?"
|
||||
return query
|
||||
|
||||
@staticmethod
|
||||
def get_notification_post()->str:
|
||||
raise NotImplementedError()
|
||||
# #TODO: this query is wrong and just a proxy for a POST request
|
||||
query = "INSERT INTO shipcall_participant_map (shipcall_id, participant_id, type) VALUES (?shipcall_id?, ?participant_id?, ?type?)"
|
||||
return query
|
||||
|
||||
@staticmethod
|
||||
def get_shipcall_put_notification_state()->str:
|
||||
raise NotImplementedError()
|
||||
# #TODO: use evaluation_notifications_sent here and consider only the shipcall_id
|
||||
# #TODO: query
|
||||
query = ...
|
||||
return query
|
||||
|
||||
|
||||
22
src/server/BreCal/database/sql_utils.py
Normal file
22
src/server/BreCal/database/sql_utils.py
Normal file
@ -0,0 +1,22 @@
|
||||
from BreCal.database.sql_handler import execute_sql_query_standalone
|
||||
import datetime
|
||||
|
||||
def get_user_data_for_id(user_id:int, expiration_time:int=90):
|
||||
"""debugging function, which is useful to pull user_data from the database, which may be used to create stub data and unit tests"""
|
||||
query = "SELECT * FROM user where id = ?id?"
|
||||
pdata = execute_sql_query_standalone(query=query, param={"id":user_id})
|
||||
pdata = pdata[0] if len(pdata)>0 else None
|
||||
assert pdata is not None, f"could not find user with id {user_id}"
|
||||
|
||||
user_data = {k:v for k,v in pdata.items() if k in ['id','participant_id','first_name','last_name','user_name','user_phone','user_email']}
|
||||
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
|
||||
@ -34,6 +34,7 @@ def update_shipcall_in_mysql_database(sql_connection, shipcall:Shipcall, relevan
|
||||
|
||||
def build_mysql_query_to_update_shipcall(shipcall, relevant_keys:list):
|
||||
"""builds a mysql query, which updates the shipcall table. In particular, the provided shipcall will be updated for each key in {relevant_keys}"""
|
||||
# #TODO: refactor into SQLQuery
|
||||
schemaModel = shipcall.__dict__
|
||||
|
||||
# prepare prefix and suffix. Then build the body of the query
|
||||
@ -68,7 +69,6 @@ def evaluate_shipcall_state(mysql_connector_instance, shipcall_id:int=None, debu
|
||||
with mysql.connector.connect(**mysql_connection_data) as mysql_connector_instance:
|
||||
evaluate_shipcall_state(mysql_connector_instance)
|
||||
returns None
|
||||
|
||||
"""
|
||||
sql_handler = SQLHandler(sql_connection=mysql_connector_instance, read_all=True)
|
||||
vr = ValidationRules(sql_handler)
|
||||
|
||||
@ -4,6 +4,7 @@ import pydapper
|
||||
|
||||
from ..schemas import model
|
||||
from .. import local_db
|
||||
from BreCal.database.sql_queries import SQLQuery
|
||||
|
||||
def GetBerths(token):
|
||||
"""
|
||||
@ -13,6 +14,8 @@ def GetBerths(token):
|
||||
try:
|
||||
pooledConnection = local_db.getPoolConnection()
|
||||
commands = pydapper.using(pooledConnection)
|
||||
# query = SQLQuery.get_berth()
|
||||
# data = commands.query(query, model=model.Berth)
|
||||
data = commands.query("SELECT id, name, `lock`, owner_id, authority_id, created, modified, deleted FROM berth WHERE deleted = 0 ORDER BY name", model=model.Berth)
|
||||
return json.dumps(data, default=model.obj_dict), 200, {'Content-Type': 'application/json; charset=utf-8'}
|
||||
|
||||
|
||||
@ -7,6 +7,7 @@ from ..schemas import model
|
||||
from ..schemas.model import History
|
||||
|
||||
from .. import local_db
|
||||
from BreCal.database.sql_queries import SQLQuery
|
||||
|
||||
def GetHistory(options):
|
||||
|
||||
@ -20,6 +21,8 @@ def GetHistory(options):
|
||||
commands = pydapper.using(pooledConnection)
|
||||
|
||||
if "shipcall_id" in options and options["shipcall_id"]:
|
||||
# query = SQLQuery.get_history()
|
||||
# data = commands.query(query, model=History.from_query_row, param={"shipcallid" : options["shipcall_id"]})
|
||||
data = commands.query("SELECT id, participant_id, shipcall_id, timestamp, eta, type, operation FROM history WHERE shipcall_id = ?shipcallid?",
|
||||
model=History.from_query_row,
|
||||
param={"shipcallid" : options["shipcall_id"]})
|
||||
|
||||
@ -6,6 +6,7 @@ import bcrypt
|
||||
from ..schemas import model
|
||||
from .. import local_db
|
||||
from ..services import jwt_handler
|
||||
from BreCal.database.sql_queries import SQLQuery
|
||||
|
||||
def GetUser(options):
|
||||
|
||||
@ -14,11 +15,13 @@ def GetUser(options):
|
||||
hash = bcrypt.hashpw(options["password"].encode('utf-8'), bcrypt.gensalt( 12 )).decode('utf8')
|
||||
pooledConnection = local_db.getPoolConnection()
|
||||
commands = pydapper.using(pooledConnection)
|
||||
# query = SQLQuery.get_user()
|
||||
# data = commands.query(query, model=model.User, param={"username" : options["username"]})
|
||||
data = commands.query("SELECT id, participant_id, first_name, last_name, user_name, user_email, user_phone, password_hash, " +
|
||||
"api_key, notify_email, notify_whatsapp, notify_signal, notify_popup, created, modified FROM user " +
|
||||
"WHERE user_name = ?username? OR user_email = ?username?",
|
||||
model=model.User, param={"username" : options["username"]})
|
||||
# print(data)
|
||||
|
||||
if len(data) == 1:
|
||||
if bcrypt.checkpw(options["password"].encode("utf-8"), bytes(data[0].password_hash, "utf-8")):
|
||||
result = {
|
||||
|
||||
@ -4,6 +4,7 @@ import pydapper
|
||||
|
||||
from ..schemas import model
|
||||
from .. import local_db
|
||||
from BreCal.database.sql_queries import SQLQuery
|
||||
|
||||
def GetNotifications(options):
|
||||
"""
|
||||
@ -16,6 +17,8 @@ def GetNotifications(options):
|
||||
|
||||
pooledConnection = local_db.getPoolConnection()
|
||||
commands = pydapper.using(pooledConnection)
|
||||
# query = SQLQuery.get_notifications()
|
||||
# data = commands.query(query, model=model.Notification.from_query_row, param={"scid" : options["shipcall_id"]})
|
||||
data = commands.query("SELECT id, shipcall_id, level, type, message, created, modified FROM notification " +
|
||||
"WHERE shipcall_id = ?scid?", model=model.Notification.from_query_row, param={"scid" : options["shipcall_id"]})
|
||||
pooledConnection.close()
|
||||
|
||||
@ -4,6 +4,7 @@ import pydapper
|
||||
|
||||
from ..schemas import model
|
||||
from .. import local_db
|
||||
from BreCal.database.sql_queries import SQLQuery
|
||||
|
||||
def GetParticipant(options):
|
||||
"""
|
||||
@ -16,8 +17,10 @@ def GetParticipant(options):
|
||||
pooledConnection = local_db.getPoolConnection()
|
||||
commands = pydapper.using(pooledConnection)
|
||||
if "user_id" in options and options["user_id"]:
|
||||
# query = SQLQuery.get_participant_by_user_id()
|
||||
data = commands.query("SELECT p.id as id, p.name as name, p.street as street, p.postal_code as postal_code, p.city as city, p.type as type, p.flags as flags, p.created as created, p.modified as modified, p.deleted as deleted FROM participant p INNER JOIN user u WHERE u.participant_id = p.id and u.id = ?userid?", model=model.Participant, param={"userid" : options["user_id"]})
|
||||
else:
|
||||
# query = SQLQuery.get_participants()
|
||||
data = commands.query("SELECT id, name, street, postal_code, city, type, flags, created, modified, deleted FROM participant p ORDER BY p.name", model=model.Participant)
|
||||
|
||||
return json.dumps(data, default=model.obj_dict), 200, {'Content-Type': 'application/json; charset=utf-8'}
|
||||
|
||||
@ -8,6 +8,9 @@ from .. import local_db
|
||||
from ..services.auth_guard import check_jwt
|
||||
|
||||
from BreCal.database.update_database import evaluate_shipcall_state
|
||||
from BreCal.database.sql_queries import create_sql_query_shipcall_get, create_sql_query_shipcall_post, create_sql_query_shipcall_put, create_sql_query_history_post, create_sql_query_history_put, SQLQuery
|
||||
from marshmallow import Schema, fields, ValidationError
|
||||
from BreCal.validators.validation_error import create_validation_error_response
|
||||
|
||||
def GetShipcalls(options):
|
||||
"""
|
||||
@ -18,19 +21,14 @@ def GetShipcalls(options):
|
||||
|
||||
pooledConnection = local_db.getPoolConnection()
|
||||
commands = pydapper.using(pooledConnection)
|
||||
query = ("SELECT s.id as id, ship_id, type, eta, voyage, etd, arrival_berth_id, departure_berth_id, tug_required, pilot_required, " +
|
||||
"flags, s.pier_side, bunkering, replenishing_terminal, replenishing_lock, draft, tidal_window_from, " +
|
||||
"tidal_window_to, rain_sensitive_cargo, recommended_tugs, anchored, moored_lock, canceled, evaluation, " +
|
||||
"evaluation_message, evaluation_time, evaluation_notifications_sent, s.created as created, s.modified as modified, time_ref_point " +
|
||||
"FROM shipcall s " +
|
||||
"LEFT JOIN times t ON t.shipcall_id = s.id AND t.participant_type = 8 " +
|
||||
"WHERE " +
|
||||
"(type = 1 AND (COALESCE(t.eta_berth, eta) >= DATE(NOW() - INTERVAL %d DAY))) OR " +
|
||||
"((type = 2 OR type = 3) AND (COALESCE(t.etd_berth, etd) >= DATE(NOW() - INTERVAL %d DAY)))" +
|
||||
"ORDER BY s.id") % (options["past_days"], options["past_days"])
|
||||
# query = SQLQuery.get_shipcalls(options)
|
||||
query = create_sql_query_shipcall_get(options)
|
||||
|
||||
data = commands.query(query, model=model.Shipcall.from_query_row, buffered=True)
|
||||
for shipcall in data:
|
||||
# participant_query = SQLQuery.get_participants()
|
||||
# participants = commands.query(participant_query, model=dict, param={"shipcall_id" : shipcall.id}, buffered=False)
|
||||
# for record in participants:
|
||||
participant_query = "SELECT participant_id, type FROM shipcall_participant_map WHERE shipcall_id=?shipcall_id?";
|
||||
for record in commands.query(participant_query, model=dict, param={"shipcall_id" : shipcall.id}, buffered=False):
|
||||
# model.Participant_Assignment = model.Participant_Assignment()
|
||||
@ -54,18 +52,29 @@ def GetShipcalls(options):
|
||||
|
||||
def PostShipcalls(schemaModel):
|
||||
"""
|
||||
This function *executes* a post-request for shipcalls. The function is accessible as part of an API route.
|
||||
|
||||
The common sequence is:
|
||||
a) issue a request to the Flask API
|
||||
b) BreCal.api.shipcalls.PostShipcalls, to verify the incoming request (which includes an authentification guard)
|
||||
c) BreCal.impl.shipcalls.PostShipcalls, to execute the incoming request
|
||||
|
||||
:param schemaModel: The deserialized dict of the request
|
||||
e.g.,
|
||||
{
|
||||
'ship_id': 1, 'type': 1, 'eta': datetime.datetime(2023, 7, 23, 7, 18, 19),
|
||||
'voyage': '43B', 'tug_required': False, 'pilot_required': True, 'flags': 0,
|
||||
'pier_side': False, 'bunkering': True, 'recommended_tugs': 2, 'type_value': 1, 'evaluation_value': 0}
|
||||
}
|
||||
"""
|
||||
|
||||
# TODO: Validate the upload data
|
||||
|
||||
# This creates a *new* entry
|
||||
try:
|
||||
|
||||
pooledConnection = local_db.getPoolConnection()
|
||||
commands = pydapper.using(pooledConnection)
|
||||
|
||||
# query = SQLQuery.get_shipcall_post(schemaModel) # create_sql_query_shipcall_post(schemaModel)
|
||||
query = "INSERT INTO shipcall ("
|
||||
isNotFirst = False
|
||||
for key in schemaModel.keys():
|
||||
@ -118,13 +127,15 @@ def PostShipcalls(schemaModel):
|
||||
isNotFirst = True
|
||||
query += "?" + param_key + "?"
|
||||
query += ")"
|
||||
|
||||
logging.info(query)
|
||||
commands.execute(query, schemaModel)
|
||||
|
||||
# lquery = SQLQuery.get_shipcall_post_last_insert_id()
|
||||
# new_id = commands.execute_scalar(lquery)
|
||||
new_id = commands.execute_scalar("select last_insert_id()")
|
||||
|
||||
# add participant assignments if we have a list of participants
|
||||
if 'participants' in schemaModel:
|
||||
# pquery = SQLQuery.get_shipcall_post_update_shipcall_participant_map()
|
||||
pquery = "INSERT INTO shipcall_participant_map (shipcall_id, participant_id, type) VALUES (?shipcall_id?, ?participant_id?, ?type?)"
|
||||
for participant_assignment in schemaModel["participants"]:
|
||||
commands.execute(pquery, param={"shipcall_id" : new_id, "participant_id" : participant_assignment["participant_id"], "type" : participant_assignment["type"]})
|
||||
@ -135,10 +146,14 @@ def PostShipcalls(schemaModel):
|
||||
# save history data
|
||||
# TODO: set ETA properly
|
||||
user_data = check_jwt()
|
||||
# query = SQLQuery.create_sql_query_history_post()
|
||||
query = "INSERT INTO history (participant_id, shipcall_id, user_id, timestamp, eta, type, operation) VALUES (?pid?, ?scid?, ?uid?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, 1, 1)"
|
||||
commands.execute(query, {"scid" : new_id, "pid" : user_data["participant_id"], "uid" : user_data["id"]})
|
||||
|
||||
return json.dumps({"id" : new_id}), 201, {'Content-Type': 'application/json; charset=utf-8'}
|
||||
|
||||
except ValidationError as ex:
|
||||
return create_validation_error_response(ex, status_code=400, create_log=True)
|
||||
|
||||
except Exception as ex:
|
||||
logging.error(traceback.format_exc())
|
||||
@ -165,14 +180,19 @@ def PutShipcalls(schemaModel):
|
||||
pooledConnection = local_db.getPoolConnection()
|
||||
commands = pydapper.using(pooledConnection)
|
||||
|
||||
user_data = check_jwt()
|
||||
|
||||
# test if object to update is found
|
||||
|
||||
sentinel = object()
|
||||
# query = SQLQuery.get_shipcall_by_id()
|
||||
# theshipcall = commands.query_single_or_default(query, sentinel, param={"id" : schemaModel["id"]})
|
||||
theshipcall = commands.query_single_or_default("SELECT * FROM shipcall where id = ?id?", sentinel, param={"id" : schemaModel["id"]})
|
||||
if theshipcall is sentinel:
|
||||
pooledConnection.close()
|
||||
return json.dumps("no such record"), 404, {'Content-Type': 'application/json; charset=utf-8'}
|
||||
|
||||
# query = SQLQuery.get_shipcall_put(schemaModel)
|
||||
query = "UPDATE shipcall SET "
|
||||
isNotFirst = False
|
||||
for key in schemaModel.keys():
|
||||
@ -203,8 +223,10 @@ def PutShipcalls(schemaModel):
|
||||
query += key + " = ?" + param_key + "? "
|
||||
|
||||
query += "WHERE id = ?id?"
|
||||
|
||||
affected_rows = commands.execute(query, param=schemaModel)
|
||||
|
||||
# pquery = SQLQuery.get_shipcall_participant_map_by_shipcall_id()
|
||||
pquery = "SELECT id, participant_id, type FROM shipcall_participant_map where shipcall_id = ?id?"
|
||||
pdata = commands.query(pquery,param={"id" : schemaModel["id"]}) # existing list of assignments
|
||||
|
||||
@ -217,6 +239,7 @@ def PutShipcalls(schemaModel):
|
||||
found_participant = True
|
||||
break
|
||||
if not found_participant:
|
||||
# nquery = SQLQuery.get_shipcall_post_update_shipcall_participant_map()
|
||||
nquery = "INSERT INTO shipcall_participant_map (shipcall_id, participant_id, type) VALUES (?shipcall_id?, ?participant_id?, ?type?)"
|
||||
commands.execute(nquery, param={"shipcall_id" : schemaModel["id"], "participant_id" : participant_assignment["participant_id"], "type" : participant_assignment["type"]})
|
||||
|
||||
@ -228,19 +251,20 @@ def PutShipcalls(schemaModel):
|
||||
found_participant = True
|
||||
break;
|
||||
if not found_participant:
|
||||
# dquery = SQLQuery.get_shipcall_participant_map_delete_by_id()
|
||||
dquery = "DELETE FROM shipcall_participant_map WHERE id = ?existing_id?"
|
||||
commands.execute(dquery, param={"existing_id" : elem["id"]})
|
||||
|
||||
# apply 'Traffic Light' evaluation to obtain 'GREEN', 'YELLOW' or 'RED' evaluation state. The function internally updates the mysql database
|
||||
# evaluate_shipcall_state(mysql_connector_instance=pooledConnection, shipcall_id=schemaModel["id"]) # schemaModel["id"] refers to the shipcall id
|
||||
|
||||
# save history data
|
||||
# TODO: set ETA properly
|
||||
user_data = check_jwt()
|
||||
# query = SQLQuery.create_sql_query_history_put()
|
||||
query = "INSERT INTO history (participant_id, shipcall_id, user_id, timestamp, eta, type, operation) VALUES (?pid?, ?scid?, ?uid?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, 1, 2)"
|
||||
commands.execute(query, {"scid" : schemaModel["id"], "pid" : user_data["participant_id"], "uid" : user_data["id"]})
|
||||
|
||||
return json.dumps({"id" : schemaModel["id"]}), 200
|
||||
|
||||
except ValidationError as ex:
|
||||
return create_validation_error_response(ex, status_code=400, create_log=True)
|
||||
|
||||
except Exception as ex:
|
||||
logging.error(traceback.format_exc())
|
||||
|
||||
@ -4,6 +4,7 @@ import pydapper
|
||||
|
||||
from ..schemas import model
|
||||
from .. import local_db
|
||||
from BreCal.database.sql_queries import SQLQuery
|
||||
|
||||
def GetShips(token):
|
||||
"""
|
||||
@ -14,6 +15,8 @@ def GetShips(token):
|
||||
|
||||
pooledConnection = local_db.getPoolConnection()
|
||||
commands = pydapper.using(pooledConnection)
|
||||
# query = SQLQuery.get_ships()
|
||||
# data = commands.query(query, model=model.Ship)
|
||||
data = commands.query("SELECT id, name, imo, callsign, participant_id, length, width, is_tug, bollard_pull, eni, created, modified, deleted FROM ship ORDER BY name", model=model.Ship)
|
||||
|
||||
return json.dumps(data, default=model.obj_dict), 200, {'Content-Type': 'application/json; charset=utf-8'}
|
||||
@ -46,6 +49,7 @@ def PostShip(schemaModel):
|
||||
pooledConnection = local_db.getPoolConnection()
|
||||
commands = pydapper.using(pooledConnection)
|
||||
|
||||
# query = SQLQuery.create_sql_query_ship_post(schemaModel)
|
||||
query = "INSERT INTO ship ("
|
||||
isNotFirst = False
|
||||
for key in schemaModel.keys():
|
||||
@ -75,6 +79,8 @@ def PostShip(schemaModel):
|
||||
query += ")"
|
||||
|
||||
commands.execute(query, schemaModel)
|
||||
# nquery = SQLQuery.get_last_insert_id()
|
||||
# new_id = commands.execute_scalar(nquery)
|
||||
new_id = commands.execute_scalar("select last_insert_id()")
|
||||
|
||||
pooledConnection.close()
|
||||
@ -100,6 +106,7 @@ def PutShip(schemaModel):
|
||||
pooledConnection = local_db.getPoolConnection()
|
||||
commands = pydapper.using(pooledConnection)
|
||||
|
||||
# query = SQLQuery.create_sql_query_ship_put(schemaModel)
|
||||
query = "UPDATE ship SET "
|
||||
isNotFirst = False
|
||||
for key in schemaModel.keys():
|
||||
@ -140,6 +147,8 @@ def DeleteShip(options):
|
||||
|
||||
pooledConnection = local_db.getPoolConnection()
|
||||
commands = pydapper.using(pooledConnection)
|
||||
# query = SQLQuery.get_ship_delete_by_id()
|
||||
# affected_rows = commands.execute(query, param={"id" : options["id"]})
|
||||
affected_rows = commands.execute("UPDATE ship SET deleted = 1 WHERE id = ?id?", param={"id" : options["id"]})
|
||||
|
||||
pooledConnection.close()
|
||||
|
||||
@ -2,10 +2,12 @@ import json
|
||||
import logging
|
||||
import traceback
|
||||
import pydapper
|
||||
from enum import Enum, Flag
|
||||
|
||||
from ..schemas import model
|
||||
from .. import local_db
|
||||
from ..services.auth_guard import check_jwt
|
||||
from BreCal.database.sql_queries import SQLQuery
|
||||
|
||||
from BreCal.database.update_database import evaluate_shipcall_state
|
||||
|
||||
@ -20,6 +22,8 @@ def GetTimes(options):
|
||||
|
||||
pooledConnection = local_db.getPoolConnection()
|
||||
commands = pydapper.using(pooledConnection)
|
||||
# query = SQLQuery.get_times()
|
||||
# data = commands.query(query, model=model.Times, param={"scid" : options["shipcall_id"]})
|
||||
data = commands.query("SELECT id, eta_berth, eta_berth_fixed, etd_berth, etd_berth_fixed, lock_time, lock_time_fixed, " +
|
||||
"zone_entry, zone_entry_fixed, operations_start, operations_end, remarks, shipcall_id, participant_id, " +
|
||||
"berth_id, berth_info, pier_side, participant_type, created, modified, ata, atd, eta_interval_end, etd_interval_end FROM times " +
|
||||
@ -80,6 +84,7 @@ def PostTimes(schemaModel):
|
||||
query += "?" + key + "?"
|
||||
query += ")"
|
||||
|
||||
schemaModel = {k:v.value if isinstance(v, (Enum, Flag)) else v for k,v in schemaModel.items()}
|
||||
commands.execute(query, schemaModel)
|
||||
new_id = commands.execute_scalar("select last_insert_id()")
|
||||
|
||||
@ -135,6 +140,7 @@ def PutTimes(schemaModel):
|
||||
|
||||
query += "WHERE id = ?id?"
|
||||
|
||||
schemaModel = {k:v.value if isinstance(v, (Enum, Flag)) else v for k,v in schemaModel.items()}
|
||||
affected_rows = commands.execute(query, param=schemaModel)
|
||||
|
||||
# apply 'Traffic Light' evaluation to obtain 'GREEN', 'YELLOW' or 'RED' evaluation state. The function internally updates the mysql database 'shipcall'
|
||||
|
||||
@ -5,6 +5,7 @@ import bcrypt
|
||||
|
||||
from ..schemas import model
|
||||
from .. import local_db
|
||||
from BreCal.database.sql_queries import SQLQuery, create_sql_query_user_put
|
||||
|
||||
def PutUser(schemaModel):
|
||||
"""
|
||||
@ -21,13 +22,21 @@ def PutUser(schemaModel):
|
||||
# test if object to update is found
|
||||
|
||||
sentinel = object()
|
||||
# query = SQLQuery.get_user_by_id()
|
||||
# theuser = commands.query_single_or_default(query, sentinel, param={"id" : schemaModel["id"]}, model=model.User)
|
||||
theuser = commands.query_single_or_default("SELECT * FROM user where id = ?id?", sentinel, param={"id" : schemaModel["id"]}, model=model.User)
|
||||
if theuser is sentinel:
|
||||
pooledConnection.close()
|
||||
# #TODO: result = {"message":"no such record"} -> json.dumps
|
||||
return json.dumps("no such record"), 404, {'Content-Type': 'application/json; charset=utf-8'}
|
||||
|
||||
# see if we need to update public fields
|
||||
# #TODO_determine: this filter blocks Put-Requests, which update the 'notify_email', 'notify_whatsapp', 'notify_signal', 'notify_popup' fields
|
||||
# should this be refactored?
|
||||
# Also, what about the 'user_name'?
|
||||
# 'participant_id' would also not trigger an update in isolation
|
||||
if "first_name" in schemaModel or "last_name" in schemaModel or "user_phone" in schemaModel or "user_email" in schemaModel:
|
||||
# query = SQLQuery.get_user_put(schemaModel)
|
||||
query = "UPDATE user SET "
|
||||
isNotFirst = False
|
||||
for key in schemaModel.keys():
|
||||
@ -49,6 +58,7 @@ def PutUser(schemaModel):
|
||||
if "old_password" in schemaModel and schemaModel["old_password"] and "new_password" in schemaModel and schemaModel["new_password"]:
|
||||
if bcrypt.checkpw(schemaModel["old_password"].encode("utf-8"), bytes(theuser.password_hash, "utf-8")): # old pw matches
|
||||
password_hash = bcrypt.hashpw(schemaModel["new_password"].encode('utf-8'), bcrypt.gensalt( 12 )).decode('utf8')
|
||||
# query = SQLQuery.get_update_user_password()
|
||||
query = "UPDATE user SET password_hash = ?password_hash? WHERE id = ?id?"
|
||||
commands.execute(query, param={"password_hash" : password_hash, "id" : schemaModel["id"]})
|
||||
else:
|
||||
|
||||
@ -7,16 +7,16 @@ import sys
|
||||
|
||||
config_path = None
|
||||
|
||||
def initPool(instancePath):
|
||||
def initPool(instancePath, connection_filename="connection_data_prod.json"):
|
||||
try:
|
||||
global config_path
|
||||
if(config_path == None):
|
||||
config_path = os.path.join(instancePath,'../../../secure/connection_data_prod.json');
|
||||
config_path = os.path.join(instancePath,f'../../../secure/{connection_filename}') #connection_data_prod.json');
|
||||
|
||||
print (config_path)
|
||||
|
||||
if not os.path.exists(config_path):
|
||||
print ('cannot find ' + config_path)
|
||||
print ('cannot find ' + os.path.abspath(config_path))
|
||||
print("instance path", instancePath)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
from dataclasses import field, dataclass
|
||||
from marshmallow import Schema, fields, post_load, INCLUDE, ValidationError
|
||||
from marshmallow import Schema, fields, INCLUDE, ValidationError, validate, validates, post_load
|
||||
from marshmallow.fields import Field
|
||||
from marshmallow_enum import EnumField
|
||||
from enum import IntEnum
|
||||
@ -9,6 +9,11 @@ from typing import List
|
||||
|
||||
import json
|
||||
import datetime
|
||||
from BreCal.validators.time_logic import validate_time_is_in_not_too_distant_future
|
||||
from BreCal.validators.validation_base_utils import check_if_string_has_special_characters
|
||||
from BreCal.database.enums import ParticipantType, ParticipantFlag
|
||||
|
||||
# from BreCal. ... import check_if_user_is_bsmd_type
|
||||
|
||||
def obj_dict(obj):
|
||||
if isinstance(obj, datetime.datetime):
|
||||
@ -49,10 +54,20 @@ class EvaluationType(IntEnum):
|
||||
def _missing_(cls, value):
|
||||
return cls.undefined
|
||||
|
||||
class NotificationType(IntEnum):
|
||||
class NotificationType(IntEnum):
|
||||
"""
|
||||
Any user has the attributes
|
||||
'notify_email' -> NotificationType.email
|
||||
'notify_popup' -> NotificationType.push
|
||||
'notify_whatsapp' -> undeclared
|
||||
'notify_signal' -> undeclared
|
||||
"""
|
||||
undefined = 0
|
||||
email = 1
|
||||
push = 2
|
||||
# whatsapp = 3
|
||||
# signal = 4
|
||||
|
||||
@classmethod
|
||||
def _missing_(cls, value):
|
||||
return cls.undefined
|
||||
@ -95,8 +110,8 @@ class History:
|
||||
"shipcall_id": self.shipcall_id,
|
||||
"timestamp": self.timestamp.isoformat() if self.timestamp else "",
|
||||
"eta": self.eta.isoformat() if self.eta else "",
|
||||
"type": self.type.name,
|
||||
"operation": self.operation.name
|
||||
"type": self.type.name if isinstance(self.type, IntEnum) else ObjectType(self.type).name,
|
||||
"operation": self.operation.name if isinstance(self.operation, IntEnum) else OperationType(self.operation).name
|
||||
}
|
||||
|
||||
@classmethod
|
||||
@ -112,11 +127,17 @@ class GetVerifyInlineResp(Schema):
|
||||
|
||||
@dataclass
|
||||
class Notification:
|
||||
"""
|
||||
Base data class for any notification.
|
||||
|
||||
Description:
|
||||
'An entry corresponds to an alarm given by a violated rule during times update'
|
||||
"""
|
||||
id: int
|
||||
shipcall_id: int
|
||||
level: int
|
||||
type: NotificationType
|
||||
message: str
|
||||
shipcall_id: int # 'shipcall record that caused the notification'
|
||||
level: int # 'severity of the notification'
|
||||
type: NotificationType # 'type of the notification'
|
||||
message: str # 'individual message'
|
||||
created: datetime
|
||||
modified: datetime
|
||||
|
||||
@ -125,7 +146,7 @@ class Notification:
|
||||
"id": self.id,
|
||||
"shipcall_id": self.shipcall_id,
|
||||
"level": self.level,
|
||||
"type": self.type.name,
|
||||
"type": self.type.name if isinstance(self.type, IntEnum) else NotificationType(self.type).name,
|
||||
"message": self.message,
|
||||
"created": self.created.isoformat() if self.created else "",
|
||||
"modified": self.modified.isoformat() if self.modified else ""
|
||||
@ -142,68 +163,98 @@ class Participant(Schema):
|
||||
street: str
|
||||
postal_code: str
|
||||
city: str
|
||||
type: int
|
||||
type: int # fields.Enum(ParticipantType ...)
|
||||
flags: int
|
||||
created: datetime
|
||||
modified: datetime
|
||||
deleted: bool
|
||||
|
||||
@validates("type")
|
||||
def validate_type(self, value):
|
||||
# e.g., when an IntFlag has the values 1,2,4; the maximum valid value is 7
|
||||
max_int = sum([int(val) for val in list(ParticipantType._value2member_map_.values())])
|
||||
min_int = 0
|
||||
|
||||
valid_type = 0 <= value < max_int
|
||||
if not valid_type:
|
||||
raise ValidationError({"type":f"the provided integer is not supported for default behaviour of the ParticipantType IntFlag. Your choice: {value}. Supported values are: 0 <= value {max_int}"})
|
||||
|
||||
|
||||
@validates("flags")
|
||||
def validate_flags(self, value):
|
||||
# e.g., when an IntFlag has the values 1,2,4; the maximum valid value is 7
|
||||
max_int = sum([int(val) for val in list(ParticipantFlag._value2member_map_.values())])
|
||||
min_int = 0
|
||||
|
||||
valid_type = 0 <= value < max_int
|
||||
if not valid_type:
|
||||
raise ValidationError({"flags":f"the provided integer is not supported for default behaviour of the ParticipantFlag IntFlag. Your choice: {value}. Supported values are: 0 <= value {max_int}"})
|
||||
|
||||
|
||||
class ParticipantList(Participant):
|
||||
pass
|
||||
|
||||
class ParticipantAssignmentSchema(Schema):
|
||||
participant_id = fields.Int()
|
||||
type = fields.Int()
|
||||
participant_id = fields.Integer()
|
||||
type = fields.Integer()
|
||||
|
||||
class ShipcallSchema(Schema):
|
||||
def __init__(self):
|
||||
super().__init__(unknown=None)
|
||||
pass
|
||||
|
||||
id = fields.Int()
|
||||
ship_id = fields.Int()
|
||||
type = fields.Enum(ShipcallType, required=True)
|
||||
eta = fields.DateTime(Required = False, allow_none=True)
|
||||
voyage = fields.Str(allow_none=True, metadata={'Required':False}) # Solving: RemovedInMarshmallow4Warning: Passing field metadata as keyword arguments is deprecated. Use the explicit `metadata=...` argument instead. Additional metadata: {'Required': False}
|
||||
etd = fields.DateTime(Required = False, allow_none=True)
|
||||
arrival_berth_id = fields.Int(Required = False, allow_none=True)
|
||||
departure_berth_id = fields.Int(Required = False, allow_none=True)
|
||||
tug_required = fields.Bool(Required = False, allow_none=True)
|
||||
pilot_required = fields.Bool(Required = False, allow_none=True)
|
||||
flags = fields.Int(Required = False, allow_none=True)
|
||||
pier_side = fields.Bool(Required = False, allow_none=True)
|
||||
bunkering = fields.Bool(Required = False, allow_none=True)
|
||||
replenishing_terminal = fields.Bool(Required = False, allow_none=True)
|
||||
replenishing_lock = fields.Bool(Required = False, allow_none=True)
|
||||
draft = fields.Float(Required = False, allow_none=True)
|
||||
tidal_window_from = fields.DateTime(Required = False, allow_none=True)
|
||||
tidal_window_to = fields.DateTime(Required = False, allow_none=True)
|
||||
rain_sensitive_cargo = fields.Bool(Required = False, allow_none=True)
|
||||
recommended_tugs = fields.Int(Required = False, allow_none=True)
|
||||
anchored = fields.Bool(Required = False, allow_none=True)
|
||||
moored_lock = fields.Bool(Required = False, allow_none=True)
|
||||
canceled = fields.Bool(Required = False, allow_none=True)
|
||||
id = fields.Integer(required=True)
|
||||
ship_id = fields.Integer(required=True)
|
||||
type = fields.Enum(ShipcallType, default=ShipcallType.undefined)
|
||||
eta = fields.DateTime(required=False, allow_none=True)
|
||||
voyage = fields.String(allow_none=True, required=False, validate=[validate.Length(max=16)])
|
||||
etd = fields.DateTime(required=False, allow_none=True)
|
||||
arrival_berth_id = fields.Integer(required=False, allow_none=True)
|
||||
departure_berth_id = fields.Integer(required=False, allow_none=True)
|
||||
tug_required = fields.Bool(required=False, allow_none=True)
|
||||
pilot_required = fields.Bool(required=False, allow_none=True)
|
||||
flags = fields.Integer(required=False, allow_none=True)
|
||||
pier_side = fields.Bool(required=False, allow_none=True)
|
||||
bunkering = fields.Bool(required=False, allow_none=True)
|
||||
replenishing_terminal = fields.Bool(required=False, allow_none=True)
|
||||
replenishing_lock = fields.Bool(required=False, allow_none=True)
|
||||
draft = fields.Float(required=False, allow_none=True, validate=[validate.Range(min=0, max=20, min_inclusive=False, max_inclusive=True)])
|
||||
tidal_window_from = fields.DateTime(required=False, allow_none=True)
|
||||
tidal_window_to = fields.DateTime(required=False, allow_none=True)
|
||||
rain_sensitive_cargo = fields.Bool(required=False, allow_none=True)
|
||||
recommended_tugs = fields.Integer(required=False, allow_none=True, validate=[validate.Range(min=0, max=10, min_inclusive=True, max_inclusive=True)])
|
||||
anchored = fields.Bool(required=False, allow_none=True)
|
||||
moored_lock = fields.Bool(required=False, allow_none=True)
|
||||
canceled = fields.Bool(required=False, allow_none=True)
|
||||
evaluation = fields.Enum(EvaluationType, required=False, allow_none=True, default=EvaluationType.undefined)
|
||||
evaluation_message = fields.Str(allow_none=True, metadata={'Required':False}) # Solving: RemovedInMarshmallow4Warning: Passing field metadata as keyword arguments is deprecated. Use the explicit `metadata=...` argument instead. Additional metadata: {'Required': False}
|
||||
evaluation_time = fields.DateTime(Required = False, allow_none=True)
|
||||
evaluation_notifications_sent = fields.Bool(Required = False, allow_none=True)
|
||||
time_ref_point = fields.Int(Required = False, allow_none=True)
|
||||
evaluation_message = fields.Str(allow_none=True, required=False)
|
||||
evaluation_time = fields.DateTime(required=False, allow_none=True)
|
||||
evaluation_notifications_sent = fields.Bool(required=False, allow_none=True)
|
||||
time_ref_point = fields.Integer(required=False, allow_none=True)
|
||||
participants = fields.List(fields.Nested(ParticipantAssignmentSchema))
|
||||
created = fields.DateTime(Required = False, allow_none=True)
|
||||
modified = fields.DateTime(Required = False, allow_none=True)
|
||||
created = fields.DateTime(required=False, allow_none=True)
|
||||
modified = fields.DateTime(required=False, allow_none=True)
|
||||
|
||||
@post_load
|
||||
def make_shipcall(self, data, **kwargs):
|
||||
if 'type' in data:
|
||||
data['type_value'] = data['type'].value
|
||||
data['type_value'] = int(data['type'])
|
||||
else:
|
||||
data['type_value'] = ShipcallType.undefined
|
||||
data['type_value'] = int(ShipcallType.undefined)
|
||||
if 'evaluation' in data:
|
||||
if data['evaluation']:
|
||||
data['evaluation_value'] = data['evaluation'].value
|
||||
data['evaluation_value'] = int(data['evaluation'])
|
||||
else:
|
||||
data['evaluation_value'] = EvaluationType.undefined
|
||||
data['evaluation_value'] = int(EvaluationType.undefined)
|
||||
return data
|
||||
|
||||
@validates("type")
|
||||
def validate_type(self, value):
|
||||
valid_shipcall_type = int(value) in [item.value for item in ShipcallType]
|
||||
|
||||
if not valid_shipcall_type:
|
||||
raise ValidationError({"type":f"the provided type is not a valid shipcall type."})
|
||||
|
||||
|
||||
@dataclass
|
||||
class Participant_Assignment:
|
||||
@ -215,6 +266,9 @@ class Participant_Assignment:
|
||||
participant_id: int
|
||||
type: int # a variant would be to use the IntFlag type (with appropriate serialization)
|
||||
|
||||
def to_json(self):
|
||||
return self.__dict__
|
||||
|
||||
|
||||
@dataclass
|
||||
class Shipcall:
|
||||
@ -230,7 +284,7 @@ class Shipcall:
|
||||
tug_required: bool
|
||||
pilot_required: bool
|
||||
flags: int
|
||||
pier_side: bool
|
||||
pier_side: bool # enumerator object in database/enum/PierSide
|
||||
bunkering: bool
|
||||
replenishing_terminal: bool
|
||||
replenishing_lock: bool
|
||||
@ -255,7 +309,7 @@ class Shipcall:
|
||||
return {
|
||||
"id": self.id,
|
||||
"ship_id": self.ship_id,
|
||||
"type": self.type.name,
|
||||
"type": self.type.name if isinstance(self.type, IntEnum) else ShipcallType(self.type).name,
|
||||
"eta": self.eta.isoformat() if self.eta else "",
|
||||
"voyage": self.voyage,
|
||||
"etd": self.etd.isoformat() if self.etd else "",
|
||||
@ -276,7 +330,7 @@ class Shipcall:
|
||||
"anchored": self.anchored,
|
||||
"moored_lock": self.moored_lock,
|
||||
"canceled": self.canceled,
|
||||
"evaluation": self.evaluation.name,
|
||||
"evaluation": self.evaluation.name if isinstance(self.evaluation, IntEnum) else EvaluationType(self.evaluation).name,
|
||||
"evaluation_message": self.evaluation_message,
|
||||
"evaluation_time": self.evaluation_time.isoformat() if self.evaluation_time else "",
|
||||
"evaluation_notifications_sent": self.evaluation_notifications_sent,
|
||||
@ -297,35 +351,106 @@ class ShipcallId(Schema):
|
||||
|
||||
# this is the way!
|
||||
|
||||
|
||||
class TimesSchema(Schema):
|
||||
def __init__(self):
|
||||
super().__init__(unknown=None)
|
||||
pass
|
||||
|
||||
id = fields.Int(Required=False)
|
||||
eta_berth = fields.DateTime(Required = False, allow_none=True)
|
||||
eta_berth_fixed = fields.Bool(Required = False, allow_none=True)
|
||||
etd_berth = fields.DateTime(Required = False, allow_none=True)
|
||||
etd_berth_fixed = fields.Bool(Required = False, allow_none=True)
|
||||
lock_time = fields.DateTime(Required = False, allow_none=True)
|
||||
lock_time_fixed = fields.Bool(Required = False, allow_none=True)
|
||||
zone_entry = fields.DateTime(Required = False, allow_none=True)
|
||||
zone_entry_fixed = fields.Bool(Required = False, allow_none=True)
|
||||
operations_start = fields.DateTime(Required = False, allow_none=True)
|
||||
operations_end = fields.DateTime(Required = False, allow_none=True)
|
||||
remarks = fields.String(Required = False, allow_none=True)
|
||||
participant_id = fields.Int(Required = True)
|
||||
berth_id = fields.Int(Required = False, allow_none = True)
|
||||
berth_info = fields.String(Required = False, allow_none=True)
|
||||
pier_side = fields.Bool(Required = False, allow_none = True)
|
||||
shipcall_id = fields.Int(Required = True)
|
||||
participant_type = fields.Int(Required = False, allow_none=True)
|
||||
ata = fields.DateTime(Required = False, allow_none=True)
|
||||
atd = fields.DateTime(Required = False, allow_none=True)
|
||||
eta_interval_end = fields.DateTime(Required = False, allow_none=True)
|
||||
etd_interval_end = fields.DateTime(Required = False, allow_none=True)
|
||||
created = fields.DateTime(Required = False, allow_none=True)
|
||||
modified = fields.DateTime(Required = False, allow_none=True)
|
||||
id = fields.Integer(required=False)
|
||||
eta_berth = fields.DateTime(required=False, allow_none=True)
|
||||
eta_berth_fixed = fields.Bool(required=False, allow_none=True)
|
||||
etd_berth = fields.DateTime(required=False, allow_none=True)
|
||||
etd_berth_fixed = fields.Bool(required=False, allow_none=True)
|
||||
lock_time = fields.DateTime(required=False, allow_none=True)
|
||||
lock_time_fixed = fields.Bool(required=False, allow_none=True)
|
||||
zone_entry = fields.DateTime(required=False, allow_none=True)
|
||||
zone_entry_fixed = fields.Bool(required=False, allow_none=True)
|
||||
operations_start = fields.DateTime(required=False, allow_none=True)
|
||||
operations_end = fields.DateTime(required=False, allow_none=True)
|
||||
remarks = fields.String(required=False, allow_none=True, validate=[validate.Length(max=512)])
|
||||
participant_id = fields.Integer(required=True)
|
||||
berth_id = fields.Integer(required=False, allow_none = True)
|
||||
berth_info = fields.String(required=False, allow_none=True, validate=[validate.Length(max=512)])
|
||||
pier_side = fields.Bool(required=False, allow_none = True)
|
||||
shipcall_id = fields.Integer(required=True)
|
||||
participant_type = fields.Integer(Required = False, allow_none=True)# TODO: could become Enum. # participant_type = fields.Enum(ParticipantType, required=False, allow_none=True, default=ParticipantType.undefined) #fields.Integer(required=False, allow_none=True)
|
||||
ata = fields.DateTime(required=False, allow_none=True)
|
||||
atd = fields.DateTime(required=False, allow_none=True)
|
||||
eta_interval_end = fields.DateTime(required=False, allow_none=True)
|
||||
etd_interval_end = fields.DateTime(required=False, allow_none=True)
|
||||
created = fields.DateTime(required=False, allow_none=True)
|
||||
modified = fields.DateTime(required=False, allow_none=True)
|
||||
|
||||
@validates("participant_type")
|
||||
def validate_participant_type(self, value):
|
||||
# #TODO: it may also make sense to block multi-assignments, whereas a value could be BSMD+AGENCY
|
||||
# while the validation fails when one of those multi-assignments is BSMD, it passes in cases,
|
||||
# such as AGENCY+PILOT
|
||||
|
||||
# a participant type should not be .BSMD
|
||||
if not isinstance(value, ParticipantType):
|
||||
value = ParticipantType(value)
|
||||
|
||||
if ParticipantType.BSMD in value:
|
||||
raise ValidationError({"participant_type":f"the participant_type must not be .BSMD"})
|
||||
|
||||
@validates("eta_berth")
|
||||
def validate_eta_berth(self, value):
|
||||
# violation when time is not in the future, but also does not exceed a threshold for the 'reasonable' future
|
||||
# when 'value' is 'None', a ValidationError is not issued.
|
||||
valid_time = validate_time_is_in_not_too_distant_future(raise_validation_error=True, value=value, months=12)
|
||||
return
|
||||
|
||||
@validates("etd_berth")
|
||||
def validate_etd_berth(self, value):
|
||||
# violation when time is not in the future, but also does not exceed a threshold for the 'reasonable' future
|
||||
# when 'value' is 'None', a ValidationError is not issued.
|
||||
valid_time = validate_time_is_in_not_too_distant_future(raise_validation_error=True, value=value, months=12)
|
||||
return
|
||||
|
||||
@validates("lock_time")
|
||||
def validate_lock_time(self, value):
|
||||
# violation when time is not in the future, but also does not exceed a threshold for the 'reasonable' future
|
||||
# when 'value' is 'None', a ValidationError is not issued.
|
||||
valid_time = validate_time_is_in_not_too_distant_future(raise_validation_error=True, value=value, months=12)
|
||||
return
|
||||
|
||||
@validates("zone_entry")
|
||||
def validate_zone_entry(self, value):
|
||||
# violation when time is not in the future, but also does not exceed a threshold for the 'reasonable' future
|
||||
# when 'value' is 'None', a ValidationError is not issued.
|
||||
valid_time = validate_time_is_in_not_too_distant_future(raise_validation_error=True, value=value, months=12)
|
||||
return
|
||||
|
||||
@validates("operations_start")
|
||||
def validate_operations_start(self, value):
|
||||
# violation when time is not in the future, but also does not exceed a threshold for the 'reasonable' future
|
||||
# when 'value' is 'None', a ValidationError is not issued.
|
||||
valid_time = validate_time_is_in_not_too_distant_future(raise_validation_error=True, value=value, months=12)
|
||||
return
|
||||
|
||||
@validates("operations_end")
|
||||
def validate_operations_end(self, value):
|
||||
# violation when time is not in the future, but also does not exceed a threshold for the 'reasonable' future
|
||||
# when 'value' is 'None', a ValidationError is not issued.
|
||||
valid_time = validate_time_is_in_not_too_distant_future(raise_validation_error=True, value=value, months=12)
|
||||
return
|
||||
|
||||
@validates("eta_interval_end")
|
||||
def validate_eta_interval_end(self, value):
|
||||
# violation when time is not in the future, but also does not exceed a threshold for the 'reasonable' future
|
||||
# when 'value' is 'None', a ValidationError is not issued.
|
||||
valid_time = validate_time_is_in_not_too_distant_future(raise_validation_error=True, value=value, months=12)
|
||||
return
|
||||
|
||||
@validates("etd_interval_end")
|
||||
def validate_etd_interval_end(self, value):
|
||||
# violation when time is not in the future, but also does not exceed a threshold for the 'reasonable' future
|
||||
# when 'value' is 'None', a ValidationError is not issued.
|
||||
valid_time = validate_time_is_in_not_too_distant_future(raise_validation_error=True, value=value, months=12)
|
||||
return
|
||||
|
||||
|
||||
# deserialize PUT object target
|
||||
|
||||
@ -333,13 +458,26 @@ class UserSchema(Schema):
|
||||
def __init__(self):
|
||||
super().__init__(unknown=None)
|
||||
pass
|
||||
id = fields.Int(required=True)
|
||||
first_name = fields.Str(allow_none=True, metadata={'Required':False})
|
||||
last_name = fields.Str(allow_none=True, metadata={'Required':False})
|
||||
user_phone = fields.Str(allow_none=True, metadata={'Required':False})
|
||||
user_email = fields.Str(allow_none=True, metadata={'Required':False})
|
||||
old_password = fields.Str(allow_none=True, metadata={'Required':False})
|
||||
new_password = fields.Str(allow_none=True, metadata={'Required':False})
|
||||
id = fields.Integer(required=True)
|
||||
first_name = fields.String(allow_none=True, required=False, validate=[validate.Length(max=64)])
|
||||
last_name = fields.String(allow_none=True, required=False, validate=[validate.Length(max=64)])
|
||||
user_phone = fields.String(allow_none=True, required=False)
|
||||
user_email = fields.String(allow_none=True, required=False, validate=[validate.Length(max=64)])
|
||||
old_password = fields.String(allow_none=True, required=False, validate=[validate.Length(max=128)])
|
||||
new_password = fields.String(allow_none=True, required=False, validate=[validate.Length(min=6, max=128)])
|
||||
# #TODO: the user schema does not (yet) include the 'notify_' fields
|
||||
|
||||
@validates("user_phone")
|
||||
def validate_user_phone(self, value):
|
||||
valid_characters = list(map(str,range(0,10)))+["+", " "]
|
||||
if not all([v in valid_characters for v in value]):
|
||||
raise ValidationError({"user_phone":f"one of the phone number values is not valid."})
|
||||
|
||||
@validates("user_email")
|
||||
def validate_user_email(self, value):
|
||||
if not "@" in value:
|
||||
raise ValidationError({"user_email":f"invalid email address"})
|
||||
|
||||
|
||||
@dataclass
|
||||
class Times:
|
||||
@ -380,10 +518,10 @@ class User:
|
||||
user_phone: str
|
||||
password_hash: str
|
||||
api_key: str
|
||||
notify_email: bool
|
||||
notify_whatsapp: bool
|
||||
notify_signal: bool
|
||||
notify_popup: bool
|
||||
notify_email: bool # #TODO_clarify: should we use an IntFlag for multi-assignment?
|
||||
notify_whatsapp: bool # #TODO_clarify: should we use an IntFlag for multi-assignment?
|
||||
notify_signal: bool # #TODO_clarify: should we use an IntFlag for multi-assignment?
|
||||
notify_popup: bool # #TODO_clarify: should we use an IntFlag for multi-assignment?
|
||||
created: datetime
|
||||
modified: datetime
|
||||
|
||||
@ -409,19 +547,50 @@ class ShipSchema(Schema):
|
||||
super().__init__(unknown=None)
|
||||
pass
|
||||
|
||||
id = fields.Int(Required=False)
|
||||
name = fields.String(allow_none=False, metadata={'Required':True})
|
||||
imo = fields.Int(allow_none=False, metadata={'Required':True})
|
||||
callsign = fields.String(allow_none=True, metadata={'Required':False})
|
||||
participant_id = fields.Int(allow_none=True, metadata={'Required':False})
|
||||
length = fields.Float(allow_none=True, metadata={'Required':False})
|
||||
width = fields.Float(allow_none=True, metadata={'Required':False})
|
||||
is_tug = fields.Bool(allow_none=True, metadata={'Required':False}, default=False)
|
||||
bollard_pull = fields.Int(allow_none=True, metadata={'Required':False})
|
||||
eni = fields.Int(allow_none=True, metadata={'Required':False})
|
||||
created = fields.DateTime(allow_none=True, metadata={'Required':False})
|
||||
modified = fields.DateTime(allow_none=True, metadata={'Required':False})
|
||||
deleted = fields.Bool(allow_none=True, metadata={'Required':False}, default=False)
|
||||
id = fields.Int(required=False)
|
||||
name = fields.String(allow_none=False, required=True)
|
||||
imo = fields.Int(allow_none=False, required=True)
|
||||
callsign = fields.String(allow_none=True, required=False)
|
||||
participant_id = fields.Int(allow_none=True, required=False)
|
||||
length = fields.Float(allow_none=True, required=False, validate=[validate.Range(min=0, max=1000, min_inclusive=False, max_inclusive=False)])
|
||||
width = fields.Float(allow_none=True, required=False, validate=[validate.Range(min=0, max=100, min_inclusive=False, max_inclusive=False)])
|
||||
is_tug = fields.Bool(allow_none=True, required=False, default=False)
|
||||
bollard_pull = fields.Int(allow_none=True, required=False)
|
||||
eni = fields.Int(allow_none=True, required=False)
|
||||
created = fields.DateTime(allow_none=True, required=False)
|
||||
modified = fields.DateTime(allow_none=True, required=False)
|
||||
deleted = fields.Bool(allow_none=True, required=False, default=False)
|
||||
|
||||
@validates("name")
|
||||
def validate_name(self, value):
|
||||
character_length = len(str(value))
|
||||
if character_length<1:
|
||||
raise ValidationError({"name":f"'name' argument should have at least one character"})
|
||||
elif character_length>=64:
|
||||
raise ValidationError({"name":f"'name' argument should have at max. 63 characters"})
|
||||
|
||||
if check_if_string_has_special_characters(value):
|
||||
raise ValidationError({"name":f"'name' argument should not have special characters."})
|
||||
return
|
||||
|
||||
@validates("imo")
|
||||
def validate_imo(self, value):
|
||||
value = str(value).zfill(7) # 1 becomes '0000001' (7 characters). 12345678 becomes '12345678' (8 characters)
|
||||
imo_length = len(value)
|
||||
if imo_length != 7:
|
||||
raise ValidationError({"imo":f"'imo' should be a 7-digit number"})
|
||||
return
|
||||
|
||||
@validates("callsign")
|
||||
def validate_callsign(self, value):
|
||||
if value is not None:
|
||||
callsign_length = len(str(value))
|
||||
if callsign_length>8:
|
||||
raise ValidationError({"callsign":f"'callsign' argument should not have more than 8 characters"})
|
||||
|
||||
if check_if_string_has_special_characters(value):
|
||||
raise ValidationError({"callsign":f"'callsign' argument should not have special characters."})
|
||||
return
|
||||
|
||||
|
||||
class TimesId(Schema):
|
||||
@ -442,3 +611,22 @@ class Shipcalls(Shipcall):
|
||||
|
||||
class TimesList(Times):
|
||||
pass
|
||||
|
||||
@dataclass
|
||||
class ShipcallParticipantMap:
|
||||
id: int
|
||||
shipcall_id: int
|
||||
participant_id: int
|
||||
type : ShipcallType
|
||||
created: datetime
|
||||
modified: datetime
|
||||
|
||||
def to_json(self):
|
||||
return {
|
||||
"id": self.id,
|
||||
"shipcall_id": self.shipcall_id,
|
||||
"participant_id": self.participant_id,
|
||||
"type": self.type.name if isinstance(self.type, IntEnum) else ShipcallType(self.type).name,
|
||||
"created": self.created.isoformat() if self.created else "",
|
||||
"modified": self.modified.isoformat() if self.modified else "",
|
||||
}
|
||||
|
||||
@ -9,6 +9,7 @@ def check_jwt():
|
||||
if not token:
|
||||
raise Exception('Missing access token')
|
||||
jwt = token.split('Bearer ')[1]
|
||||
|
||||
try:
|
||||
return decode_jwt(jwt)
|
||||
except Exception as e:
|
||||
|
||||
174
src/server/BreCal/services/email_handling.py
Normal file
174
src/server/BreCal/services/email_handling.py
Normal file
@ -0,0 +1,174 @@
|
||||
import os
|
||||
import typing
|
||||
import smtplib
|
||||
from getpass import getpass
|
||||
from email.message import EmailMessage
|
||||
import mimetypes
|
||||
|
||||
import email
|
||||
# from email.mime.base import MIMEBase
|
||||
# from email.mime.image import MIMEImage
|
||||
# from email.mime.text import MIMEText
|
||||
from email.mime.multipart import MIMEMultipart
|
||||
from email.mime.application import MIMEApplication
|
||||
|
||||
|
||||
class EmailHandler():
|
||||
"""
|
||||
Creates an EmailHandler, which is capable of connecting to a mail server at a respective port,
|
||||
as well as logging into a specific user's mail address.
|
||||
Upon creating messages, these can be sent via this handler.
|
||||
|
||||
Options:
|
||||
mail_server: address of the server, such as 'smtp.gmail.com' or 'w01d5503.kasserver.com
|
||||
mail_port:
|
||||
25 - SMTP Port, to send emails
|
||||
110 - POP3 Port, to receive emails
|
||||
143 - IMAP Port, to receive from IMAP
|
||||
465 - SSL Port of SMTP
|
||||
587 - alternative SMTP Port
|
||||
993 - SSL/TLS-Port of IMAP
|
||||
995 - SSL/TLS-Port of POP3
|
||||
mail_address: a specific user's Email address, which will be used to send Emails. Example: "my_user@gmail.com"
|
||||
"""
|
||||
def __init__(self, mail_server:str, mail_port:int, mail_address:str):
|
||||
self.mail_server = mail_server
|
||||
self.mail_port = mail_port
|
||||
self.mail_address = mail_address
|
||||
|
||||
self.server = smtplib.SMTP_SSL(self.mail_server, self.mail_port) # alternatively, SMTP
|
||||
|
||||
def check_state(self):
|
||||
"""check, whether the server login took place and is open."""
|
||||
try:
|
||||
(status_code, status_msg) = self.server.noop()
|
||||
return status_code==250 # 250: b'2.0.0 Ok'
|
||||
except smtplib.SMTPServerDisconnected:
|
||||
return False
|
||||
|
||||
def check_connection(self):
|
||||
"""check, whether the server object is connected to the server. If not, connect it. """
|
||||
try:
|
||||
self.server.ehlo()
|
||||
except smtplib.SMTPServerDisconnected:
|
||||
self.server.connect(self.mail_server, self.mail_port)
|
||||
return
|
||||
|
||||
def check_login(self)->bool:
|
||||
"""check, whether the server object is logged in as a user"""
|
||||
user = self.server.__dict__.get("user",None)
|
||||
return user is not None
|
||||
|
||||
def login(self, interactive:bool=True):
|
||||
"""
|
||||
login on the determined mail server's mail address. By default, this function opens an interactive window to
|
||||
type the password without echoing (printing '*******' instead of readable characters).
|
||||
|
||||
returns (status_code, status_msg)
|
||||
"""
|
||||
self.check_connection()
|
||||
if interactive:
|
||||
(status_code, status_msg) = self.server.login(self.mail_address, password=getpass())
|
||||
else:
|
||||
# fernet + password file
|
||||
raise NotImplementedError()
|
||||
return (status_code, status_msg) # should be: (235, b'2.7.0 Authentication successful')
|
||||
|
||||
def create_email(self, subject:str, message_body:str)->EmailMessage:
|
||||
"""
|
||||
Create an EmailMessage object, which contains the Email's header ("Subject"), content ("Message Body") and the sender's address ("From").
|
||||
The EmailMessage object does not contain the recipients yet, as these will be defined upon sending the Email.
|
||||
"""
|
||||
msg = EmailMessage()
|
||||
msg["Subject"] = subject
|
||||
msg["From"] = self.mail_address
|
||||
#msg["To"] = email_tgts # will be defined in self.send_email
|
||||
msg.set_content(message_body)
|
||||
return msg
|
||||
|
||||
def build_recipients(self, email_tgts:list[str]):
|
||||
"""
|
||||
email formatting does not support lists. Instead, items are joined into a comma-space-separated string.
|
||||
Example:
|
||||
[mail1@mail.com, mail2@mail.com] becomes
|
||||
'mail1@mail.com, mail2@mail.com'
|
||||
"""
|
||||
return ', '.join(email_tgts)
|
||||
|
||||
def open_mime_application(self, path:str)->MIMEApplication:
|
||||
"""open a local file, read the bytes into a MIMEApplication object, which is built with the proper subtype (based on the file extension)"""
|
||||
with open(path, 'rb') as file:
|
||||
attachment = MIMEApplication(file.read(), _subtype=mimetypes.MimeTypes().guess_type(path))
|
||||
|
||||
attachment.add_header('Content-Disposition','attachment',filename=str(os.path.basename(path)))
|
||||
return attachment
|
||||
|
||||
def attach_file(self, path:str, msg:email.mime.multipart.MIMEMultipart)->None:
|
||||
"""
|
||||
attach a file to the message. This function opens the file, reads its bytes, defines the mime type by the
|
||||
path extension. The filename is appended as the header.
|
||||
|
||||
mimetypes: # https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types
|
||||
"""
|
||||
attachment = self.open_mime_application(path)
|
||||
msg.attach(attachment)
|
||||
return
|
||||
|
||||
def send_email(self, msg:EmailMessage, email_tgts:list[str], cc_tgts:typing.Optional[list[str]]=None, bcc_tgts:typing.Optional[list[str]]=None, debug:bool=False)->typing.Union[dict,EmailMessage]:
|
||||
"""
|
||||
send a prepared email message to recipients (email_tgts), copy (cc_tgts) and blind copy (bcc_tgts).
|
||||
Returns a dictionary of feedback, which is commonly empty and the EmailMessage.
|
||||
|
||||
When failing, this function returns an SMTP error instead of returning the default outputs.
|
||||
"""
|
||||
# Set the Recipients
|
||||
msg["To"] = self.build_recipients(email_tgts)
|
||||
|
||||
# optionally, add CC and BCC (copy and blind-copy)
|
||||
if cc_tgts is not None:
|
||||
msg["Cc"] = self.build_recipients(cc_tgts)
|
||||
if bcc_tgts is not None:
|
||||
msg["Bcc"] = self.build_recipients(bcc_tgts)
|
||||
|
||||
# when debugging, do not send the Email, but return the EmailMessage.
|
||||
if debug:
|
||||
return {}, msg
|
||||
|
||||
assert self.check_login(), f"currently not logged in. Cannot send an Email. Make sure to properly use self.login first. "
|
||||
# send the prepared EmailMessage via the server.
|
||||
feedback = self.server.send_message(msg)
|
||||
return feedback, msg
|
||||
|
||||
def translate_mail_to_multipart(self, msg:EmailMessage):
|
||||
"""EmailMessage does not support HTML and attachments. Hence, one can convert an EmailMessage object."""
|
||||
if msg.is_multipart():
|
||||
return msg
|
||||
|
||||
# create a novel MIMEMultipart email
|
||||
msg_new = MIMEMultipart("mixed")
|
||||
headers = list((k, v) for (k, v) in msg.items() if k not in ("Content-Type", "Content-Transfer-Encoding"))
|
||||
|
||||
# add the headers of msg to the new message
|
||||
for k,v in headers:
|
||||
msg_new[k] = v
|
||||
|
||||
# delete the headers from msg
|
||||
for k,v in headers:
|
||||
del msg[k]
|
||||
|
||||
# attach the remainder of the msg, such as the body, to the MIMEMultipart
|
||||
msg_new.attach(msg)
|
||||
return msg_new
|
||||
|
||||
def print_email_attachments(self, msg:MIMEMultipart)->list[str]:
|
||||
"""return a list of lines of an Email, which contain 'filename=' as a list. """
|
||||
return [line_ for line_ in msg.as_string().split("\n") if "filename=" in line_]
|
||||
|
||||
def close(self):
|
||||
self.server.__dict__.pop("user",None)
|
||||
self.server.__dict__.pop("password",None)
|
||||
|
||||
# quit the server connection (internally uses .close)
|
||||
self.server.quit()
|
||||
return
|
||||
|
||||
@ -7,11 +7,41 @@ def create_api_key():
|
||||
return secrets.token_urlsafe(16)
|
||||
|
||||
def generate_jwt(payload, lifetime=None):
|
||||
"""
|
||||
creates an encoded token, which is based on the 'SECRET_KEY' environment variable. The environment variable
|
||||
is set when the .wsgi application is started or can theoretically be set on system-level.
|
||||
|
||||
args:
|
||||
payload:
|
||||
json-dictionary with key:value pairs.
|
||||
|
||||
lifetime:
|
||||
When a 'lifetime' (integer) is provided, the payload will be extended by an expiration key 'exp', which is
|
||||
valid for the next {lifetime} minutes.
|
||||
|
||||
returns: token, a JWT-encoded string
|
||||
"""
|
||||
if lifetime:
|
||||
payload['exp'] = (datetime.datetime.now() + datetime.timedelta(minutes=lifetime)).timestamp()
|
||||
return jwt.encode(payload, os.environ.get('SECRET_KEY'), algorithm="HS256")
|
||||
|
||||
def decode_jwt(token):
|
||||
"""
|
||||
this function reverts the {generate_jwt} function. An encoded JWT token is decoded into a JSON dictionary.
|
||||
The function is commonly used to decode a login-token and obtain a 'user_data' variable, which is a dictionary.
|
||||
|
||||
Example of 'user_data':
|
||||
{
|
||||
'id': 1,
|
||||
'participant_id': 1,
|
||||
'first_name': 'Firstname',
|
||||
'last_name': 'Lastname',
|
||||
'user_name': 'xUsername01',
|
||||
'user_phone': '+01 123 456 7890',
|
||||
'user_email': 'firstname.lastname@internet.com',
|
||||
'exp': 1716881626.056438 # expiration timestamp
|
||||
}
|
||||
"""
|
||||
return jwt.decode(token, os.environ.get('SECRET_KEY'), algorithms=["HS256"])
|
||||
|
||||
|
||||
|
||||
@ -3,6 +3,7 @@ import pydapper
|
||||
from BreCal.schemas import model
|
||||
from BreCal.local_db import getPoolConnection
|
||||
from BreCal.database.update_database import evaluate_shipcall_state
|
||||
from BreCal.database.sql_queries import create_sql_query_shipcall_get
|
||||
|
||||
import threading
|
||||
import schedule
|
||||
@ -26,16 +27,8 @@ def UpdateShipcalls(options:dict = {'past_days':2}):
|
||||
pooledConnection = getPoolConnection()
|
||||
commands = pydapper.using(pooledConnection)
|
||||
|
||||
query = ("SELECT s.id as id, ship_id, type, eta, voyage, etd, arrival_berth_id, departure_berth_id, tug_required, pilot_required, "
|
||||
"flags, s.pier_side, bunkering, replenishing_terminal, replenishing_lock, draft, tidal_window_from, tidal_window_to, rain_sensitive_cargo, recommended_tugs, "
|
||||
"anchored, moored_lock, canceled, evaluation, evaluation_message, evaluation_notifications_sent, evaluation_time, s.created as created, s.modified as modified, time_ref_point FROM shipcall s " +
|
||||
"LEFT JOIN times t ON t.shipcall_id = s.id AND t.participant_type = 8 "
|
||||
"WHERE "
|
||||
"(type = 1 AND (COALESCE(t.eta_berth, eta) >= DATE(NOW() - INTERVAL %d DAY))) OR "
|
||||
"((type = 2 OR type = 3) AND (COALESCE(t.etd_berth, etd) >= DATE(NOW() - INTERVAL %d DAY)))"
|
||||
"ORDER BY s.id") % (options["past_days"], options["past_days"])
|
||||
|
||||
# obtain data from the MYSQL database
|
||||
# obtain data from the MYSQL database (uses 'options' to filter the resulting data by the ETA, considering those entries of 'past_days'-range)
|
||||
query = create_sql_query_shipcall_get(options)
|
||||
data = commands.query(query, model=model.Shipcall)
|
||||
|
||||
# get the shipcall ids, which are of interest
|
||||
@ -57,6 +50,11 @@ def add_function_to_schedule__update_shipcalls(interval_in_minutes:int, options:
|
||||
schedule.every(interval_in_minutes).minutes.do(UpdateShipcalls, **kwargs_)
|
||||
return
|
||||
|
||||
def add_function_to_schedule__send_notifications(vr, interval_in_minutes:int=10):
|
||||
schedule.every(interval_in_minutes).minutes.do(vr.notifier.send_notifications)
|
||||
return
|
||||
|
||||
|
||||
def setup_schedule(update_shipcalls_interval_in_minutes:int=60):
|
||||
|
||||
logging.getLogger('schedule').setLevel(logging.INFO); # set the logging level of the schedule module to INFO
|
||||
|
||||
@ -7,7 +7,6 @@ def get_notification_simple():
|
||||
"""creates a default notification, where 'created' is now, and modified is now+10 seconds"""
|
||||
notification_id = generate_uuid1_int() # uid?
|
||||
times_id = generate_uuid1_int() # uid?
|
||||
acknowledged = False
|
||||
level = 10
|
||||
type = 0
|
||||
message = "hello world"
|
||||
@ -17,7 +16,6 @@ def get_notification_simple():
|
||||
notification = Notification(
|
||||
notification_id,
|
||||
times_id,
|
||||
acknowledged,
|
||||
level,
|
||||
type,
|
||||
message,
|
||||
|
||||
@ -30,3 +30,8 @@ def get_participant_simple():
|
||||
deleted
|
||||
)
|
||||
return participant
|
||||
|
||||
|
||||
def get_stub_list_of_valid_participants():
|
||||
participants = [{'participant_id': 2, 'type': 4}, {'participant_id': 3, 'type': 1}, {'participant_id': 4, 'type': 2}, {'participant_id': 5, 'type': 8}]
|
||||
return participants
|
||||
|
||||
@ -36,3 +36,27 @@ def get_ship_simple():
|
||||
)
|
||||
return ship
|
||||
|
||||
def get_stub_valid_ship():
|
||||
post_data = {
|
||||
'name': 'BOTHNIABORG',
|
||||
'imo': 9267728,
|
||||
'callsign': "PBIO",
|
||||
'participant_id': None,
|
||||
'length': 153.05,
|
||||
'width': 21.8,
|
||||
'is_tug': 0,
|
||||
'bollard_pull': None,
|
||||
'eni': None,
|
||||
'created': '2023-10-04 11:52:32',
|
||||
'modified': None,
|
||||
'deleted': 0
|
||||
}
|
||||
return post_data
|
||||
|
||||
def get_stub_valid_ship_loaded_model(post_data=None):
|
||||
from BreCal.schemas import model
|
||||
if post_data is None:
|
||||
post_data = get_stub_valid_ship()
|
||||
loadedModel = model.ShipSchema().load(data=post_data, many=False, partial=True)
|
||||
return loadedModel
|
||||
|
||||
@ -3,25 +3,30 @@ from BreCal.stubs import generate_uuid1_int
|
||||
from BreCal.schemas.model import Shipcall
|
||||
from dataclasses import field
|
||||
|
||||
import json
|
||||
import datetime
|
||||
from BreCal.schemas.model import ShipcallType
|
||||
from BreCal.stubs.participant import get_stub_list_of_valid_participants
|
||||
|
||||
def get_shipcall_simple():
|
||||
# only used for the stub
|
||||
base_time = datetime.datetime.now()
|
||||
|
||||
shipcall_id = generate_uuid1_int()
|
||||
ship_id = generate_uuid1_int()
|
||||
shipcall_id = 124 # generate_uuid1_int()
|
||||
ship_id = 5 # generate_uuid1_int()
|
||||
|
||||
eta = base_time+datetime.timedelta(hours=3, minutes=12)
|
||||
role_type = 1
|
||||
voyage = "987654321"
|
||||
etd = base_time+datetime.timedelta(hours=6, minutes=12) # should never be before eta
|
||||
|
||||
arrival_berth_id = generate_uuid1_int()
|
||||
departure_berth_id = generate_uuid1_int()
|
||||
arrival_berth_id = 140 #generate_uuid1_int()
|
||||
departure_berth_id = 140 #generate_uuid1_int()
|
||||
|
||||
tug_required = False
|
||||
pilot_required = False
|
||||
|
||||
flags = 0 # #TODO_shipcall_flags. What is meant here? What should be tested?
|
||||
flags = 0 # #TODO_shipcall_flags. What is meant here? What should be tested?
|
||||
pier_side = False # whether a ship will be fixated on the pier side. en: pier side, de: Anlegestelle. From 'BremenCalling_Datenmodell.xlsx': gedreht/ungedreht
|
||||
bunkering = False # #TODO_bunkering_unclear
|
||||
replenishing_terminal = False # en: replenishing terminal, de: Nachfüll-Liegeplatz
|
||||
@ -37,11 +42,13 @@ def get_shipcall_simple():
|
||||
recommended_tugs = 2 # assert 0<recommended_tugs<={threshold}. E.g., 20 should not be exceeded.
|
||||
anchored = False
|
||||
moored_lock = False # de: 'Festmacherschleuse', en: 'moored lock'
|
||||
|
||||
canceled = False
|
||||
|
||||
time_ref_point = 0
|
||||
|
||||
evaluation = None
|
||||
evaluation_message = ""
|
||||
evaluation_time = None
|
||||
evaluation_time = datetime.datetime.now()
|
||||
evaluation_notifications_sent = False
|
||||
created = datetime.datetime.now()
|
||||
modified = created+datetime.timedelta(seconds=10)
|
||||
@ -76,10 +83,127 @@ def get_shipcall_simple():
|
||||
evaluation_message,
|
||||
evaluation_time,
|
||||
evaluation_notifications_sent,
|
||||
time_ref_point,
|
||||
created,
|
||||
modified,
|
||||
participants,
|
||||
)
|
||||
return shipcall
|
||||
|
||||
def create_postman_stub_shipcall():
|
||||
"""
|
||||
this function returns the common stub, which is used to POST data to shipcalls via POSTMAN. However,
|
||||
the stub-function is updated with a dynamic ETA in the future, so the POST-request does not fail.
|
||||
|
||||
Also provides a stub arrival_berth_id, so the POST-request succeeds.
|
||||
"""
|
||||
shipcall = {
|
||||
'ship_id': 1,
|
||||
'type': "arrival", #1,
|
||||
'eta': (datetime.datetime.now()+datetime.timedelta(hours=3)).isoformat(),
|
||||
'voyage': '43B',
|
||||
'arrival_berth_id':142,
|
||||
'tug_required': False,
|
||||
'pilot_required': True,
|
||||
'flags': 0,
|
||||
'pier_side': False,
|
||||
'bunkering': True,
|
||||
'recommended_tugs': 2
|
||||
}
|
||||
return shipcall
|
||||
|
||||
|
||||
def get_stub_valid_shipcall_base():
|
||||
tidal_window_from = (datetime.datetime.now()+datetime.timedelta(minutes=15)).isoformat()
|
||||
tidal_window_to = (datetime.datetime.now()+datetime.timedelta(minutes=115)).isoformat()
|
||||
|
||||
shipcall_base = {
|
||||
'ship_id': 1,
|
||||
'voyage': '43B',
|
||||
'tug_required': False,
|
||||
'pilot_required': True,
|
||||
'flags': 0,
|
||||
'pier_side': False,
|
||||
'bunkering': True,
|
||||
'recommended_tugs': 2,
|
||||
'tidal_window_from' : tidal_window_from,
|
||||
'tidal_window_to' : tidal_window_to
|
||||
}
|
||||
return shipcall_base
|
||||
|
||||
def get_stub_valid_shipcall_arrival():
|
||||
eta = (datetime.datetime.now()+datetime.timedelta(minutes=45)).isoformat()
|
||||
|
||||
post_data = {
|
||||
**get_stub_valid_shipcall_base(),
|
||||
**{
|
||||
'type': ShipcallType.arrival.name, #int(ShipcallType.arrival),
|
||||
'eta': eta,
|
||||
'participants':get_stub_list_of_valid_participants(),
|
||||
'arrival_berth_id':139,
|
||||
}
|
||||
}
|
||||
post_data.pop('etd',None)
|
||||
return post_data
|
||||
|
||||
def get_stub_valid_shipcall_departure():
|
||||
etd = (datetime.datetime.now()+datetime.timedelta(minutes=45)).isoformat()
|
||||
|
||||
post_data = {
|
||||
**get_stub_valid_shipcall_base(),
|
||||
**{
|
||||
'type': ShipcallType.departure.name, #int(ShipcallType.departure),
|
||||
'etd': etd,
|
||||
'participants':get_stub_list_of_valid_participants(),
|
||||
'departure_berth_id':139,
|
||||
}
|
||||
}
|
||||
post_data.pop('eta',None)
|
||||
return post_data
|
||||
|
||||
def get_stub_valid_shipcall_shifting():
|
||||
etd = (datetime.datetime.now()+datetime.timedelta(minutes=45)).isoformat()
|
||||
eta = (datetime.datetime.now()+datetime.timedelta(minutes=60)).isoformat()
|
||||
|
||||
post_data = {
|
||||
**get_stub_valid_shipcall_base(),
|
||||
**{
|
||||
'type': ShipcallType.shifting.name, #int(ShipcallType.shifting),
|
||||
'eta': eta,
|
||||
'etd': etd,
|
||||
'participants':get_stub_list_of_valid_participants(),
|
||||
'arrival_berth_id':139,
|
||||
'departure_berth_id':139,
|
||||
}
|
||||
}
|
||||
return post_data
|
||||
|
||||
def get_stub_shipcall_arrival_invalid_missing_eta():
|
||||
post_data = get_stub_valid_shipcall_arrival()
|
||||
post_data.pop("eta", None)
|
||||
return post_data
|
||||
|
||||
def get_stub_shipcall_departure_invalid_missing_etd():
|
||||
post_data = get_stub_valid_shipcall_departure()
|
||||
post_data.pop("etd", None)
|
||||
return post_data
|
||||
|
||||
def get_stub_shipcall_shifting_invalid_missing_eta():
|
||||
post_data = get_stub_valid_shipcall_shifting()
|
||||
post_data.pop("eta", None)
|
||||
return post_data
|
||||
|
||||
def get_stub_shipcall_shifting_invalid_missing_etd():
|
||||
post_data = get_stub_valid_shipcall_shifting()
|
||||
post_data.pop("etd", None)
|
||||
return post_data
|
||||
|
||||
def get_stub_shipcall_arrival_invalid_missing_type():
|
||||
post_data = get_stub_valid_shipcall_arrival()
|
||||
post_data.pop("type", None)
|
||||
return post_data
|
||||
|
||||
def get_stub_valid_ship_loaded_model(post_data):
|
||||
from BreCal.schemas import model
|
||||
loadedModel = model.ShipcallSchema().load(data=post_data, many=False, partial=True)
|
||||
return loadedModel
|
||||
|
||||
12
src/server/BreCal/stubs/times.py
Normal file
12
src/server/BreCal/stubs/times.py
Normal file
@ -0,0 +1,12 @@
|
||||
import datetime
|
||||
from BreCal.schemas import model
|
||||
from BreCal.schemas.model import ParticipantType
|
||||
|
||||
def get_schema_model_stub_departure():
|
||||
schemaModel = {'id': 0, 'eta_berth': None, 'eta_berth_fixed': None, 'etd_berth': datetime.datetime(2024, 9, 7, 15, 12, 58), 'etd_berth_fixed': None, 'lock_time': None, 'lock_time_fixed': None, 'zone_entry': None, 'zone_entry_fixed': None, 'operations_start': None, 'operations_end': None, 'remarks': 'test', 'participant_id': 10, 'berth_id': 146, 'berth_info': '', 'pier_side': None, 'shipcall_id': 115, 'participant_type': ParticipantType.AGENCY, 'ata': None, 'atd': None, 'eta_interval_end': None, 'etd_interval_end': None, 'created': None, 'modified': None}
|
||||
return schemaModel
|
||||
|
||||
def get_schema_model_stub_arrival():
|
||||
schemaModel = {'id': 0, 'eta_berth': datetime.datetime(2024, 9, 7, 15, 12, 58), 'eta_berth_fixed': None, 'etd_berth': None, 'etd_berth_fixed': None, 'lock_time': None, 'lock_time_fixed': None, 'zone_entry': None, 'zone_entry_fixed': None, 'operations_start': None, 'operations_end': None, 'remarks': 'test', 'participant_id': 10, 'berth_id': 146, 'berth_info': '', 'pier_side': None, 'shipcall_id': 115, 'participant_type': ParticipantType.AGENCY, 'ata': None, 'atd': None, 'eta_interval_end': None, 'etd_interval_end': None, 'created': None, 'modified': None}
|
||||
return schemaModel
|
||||
|
||||
@ -5,10 +5,12 @@ users will thereby be able to modify these values
|
||||
import datetime
|
||||
|
||||
from BreCal.stubs import generate_uuid1_int
|
||||
from BreCal.schemas.model import Times
|
||||
from BreCal.schemas.model import Times, ParticipantType
|
||||
from BreCal.database.sql_utils import get_user_data_for_id
|
||||
|
||||
|
||||
def get_times_full_simple():
|
||||
|
||||
def get_times_full_simple(return_dataclass=True):
|
||||
# only used for the stub
|
||||
base_time = datetime.datetime.now()
|
||||
|
||||
@ -27,6 +29,11 @@ def get_times_full_simple():
|
||||
|
||||
zone_entry = etd_berth+datetime.timedelta(hours=0, minutes=15)
|
||||
zone_entry_fixed = False
|
||||
|
||||
ata = eta_berth+datetime.timedelta(hours=0, minutes=15)
|
||||
atd = etd_berth+datetime.timedelta(hours=0, minutes=15)
|
||||
eta_interval_end = eta_berth + datetime.timedelta(hours=0, minutes=25)
|
||||
etd_interval_end = etd_berth + datetime.timedelta(hours=0, minutes=25)
|
||||
|
||||
operations_start = zone_entry+datetime.timedelta(hours=1, minutes=30)
|
||||
operations_end = operations_start+datetime.timedelta(hours=4, minutes=30)
|
||||
@ -44,28 +51,86 @@ def get_times_full_simple():
|
||||
created = datetime.datetime.now()
|
||||
modified = created+datetime.timedelta(seconds=10)
|
||||
|
||||
times = Times(
|
||||
id=times_id,
|
||||
eta_berth=eta_berth,
|
||||
eta_berth_fixed=eta_berth_fixed,
|
||||
etd_berth=etd_berth,
|
||||
etd_berth_fixed=etd_berth_fixed,
|
||||
lock_time=lock_time,
|
||||
lock_time_fixed=lock_time_fixed,
|
||||
zone_entry=zone_entry,
|
||||
zone_entry_fixed=zone_entry_fixed,
|
||||
operations_start=operations_start,
|
||||
operations_end=operations_end,
|
||||
remarks=remarks,
|
||||
participant_id=participant_id,
|
||||
berth_id=berth_id,
|
||||
berth_info=berth_info,
|
||||
pier_side=pier_side,
|
||||
participant_type=participant_type,
|
||||
shipcall_id=shipcall_id,
|
||||
created=created,
|
||||
modified=modified,
|
||||
)
|
||||
if return_dataclass:
|
||||
times = Times(
|
||||
id=times_id,
|
||||
eta_berth=eta_berth,
|
||||
eta_berth_fixed=eta_berth_fixed,
|
||||
etd_berth=etd_berth,
|
||||
etd_berth_fixed=etd_berth_fixed,
|
||||
lock_time=lock_time,
|
||||
lock_time_fixed=lock_time_fixed,
|
||||
zone_entry=zone_entry,
|
||||
zone_entry_fixed=zone_entry_fixed,
|
||||
operations_start=operations_start,
|
||||
operations_end=operations_end,
|
||||
remarks=remarks,
|
||||
participant_id=participant_id,
|
||||
berth_id=berth_id,
|
||||
berth_info=berth_info,
|
||||
pier_side=pier_side,
|
||||
participant_type=participant_type,
|
||||
shipcall_id=shipcall_id,
|
||||
ata=ata,
|
||||
atd=atd,
|
||||
eta_interval_end=eta_interval_end,
|
||||
etd_interval_end=etd_interval_end,
|
||||
created=created,
|
||||
modified=modified,
|
||||
)
|
||||
else:
|
||||
times = dict(
|
||||
id=times_id,
|
||||
eta_berth=eta_berth,
|
||||
eta_berth_fixed=eta_berth_fixed,
|
||||
etd_berth=etd_berth,
|
||||
etd_berth_fixed=etd_berth_fixed,
|
||||
lock_time=lock_time,
|
||||
lock_time_fixed=lock_time_fixed,
|
||||
zone_entry=zone_entry,
|
||||
zone_entry_fixed=zone_entry_fixed,
|
||||
operations_start=operations_start,
|
||||
operations_end=operations_end,
|
||||
remarks=remarks,
|
||||
participant_id=participant_id,
|
||||
berth_id=berth_id,
|
||||
berth_info=berth_info,
|
||||
pier_side=pier_side,
|
||||
participant_type=participant_type,
|
||||
shipcall_id=shipcall_id,
|
||||
ata=ata,
|
||||
atd=atd,
|
||||
eta_interval_end=eta_interval_end,
|
||||
etd_interval_end=etd_interval_end,
|
||||
created=created,
|
||||
modified=modified,)
|
||||
times = {k:v.isoformat() if isinstance(v, datetime.datetime) else v for k,v in times.items()}
|
||||
return times
|
||||
|
||||
def get_valid_stub_times():
|
||||
"""create a stub entry for a times dataset, which is valid"""
|
||||
times_entry = get_times_full_simple(return_dataclass=False)
|
||||
|
||||
times_entry.pop('id',None)
|
||||
times_entry["participant_id"] = 136
|
||||
times_entry["participant_type"] = int(ParticipantType.PILOT)
|
||||
times_entry["shipcall_id"] = 222
|
||||
times_entry["berth_id"] = 143
|
||||
times_entry["remarks"] = "stub entry."
|
||||
return times_entry
|
||||
|
||||
|
||||
def get_valid_stub_times_loaded_model(post_data=None):
|
||||
from BreCal.schemas import model
|
||||
if post_data is None:
|
||||
post_data = get_valid_stub_times()
|
||||
loadedModel = model.TimesSchema().load(data=post_data, many=False, partial=True)
|
||||
return loadedModel
|
||||
|
||||
def get_valid_stub_for_pytests(user_id:int=3):
|
||||
user_data = get_user_data_for_id(user_id=user_id)
|
||||
post_data = get_valid_stub_times()
|
||||
|
||||
content = post_data
|
||||
loadedModel = get_valid_stub_times_loaded_model(post_data=post_data)
|
||||
return user_data, loadedModel, content
|
||||
|
||||
@ -18,6 +18,11 @@ def get_user_simple():
|
||||
|
||||
created = datetime.datetime.now()
|
||||
modified = created+datetime.timedelta(seconds=10)
|
||||
|
||||
notify_email = True
|
||||
notify_whatsapp = True
|
||||
notify_signal = True
|
||||
notify_popup = True
|
||||
|
||||
user = User(
|
||||
user_id,
|
||||
@ -29,6 +34,10 @@ def get_user_simple():
|
||||
user_phone,
|
||||
password_hash,
|
||||
api_key,
|
||||
notify_email,
|
||||
notify_whatsapp,
|
||||
notify_signal,
|
||||
notify_popup,
|
||||
created,
|
||||
modified
|
||||
)
|
||||
|
||||
@ -1,28 +1,146 @@
|
||||
|
||||
####################################### InputValidation #######################################
|
||||
|
||||
import json
|
||||
import datetime
|
||||
from abc import ABC, abstractmethod
|
||||
from BreCal.schemas.model import Ship, Shipcall, Berth, User, Participant
|
||||
from marshmallow import ValidationError
|
||||
from string import ascii_letters, digits
|
||||
|
||||
from BreCal.schemas.model import Ship, Shipcall, Berth, User, Participant, ShipcallType
|
||||
from BreCal.impl.participant import GetParticipant
|
||||
from BreCal.impl.ships import GetShips
|
||||
from BreCal.impl.berths import GetBerths
|
||||
|
||||
from BreCal.database.enums import ParticipantType
|
||||
|
||||
|
||||
from BreCal.validators.input_validation_utils import check_if_user_is_bsmd_type, get_participant_id_dictionary, check_if_ship_id_is_valid, check_if_berth_id_is_valid, check_if_participant_ids_are_valid, get_berth_id_dictionary, get_ship_id_dictionary
|
||||
from BreCal.validators.validation_base_utils import check_if_string_has_special_characters
|
||||
|
||||
def validation_error_default_asserts(response):
|
||||
"""creates assertions, when the response does not fail as expected. This function is extensively used in the input validation pytests"""
|
||||
assert response.status_code == 400
|
||||
assert 'message' in list(response.json().keys())
|
||||
return
|
||||
|
||||
|
||||
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.
|
||||
|
||||
##### Section 1: check user_data #####
|
||||
# check, whether the user belongs to a participant, which is of type ParticipantType.BSMD
|
||||
# as ParticipantType is an IntFlag, a user belonging to multiple groups is properly evaluated.
|
||||
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)}"})
|
||||
|
||||
|
||||
##### Section 3: check content #####
|
||||
# loadedModel fills missing values, sometimes using optional values. Hence, check content
|
||||
|
||||
# the following keys should not be set in a POST-request.
|
||||
for forbidden_key in ["canceled", "evaluation", "evaluation_message"]:
|
||||
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')}"})
|
||||
|
||||
|
||||
##### 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.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!"})
|
||||
elif int(type_)==int(ShipcallType.arrival):
|
||||
eta = loadedModel.get("eta")
|
||||
if (content.get("eta", None) is None):
|
||||
raise ValidationError({"eta":f"providing 'eta' is mandatory. Missing key!"})
|
||||
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' 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):
|
||||
raise ValidationError({"etd":f"providing 'etd' is mandatory. Missing key!"})
|
||||
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' is too far in the past. Incorrect datetime provided."})
|
||||
elif int(type_)==int(ShipcallType.shifting):
|
||||
eta = loadedModel.get("eta")
|
||||
etd = loadedModel.get("etd")
|
||||
# * arrival_berth_id / departure_berth_id (depending on type, see above)
|
||||
if (content.get("eta", None) is None) or (content.get("etd", None) is None):
|
||||
raise ValidationError({"eta_or_etd":f"providing 'eta' and 'etd' is mandatory. Missing one of those keys!"})
|
||||
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' 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' 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' 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
|
||||
|
||||
|
||||
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)
|
||||
|
||||
@ -39,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:
|
||||
"""
|
||||
@ -58,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
|
||||
@ -76,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()
|
||||
@ -84,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'"):
|
||||
@ -107,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
|
||||
]
|
||||
]
|
||||
@ -125,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()
|
||||
@ -136,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()
|
||||
@ -148,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)"""
|
||||
|
||||
@ -162,4 +280,4 @@ class ParticipantValidation(DataclassValidation):
|
||||
]
|
||||
]
|
||||
return all_rules
|
||||
|
||||
|
||||
|
||||
150
src/server/BreCal/validators/input_validation_ship.py
Normal file
150
src/server/BreCal/validators/input_validation_ship.py
Normal file
@ -0,0 +1,150 @@
|
||||
import typing
|
||||
import json
|
||||
import datetime
|
||||
from abc import ABC, abstractmethod
|
||||
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_queries import SQLQuery
|
||||
from BreCal.impl.participant import GetParticipant
|
||||
from BreCal.impl.ships import GetShips
|
||||
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 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
|
||||
import werkzeug
|
||||
|
||||
class InputValidationShip():
|
||||
"""
|
||||
This class combines a complex set of individual input validation functions into a joint object.
|
||||
It uses static methods, so the object does not need to be instantiated, but functions can be called immediately.
|
||||
|
||||
Example:
|
||||
InputValidationShip.evaluate(user_data, loadedModel, content)
|
||||
|
||||
When the data violates one of the rules, a marshmallow.ValidationError is raised, which details the issues.
|
||||
"""
|
||||
def __init__(self) -> None:
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def evaluate_post_data(user_data:dict, loadedModel:dict, content:dict):
|
||||
# 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.
|
||||
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
|
||||
InputValidationShip.check_user_is_bsmd_type(user_data)
|
||||
|
||||
# 2.) ID field is mandatory
|
||||
InputValidationShip.content_contains_ship_id(content)
|
||||
|
||||
# 3.) The IMO number field may not be changed
|
||||
InputValidationShip.put_content_may_not_contain_imo_number(content)
|
||||
|
||||
# 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)
|
||||
is_tug = content.get("is_tug", False) # default to 'False', so the bollard pull entry fails unless the property is also actively set
|
||||
|
||||
if bollard_pull is not None:
|
||||
if not is_tug:
|
||||
raise ValidationError({"bollard_pull":f"'bollard_pull' is only allowed, when a ship is a tug ('is_tug')."})
|
||||
|
||||
if (not (0 < bollard_pull < 500)) & (is_tug):
|
||||
raise ValidationError({"bollard_pull":f"when a ship is a tug, the bollard pull must be 0 < value < 500. "})
|
||||
|
||||
@staticmethod
|
||||
def check_user_is_bsmd_type(user_data:dict):
|
||||
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
|
||||
response, status_code, header = GetShips(token=None)
|
||||
ships = json.loads(response)
|
||||
|
||||
# extract only the 'imo' values
|
||||
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]):
|
||||
"""
|
||||
When calling a delete request for ships, the dataset may not be deleted already. This method
|
||||
makes sure, that the request contains and ID, has a matching entry in the database, and the
|
||||
database entry may not have a deletion state already.
|
||||
"""
|
||||
response, status_code, header = GetShips(token=None)
|
||||
ships = json.loads(response)
|
||||
existing_database_entries = [ship for ship in ships if ship.get("id")==int(ship_id)]
|
||||
if len(existing_database_entries)==0:
|
||||
raise ValidationError({"id":f"Could not find a ship with the specified ID. Selected: {ship_id}"})
|
||||
|
||||
existing_database_entry = existing_database_entries[0]
|
||||
|
||||
deletion_state = existing_database_entry.get("deleted",None)
|
||||
if deletion_state:
|
||||
raise ValidationError({"deleted":f"The selected ship entry is already deleted."})
|
||||
return
|
||||
|
||||
|
||||
|
||||
577
src/server/BreCal/validators/input_validation_shipcall.py
Normal file
577
src/server/BreCal/validators/input_validation_shipcall.py
Normal file
@ -0,0 +1,577 @@
|
||||
import typing
|
||||
import json
|
||||
import datetime
|
||||
from abc import ABC, abstractmethod
|
||||
from marshmallow import ValidationError
|
||||
from string import ascii_letters, digits
|
||||
|
||||
from BreCal.schemas.model import Ship, Shipcall, Berth, User, Participant, ShipcallType, ShipcallParticipantMap
|
||||
from BreCal.impl.participant import GetParticipant
|
||||
from BreCal.impl.ships import GetShips
|
||||
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
|
||||
from BreCal.database.sql_queries import SQLQuery
|
||||
import werkzeug
|
||||
|
||||
|
||||
|
||||
class InputValidationShipcall():
|
||||
"""
|
||||
This class combines a complex set of individual input validation functions into a joint object.
|
||||
It uses static methods, so the object does not need to be instantiated, but functions can be called immediately.
|
||||
|
||||
Example:
|
||||
InputValidationShipcall.evaluate(user_data, loadedModel, content)
|
||||
|
||||
When the data violates one of the rules, a marshmallow.ValidationError is raised, which details the issues.
|
||||
|
||||
"""
|
||||
def __init__(self) -> None:
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
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
|
||||
3. existance of required fields
|
||||
4. reasonable values: validates the values within the Shipcall
|
||||
"""
|
||||
# check for permission (only BSMD-type or AGENT-type participants)
|
||||
InputValidationShipcall.check_user_is_bsmd_or_agent_type(user_data)
|
||||
|
||||
# check references (referred IDs must exist)
|
||||
InputValidationShipcall.check_referenced_ids(loadedModel)
|
||||
|
||||
# POST-request only: check the existance of required fields based on the ShipcallType
|
||||
InputValidationShipcall.check_required_fields_exist_based_on_type(loadedModel, content)
|
||||
|
||||
# POST-request only: check the existance of a participant list, when the user is of type agency
|
||||
InputValidationShipcall.check_participant_list_not_empty_when_user_is_agency(loadedModel)
|
||||
|
||||
# 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
|
||||
2. existance of required fields
|
||||
3. all value-rules of the POST evaluation
|
||||
4. a canceled shipcall may not be changed
|
||||
"""
|
||||
# check, whether the shipcall_id exists
|
||||
InputValidationShipcall.check_shipcall_id_exists(loadedModel)
|
||||
|
||||
# check, whether an agency is listed in the shipcall-participant-map
|
||||
# deprecated: InputValidationShipcall.check_agency_in_shipcall_participant_map(user_data, loadedModel, content)
|
||||
|
||||
# check, whether the user belongs to the assigned agency or to BSMD in case the special flag is enabled
|
||||
InputValidationShipcall.check_user_is_authorized_for_put_request(user_data, loadedModel, content)
|
||||
|
||||
# the ID field is required, all missing fields will be ignored in the update
|
||||
InputValidationShipcall.check_required_fields_of_put_request(content)
|
||||
|
||||
# check the referenced IDs
|
||||
InputValidationShipcall.check_referenced_ids(loadedModel)
|
||||
|
||||
# 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.
|
||||
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.
|
||||
"""
|
||||
# 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.
|
||||
# This function focuses on more complex input validation, which may require more sophisticated methods
|
||||
|
||||
# loadedModel fills missing values, sometimes using optional values. Hence, the 'content'-variable is prefered for some of these verifications
|
||||
# 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')}"})
|
||||
|
||||
# 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."})
|
||||
|
||||
existing_shipcall = None
|
||||
if is_put_data:
|
||||
|
||||
existing_shipcall = InputValidationShipcall.get_shipcall_by_id(loadedModel.get("id"))
|
||||
# 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, existing_shipcall)
|
||||
|
||||
InputValidationShipcall.check_times_are_in_future(loadedModel, content, existing_shipcall)
|
||||
|
||||
# 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):
|
||||
"""
|
||||
When the request is issued by a user of type 'AGENCY', there must be special caution. Agency users cannot self-assign as participants
|
||||
of a shipcall. Further, when no AGENCY is assigned to the shipcall, a PUT-request is not feasible. In those cases, the
|
||||
BSMD must first assign an agency, before a PUT-request can assign further participants.
|
||||
|
||||
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
|
||||
d) the user must be of ParticipantType BSMD or AGENCY
|
||||
|
||||
args:
|
||||
spm_shipcall_data:
|
||||
a list of entries obtained from the ShipcallParticipantMap. These are deserialized dictionaries.
|
||||
e.g., [{'participant_id': 136, 'type': 8}, ]
|
||||
"""
|
||||
raise Exception("deprecated")
|
||||
if spm_shipcall_data is None:
|
||||
# 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?",
|
||||
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)
|
||||
assert isinstance(user_type, ParticipantType)
|
||||
|
||||
# select the matching entries from the ShipcallParticipantMap
|
||||
agency_entries = [spm_entry for spm_entry in spm_shipcall_data if int(spm_entry.get("type"))==int(ParticipantType.AGENCY)] # find all entries of type AGENCY (there should be at max. 1)
|
||||
|
||||
# when the request stems from an AGENCY user, and the user wants to PUT an AGENCY role, the request should fail
|
||||
# boolean: check, whether any of the assigned participants is of type AGENCY
|
||||
types = [participant.get("type",0) for participant in loadedModel["participants"]] # readout the participants from the loadedModel, which shall be assigned by the PUT request
|
||||
any_type_is_agency = any([int(type_) == int(ParticipantType.AGENCY) for type_ in types]) # check, whether *any* of the participants is an agency
|
||||
|
||||
if not ((ParticipantType.AGENCY in user_type) or (ParticipantType.BSMD in user_type)):
|
||||
# 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,
|
||||
|
||||
if (ParticipantType.AGENCY in user_type) & (any_type_is_agency):
|
||||
# self-assignment: agency sets agency participant
|
||||
raise werkzeug.exceptions.Forbidden(f"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.") # Forbidden: 403
|
||||
|
||||
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
|
||||
else:
|
||||
# a matching agency was found: no violation
|
||||
return
|
||||
|
||||
else:
|
||||
# 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):
|
||||
"""
|
||||
check, whether the user belongs to a participant, which is of type ParticipantType.BSMD
|
||||
as ParticipantType is an IntFlag, a user belonging to multiple groups is properly evaluated.
|
||||
"""
|
||||
# use the decoded JWT token and extract the participant type
|
||||
participant_type = get_participant_type_from_user_data(user_data)
|
||||
|
||||
is_bsmd = (ParticipantType.BSMD in participant_type)
|
||||
is_agency = (ParticipantType.AGENCY in participant_type)
|
||||
|
||||
is_bsmd_or_agency = (is_bsmd) or (is_agency)
|
||||
|
||||
if not is_bsmd_or_agency:
|
||||
raise ValidationError({"participant_type":f"current user must be either of participant type BSMD or AGENCY. Cannot post or put shipcalls. Found user data: {user_data} and participant_type: {participant_type}"})
|
||||
return
|
||||
|
||||
@staticmethod
|
||||
def check_referenced_ids(loadedModel):
|
||||
"""
|
||||
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.
|
||||
# a similar logic has already been implemented to the eta/etd or for the operation windows
|
||||
|
||||
# get all IDs from the loadedModel
|
||||
ship_id = loadedModel.get("ship_id", None)
|
||||
arrival_berth_id = loadedModel.get("arrival_berth_id", None)
|
||||
departure_berth_id = loadedModel.get("departure_berth_id", None)
|
||||
participants = loadedModel.get("participants",[])
|
||||
|
||||
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, existing_shipcall:object):
|
||||
if int(loadedModel["type"]) != int(existing_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 get_shipcall_by_id(shipcall_id:int):
|
||||
query = SQLQuery.get_shipcall_by_id()
|
||||
shipcall = execute_sql_query_standalone(query=query, model=Shipcall, param={"id":shipcall_id}, command_type="single")
|
||||
return shipcall
|
||||
|
||||
@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
|
||||
|
||||
@staticmethod
|
||||
def check_required_fields_exist_based_on_type(loadedModel:dict, content:dict):
|
||||
"""
|
||||
depending on the ShipcallType, some fields are *required* in a POST-request
|
||||
"""
|
||||
type_ = loadedModel.get("type", int(ShipcallType.undefined))
|
||||
ship_id = content.get("ship_id", None)
|
||||
eta = content.get("eta", None)
|
||||
etd = content.get("etd", None)
|
||||
arrival_berth_id = content.get("arrival_berth_id", None)
|
||||
departure_berth_id = content.get("departure_berth_id", None)
|
||||
|
||||
if ship_id is None:
|
||||
raise ValidationError({"ship_id":f"providing 'ship_id' is mandatory. Missing key!"})
|
||||
|
||||
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
|
||||
|
||||
@staticmethod
|
||||
def check_times_are_in_future(loadedModel:dict, content:dict, existing_shipcall:object):
|
||||
"""
|
||||
Dates should be in the future. Depending on the ShipcallType, specific values should be checked
|
||||
Performs datetime checks in the loadedModel (datetime.datetime objects).
|
||||
"""
|
||||
# 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
|
||||
type_ = ShipcallType[type_]
|
||||
|
||||
# #TODO: *if* this is a PUT-request, one shall load the existing values from the database, overwrite the none-null
|
||||
# values *and then* perform the validation.
|
||||
# Example: eta and etd are set in the POST-request. User wants to execute a PUT-request with only the etd.
|
||||
# Internally, the backend must still verify, that eta < etd!
|
||||
# Same applies to tidal_window_from & tidal_window_to
|
||||
|
||||
eta = loadedModel.get("eta")
|
||||
etd = loadedModel.get("etd")
|
||||
tidal_window_from = loadedModel.get("tidal_window_from", None)
|
||||
tidal_window_to = loadedModel.get("tidal_window_to", None)
|
||||
existing_eta = None
|
||||
existing_etd = None
|
||||
existing_tidal_window_from = None
|
||||
existing_tidal_window_to = None
|
||||
|
||||
if existing_shipcall is not None:
|
||||
existing_eta = existing_shipcall.eta
|
||||
existing_etd = existing_shipcall.etd
|
||||
existing_tidal_window_from = existing_shipcall.tidal_window_from
|
||||
existing_tidal_window_to = existing_shipcall.tidal_window_to
|
||||
|
||||
if eta != existing_eta or etd != existing_etd:
|
||||
# Estimated arrival or departure times
|
||||
InputValidationShipcall.check_times_in_future_based_on_type(type_, time_ref, eta, etd)
|
||||
|
||||
if tidal_window_from != existing_tidal_window_from or tidal_window_to != existing_tidal_window_to:
|
||||
# Tidal Window
|
||||
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_ref, eta, etd):
|
||||
"""
|
||||
checks, whether the ETA & ETD times are in the future.
|
||||
based on the type, this function checks:
|
||||
arrival: eta
|
||||
departure: etd
|
||||
shifting: eta & etd
|
||||
"""
|
||||
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."})
|
||||
|
||||
if not isinstance(type_, (int, ShipcallType)):
|
||||
type_ = ShipcallType[type_]
|
||||
|
||||
# #TODO: properly handle what happens, when eta or etd (or both) are None
|
||||
if int(type_)==int(ShipcallType.undefined):
|
||||
raise ValidationError({"type":f"providing 'type' is mandatory. Missing key!"})
|
||||
elif int(type_)==int(ShipcallType.arrival):
|
||||
if eta is None: # null values -> no violation
|
||||
return
|
||||
|
||||
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_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
|
||||
return
|
||||
|
||||
if not ((eta is not None) and (etd is not None)):
|
||||
# for PUT-requests, a user could try modifying only 'eta' or only 'etd'. To simplify the
|
||||
# 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_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_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_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_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):
|
||||
"""
|
||||
For each POST request, one of the participants in the list must be assigned as a ParticipantType.AGENCY
|
||||
"""
|
||||
participants = loadedModel.get("participants", [])
|
||||
is_agency_participant = [ParticipantType.AGENCY in ParticipantType(participant.get("type")) for participant in participants]
|
||||
|
||||
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
|
||||
shipcall_id = loadedModel.get("id")
|
||||
|
||||
# get all shipcalls in the database
|
||||
shipcalls = get_shipcall_id_dictionary()
|
||||
|
||||
# search for the matching shipcall in the database
|
||||
shipcall = shipcalls.get(shipcall_id,{})
|
||||
|
||||
# if the *existing* shipcall in the database is canceled, it may not be changed
|
||||
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:
|
||||
raise ValidationError({"id":f"unknown shipcall_id. There are no shipcalls with the ID {shipcall_id}"})
|
||||
return
|
||||
|
||||
@staticmethod
|
||||
def check_user_is_authorized_for_put_request(user_data:dict, loadedModel:dict, content:dict, shipcall_participant_map:typing.Optional[list[ShipcallParticipantMap]]=None):
|
||||
"""
|
||||
This method verifies, whether a user is authorized to create a PUT-request for shipcalls.
|
||||
To be authorized, a user should either
|
||||
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, 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
|
||||
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)
|
||||
|
||||
# get the shipcall id
|
||||
shipcall_id = loadedModel.get("id")
|
||||
|
||||
### 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)})
|
||||
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:
|
||||
# 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]
|
||||
|
||||
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)}"
|
||||
|
||||
# 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 = (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
|
||||
|
||||
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: {assigned_agency.flags}") # Forbidden: 403
|
||||
|
||||
else:
|
||||
# when there is no assigned agency, only BSMD users can update the shipcall
|
||||
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
|
||||
|
||||
592
src/server/BreCal/validators/input_validation_times.py
Normal file
592
src/server/BreCal/validators/input_validation_times.py
Normal file
@ -0,0 +1,592 @@
|
||||
import typing
|
||||
import json
|
||||
import datetime
|
||||
from abc import ABC, abstractmethod
|
||||
from marshmallow import ValidationError
|
||||
from string import ascii_letters, digits
|
||||
|
||||
from BreCal.schemas.model import Ship, Shipcall, Berth, User, Participant, ShipcallType, Times
|
||||
from BreCal.impl.participant import GetParticipant
|
||||
from BreCal.impl.ships import GetShips
|
||||
from BreCal.impl.berths import GetBerths
|
||||
from BreCal.impl.times import GetTimes
|
||||
|
||||
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
|
||||
|
||||
def 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'.
|
||||
"""
|
||||
post_data_type_dependent_required_fields_dict = {
|
||||
ShipcallType.arrival:{
|
||||
ParticipantType.undefined:[], # should not be set in POST requests
|
||||
ParticipantType.BSMD:[], # should not be set in POST requests
|
||||
ParticipantType.TERMINAL:[],
|
||||
ParticipantType.AGENCY:[],
|
||||
ParticipantType.MOORING:[],
|
||||
ParticipantType.PILOT:[],
|
||||
ParticipantType.PORT_ADMINISTRATION:[],
|
||||
ParticipantType.TUG:[],
|
||||
},
|
||||
ShipcallType.departure:{
|
||||
ParticipantType.undefined:[], # should not be set in POST requests
|
||||
ParticipantType.BSMD:[], # should not be set in POST requests
|
||||
ParticipantType.TERMINAL:[],
|
||||
ParticipantType.AGENCY:[],
|
||||
ParticipantType.MOORING:[],
|
||||
ParticipantType.PILOT:[],
|
||||
ParticipantType.PORT_ADMINISTRATION:[],
|
||||
ParticipantType.TUG:[],
|
||||
},
|
||||
ShipcallType.shifting:{
|
||||
ParticipantType.undefined:[], # should not be set in POST requests
|
||||
ParticipantType.BSMD:[], # should not be set in POST requests
|
||||
ParticipantType.TERMINAL:[],
|
||||
ParticipantType.AGENCY:[],
|
||||
ParticipantType.MOORING:[],
|
||||
ParticipantType.PILOT:[],
|
||||
ParticipantType.PORT_ADMINISTRATION:[],
|
||||
ParticipantType.TUG:[],
|
||||
},
|
||||
}
|
||||
return post_data_type_dependent_required_fields_dict
|
||||
|
||||
|
||||
|
||||
class InputValidationTimes():
|
||||
"""
|
||||
This class combines a complex set of individual input validation functions into a joint object.
|
||||
It uses static methods, so the object does not need to be instantiated, but functions can be called immediately.
|
||||
|
||||
Example:
|
||||
InputValidationTimes.evaluate(user_data, loadedModel, content)
|
||||
|
||||
When the data violates one of the rules, a marshmallow.ValidationError is raised, which details the issues.
|
||||
"""
|
||||
def __init__(self) -> None:
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def evaluate_post_data(user_data:dict, loadedModel:dict, content:dict):
|
||||
# 0.) Check for the presence of required fields
|
||||
InputValidationTimes.check_times_required_fields_post_data(loadedModel, content)
|
||||
|
||||
# 1.) datasets may only be created, if the current user fits the appropriate type in the ShipcallParticipantMap
|
||||
InputValidationTimes.check_if_user_fits_shipcall_participant_map(user_data, loadedModel, content)
|
||||
|
||||
# 2.) datasets may only be created, if the respective participant type did not already create one.
|
||||
InputValidationTimes.check_if_entry_already_exists_for_participant_type(user_data, loadedModel, content)
|
||||
|
||||
# 3.) Reference checking
|
||||
InputValidationTimes.check_dataset_references(content)
|
||||
|
||||
# 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)
|
||||
|
||||
# 3.) Reference checking
|
||||
InputValidationTimes.check_dataset_references(content)
|
||||
|
||||
# 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
|
||||
if times_id is None:
|
||||
raise ValidationError({"id":"no times id provided"})
|
||||
times_id = int(times_id) if not isinstance(times_id, int) else times_id
|
||||
|
||||
# 1.) The dataset entry may not be deleted already
|
||||
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
|
||||
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.
|
||||
|
||||
To identify deleted entries, query from the database and check, whether there is a match for the times id.
|
||||
|
||||
"""
|
||||
# perform an SQL query. Creates a pooled connection internally, queries the database, then closes the connection.
|
||||
query = "SELECT shipcall_id FROM times WHERE id = ?id?"
|
||||
pdata = execute_sql_query_standalone(query=query, param={"id":times_id}, pooledConnection=None)
|
||||
|
||||
if len(pdata)==0:
|
||||
raise ValidationError({"deleted":f"The selected time entry is already deleted. ID: {times_id}"})
|
||||
return
|
||||
|
||||
@staticmethod
|
||||
def check_user_is_not_bsmd_type(user_data:dict):
|
||||
"""a new dataset may only be created by a user who is *not* belonging to participant group BSMD"""
|
||||
is_bsmd = check_if_user_is_bsmd_type(user_data)
|
||||
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 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.
|
||||
|
||||
# ensure loadedModel["participant_type"] is of type ParticipantType
|
||||
if not isinstance(loadedModel["participant_type"], ParticipantType):
|
||||
loadedModel["participant_type"] = ParticipantType(loadedModel["participant_type"])
|
||||
|
||||
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 ("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 ("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):
|
||||
"""
|
||||
When IDs are referenced, they must exist in the database. This method individually validates the existance of referred
|
||||
berth ID, participant IDs and shipcall ID.
|
||||
|
||||
Note: whenever an ID is 'None', there is no exception, because a different method is supposed to capture non-existant mandatory fields.
|
||||
"""
|
||||
# extract the IDs
|
||||
berth_id, shipcall_id, participant_id = content.get("berth_id"), content.get("shipcall_id"), content.get("participant_id")
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
"""
|
||||
participant_type = loadedModel["participant_type"]
|
||||
shipcall_id = loadedModel["shipcall_id"]
|
||||
|
||||
# build a dictionary of id:item pairs, so one can select the respective participant
|
||||
# must look-up the shipcall_type based on the shipcall_id
|
||||
shipcalls = get_shipcall_id_dictionary()
|
||||
shipcall_type = ShipcallType[shipcalls.get(shipcall_id,{}).get("type",ShipcallType.undefined.name)]
|
||||
|
||||
if (participant_type is None) or (int(shipcall_type) == int(ShipcallType.undefined)):
|
||||
raise ValidationError({"required_fields":f"At least one of the required fields is missing. Missing: 'participant_type' or 'shipcall_type'"})
|
||||
|
||||
|
||||
# build a list of required fields based on shipcall and participant type, as well as type-independent fields
|
||||
independent_required_fields = InputValidationTimes.get_post_data_type_independent_fields()
|
||||
dependent_required_fields = InputValidationTimes.get_post_data_type_dependent_fields(shipcall_type, participant_type)
|
||||
|
||||
required_fields = independent_required_fields + dependent_required_fields
|
||||
|
||||
# generate a list of booleans, where each element shows, whether one of the required fields is missing.
|
||||
missing_required_fields = [
|
||||
content.get(field,None) is None for field in required_fields
|
||||
]
|
||||
|
||||
if any(missing_required_fields):
|
||||
# create a tuple of (field_key, bool) to describe to a user, which one of the fields may be missing
|
||||
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]:
|
||||
"""
|
||||
Independent of the ShipcallType and ParticipantType, any POST request for times should always include the default fields.
|
||||
"""
|
||||
independent_required_fields = [
|
||||
"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').
|
||||
|
||||
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.
|
||||
if not isinstance(shipcall_type, ShipcallType):
|
||||
shipcall_type = ShipcallType(shipcall_type)
|
||||
if not isinstance(participant_type, ParticipantType):
|
||||
participant_type = ParticipantType(participant_type)
|
||||
|
||||
# build a dictionary, which maps shipcall type and participant type to a list of fields
|
||||
dependent_required_fields_dict = build_post_data_type_dependent_required_fields_dict()
|
||||
|
||||
# select shipcall type & participant type
|
||||
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):
|
||||
"""
|
||||
a new dataset may only be created, if the user belongs to the participant group (participant_id),
|
||||
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.
|
||||
|
||||
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:
|
||||
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.
|
||||
"""
|
||||
### TIMES DATASET (ShipcallParticipantMap) ###
|
||||
# identify shipcall_id
|
||||
shipcall_id = loadedModel["shipcall_id"]
|
||||
DATASET_participant_type = ParticipantType(loadedModel["participant_type"]) if not isinstance(loadedModel["participant_type"],ParticipantType) else loadedModel["participant_type"]
|
||||
|
||||
# get ShipcallParticipantMap for the shipcall_id
|
||||
if spm_shipcall_data is None:
|
||||
# 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?)",
|
||||
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"))
|
||||
|
||||
if (ParticipantType.AGENCY in DATASET_participant_type):
|
||||
special_case__bsmd_may_edit_agency_dataset = InputValidationTimes.check_if_bsmd_may_edit_agency_dataset(user_participant_id, DATASET_participant_id, DATASET_participant_type)
|
||||
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
|
||||
for spm in spm_shipcall_data
|
||||
if spm.get("participant_id")==user_participant_id
|
||||
]
|
||||
|
||||
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"""
|
||||
# determine participant_type and shipcall_id from the loadedModel
|
||||
participant_type = loadedModel["participant_type"]
|
||||
if not isinstance(participant_type, ParticipantType): # ensure the correct data type
|
||||
participant_type = ParticipantType(participant_type)
|
||||
shipcall_id = loadedModel["shipcall_id"]
|
||||
|
||||
# get all times entries of the shipcall_id from the database
|
||||
times, status_code, headers = GetTimes(options={"shipcall_id":shipcall_id})
|
||||
times = json.loads(times)
|
||||
|
||||
# check, if there is already a dataset for the participant type
|
||||
participant_type_exists_already = any([ParticipantType(time_.get("participant_type",0)) in participant_type for time_ in times])
|
||||
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):
|
||||
"""
|
||||
This method checks, whether a user belongs to the same participant_id, as the dataset entry refers to.
|
||||
It is used in, both, PUT requests and DELETE requests, but uses different arguments to determine the matching
|
||||
time dataset entry.
|
||||
|
||||
PUT:
|
||||
loadedModel is unbundled to identify the matching times entry by the shipcall id
|
||||
|
||||
DELETE:
|
||||
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.
|
||||
"""
|
||||
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."
|
||||
|
||||
# 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, times_assigned_participant) = InputValidationTimes.prepare_authority_check_for_put_request(loadedModel)
|
||||
else:
|
||||
loadedModel = get_times_data_for_id(times_id)
|
||||
|
||||
# commonly used in the DELETE-request
|
||||
if times_id is not None:
|
||||
# #TODO_refactor:
|
||||
(shipcall_id, times_assigned_participant) = InputValidationTimes.prepare_authority_check_for_delete_request(times_id, pdata)
|
||||
|
||||
# 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)
|
||||
|
||||
# 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}"})
|
||||
|
||||
@staticmethod
|
||||
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},
|
||||
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:
|
||||
"""
|
||||
This method determines, whether a BSMD user is allowed to edit an AGENCY dataset.
|
||||
When the dataset does not refer to an agency, the method is not applicable (returns False).
|
||||
|
||||
If it is applicable,
|
||||
a) find out, whether the assigned participant (AGENCY) has the BSMD flag
|
||||
b) find out, whether the user is of type BSMD
|
||||
If both is true, return True
|
||||
|
||||
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
|
||||
"""
|
||||
# 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.
|
||||
"""
|
||||
# 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",
|
||||
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'.
|
||||
"""
|
||||
post_data_type_dependent_required_fields_dict = {
|
||||
ShipcallType.arrival:{
|
||||
ParticipantType.undefined:[], # should not be set in POST requests
|
||||
ParticipantType.BSMD:[], # should not be set in POST requests
|
||||
ParticipantType.TERMINAL:[],
|
||||
ParticipantType.AGENCY:["eta_berth"],
|
||||
ParticipantType.MOORING:["eta_berth"],
|
||||
ParticipantType.PILOT:["eta_berth"],
|
||||
ParticipantType.PORT_ADMINISTRATION:["eta_berth"],
|
||||
ParticipantType.TUG:["eta_berth"],
|
||||
},
|
||||
ShipcallType.departure:{
|
||||
ParticipantType.undefined:[], # should not be set in POST requests
|
||||
ParticipantType.BSMD:[], # should not be set in POST requests
|
||||
ParticipantType.TERMINAL:[],
|
||||
ParticipantType.AGENCY:["etd_berth"],
|
||||
ParticipantType.MOORING:["etd_berth"],
|
||||
ParticipantType.PILOT:["etd_berth"],
|
||||
ParticipantType.PORT_ADMINISTRATION:["etd_berth"],
|
||||
ParticipantType.TUG:["etd_berth"],
|
||||
},
|
||||
ShipcallType.shifting:{
|
||||
ParticipantType.undefined:[], # should not be set in POST requests
|
||||
ParticipantType.BSMD:[], # should not be set in POST requests
|
||||
ParticipantType.TERMINAL:[],
|
||||
ParticipantType.AGENCY:["etd_berth"],
|
||||
ParticipantType.MOORING:["etd_berth"],
|
||||
ParticipantType.PILOT:["etd_berth"],
|
||||
ParticipantType.PORT_ADMINISTRATION:["etd_berth"],
|
||||
ParticipantType.TUG:["etd_berth"],
|
||||
},
|
||||
}
|
||||
return post_data_type_dependent_required_fields_dict
|
||||
227
src/server/BreCal/validators/input_validation_utils.py
Normal file
227
src/server/BreCal/validators/input_validation_utils.py
Normal file
@ -0,0 +1,227 @@
|
||||
import logging
|
||||
import json
|
||||
from collections import Counter
|
||||
|
||||
from BreCal.impl.participant import GetParticipant
|
||||
from BreCal.impl.ships import GetShips
|
||||
from BreCal.impl.berths import GetBerths
|
||||
from BreCal.impl.shipcalls import GetShipcalls
|
||||
|
||||
from BreCal.database.enums import ParticipantType
|
||||
from marshmallow import ValidationError
|
||||
|
||||
def get_participant_id_dictionary():
|
||||
"""
|
||||
get a dictionary of all participants, where the key is the participant's id, and the value is a dictionary
|
||||
of common participant data (not a data model).
|
||||
"""
|
||||
# get all participants
|
||||
response,status_code,header = GetParticipant(options={})
|
||||
|
||||
# build a dictionary of id:item pairs, so one can select the respective participant
|
||||
participants = json.loads(response)
|
||||
participants = {items.get("id"):{**items, "participant_id":items.get("id")} for items in participants}
|
||||
assert all(["participant_id" in participant for k,participant in participants.items()])
|
||||
return participants
|
||||
|
||||
def get_berth_id_dictionary():
|
||||
# get all berths
|
||||
response,status_code,header = GetBerths(token=None)
|
||||
|
||||
# build a dictionary of id:item pairs, so one can select the respective participant
|
||||
berths = json.loads(response)
|
||||
berths = {items.get("id"):items for items in berths}
|
||||
return berths
|
||||
|
||||
def get_ship_id_dictionary():
|
||||
# get all ships
|
||||
response,status_code,header = GetShips(token=None)
|
||||
|
||||
# build a dictionary of id:item pairs, so one can select the respective participant
|
||||
ships = json.loads(response)
|
||||
ships = {items.get("id"):items for items in ships}
|
||||
return ships
|
||||
|
||||
def get_shipcall_id_dictionary():
|
||||
# get all ships
|
||||
response,status_code,header = GetShipcalls(options={'past_days':30000})
|
||||
|
||||
# build a dictionary of id:item pairs, so one can select the respective participant
|
||||
shipcalls = json.loads(response)
|
||||
shipcalls = {items.get("id"):items for items in shipcalls}
|
||||
return shipcalls
|
||||
|
||||
def get_participant_type_from_participant_id(participant_id:int)->ParticipantType:
|
||||
# build a dictionary of id:item pairs, so one can select the respective participant
|
||||
participants = get_participant_id_dictionary()
|
||||
participant = participants.get(participant_id,{})
|
||||
participant_type = ParticipantType(participant.get("type",0))
|
||||
return participant_type
|
||||
|
||||
|
||||
def get_participant_type_from_user_data(user_data:dict)->ParticipantType:
|
||||
# user_data = decode token
|
||||
participant_id = user_data.get("participant_id")
|
||||
|
||||
# builds an internal dictionary of id:item pairs, so one can select the respective participant
|
||||
participant_type = get_participant_type_from_participant_id(participant_id)
|
||||
return participant_type
|
||||
|
||||
def check_if_user_is_bsmd_type(user_data:dict)->bool:
|
||||
"""
|
||||
given a dictionary of user data, determine the respective participant id and read, whether
|
||||
that participant is a .BSMD-type
|
||||
|
||||
Note: ParticipantType is an IntFlag.
|
||||
Hence, ParticipantType(1) is ParticipantType.BSMD,
|
||||
and ParticipantType(7) is [ParticipantType.BSMD, ParticipantType.TERMINAL, ParticipantType.PILOT]
|
||||
|
||||
both would return 'True'
|
||||
|
||||
returns: boolean. Whether the participant id is a .BSMD type element
|
||||
"""
|
||||
# use the decoded JWT token and extract the participant type
|
||||
participant_type = get_participant_type_from_user_data(user_data)
|
||||
|
||||
# boolean check: is the participant of type .BSMD?
|
||||
is_bsmd = ParticipantType.BSMD in participant_type
|
||||
return is_bsmd
|
||||
|
||||
def check_if_user_has_bsmd_flag(user_data:dict)->bool:
|
||||
"""
|
||||
given a dictionary of user data, determine the respective participant id and read, whether
|
||||
that participant is a .BSMD-type
|
||||
|
||||
Note: ParticipantType is an IntFlag.
|
||||
Hence, ParticipantType(1) is ParticipantType.BSMD,
|
||||
and ParticipantType(7) is [ParticipantType.BSMD, ParticipantType.TERMINAL, ParticipantType.PILOT]
|
||||
|
||||
both would return 'True'
|
||||
|
||||
returns: boolean. Whether the participant id is a .BSMD type element
|
||||
"""
|
||||
# use the decoded JWT token and extract the participant type
|
||||
participant_type = get_participant_type_from_user_data(user_data)
|
||||
|
||||
# boolean check: is the participant of type .BSMD?
|
||||
is_bsmd = ParticipantType.BSMD in participant_type
|
||||
return is_bsmd
|
||||
|
||||
|
||||
def check_if_ship_id_is_valid(ship_id):
|
||||
"""check, whether the provided ID is valid. If it is 'None', it will be considered valid. This is, because a shipcall POST-request, does not have to include all IDs at once"""
|
||||
if ship_id is None:
|
||||
return True
|
||||
|
||||
# build a dictionary of id:item pairs, so one can select the respective participant
|
||||
ships = get_ship_id_dictionary()
|
||||
|
||||
# boolean check
|
||||
ship_id_is_valid = ship_id in list(ships.keys())
|
||||
return ship_id_is_valid
|
||||
|
||||
def check_if_berth_id_is_valid(berth_id):
|
||||
"""check, whether the provided ID is valid. If it is 'None', it will be considered valid. This is, because a shipcall POST-request, does not have to include all IDs at once"""
|
||||
if berth_id is None:
|
||||
return True
|
||||
|
||||
# build a dictionary of id:item pairs, so one can select the respective participant
|
||||
berths = get_berth_id_dictionary()
|
||||
|
||||
# boolean check
|
||||
berth_id_is_valid = berth_id in list(berths.keys())
|
||||
return berth_id_is_valid
|
||||
|
||||
def check_if_shipcall_id_is_valid(shipcall_id:int):
|
||||
"""check, whether the provided ID is valid. If it is 'None', it will be considered valid. This is, because a request, may not have to include all IDs at once"""
|
||||
if shipcall_id is None:
|
||||
return True
|
||||
|
||||
# build a dictionary of id:item pairs, so one can select the respective participant
|
||||
shipcalls = get_shipcall_id_dictionary()
|
||||
|
||||
# boolean check
|
||||
shipcall_id_is_valid = shipcall_id in list(shipcalls.keys())
|
||||
return shipcall_id_is_valid
|
||||
|
||||
import typing
|
||||
def check_if_participant_id_is_valid_standalone(participant_id:int, participant_type:typing.Optional[ParticipantType]):
|
||||
"""check, whether the provided ID is valid. If it is 'None', it will be considered valid. This is, because a request, may not have to include all IDs at once"""
|
||||
if participant_id is None:
|
||||
return True
|
||||
|
||||
# build a dictionary of id:item pairs, so one can select the respective participant
|
||||
participants = get_participant_id_dictionary()
|
||||
|
||||
# boolean check
|
||||
participant_id_is_valid = participant_id in list(participants.keys())
|
||||
|
||||
if participant_type is not None:
|
||||
if participant_id not in list(participants.keys()):
|
||||
raise ValidationError({"participant_id":f"the provided participant_id {participant_id} does not exist in the database."})
|
||||
|
||||
# IntFlag object
|
||||
participant_type_in_db = ParticipantType(int(participants.get(participant_id).get("type", ParticipantType.undefined)))
|
||||
assert isinstance(participant_type_in_db, ParticipantType), f"{type(participant_type_in_db)}"
|
||||
|
||||
# IntFlag comparison. A user may be assigned as a pilot, but the participant may be multiple roles
|
||||
participant_type_matches_db = (participant_type in participant_type_in_db)
|
||||
|
||||
participant_is_valid = (participant_id_is_valid and participant_type_matches_db)
|
||||
return participant_is_valid
|
||||
else:
|
||||
# when the participant_type is not provided, only evaluate the ID
|
||||
return participant_id_is_valid
|
||||
|
||||
def check_if_participant_id_is_valid(participant:dict):
|
||||
"""
|
||||
check, whether the provided ID is valid. If it is 'None', it will be considered valid. This is, because a shipcall POST-request, does not have to include all IDs at once
|
||||
|
||||
Following the common BreCal.schemas.model.ParticipantAssignmentSchema, a participant dictionary contains the keys:
|
||||
'participant_id' : int
|
||||
'type' : ParticipantType
|
||||
"""
|
||||
# #TODO1: Daniel Schick: 'types may only appear once and must not include type "BSMD"'
|
||||
participant_id = participant.get("participant_id", None)
|
||||
participant_type = ParticipantType(int(participant.get("type", ParticipantType.undefined)))
|
||||
participant_id_is_valid = check_if_participant_id_is_valid_standalone(participant_id, participant_type)
|
||||
return participant_id_is_valid
|
||||
|
||||
def check_if_participant_ids_are_valid(participants:list[dict]):
|
||||
"""
|
||||
|
||||
args:
|
||||
participants (list of participant-elements)
|
||||
Following the common BreCal.schemas.model.ParticipantAssignmentSchema, a participant dictionary contains the keys:
|
||||
'participant_id' : int
|
||||
'type' : ParticipantType
|
||||
"""
|
||||
# empty list -> invalid
|
||||
if participants is None:
|
||||
return False
|
||||
|
||||
# check each participant id individually
|
||||
valid_participant_ids = [check_if_participant_id_is_valid(participant) for participant in participants]
|
||||
|
||||
# boolean check, whether all participant ids are valid
|
||||
return all(valid_participant_ids)
|
||||
|
||||
def check_if_participant_ids_and_types_are_valid(participants:list[dict[str,int]]):
|
||||
# creates a Counter object, which counts the number of unique elements
|
||||
# key of counter: type, value of counter: number of listings in 'participants'
|
||||
# e.g., {1: 4, 2: 1, 8: 1} (type 1 occurs 4 times in this example)
|
||||
counter_type = Counter([participant.get("type") for participant in participants])
|
||||
counter_id = Counter([participant.get("type") for participant in participants])
|
||||
|
||||
# obtains the maximum count from the counter's values
|
||||
max_count_type = max(list(counter_type.values())) if len(list(counter_type.values()))>0 else 0
|
||||
max_count_ids = max(list(counter_id.values())) if len(list(counter_id.values()))>0 else 0
|
||||
|
||||
# when 0 or 1 count for the participant ids or types, return true. Return false, when there is more than one entry.
|
||||
return max_count_type <= 1 and max_count_ids <= 1
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@ -2,6 +2,8 @@ import datetime
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
|
||||
from marshmallow import ValidationError
|
||||
|
||||
def validate_time_exceeds_threshold(value:datetime.datetime, seconds:int=60, minutes:int=60, hours:int=24, days:int=30, months:int=12)->bool:
|
||||
"""returns a boolean when the input value is very distant in the future. The parameters provide the threshold"""
|
||||
# time difference in seconds. Positive: in the future, Negative: in the past
|
||||
@ -10,6 +12,37 @@ def validate_time_exceeds_threshold(value:datetime.datetime, seconds:int=60, min
|
||||
threshold = seconds*minutes*hours*days*months
|
||||
return time_>=threshold
|
||||
|
||||
def validate_time_is_in_future(value:datetime.datetime):
|
||||
"""returns a boolean when the input value is in the future."""
|
||||
current_time = datetime.datetime.now()
|
||||
return value >= current_time
|
||||
|
||||
def validate_time_is_in_not_too_distant_future(raise_validation_error:bool, value:datetime.datetime, seconds:int=60, minutes:int=60, hours:int=24, days:int=30, months:int=12)->bool:
|
||||
"""
|
||||
A time entry is considerd valid, when it meets the following condition(s):
|
||||
a) value is not too distant (e.g., at max. 1 year in the future)
|
||||
Previous variants of this function also included validating that a time must be in the future. This is deprecated.
|
||||
|
||||
When the value is 'None', the validation will be skipped. A ValidationError is never issued, but the method returns 'False'.
|
||||
|
||||
options:
|
||||
raise_validation_error: boolean. If set to True, this method issues a marshmallow.ValidationError, when the conditions fail.
|
||||
"""
|
||||
if value is None:
|
||||
return False
|
||||
|
||||
# is_in_future = validate_time_is_in_future(value)
|
||||
is_too_distant = validate_time_exceeds_threshold(value, seconds, minutes, hours, days, months)
|
||||
|
||||
if raise_validation_error:
|
||||
#if not is_in_future:
|
||||
#raise ValidationError({"any_date":f"The provided value must be in the future. Current Time: {datetime.datetime.now()}, Value: {value}"})
|
||||
|
||||
if is_too_distant:
|
||||
raise ValidationError({"any_date":f"The provided value is in the too distant future and exceeds a threshold for 'reasonable' entries. Found: {value}"})
|
||||
|
||||
return (not is_too_distant) # & is_in_future
|
||||
|
||||
class TimeLogic():
|
||||
def __init__(self):
|
||||
return
|
||||
|
||||
26
src/server/BreCal/validators/validation_base_utils.py
Normal file
26
src/server/BreCal/validators/validation_base_utils.py
Normal file
@ -0,0 +1,26 @@
|
||||
import typing
|
||||
from string import ascii_letters, digits
|
||||
|
||||
|
||||
def check_if_string_has_special_characters(text:typing.Optional[str]):
|
||||
"""
|
||||
check, whether there are any characters within the provided string, which are not found in the ascii letters or digits
|
||||
ascii_letters: abcd (...) and ABCD (...)
|
||||
digits: 0123 (...)
|
||||
|
||||
Source: https://stackoverflow.com/questions/57062794/is-there-a-way-to-check-if-a-string-contains-special-characters
|
||||
User: https://stackoverflow.com/users/10035985/andrej-kesely
|
||||
returns bool
|
||||
"""
|
||||
if text is None:
|
||||
return False
|
||||
return bool(set(text).difference(ascii_letters + digits + ' '))
|
||||
|
||||
|
||||
def check_if_int_is_valid_flag(value, enum_object):
|
||||
if value is None: # when no value is provided, the value is considered valid.
|
||||
return False
|
||||
|
||||
# e.g., when an IntFlag has the values 1,2,4; the maximum valid value is 7
|
||||
max_int = sum([int(val) for val in list(enum_object._value2member_map_.values())])
|
||||
return 0 < value <= max_int
|
||||
87
src/server/BreCal/validators/validation_error.py
Normal file
87
src/server/BreCal/validators/validation_error.py
Normal file
@ -0,0 +1,87 @@
|
||||
import logging
|
||||
import typing
|
||||
import json
|
||||
import sys
|
||||
from marshmallow import ValidationError
|
||||
from werkzeug.exceptions import Forbidden
|
||||
|
||||
|
||||
def create_default_json_response_format(error_field:str, error_description:str):
|
||||
"""
|
||||
The default format of a JSON response for exceptions is:
|
||||
{
|
||||
"error_field":str,
|
||||
"error_description":str
|
||||
}
|
||||
"""
|
||||
json_response = {
|
||||
"error_field":error_field,
|
||||
"error_description":error_description
|
||||
}
|
||||
return json_response
|
||||
|
||||
def unbundle_(errors, unbundled=[]):
|
||||
"""
|
||||
unbundles a dictionary entry into separate items and appends them to the list {unbundled}.
|
||||
Example:
|
||||
errors = {"key1":{"keya":"keyb","keyc":{"keyc12":12}}}
|
||||
Returns:
|
||||
[{'error_field':'keya', 'error_description':['keyb']}, {'error_field':'keyc12', 'error_description':[12]}]
|
||||
As can be seen, only the subkeys and their respective value are received. Each value is *always* a list.
|
||||
"""
|
||||
{k:unbundle_(v,unbundled=unbundled) if isinstance(v,dict) else unbundled.append({"error_field":k, "error_description":v[0] if isinstance(v,list) else str(v)}) for k,v in errors.items()}
|
||||
return
|
||||
|
||||
def unbundle_validation_error_message(message):
|
||||
"""
|
||||
inputs:
|
||||
message: ValidationError.messages object. A str, list or dictionary
|
||||
"""
|
||||
unbundled = []
|
||||
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 = " " . join([unb["error_description"] for unb in unbundled])
|
||||
else:
|
||||
error_field = "ValidationError"
|
||||
error_description = "unknown validation error"
|
||||
return (error_field, error_description)
|
||||
|
||||
def create_validation_error_response(ex:ValidationError, status_code:int=400, create_log:bool=True)->typing.Tuple[str,int]:
|
||||
# unbundles ValidationError into a dictionary of {'error_field':str, 'error_description':str}-format
|
||||
message = ex.messages
|
||||
(error_field, error_description) = unbundle_validation_error_message(message)
|
||||
json_response = create_default_json_response_format(error_field=error_field, error_description=error_description)
|
||||
|
||||
# json.dumps with default=str automatically converts non-serializable values to strings. Hence, datetime objects (which are not)
|
||||
# natively serializable are properly serialized.
|
||||
serialized_response = json.dumps(json_response, default=str)
|
||||
|
||||
if create_log:
|
||||
logging.warning(ex) if ex is not None else logging.warning(message)
|
||||
print(ex) if ex is not None else print(message)
|
||||
return (serialized_response, status_code)
|
||||
|
||||
def create_werkzeug_error_response(ex:Forbidden, status_code:int=403, create_log:bool=True)->typing.Tuple[str,int]:
|
||||
# json.dumps with default=str automatically converts non-serializable values to strings. Hence, datetime objects (which are not)
|
||||
# natively serializable are properly serialized.
|
||||
message = ex.description
|
||||
json_response = create_default_json_response_format(error_field=str(repr(ex)), error_description=message)
|
||||
serialized_response = json.dumps(json_response, default=str)
|
||||
|
||||
if create_log:
|
||||
logging.warning(ex) if ex is not None else logging.warning(message)
|
||||
print(ex) if ex is not None else print(message)
|
||||
return serialized_response, status_code
|
||||
|
||||
def create_dynamic_exception_response(ex, status_code:int=400, message:typing.Optional[str]=None, create_log:bool=True):
|
||||
message = repr(ex) if message is None else message
|
||||
json_response = create_default_json_response_format(error_field="Exception", error_description=message)
|
||||
json_response["message"] = "call failed"
|
||||
|
||||
serialized_response = json.dumps(json_response, default=str)
|
||||
|
||||
if create_log:
|
||||
logging.warning(ex) if ex is not None else logging.warning(message)
|
||||
print(ex) if ex is not None else print(message)
|
||||
return (serialized_response, status_code)
|
||||
@ -27,7 +27,7 @@ error_message_dict = {
|
||||
# 0002 A+B+C
|
||||
"validation_rule_fct_shipcall_incoming_participants_disagree_on_eta":"There are deviating times between agency, mooring, port authority, pilot and tug for the estimated time of arrival (ETA) {Rule #0002A}",
|
||||
"validation_rule_fct_shipcall_outgoing_participants_disagree_on_etd":"There are deviating times between agency, mooring, port authority, pilot and tug for the estimated time of departure (ETD) {Rule #0002B}",
|
||||
"validation_rule_fct_shipcall_shifting_participants_disagree_on_eta_or_etd":"There are deviating times between agency, mooring, port authority, pilot and tug for ETA and ETD {Rule #0002C}",
|
||||
"validation_rule_fct_shipcall_shifting_participants_disagree_on_etd":"There are deviating times between agency, mooring, port authority, pilot and tug for the estimated time of departure (ETD) {Rule #0002C}",
|
||||
|
||||
# 0003 A+B
|
||||
"validation_rule_fct_eta_time_not_in_operation_window":"The estimated time of arrival will be AFTER the planned start of operations. {Rule #0003A}",
|
||||
@ -38,14 +38,16 @@ error_message_dict = {
|
||||
"validation_rule_fct_etd_time_not_in_tidal_window":"The tidal window does not fit to the agency's estimated time of departure (ETD) {Rule #0004B}",
|
||||
|
||||
# 0005 A+B
|
||||
"validation_rule_fct_too_many_identical_eta_times":"There are more than three ships with the same planned time of arrival (ETA) {Rule #0005A}",
|
||||
"validation_rule_fct_too_many_identical_etd_times":"There are more than three ships with the same planned time of departure (ETD) {Rule #0005B}",
|
||||
"validation_rule_fct_too_many_identical_eta_times":"More than three shipcalls are planned at the same time as the defined ETA {Rule #0005A}",
|
||||
"validation_rule_fct_too_many_identical_etd_times":"More than three shipcalls are planned at the same time as the defined ETD {Rule #0005B}",
|
||||
|
||||
# 0006 A+B
|
||||
"validation_rule_fct_agency_and_terminal_berth_id_disagreement":"Agency and Terminal are planning with different berths (the berth_id deviates). {Rule #0006A}",
|
||||
"validation_rule_fct_agency_and_terminal_pier_side_disagreement":"Agency and Terminal are planning with different pier sides (the pier_side deviates). {Rule #0006B}",
|
||||
}
|
||||
|
||||
|
||||
|
||||
class ValidationRuleBaseFunctions():
|
||||
"""
|
||||
Base object with individual functions, which the {ValidationRuleFunctions}-child refers to.
|
||||
@ -71,6 +73,18 @@ class ValidationRuleBaseFunctions():
|
||||
def get_no_violation_default_output(self):
|
||||
"""return the default output of a validation function with no validation: a tuple of (GREEN state, None)"""
|
||||
return (StatusFlags.GREEN, None)
|
||||
|
||||
def check_if_header_exists(self, df_times:pd.DataFrame, participant_type:ParticipantType)->bool:
|
||||
"""
|
||||
Given a pandas DataFrame, which contains times entries for a specific shipcall id,
|
||||
this function checks, whether one of the times entries belongs to the requested ParticipantType.
|
||||
|
||||
returns bool
|
||||
"""
|
||||
# empty DataFrames form a special case, as they might miss the 'participant_type' column.
|
||||
if len(df_times)==0:
|
||||
return False
|
||||
return participant_type in df_times.loc[:,"participant_type"].values
|
||||
|
||||
def check_time_delta_violation_query_time_to_now(self, query_time:pd.Timestamp, key_time:pd.Timestamp, threshold:float)->bool:
|
||||
"""
|
||||
@ -144,7 +158,6 @@ class ValidationRuleBaseFunctions():
|
||||
return violation_state
|
||||
|
||||
df_times = df_times.loc[df_times["participant_type"].isin(participant_types),:]
|
||||
agency_time = [time_ for time_ in agency_times.loc[:,query].tolist() if isinstance(time_, pd.Timestamp)]
|
||||
|
||||
# for the given query, e.g., 'eta_berth', sample all times from the pandas DataFrame
|
||||
# exclude missing entries and consider only pd.Timestamp entries (which ignores pd.NaT/null entries)
|
||||
@ -172,6 +185,7 @@ class ValidationRuleBaseFunctions():
|
||||
violation_state = any(time_difference_exceeds_threshold)
|
||||
|
||||
# this (previous) solution compares times to the reference (agency) time and checks if the difference is greater than 15 minutes
|
||||
# agency_time = [time_ for time_ in agency_times.loc[:,query].tolist() if isinstance(time_, pd.Timestamp)]
|
||||
# violation_state = ((np.max(estimated_times) - agency_time[0]) > pd.Timedelta("15min")) or ((agency_time[0] - np.min(estimated_times)) > pd.Timedelta("15min"))
|
||||
|
||||
# this solution to the rule compares all times to each other. When there is a total difference of more than 15 minutes, a violation occurs
|
||||
@ -709,21 +723,13 @@ class ValidationRuleFunctions(ValidationRuleBaseFunctions):
|
||||
else:
|
||||
return self.get_no_violation_default_output()
|
||||
|
||||
def validation_rule_fct_shipcall_shifting_participants_disagree_on_eta_or_etd(self, shipcall, df_times, *args, **kwargs):
|
||||
def validation_rule_fct_shipcall_shifting_participants_disagree_on_etd(self, shipcall, df_times, *args, **kwargs):
|
||||
"""
|
||||
Code: #0002-C
|
||||
Type: Local Rule
|
||||
Description: this validation checks, whether the participants expect different ETA or ETD times
|
||||
Description: this validation checks, whether the participants expect different ETD times
|
||||
Filter: only applies to shifting shipcalls
|
||||
"""
|
||||
violation_state_eta = self.check_participants_agree_on_estimated_time(
|
||||
shipcall = shipcall,
|
||||
|
||||
query="eta_berth",
|
||||
df_times=df_times,
|
||||
applicable_shipcall_type=ShipcallType.SHIFTING
|
||||
)
|
||||
|
||||
violation_state_etd = self.check_participants_agree_on_estimated_time(
|
||||
shipcall = shipcall,
|
||||
|
||||
@ -732,16 +738,14 @@ class ValidationRuleFunctions(ValidationRuleBaseFunctions):
|
||||
applicable_shipcall_type=ShipcallType.SHIFTING
|
||||
)
|
||||
|
||||
# apply 'eta_berth' check
|
||||
# apply 'etd_berth'
|
||||
# violation: if either 'eta_berth' or 'etd_berth' is violated
|
||||
# violation: if either 'etd_berth' is violated
|
||||
# functionally, this is the same as individually comparing all times for the participants
|
||||
# times_agency.eta_berth==times_mooring.eta_berth==times_portadministration.eta_berth==times_pilot.eta_berth==times_tug.eta_berth
|
||||
# times_agency.etd_berth==times_mooring.etd_berth==times_portadministration.etd_berth==times_pilot.etd_berth==times_tug.etd_berth
|
||||
violation_state = (violation_state_eta) or (violation_state_etd)
|
||||
violation_state = (violation_state_etd)
|
||||
|
||||
if violation_state:
|
||||
validation_name = "validation_rule_fct_shipcall_shifting_participants_disagree_on_eta_or_etd"
|
||||
validation_name = "validation_rule_fct_shipcall_shifting_participants_disagree_on_etd"
|
||||
return (StatusFlags.RED, validation_name)
|
||||
else:
|
||||
return self.get_no_violation_default_output()
|
||||
@ -762,10 +766,12 @@ class ValidationRuleFunctions(ValidationRuleBaseFunctions):
|
||||
return self.get_no_violation_default_output()
|
||||
|
||||
# check, if the header is filled in (agency & terminal)
|
||||
if len(df_times.loc[df_times["participant_type"]==ParticipantType.AGENCY.value]) != 1:
|
||||
if not self.check_if_header_exists(df_times, participant_type=ParticipantType.AGENCY):
|
||||
# if len(df_times.loc[df_times["participant_type"]==ParticipantType.AGENCY.value]) != 1:
|
||||
return self.get_no_violation_default_output() # rule not applicable
|
||||
|
||||
if len(df_times.loc[df_times["participant_type"]==ParticipantType.TERMINAL.value]) != 1:
|
||||
if not self.check_if_header_exists(df_times, participant_type=ParticipantType.TERMINAL):
|
||||
#if len(df_times.loc[df_times["participant_type"]==ParticipantType.TERMINAL.value]) != 1:
|
||||
return self.get_no_violation_default_output() # rule not applicable
|
||||
|
||||
# get agency & terminal times
|
||||
@ -805,10 +811,12 @@ class ValidationRuleFunctions(ValidationRuleBaseFunctions):
|
||||
return self.get_no_violation_default_output()
|
||||
|
||||
# check, if the header is filled in (agency & terminal)
|
||||
if len(df_times.loc[df_times["participant_type"]==ParticipantType.AGENCY.value]) != 1:
|
||||
# if len(df_times.loc[df_times["participant_type"]==ParticipantType.AGENCY.value]) != 1:
|
||||
if not self.check_if_header_exists(df_times, participant_type=ParticipantType.AGENCY):
|
||||
return self.get_no_violation_default_output() # rule not applicable
|
||||
|
||||
if len(df_times.loc[df_times["participant_type"]==ParticipantType.TERMINAL.value]) != 1:
|
||||
# if len(df_times.loc[df_times["participant_type"]==ParticipantType.TERMINAL.value]) != 1:
|
||||
if not self.check_if_header_exists(df_times, participant_type=ParticipantType.TERMINAL):
|
||||
return self.get_no_violation_default_output() # rule not applicable
|
||||
|
||||
# get agency & terminal times
|
||||
@ -845,7 +853,8 @@ class ValidationRuleFunctions(ValidationRuleBaseFunctions):
|
||||
return self.get_no_violation_default_output()
|
||||
|
||||
# check, if the header is filled in (agency)
|
||||
if len(df_times.loc[df_times["participant_type"].isin([ParticipantType.AGENCY.value])]) != 1:
|
||||
# if len(df_times.loc[df_times["participant_type"].isin([ParticipantType.AGENCY.value])]) != 1:
|
||||
if not self.check_if_header_exists(df_times, participant_type=ParticipantType.AGENCY):
|
||||
return self.get_no_violation_default_output()
|
||||
times_agency = self.sql_handler.get_times_for_participant_type(df_times, participant_type=ParticipantType.AGENCY.value)
|
||||
|
||||
@ -876,7 +885,8 @@ class ValidationRuleFunctions(ValidationRuleBaseFunctions):
|
||||
return self.get_no_violation_default_output()
|
||||
|
||||
# check, if the header is filled in (agency)
|
||||
if len(df_times.loc[df_times["participant_type"].isin([ParticipantType.AGENCY.value])]) != 1:
|
||||
# if len(df_times.loc[df_times["participant_type"].isin([ParticipantType.AGENCY.value])]) != 1:
|
||||
if not self.check_if_header_exists(df_times, participant_type=ParticipantType.AGENCY):
|
||||
return self.get_no_violation_default_output()
|
||||
times_agency = self.sql_handler.get_times_for_participant_type(df_times, participant_type=ParticipantType.AGENCY.value)
|
||||
|
||||
@ -898,16 +908,23 @@ class ValidationRuleFunctions(ValidationRuleBaseFunctions):
|
||||
"""
|
||||
Code: #0005-A
|
||||
Type: Global Rule
|
||||
Description: this validation rule checks, whether there are too many shipcalls with identical ETA times.
|
||||
Description: this validation rule checks, whether there are too many shipcalls with identical times to the query ETA.
|
||||
"""
|
||||
times_agency = df_times.loc[df_times["participant_type"]==ParticipantType.AGENCY.value]
|
||||
if all_times_agency is None:
|
||||
all_times_agency = self.sql_handler.get_times_for_agency(non_null_column="eta_berth")
|
||||
|
||||
# check, if the header is filled in (agency)
|
||||
if len(times_agency) != 1:
|
||||
if not self.check_if_header_exists(df_times, participant_type=ParticipantType.AGENCY): # if len(times_agency) != 1:
|
||||
return self.get_no_violation_default_output()
|
||||
|
||||
# when ANY of the unique values exceeds the threshold, a violation is observed
|
||||
query = "eta_berth"
|
||||
violation_state = self.check_unique_shipcall_counts(query, times_agency=times_agency, rounding=rounding, maximum_threshold=maximum_threshold, all_times_agency=all_times_agency)
|
||||
# get the agency's query time
|
||||
times_agency = df_times.loc[df_times["participant_type"]==ParticipantType.AGENCY.value]
|
||||
query_time = times_agency.iloc[0].eta_berth
|
||||
|
||||
# count the number of times, where a times entry is very close to the query time (uses an internal threshold, such as 15 minutes)
|
||||
|
||||
counts = self.sql_handler.count_synchronous_shipcall_times(query_time, all_df_times=all_times_agency)
|
||||
violation_state = counts > maximum_threshold
|
||||
|
||||
if violation_state:
|
||||
validation_name = "validation_rule_fct_too_many_identical_eta_times"
|
||||
@ -919,16 +936,22 @@ class ValidationRuleFunctions(ValidationRuleBaseFunctions):
|
||||
"""
|
||||
Code: #0005-B
|
||||
Type: Global Rule
|
||||
Description: this validation rule checks, whether there are too many shipcalls with identical ETD times.
|
||||
Description: this validation rule checks, whether there are too many shipcalls with identical times to the query ETD.
|
||||
"""
|
||||
times_agency = df_times.loc[df_times["participant_type"]==ParticipantType.AGENCY.value]
|
||||
if all_times_agency is None:
|
||||
all_times_agency = self.sql_handler.get_times_for_agency(non_null_column="etd_berth")
|
||||
|
||||
# check, if the header is filled in (agency)
|
||||
if len(times_agency) != 1:
|
||||
if not self.check_if_header_exists(df_times, participant_type=ParticipantType.AGENCY): #if len(times_agency) != 1:
|
||||
return self.get_no_violation_default_output()
|
||||
|
||||
# when ANY of the unique values exceeds the threshold, a violation is observed
|
||||
query = "etd_berth"
|
||||
violation_state = self.check_unique_shipcall_counts(query, times_agency=times_agency, rounding=rounding, maximum_threshold=maximum_threshold, all_times_agency=all_times_agency)
|
||||
# get the agency's query time
|
||||
times_agency = df_times.loc[df_times["participant_type"]==ParticipantType.AGENCY.value]
|
||||
query_time = times_agency.iloc[0].etd_berth
|
||||
|
||||
# count the number of times, where a times entry is very close to the query time (uses an internal threshold, such as 15 minutes)
|
||||
counts = self.sql_handler.count_synchronous_shipcall_times(query_time, all_df_times=all_times_agency)
|
||||
violation_state = counts > maximum_threshold
|
||||
|
||||
if violation_state:
|
||||
validation_name = "validation_rule_fct_too_many_identical_etd_times"
|
||||
@ -943,10 +966,12 @@ class ValidationRuleFunctions(ValidationRuleBaseFunctions):
|
||||
Description: This validation rule checks, whether agency and terminal agree with their designated berth place by checking berth_id.
|
||||
"""
|
||||
# check, if the header is filled in (agency & terminal)
|
||||
if len(df_times.loc[df_times["participant_type"]==ParticipantType.AGENCY.value]) == 0:
|
||||
# if len(df_times.loc[df_times["participant_type"]==ParticipantType.AGENCY.value]) == 0:
|
||||
if not self.check_if_header_exists(df_times, participant_type=ParticipantType.AGENCY):
|
||||
return self.get_no_violation_default_output() # rule not applicable
|
||||
|
||||
if len(df_times.loc[df_times["participant_type"]==ParticipantType.TERMINAL.value]) == 0:
|
||||
# if len(df_times.loc[df_times["participant_type"]==ParticipantType.TERMINAL.value]) == 0:
|
||||
if not self.check_if_header_exists(df_times, participant_type=ParticipantType.TERMINAL):
|
||||
return self.get_no_violation_default_output() # rule not applicable
|
||||
|
||||
times_agency = self.sql_handler.get_times_for_participant_type(df_times, participant_type=ParticipantType.AGENCY.value)
|
||||
@ -979,13 +1004,14 @@ class ValidationRuleFunctions(ValidationRuleBaseFunctions):
|
||||
Description: This validation rule checks, whether agency and terminal agree with their designated pier side by checking pier_side.
|
||||
"""
|
||||
# check, if the header is filled in (agency & terminal)
|
||||
if len(df_times.loc[df_times["participant_type"]==ParticipantType.AGENCY.value]) == 0:
|
||||
# if len(df_times.loc[df_times["participant_type"]==ParticipantType.AGENCY.value]) == 0:
|
||||
if not self.check_if_header_exists(df_times, participant_type=ParticipantType.AGENCY):
|
||||
return self.get_no_violation_default_output() # rule not applicable
|
||||
|
||||
if len(df_times.loc[df_times["participant_type"]==ParticipantType.TERMINAL.value]) == 0:
|
||||
# if len(df_times.loc[df_times["participant_type"]==ParticipantType.TERMINAL.value]) == 0:
|
||||
if not self.check_if_header_exists(df_times, participant_type=ParticipantType.TERMINAL):
|
||||
return self.get_no_violation_default_output() # rule not applicable
|
||||
|
||||
times_agency = self.sql_handler.get_times_for_participant_type(df_times, participant_type=ParticipantType.AGENCY.value)
|
||||
times_terminal = self.sql_handler.get_times_for_participant_type(df_times, participant_type=ParticipantType.TERMINAL.value)
|
||||
|
||||
# when one of the two values is null, the state is GREEN
|
||||
|
||||
@ -3,6 +3,7 @@ import logging
|
||||
import re
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
import datetime
|
||||
from BreCal.database.enums import StatusFlags
|
||||
from BreCal.validators.validation_rule_functions import ValidationRuleFunctions
|
||||
from BreCal.schemas.model import Shipcall
|
||||
@ -17,10 +18,6 @@ class ValidationRules(ValidationRuleFunctions):
|
||||
"""
|
||||
def __init__(self, sql_handler): # use the entire data that is provided for this query (e.g., json input)
|
||||
super().__init__(sql_handler)
|
||||
|
||||
self.validation_state = self.determine_validation_state()
|
||||
# currently flagged: notification_state initially was based on using one ValidationRules object for each query. This is deprecated.
|
||||
# self.notification_state = self.determine_notification_state() # (state:str, should_notify:bool)
|
||||
return
|
||||
|
||||
def evaluate(self, shipcall):
|
||||
@ -31,9 +28,9 @@ class ValidationRules(ValidationRuleFunctions):
|
||||
returns: (evaluation_state, violations)
|
||||
"""
|
||||
# prepare df_times, which every validation rule tends to use
|
||||
df_times = self.sql_handler.df_dict.get('times', pd.DataFrame()) # -> pd.DataFrame
|
||||
all_df_times = self.sql_handler.df_dict.get('times', pd.DataFrame()) # -> pd.DataFrame
|
||||
|
||||
if len(df_times)==0:
|
||||
if len(all_df_times)==0:
|
||||
return (StatusFlags.GREEN.value, [])
|
||||
|
||||
spm = self.sql_handler.df_dict["shipcall_participant_map"]
|
||||
@ -74,16 +71,26 @@ class ValidationRules(ValidationRuleFunctions):
|
||||
return evaluation_state, violations
|
||||
|
||||
def evaluate_shipcalls(self, shipcall_df:pd.DataFrame)->pd.DataFrame:
|
||||
"""apply 'evaluate_shipcall_from_df' to each individual shipcall in {shipcall_df}. Returns shipcall_df ('evaluation' and 'evaluation_message' are updated)"""
|
||||
results = shipcall_df.apply(lambda x: self.evaluate_shipcall_from_df(x), axis=1).values
|
||||
"""apply 'evaluate_shipcall_from_df' to each individual shipcall in {shipcall_df}. Returns shipcall_df ('evaluation', 'evaluation_message', 'evaluation_time' and 'evaluation_notifications_sent' are updated)"""
|
||||
evaluation_states_old = [state_old for state_old in shipcall_df.loc[:,"evaluation"]]
|
||||
evaluation_states_old = [state_old if not pd.isna(state_old) else 0 for state_old in evaluation_states_old]
|
||||
results = shipcall_df.apply(lambda x: self.evaluate_shipcall_from_df(x), axis=1).values # returns tuple (state, message)
|
||||
|
||||
# unbundle individual results. evaluation_state becomes an integer, violation
|
||||
evaluation_state = [StatusFlags(res[0]).value for res in results]
|
||||
# unbundle individual results. evaluation_states becomes an integer, violation
|
||||
evaluation_states_new = [StatusFlags(res[0]).value for res in results]
|
||||
violations = [",\r\n".join(res[1]) if len(res[1])>0 else None for res in results]
|
||||
violations = [self.concise_evaluation_message_if_too_long(violation) for violation in violations]
|
||||
|
||||
shipcall_df.loc[:,"evaluation"] = evaluation_state
|
||||
# build the list of evaluation times ('now', as isoformat)
|
||||
#evaluation_time = self.get_notification_times(evaluation_states_new)
|
||||
|
||||
# build the list of 'evaluation_notifications_sent'. The value is 'False', when a notification should be created
|
||||
#evaluation_notifications_sent = self.get_notification_states(evaluation_states_old, evaluation_states_new)
|
||||
|
||||
shipcall_df.loc[:,"evaluation"] = evaluation_states_new
|
||||
shipcall_df.loc[:,"evaluation_message"] = violations
|
||||
#shipcall_df.loc[:,"evaluation_time"] = evaluation_time
|
||||
#shipcall_df.loc[:,"evaluation_notifications_sent"] = evaluation_notifications_sent
|
||||
return shipcall_df
|
||||
|
||||
def concise_evaluation_message_if_too_long(self, violation):
|
||||
@ -100,36 +107,27 @@ class ValidationRules(ValidationRuleFunctions):
|
||||
# e.g.: Evaluation message too long. Violated Rules: ['Rule #0001C', 'Rule #0001H', 'Rule #0001F', 'Rule #0001G', 'Rule #0001L', 'Rule #0001M', 'Rule #0001J', 'Rule #0001K']
|
||||
violation = f"Evaluation message too long. Violated Rules: {concise}"
|
||||
return violation
|
||||
|
||||
def determine_validation_state(self) -> str:
|
||||
|
||||
def undefined_method(self) -> str:
|
||||
"""this function should apply the ValidationRules to the respective .shipcall, in regards to .times"""
|
||||
return (StatusFlags.GREEN, False) # (state:str, should_notify:bool)
|
||||
|
||||
def determine_notification_state(self, state_old, state_new):
|
||||
"""
|
||||
this method determines the validation state of a shipcall. The state is either ['green', 'yellow', 'red'] and signals,
|
||||
whether an entry causes issues within the workflow of users.
|
||||
|
||||
returns: validation_state_new (str)
|
||||
this method determines state changes in the notification state. When the state increases, a user is notified about it.
|
||||
state order: (NONE = GREEN < YELLOW < RED)
|
||||
"""
|
||||
(validation_state_new, description) = self.undefined_method()
|
||||
# should there also be notifications for critical validation states? In principle, the traffic light itself provides that notification.
|
||||
self.validation_state = validation_state_new
|
||||
return validation_state_new
|
||||
# identify a state increase
|
||||
should_notify = self.identify_notification_state_change(state_old=state_old, state_new=state_new)
|
||||
|
||||
def determine_notification_state(self) -> (str, bool):
|
||||
# when a state increases, a notification must be sent. Thereby, the field should be set to False ({evaluation_notifications_sent})
|
||||
evaluation_notifications_sent = False if bool(should_notify) else None
|
||||
return evaluation_notifications_sent
|
||||
|
||||
def identify_notification_state_change(self, state_old, state_new) -> bool:
|
||||
"""
|
||||
this method determines state changes in the notification state. When the state is changed to yellow or red,
|
||||
a user is notified about it. The only exception for this rule is when the state was yellow or red before,
|
||||
as the user has then already been notified.
|
||||
|
||||
returns: notification_state_new (str), should_notify (bool)
|
||||
"""
|
||||
(state_new, description) = self.undefined_method() # determine the successor
|
||||
should_notify = self.identify_notification_state_change(state_new)
|
||||
self.notification_state = state_new # overwrite the predecessor
|
||||
return state_new, should_notify
|
||||
|
||||
def identify_notification_state_change(self, state_new) -> bool:
|
||||
"""
|
||||
determines, whether the observed state change should trigger a notification.
|
||||
internally, this function maps a color string to an integer and determines, if the successor state is more severe than the predecessor.
|
||||
determines, whether the observed state change should trigger a notification.
|
||||
internally, this function maps StatusFlags to an integer and determines, if the successor state is more severe than the predecessor.
|
||||
|
||||
state changes trigger a notification in the following cases:
|
||||
green -> yellow
|
||||
@ -138,15 +136,46 @@ class ValidationRules(ValidationRuleFunctions):
|
||||
|
||||
(none -> yellow) or (none -> red)
|
||||
due to the values in the enumeration objects, the states are mapped to provide this function.
|
||||
green=1, yellow=2, red=3, none=1. Hence, critical changes can be observed by simply checking with "greater than".
|
||||
green=1, yellow=2, red=3, none=1. Hence, critical changes can be observed by simply checking with "greater than".
|
||||
|
||||
returns bool, whether a notification should be triggered
|
||||
"""
|
||||
# state_old is always considered at least 'Green' (1)
|
||||
state_old = max(copy.copy(self.notification_state) if "notification_state" in list(self.__dict__.keys()) else StatusFlags.NONE, StatusFlags.GREEN.value)
|
||||
return state_new.value > state_old.value
|
||||
if state_old is None:
|
||||
state_old = StatusFlags.NONE.value
|
||||
state_old = max(int(state_old), StatusFlags.GREEN.value)
|
||||
return int(state_new) > int(state_old)
|
||||
|
||||
def get_notification_times(self, evaluation_states_new)->list[datetime.datetime]:
|
||||
"""# build the list of evaluation times ('now', as isoformat)"""
|
||||
evaluation_times = [datetime.datetime.now().isoformat() for _i in range(len(evaluation_states_new))]
|
||||
return evaluation_times
|
||||
|
||||
def get_notification_states(self, evaluation_states_old, evaluation_states_new)->list[bool]:
|
||||
"""# build the list of 'evaluation_notifications_sent'. The value is 'False', when a notification should be created"""
|
||||
evaluation_notifications_sent = [self.determine_notification_state(state_old=int(state_old), state_new=int(state_new)) for state_old, state_new in zip(evaluation_states_old, evaluation_states_new)]
|
||||
return evaluation_notifications_sent
|
||||
|
||||
def undefined_method(self) -> str:
|
||||
"""this function should apply the ValidationRules to the respective .shipcall, in regards to .times"""
|
||||
# #TODO_traffic_state
|
||||
return (StatusFlags.GREEN, False) # (state:str, should_notify:bool)
|
||||
|
||||
def inspect_shipcall_evaluation(vr, sql_handler, shipcall_id):
|
||||
"""
|
||||
# debug only!
|
||||
|
||||
a simple debugging function, which serves in inspecting an evaluation function for a single shipcall id. It returns the result and all related data.
|
||||
returns: result, shipcall_df (filtered by shipcall id), shipcall, spm (shipcall participant map, filtered by shipcall id), times_df (filtered by shipcall id)
|
||||
"""
|
||||
shipcall_df = sql_handler.df_dict.get("shipcall").loc[shipcall_id:shipcall_id,:]
|
||||
|
||||
shipcall = Shipcall(**{**{"id":shipcall_id},**sql_handler.df_dict.get("shipcall").loc[shipcall_id].to_dict()})
|
||||
result = vr.evaluate(shipcall=shipcall)
|
||||
notification_state = vr.identify_notification_state_change(state_old=int(shipcall.evaluation), state_new=int(result[0]))
|
||||
print(f"Previous state: {int(shipcall.evaluation)}, New State: {result[0]}, Notification State: {notification_state}")
|
||||
|
||||
times_df = sql_handler.df_dict.get("times")
|
||||
times_df = times_df.loc[times_df["shipcall_id"]==shipcall_id]
|
||||
|
||||
|
||||
spm = sql_handler.df_dict["shipcall_participant_map"]
|
||||
spm = spm.loc[spm["shipcall_id"]==shipcall_id]
|
||||
|
||||
return result, shipcall_df, shipcall, spm, times_df
|
||||
|
||||
@ -3,7 +3,7 @@ import sys
|
||||
import logging
|
||||
|
||||
sys.path.insert(0, '/var/www/brecal/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
|
||||
|
||||
|
||||
@ -2,7 +2,7 @@ from setuptools import find_packages, setup
|
||||
|
||||
setup(
|
||||
name='BreCal',
|
||||
version='1.4.0',
|
||||
version='1.5.0',
|
||||
packages=find_packages(),
|
||||
include_package_data=True,
|
||||
zip_safe=False,
|
||||
|
||||
0
src/server/tests/database/__init__.py
Normal file
0
src/server/tests/database/__init__.py
Normal file
534
src/server/tests/database/test_sql_queries.py
Normal file
534
src/server/tests/database/test_sql_queries.py
Normal file
@ -0,0 +1,534 @@
|
||||
import pytest
|
||||
|
||||
import os
|
||||
import bcrypt
|
||||
import pydapper
|
||||
from BreCal import local_db
|
||||
from BreCal.database.sql_handler import execute_sql_query_standalone
|
||||
from BreCal.database.sql_queries import SQLQuery
|
||||
from BreCal.schemas import model
|
||||
from BreCal.stubs.user import get_user_simple
|
||||
|
||||
instance_path = os.path.join(os.path.expanduser('~'), "brecal", "src", "server", "instance")
|
||||
local_db.initPool(os.path.dirname(instance_path), connection_filename="connection_data_local.json")
|
||||
|
||||
def test_sql_query_every_call_returns_str():
|
||||
assert isinstance(SQLQuery.get_berth(), str)
|
||||
return
|
||||
|
||||
def test_sql_query_get_berths():
|
||||
schemas = execute_sql_query_standalone(query=SQLQuery.get_berth(), param={})
|
||||
berths = [model.Berth(**schema) for schema in schemas]
|
||||
assert all([isinstance(berth, model.Berth) for berth in berths]), f"one of the returned schemas is not a Berth object"
|
||||
return
|
||||
|
||||
def test_sql_query_get_history():
|
||||
options = {"shipcall_id":157}
|
||||
history = execute_sql_query_standalone(query=SQLQuery.get_history(), param={"shipcallid" : options["shipcall_id"]}, model=model.History.from_query_row)
|
||||
assert all([isinstance(hist,model.History) for hist in history])
|
||||
return
|
||||
|
||||
def test_sql_query_get_user():
|
||||
options = {"username":"maxm"}
|
||||
users = execute_sql_query_standalone(query=SQLQuery.get_user(), param={"username" : options["username"]}, model=model.User)
|
||||
assert all([isinstance(user,model.User) for user in users])
|
||||
assert users[0].user_name==options["username"]
|
||||
return
|
||||
|
||||
def test_sql_get_notifications():
|
||||
import mysql.connector
|
||||
|
||||
# unfortunately, there currently is *no* notification in the database.
|
||||
options = {"shipcall_id":85}
|
||||
notifications = execute_sql_query_standalone(query=SQLQuery.get_notifications(), param={"scid" : options["shipcall_id"]}, model=model.Notification.from_query_row)
|
||||
assert all([isinstance(notification,model.Notification) for notification in notifications])
|
||||
assert all([isinstance(notification.type,model.NotificationType) for notification in notifications])
|
||||
return
|
||||
|
||||
def test_sql_get_participants():
|
||||
participants = execute_sql_query_standalone(query=SQLQuery.get_participants(), param={}, model=model.Participant)
|
||||
assert all([isinstance(participant,model.Participant) for participant in participants])
|
||||
return
|
||||
|
||||
def test_sql_get_participants():
|
||||
options = {"user_id":29}
|
||||
query = SQLQuery.get_participant_by_user_id()
|
||||
participants = execute_sql_query_standalone(query=query, param={"userid" : options["user_id"]}, model=model.Participant)
|
||||
assert all([isinstance(participant,model.Participant) for participant in participants])
|
||||
assert len(participants)==1, f"there should only be one match for the respective user id"
|
||||
assert participants[0].id == 136, f"user 29 belongs to participant_id 136"
|
||||
return
|
||||
|
||||
def test_sql_get_shipcalls():
|
||||
from BreCal.database.sql_queries import create_sql_query_shipcall_get
|
||||
|
||||
# different styles for the same outcome
|
||||
options = {"past_days":3000}
|
||||
query = create_sql_query_shipcall_get(options)
|
||||
shipcalls = execute_sql_query_standalone(query=query, param=options, model=model.Shipcall.from_query_row)
|
||||
assert all([isinstance(shipcall,model.Shipcall) for shipcall in shipcalls])
|
||||
|
||||
query = SQLQuery.get_shipcalls() # defaults to 'past_days'=3
|
||||
shipcalls = execute_sql_query_standalone(query=query, param=options, model=model.Shipcall.from_query_row)
|
||||
assert all([isinstance(shipcall,model.Shipcall) for shipcall in shipcalls])
|
||||
|
||||
query = SQLQuery.get_shipcalls({'past_days':3000})
|
||||
shipcalls = execute_sql_query_standalone(query=query, param=options, model=model.Shipcall.from_query_row)
|
||||
assert all([isinstance(shipcall,model.Shipcall) for shipcall in shipcalls])
|
||||
|
||||
# fails: options must contain 'past_days' key
|
||||
with pytest.raises(AssertionError, match="there must be a key 'past_days' in the options, which determines"):
|
||||
query = SQLQuery.get_shipcalls(options={})
|
||||
shipcalls = execute_sql_query_standalone(query=query, param=options, model=model.Shipcall.from_query_row)
|
||||
return
|
||||
|
||||
def test_sql_get_ships():
|
||||
ships = execute_sql_query_standalone(query=SQLQuery.get_ships(), model=model.Ship)
|
||||
assert all([isinstance(ship, model.Ship) for ship in ships])
|
||||
return
|
||||
|
||||
def test_sql_get_ship_by_id():
|
||||
query = #"SELECT * FROM ship where id = ?id?" # #TODO_refactor: put into the SQLQuery object
|
||||
ship = execute_sql_query_standalone(SQLQuery.get_ship_by_id(), param={"id":1}, command_type="single", model=model.Ship)
|
||||
assert isinstance(ship, model.Ship)
|
||||
return
|
||||
|
||||
|
||||
def test_sql_get_times():
|
||||
options = {'shipcall_id':153}
|
||||
times = execute_sql_query_standalone(query=SQLQuery.get_times(), model=model.Times, param={"scid" : options["shipcall_id"]})
|
||||
assert all([isinstance(time_,model.Times) for time_ in times])
|
||||
assert times[0].shipcall_id==options["shipcall_id"]
|
||||
return
|
||||
|
||||
def test_sql_get_user_by_id():
|
||||
# success: id 29 exists
|
||||
schemaModel = get_user_simple().__dict__
|
||||
schemaModel["id"] = 29
|
||||
|
||||
sentinel = object()
|
||||
query = SQLQuery.get_user_by_id()
|
||||
pooledConnection = local_db.getPoolConnection()
|
||||
commands = pydapper.using(pooledConnection)
|
||||
theuser = commands.query_single_or_default(query, sentinel, param={"id" : schemaModel["id"]}, model=model.User)
|
||||
|
||||
if theuser is sentinel:
|
||||
pooledConnection.close()
|
||||
|
||||
theuser.id == schemaModel["id"]
|
||||
|
||||
# fails: id 292212 does not exist (returns default, which is the sentinel object in this case)
|
||||
schemaModel = get_user_simple().__dict__
|
||||
schemaModel["id"] = 292212
|
||||
|
||||
sentinel = object()
|
||||
query = SQLQuery.get_user_by_id()
|
||||
pooledConnection = local_db.getPoolConnection()
|
||||
commands = pydapper.using(pooledConnection)
|
||||
theuser = commands.query_single_or_default(query, sentinel, param={"id" : schemaModel["id"]}, model=model.User)
|
||||
|
||||
if theuser is sentinel:
|
||||
pooledConnection.close()
|
||||
|
||||
assert theuser is sentinel
|
||||
return
|
||||
|
||||
def test_sql_get_user_put():
|
||||
"""the PUT query for a user must be built based on a schemaModel, as the available data is dynamic and the query must be adaptive."""
|
||||
schemaModel = get_user_simple().__dict__
|
||||
schemaModel["id"] = 29
|
||||
|
||||
query = "UPDATE user SET "
|
||||
isNotFirst = False
|
||||
for key in schemaModel.keys():
|
||||
if key == "id":
|
||||
continue
|
||||
if key == "old_password":
|
||||
continue
|
||||
if key == "new_password":
|
||||
continue
|
||||
if isNotFirst:
|
||||
query += ", "
|
||||
isNotFirst = True
|
||||
query += key + " = ?" + key + "? "
|
||||
|
||||
query += "WHERE id = ?id?"
|
||||
|
||||
assert SQLQuery.get_user_put(schemaModel) == query
|
||||
return
|
||||
|
||||
def test_sql_user_put_set_lastname_check_unset_lastname_check():
|
||||
"""
|
||||
Simply put, this method updates the last_name of user 29 to "Metz", verifies the change,
|
||||
and then proceeds to set it back to "Mustermann", and verifies the change.
|
||||
"""
|
||||
# 1.) SET the last_name of user_id 29 to 'Metz' by a PUT command
|
||||
schemaModel = get_user_simple().__dict__
|
||||
schemaModel["id"] = 29
|
||||
schemaModel["last_name"] = "Metz" # -> "Metz" -> "Mustermann"
|
||||
schemaModel = {k:v for k,v in schemaModel.items() if k in ["id", "last_name"]}
|
||||
|
||||
query = SQLQuery.get_user_put(schemaModel)
|
||||
affected_rows = execute_sql_query_standalone(query=query, param=schemaModel, command_type="execute")
|
||||
assert affected_rows==1, f"at least one row should be affected by this call."
|
||||
|
||||
# 2.) GET the user_id 29 and verify the last_name is 'Metz'
|
||||
sentinel = object()
|
||||
query = SQLQuery.get_user_by_id()
|
||||
pooledConnection = local_db.getPoolConnection()
|
||||
commands = pydapper.using(pooledConnection)
|
||||
theuser = commands.query_single_or_default(query, sentinel, param={"id" : schemaModel["id"]}, model=model.User)
|
||||
pooledConnection.close()
|
||||
assert not theuser is sentinel, f"failed GET user query"
|
||||
assert theuser.last_name=="Metz", f"PUT command has been unsuccessful."
|
||||
|
||||
# 3.) SET the last_name of user_id 29 to 'Mustermann' by a PUT command
|
||||
schemaModel = theuser.__dict__
|
||||
schemaModel["last_name"] = "Mustermann"
|
||||
schemaModel = {k:v for k,v in schemaModel.items() if k in ["id", "last_name"]}
|
||||
|
||||
query = SQLQuery.get_user_put(schemaModel)
|
||||
affected_rows = execute_sql_query_standalone(query=query, param=schemaModel, command_type="execute")
|
||||
assert affected_rows==1, f"at least one row should be affected by this call."
|
||||
|
||||
# 4.) GET the user_id 29 and verify the last_name is 'Mustermann'
|
||||
sentinel = object()
|
||||
query = SQLQuery.get_user_by_id()
|
||||
pooledConnection = local_db.getPoolConnection()
|
||||
commands = pydapper.using(pooledConnection)
|
||||
theuser = commands.query_single_or_default(query, sentinel, param={"id" : schemaModel["id"]}, model=model.User)
|
||||
pooledConnection.close()
|
||||
assert not theuser is sentinel, f"failed GET user query"
|
||||
assert theuser.last_name=="Mustermann", f"PUT command has been unsuccessful."
|
||||
return
|
||||
|
||||
def test_sql_user_update_password():
|
||||
"""This test updates the default password of user 29 from 'Start1234' to 'Start4321' and afterwards sets it back to 'Start1234'."""
|
||||
# #TODO: this test very openly displays the password of 'maxm'. It makes sense to create a stub user in the database, which can be
|
||||
# used for these tests, so an account without any importance or valuable assigned role is insecure instead.
|
||||
|
||||
# Set. Update the password of user 29 from 'Start1234' to 'Start4321'
|
||||
schemaModel = get_user_simple().__dict__
|
||||
schemaModel["id"] = 29
|
||||
schemaModel["old_password"] = "Start1234"
|
||||
schemaModel["new_password"] = "Start4321"
|
||||
|
||||
sentinel = object()
|
||||
query = SQLQuery.get_user_by_id()
|
||||
pooledConnection = local_db.getPoolConnection()
|
||||
commands = pydapper.using(pooledConnection)
|
||||
theuser = commands.query_single_or_default(query, sentinel, param={"id" : schemaModel["id"]}, model=model.User)
|
||||
pooledConnection.close()
|
||||
assert not theuser is sentinel, f"failed GET user query"
|
||||
|
||||
assert bcrypt.checkpw(schemaModel["old_password"].encode("utf-8"), bytes(theuser.password_hash, "utf-8")), f"old password does not match to the database entry"
|
||||
|
||||
password_hash = bcrypt.hashpw(schemaModel["new_password"].encode('utf-8'), bcrypt.gensalt( 12 )).decode('utf8')
|
||||
query = SQLQuery.get_update_user_password()
|
||||
affected_rows = execute_sql_query_standalone(query=query, param={"password_hash" : password_hash, "id" : schemaModel["id"]}, command_type="execute")
|
||||
assert affected_rows == 1
|
||||
|
||||
# 2.) Revert. Set password back to the default (from 'Start4321' to 'Start1234')
|
||||
schemaModel = get_user_simple().__dict__
|
||||
schemaModel["id"] = 29
|
||||
schemaModel["old_password"] = "Start4321"
|
||||
schemaModel["new_password"] = "Start1234"
|
||||
|
||||
sentinel = object()
|
||||
query = SQLQuery.get_user_by_id()
|
||||
pooledConnection = local_db.getPoolConnection()
|
||||
commands = pydapper.using(pooledConnection)
|
||||
theuser = commands.query_single_or_default(query, sentinel, param={"id" : schemaModel["id"]}, model=model.User)
|
||||
pooledConnection.close()
|
||||
assert not theuser is sentinel, f"failed GET user query"
|
||||
|
||||
assert bcrypt.checkpw(schemaModel["old_password"].encode("utf-8"), bytes(theuser.password_hash, "utf-8")), f"old password does not match to the database entry"
|
||||
|
||||
password_hash = bcrypt.hashpw(schemaModel["new_password"].encode('utf-8'), bcrypt.gensalt( 12 )).decode('utf8')
|
||||
query = SQLQuery.get_update_user_password()
|
||||
affected_rows = execute_sql_query_standalone(query=query, param={"password_hash" : password_hash, "id" : schemaModel["id"]}, command_type="execute")
|
||||
assert affected_rows == 1
|
||||
return
|
||||
|
||||
def test_sql_get_participants_of_shipcall_id():
|
||||
shipcall_id = 389
|
||||
query = SQLQuery.get_participants()
|
||||
participants = execute_sql_query_standalone(query=query, model=dict, param={"shipcall_id" : shipcall_id})
|
||||
assert all([part.get("participant_id") is not None for part in participants])
|
||||
assert all([part.get("type") is not None for part in participants])
|
||||
|
||||
# try to convert every participant into a model.Participant_Assignment
|
||||
participants = [
|
||||
model.Participant_Assignment(part["participant_id"], part["type"])
|
||||
for part in participants
|
||||
]
|
||||
assert all([isinstance(part,model.Participant_Assignment) for part in participants])
|
||||
return
|
||||
|
||||
def test_sql_get_all_shipcalls_and_assign_participants():
|
||||
"""
|
||||
this test reproduces the SQL query within BreCal.impl.shipcalls to make sure, that the
|
||||
query first returns all shipcalls, and then assigns all participants of the respective shipcall to it.
|
||||
"""
|
||||
# get all shipcalls
|
||||
options = {'past_days':30000}
|
||||
query = SQLQuery.get_shipcalls(options)
|
||||
shipcalls = execute_sql_query_standalone(query=query, model=model.Shipcall.from_query_row)
|
||||
assert all([isinstance(shipcall,model.Shipcall) for shipcall in shipcalls])
|
||||
|
||||
# for every shipcall, assign all of its participants to it
|
||||
for shipcall in shipcalls:
|
||||
participant_query = SQLQuery.get_participants()
|
||||
participants = execute_sql_query_standalone(query=participant_query, model=dict, param={"shipcall_id" : shipcall.id})
|
||||
|
||||
for record in participants:
|
||||
pa = model.Participant_Assignment(record["participant_id"], record["type"])
|
||||
shipcall.participants.append(pa)
|
||||
|
||||
assert any([
|
||||
any([isinstance(participant, model.Participant_Assignment) for participant in shipcall.participants])
|
||||
for shipcall in shipcalls
|
||||
]), f"at least one of the shipcalls should have an assigned model.Participant_Assignment"
|
||||
return
|
||||
|
||||
def test_sqlquery_get_shipcal_post_identical_to_create_sql_query_shipcall_post():
|
||||
from BreCal.database.sql_queries import create_sql_query_shipcall_post
|
||||
from BreCal.stubs.shipcall import get_stub_valid_shipcall_shifting, get_stub_valid_ship_loaded_model
|
||||
|
||||
post_data = get_stub_valid_shipcall_shifting()
|
||||
schemaModel = get_stub_valid_ship_loaded_model(post_data)
|
||||
query1 = SQLQuery.get_shipcall_post(schemaModel) # refactored variant of create_sql_query_shipcall_post (more concise)
|
||||
query2 = create_sql_query_shipcall_post(schemaModel)
|
||||
|
||||
assert query1==query2
|
||||
return
|
||||
|
||||
def test_sql_post_shipcall():
|
||||
"""issues a post-request with stub data and adds it to the database."""
|
||||
from BreCal.database.sql_queries import create_sql_query_shipcall_post
|
||||
from BreCal.stubs.shipcall import get_stub_valid_shipcall_shifting, get_stub_valid_ship_loaded_model
|
||||
|
||||
pooledConnection = local_db.getPoolConnection()
|
||||
try:
|
||||
commands = pydapper.using(pooledConnection)
|
||||
|
||||
post_data = get_stub_valid_shipcall_shifting()
|
||||
post_data["voyage"] = "pytestRS71" # perform tagging to identify the shipcalls created by pytests (<16 characters, no special characters).
|
||||
schemaModel = get_stub_valid_ship_loaded_model(post_data)
|
||||
query = SQLQuery.get_shipcall_post(schemaModel) # refactored variant of create_sql_query_shipcall_post (more concise)
|
||||
schemas = execute_sql_query_standalone(query=query, param=schemaModel, command_type="execute", pooledConnection=pooledConnection)
|
||||
assert schemas==1, f"unsuccessful query execution. Query: {query}"
|
||||
|
||||
# within the same pooledConnection, ask for the last inserted id
|
||||
query = SQLQuery.get_shipcall_post_last_insert_id()
|
||||
new_id = commands.execute_scalar(query)
|
||||
assert new_id > 0, f"the new id should be unlike 0.."
|
||||
|
||||
# add participant assignments if we have a list of participants
|
||||
if 'participants' in schemaModel:
|
||||
pquery = SQLQuery.get_shipcall_post_update_shipcall_participant_map()
|
||||
for participant_assignment in schemaModel["participants"]:
|
||||
schemas = execute_sql_query_standalone(query=pquery, param={"shipcall_id" : new_id, "participant_id" : participant_assignment["participant_id"], "type" : participant_assignment["type"]}, command_type="execute", pooledConnection=pooledConnection)
|
||||
|
||||
|
||||
from BreCal.stubs.user import get_user_simple
|
||||
# assign an artificial user with id 29 (maxm) & participant type 136
|
||||
user_data = get_user_simple().__dict__
|
||||
user_data["id"] = 29
|
||||
user_data["participant_id"] = 136
|
||||
|
||||
# POST in the history
|
||||
query = SQLQuery.create_sql_query_history_post()
|
||||
schemas = execute_sql_query_standalone(query=query, param={"scid" : new_id, "pid" : user_data["participant_id"], "uid" : user_data["id"]}, command_type="execute", pooledConnection=pooledConnection)
|
||||
assert schemas == 1, f"unsuccessful history POST"
|
||||
|
||||
|
||||
finally:
|
||||
pooledConnection.close()
|
||||
return
|
||||
|
||||
def test_sql_create_history_post_matches_legacy_function():
|
||||
from BreCal.database.sql_queries import create_sql_query_history_post
|
||||
|
||||
query_refactored = SQLQuery.create_sql_query_history_post()
|
||||
query_legacy = create_sql_query_history_post()
|
||||
assert isinstance(query_refactored,str)
|
||||
assert query_refactored==query_legacy, f"the refactored code to generate the query must be absolutely identical to the legacy version"
|
||||
return
|
||||
|
||||
def test_sql_get_shipcall_by_id():
|
||||
schemaModel = {"id":63}
|
||||
|
||||
sentinel = object()
|
||||
query = SQLQuery.get_shipcall_by_id()
|
||||
pooledConnection = local_db.getPoolConnection()
|
||||
commands = pydapper.using(pooledConnection)
|
||||
theshipcall = commands.query_single_or_default(query, sentinel, param={"id" : schemaModel["id"]}, model=model.Shipcall)
|
||||
pooledConnection.close()
|
||||
assert not theshipcall is sentinel, f"failed GET user query"
|
||||
assert theshipcall.id==schemaModel["id"]
|
||||
return
|
||||
|
||||
def test_sql_get_shipcall_by_id_short_version():
|
||||
schemaModel = {"id":63}
|
||||
|
||||
# when model is defined, returns the data model
|
||||
query = SQLQuery.get_shipcall_by_id()
|
||||
schemas = execute_sql_query_standalone(query=query, param={"id" : schemaModel["id"]}, model=model.Shipcall, command_type="single")
|
||||
|
||||
assert schemas.id==schemaModel["id"]
|
||||
assert isinstance(schemas, model.Shipcall)
|
||||
|
||||
# when model = None, returns a dictionary
|
||||
query = SQLQuery.get_shipcall_by_id()
|
||||
schemas = execute_sql_query_standalone(query=query, param={"id" : schemaModel["id"]}, command_type="single")
|
||||
assert isinstance(schemas, dict)
|
||||
assert schemas.get("id")==schemaModel["id"]
|
||||
return
|
||||
|
||||
def test_sql_get_shipcall_put_refactored_equals_extended_version():
|
||||
from BreCal.database.sql_queries import create_sql_query_shipcall_put
|
||||
from BreCal.stubs.shipcall import get_stub_valid_shipcall_shifting, get_stub_valid_ship_loaded_model
|
||||
|
||||
post_data = get_stub_valid_shipcall_shifting()
|
||||
post_data["voyage"] = "pytestRS71" # perform tagging to identify the shipcalls created by pytests (<16 characters, no special characters).
|
||||
schemaModel = get_stub_valid_ship_loaded_model(post_data)
|
||||
|
||||
legacy_query = create_sql_query_shipcall_put(schemaModel)
|
||||
refactored_query = SQLQuery.get_shipcall_put(schemaModel)
|
||||
|
||||
assert refactored_query == legacy_query, f"version conflict. the refactored query must precisely match the legacy query!"
|
||||
return
|
||||
|
||||
def test_sql_get_shipcall_participant_map_by_shipcall_id():
|
||||
schemaModel = {"id":152}
|
||||
|
||||
query = SQLQuery.get_shipcall_participant_map_by_shipcall_id()
|
||||
pdata = execute_sql_query_standalone(query=query, param={"id" : schemaModel["id"]}, command_type="query") # existing list of assignments
|
||||
|
||||
assert len(pdata)==4, f"there should be four assigned participants for the shipcall with id {schemaModel.get('id')}"
|
||||
return
|
||||
|
||||
def test_sql__get_shipcall__get_spm__optionally_update_shipcall():
|
||||
schemaModel = {'id': 152}
|
||||
query = SQLQuery.get_shipcall_by_id()
|
||||
shipcall = execute_sql_query_standalone(query=query, param={"id" : schemaModel["id"]}, command_type="single", model=model.Shipcall)
|
||||
|
||||
query = SQLQuery.get_shipcall_participant_map_by_shipcall_id()
|
||||
pdata = execute_sql_query_standalone(query=query, param={"id" : shipcall.id}, command_type="query") # existing list of assignments
|
||||
|
||||
assert len(pdata)==4, f"there should be four assigned participants for the shipcall with id {shipcall.id}"
|
||||
|
||||
for participant_assignment in shipcall.participants:
|
||||
found_participant = False
|
||||
for elem in pdata:
|
||||
if elem["participant_id"] == participant_assignment["participant_id"] and elem["type"] == participant_assignment["type"]:
|
||||
found_participant = True
|
||||
break
|
||||
if not found_participant:
|
||||
nquery = SQLQuery.get_shipcall_post_update_shipcall_participant_map()
|
||||
ndata = execute_sql_query_standalone(query=nquery, param={"shipcall_id" : shipcall.id, "participant_id" : participant_assignment["participant_id"], "type" : participant_assignment["type"]}, command_type="execute") # existing list of assignments
|
||||
return
|
||||
|
||||
def test_sql__shipcall_post__get_last_insert_id__get_spm__update_participants__verify_changes():
|
||||
"""
|
||||
this combinatorial test:
|
||||
1.) creates a novel shipcall
|
||||
2.) obtains the ID of the just-created shipcall
|
||||
3.) reads the participant map for that ID and verifies, that there are no participants listed
|
||||
4.) iteratively updates the participant map of the ID (using proxy data)
|
||||
5.) verifies the update
|
||||
"""
|
||||
from BreCal.stubs.shipcall import get_stub_valid_shipcall_shifting, get_stub_valid_ship_loaded_model
|
||||
pooledConnection = local_db.getPoolConnection()
|
||||
commands = pydapper.using(pooledConnection)
|
||||
try:
|
||||
|
||||
# 1.) create shipcall
|
||||
post_data = get_stub_valid_shipcall_shifting()
|
||||
post_data["voyage"] = "pytestRS71" # perform tagging to identify the shipcalls created by pytests (<16 characters, no special characters).
|
||||
schemaModel = get_stub_valid_ship_loaded_model(post_data)
|
||||
query = SQLQuery.get_shipcall_post(schemaModel) # refactored variant of create_sql_query_shipcall_post (more concise)
|
||||
schemas = execute_sql_query_standalone(query=query, param=schemaModel, command_type="execute", pooledConnection=pooledConnection)
|
||||
assert schemas==1, f"unsuccessful query execution. Query: {query}"
|
||||
|
||||
# 2.) obtain the ID of the novel shipcall
|
||||
# within the same pooledConnection, ask for the last inserted id
|
||||
query = SQLQuery.get_shipcall_post_last_insert_id()
|
||||
new_id = commands.execute_scalar(query)
|
||||
assert new_id > 0, f"the new id should be unlike 0.."
|
||||
|
||||
# 3.) read the ShipcallParticipantMap for the novel id
|
||||
query = SQLQuery.get_shipcall_participant_map_by_shipcall_id()
|
||||
pdata = execute_sql_query_standalone(query=query, param={"id" : new_id}, command_type="query") # existing list of assignments
|
||||
assert len(pdata)==0, f"as the POST query does not include participants in this case, the novel id should not have assigned participants."
|
||||
|
||||
### proxy data ###
|
||||
# loop across passed participant ids, creating entries for those not present in pdata
|
||||
schemaModel = {'id': new_id, "participants":[{'id': 128, 'participant_id': 2, 'type': 4}, {'id': 129, 'participant_id': 3, 'type': 1}, {'id': 130, 'participant_id': 4, 'type': 2}, {'id': 131, 'participant_id': 6, 'type': 8}, {'id': 132, 'participant_id': 136, 'type': 16}]}
|
||||
|
||||
# 4.) assign the participants
|
||||
for participant_assignment in schemaModel["participants"]:
|
||||
found_participant = False
|
||||
for elem in pdata:
|
||||
if elem["participant_id"] == participant_assignment["participant_id"] and elem["type"] == participant_assignment["type"]:
|
||||
found_participant = True
|
||||
break
|
||||
if not found_participant:
|
||||
nquery = SQLQuery.get_shipcall_post_update_shipcall_participant_map()
|
||||
ndata = execute_sql_query_standalone(query=nquery, param={"shipcall_id" : new_id, "participant_id" : participant_assignment["participant_id"], "type" : participant_assignment["type"]}, command_type="execute") # existing list of assignments
|
||||
|
||||
# 5.) verify the update (5 participants, including the false one)
|
||||
query = SQLQuery.get_shipcall_participant_map_by_shipcall_id()
|
||||
pdata = execute_sql_query_standalone(query=query, param={"id" : new_id}, command_type="query") # existing list of assignments
|
||||
assert len(pdata)==5, f"due to the PUT, there shall now be five participants, as defined in schemaModel."
|
||||
|
||||
# 6.) delete the incorrect participant (last entry in the list in this case)
|
||||
dquery = SQLQuery.get_shipcall_participant_map_delete_by_id()
|
||||
ddata = execute_sql_query_standalone(query=dquery, param={"existing_id" : pdata[-1].get("id")}, command_type="execute")
|
||||
|
||||
# 7.) verify the update (now 4 participants)
|
||||
query = SQLQuery.get_shipcall_participant_map_by_shipcall_id()
|
||||
pdata = execute_sql_query_standalone(query=query, param={"id" : new_id}, command_type="query") # existing list of assignments
|
||||
assert len(pdata)==4, f"due to the PUT, there shall now be five participants, as defined in schemaModel."
|
||||
|
||||
finally:
|
||||
pooledConnection.close()
|
||||
return
|
||||
|
||||
def test_sql_query_get_shipcalls_is_identical_to_legacy_query():
|
||||
from BreCal.database.sql_queries import create_sql_query_shipcall_get
|
||||
|
||||
options = {'past_days':7}
|
||||
query_refactored = SQLQuery.get_shipcalls(options)
|
||||
query_legacy = create_sql_query_shipcall_get(options)
|
||||
|
||||
assert query_refactored == query_legacy, f"the refactored code to generate the query must be absolutely identical to the legacy version"
|
||||
return
|
||||
|
||||
def test_sql_query_post_ship_is_identical_to_legacy_query():
|
||||
from BreCal.database.sql_queries import SQLQuery, create_sql_query_ship_post, create_sql_query_ship_put
|
||||
from BreCal.stubs.ship import get_stub_valid_ship_loaded_model
|
||||
|
||||
schemaModel = get_stub_valid_ship_loaded_model()
|
||||
|
||||
query_refactored = SQLQuery.get_ship_post(schemaModel)
|
||||
query_legacy = create_sql_query_ship_post(schemaModel)
|
||||
|
||||
assert query_refactored == query_legacy, f"the refactored code to generate the query must be absolutely identical to the legacy version"
|
||||
return
|
||||
|
||||
def test_sql_query_put_ship_is_identical_to_legacy_query():
|
||||
from BreCal.database.sql_queries import SQLQuery, create_sql_query_ship_post, create_sql_query_ship_put
|
||||
from BreCal.stubs.ship import get_stub_valid_ship_loaded_model
|
||||
|
||||
schemaModel = get_stub_valid_ship_loaded_model()
|
||||
query_refactored = SQLQuery.get_ship_put(schemaModel)
|
||||
query_legacy = create_sql_query_ship_put(schemaModel)
|
||||
|
||||
assert query_refactored == query_legacy, f"the refactored code to generate the query must be absolutely identical to the legacy version"
|
||||
return
|
||||
|
||||
|
||||
|
||||
#schemas = execute_sql_query_standalone(query=SQLQuery.get_berth(), param={})
|
||||
80
src/server/tests/schemas/test_model.py
Normal file
80
src/server/tests/schemas/test_model.py
Normal file
@ -0,0 +1,80 @@
|
||||
from marshmallow import ValidationError
|
||||
import pytest
|
||||
from BreCal.schemas.model import ShipcallSchema
|
||||
|
||||
|
||||
@pytest.fixture(scope="function") # function: destroy fixture at the end of each test
|
||||
def prepare_shipcall_content():
|
||||
import datetime
|
||||
from BreCal.stubs.shipcall import get_shipcall_simple
|
||||
shipcall_stub = get_shipcall_simple()
|
||||
content = shipcall_stub.__dict__
|
||||
content["participants"] = []
|
||||
content = {k:v.isoformat() if isinstance(v, datetime.datetime) else v for k,v in content.items()}
|
||||
return locals()
|
||||
|
||||
def test_shipcall_input_validation_draft(prepare_shipcall_content):
|
||||
content = prepare_shipcall_content["content"]
|
||||
content["draft"] = 24.11
|
||||
|
||||
schemaModel = ShipcallSchema()
|
||||
with pytest.raises(ValidationError, match="Must be greater than 0 and less than or equal to 20."):
|
||||
loadedModel = schemaModel.load(data=content, many=False, partial=True)
|
||||
return
|
||||
|
||||
def test_shipcall_input_validation_voyage(prepare_shipcall_content):
|
||||
content = prepare_shipcall_content["content"]
|
||||
content["voyage"] = "".join(list(map(str,list(range(0,24))))) # 38 characters
|
||||
|
||||
schemaModel = ShipcallSchema()
|
||||
with pytest.raises(ValidationError, match="Longer than maximum length "):
|
||||
loadedModel = schemaModel.load(data=content, many=False, partial=True)
|
||||
return
|
||||
|
||||
|
||||
@pytest.fixture(scope="function") # function: destroy fixture at the end of each test
|
||||
def prepare_user_content():
|
||||
import datetime
|
||||
from BreCal.stubs.user import get_user_simple
|
||||
from BreCal.schemas.model import UserSchema
|
||||
schemaModel = UserSchema()
|
||||
|
||||
user_stub = get_user_simple()
|
||||
content = user_stub.__dict__
|
||||
content = {k:v.isoformat() if isinstance(v, datetime.datetime) else v for k,v in content.items()}
|
||||
content = {k:v for k,v in content.items() if k in list(schemaModel.fields.keys())}
|
||||
content["old_password"] = "myfavoritedog123"
|
||||
content["new_password"] = "SecuRepassW0rd!"
|
||||
return locals()
|
||||
|
||||
|
||||
def test_input_validation_berth_phone_number_is_valid(prepare_user_content):
|
||||
content, schemaModel = prepare_user_content["content"], prepare_user_content["schemaModel"]
|
||||
content["user_phone"] = "+49123 45678912" # whitespace and + are valid
|
||||
|
||||
loadedModel = schemaModel.load(data=content, many=False, partial=True)
|
||||
return
|
||||
|
||||
def test_input_validation_berth_phone_number_is_invalid(prepare_user_content):
|
||||
content, schemaModel = prepare_user_content["content"], prepare_user_content["schemaModel"]
|
||||
content["user_phone"] = "+49123 45678912!" # ! is invalid
|
||||
|
||||
with pytest.raises(ValidationError, match="one of the phone number values is not valid."):
|
||||
loadedModel = schemaModel.load(data=content, many=False, partial=True)
|
||||
return
|
||||
|
||||
def test_input_validation_new_password_too_short(prepare_user_content):
|
||||
content, schemaModel = prepare_user_content["content"], prepare_user_content["schemaModel"]
|
||||
content["new_password"] = "1234" # must have between 6 and 128 characters
|
||||
|
||||
with pytest.raises(ValidationError, match="Length must be between 6 and 128."):
|
||||
loadedModel = schemaModel.load(data=content, many=False, partial=True)
|
||||
return
|
||||
|
||||
def test_input_validation_user_email_invalid(prepare_user_content):
|
||||
content, schemaModel = prepare_user_content["content"], prepare_user_content["schemaModel"]
|
||||
content["user_email"] = "userbrecal.com" # forgot @ -> invalid
|
||||
|
||||
with pytest.raises(ValidationError, match="invalid email address"):
|
||||
loadedModel = schemaModel.load(data=content, many=False, partial=True)
|
||||
return
|
||||
@ -8,13 +8,14 @@ def test_create_app():
|
||||
import sys
|
||||
from BreCal import get_project_root
|
||||
|
||||
project_root = get_project_root("brecal")
|
||||
project_root = os.path.join(os.path.expanduser("~"), "brecal")
|
||||
lib_location = os.path.join(project_root, "src", "server")
|
||||
sys.path.append(lib_location)
|
||||
|
||||
from BreCal import create_app
|
||||
os.chdir(os.path.join(lib_location,"BreCal")) # set the current directory to ~/brecal/src/server/BreCal, so the config is found
|
||||
application = create_app()
|
||||
instance_path = os.path.join(os.path.expanduser('~'), "brecal", "src", "server", "instance", "instance")
|
||||
application = create_app(test_config=None, instance_path=instance_path)
|
||||
return
|
||||
|
||||
if __name__=="__main__":
|
||||
|
||||
275
src/server/tests/validators/test_input_validation_ship.py
Normal file
275
src/server/tests/validators/test_input_validation_ship.py
Normal file
@ -0,0 +1,275 @@
|
||||
import pytest
|
||||
|
||||
import os
|
||||
import jwt
|
||||
import json
|
||||
import requests
|
||||
import datetime
|
||||
import werkzeug
|
||||
import re
|
||||
from marshmallow import ValidationError
|
||||
|
||||
from BreCal import local_db
|
||||
from BreCal.schemas import model
|
||||
|
||||
from BreCal.impl.ships import GetShips
|
||||
|
||||
from BreCal.schemas.model import Participant_Assignment, EvaluationType, ShipcallType
|
||||
from BreCal.stubs.ship import get_stub_valid_ship, get_stub_valid_ship_loaded_model
|
||||
from BreCal.validators.input_validation import validation_error_default_asserts
|
||||
from BreCal.schemas.model import ParticipantType
|
||||
from BreCal.validators.input_validation_ship import InputValidationShip
|
||||
from BreCal.database.sql_handler import execute_sql_query_standalone
|
||||
from BreCal.database.sql_queries import SQLQuery
|
||||
|
||||
instance_path = os.path.join(os.path.expanduser('~'), "brecal", "src", "server", "instance")
|
||||
local_db.initPool(os.path.dirname(instance_path), connection_filename="connection_data_local.json")
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def get_stub_token():
|
||||
"""
|
||||
performs a login to the user 'maxm' and returns the respective url and the token. The token will be used in
|
||||
further requests in the following format (example of post-request):
|
||||
requests.post(f"{url}/shipcalls", headers={"Content-Type":"text", "Authorization":f"Bearer {token}"}, json=post_data)
|
||||
|
||||
"""
|
||||
port = 9013
|
||||
url = f"http://127.0.0.1:{port}"
|
||||
|
||||
# set the JWT key
|
||||
os.environ['SECRET_KEY'] = 'zdiTz8P3jXOc7jztIQAoelK4zztyuCpJ'
|
||||
|
||||
try:
|
||||
response = requests.post(f"{url}/login", json=jwt.decode("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6Im1heG0iLCJwYXNzd29yZCI6IlN0YXJ0MTIzNCJ9.uIrbz3g-IwwTLz6C1zXELRGtAtRJ_myYJ4J4x0ozjAI", key=os.environ.get("SECRET_KEY"), algorithms=["HS256"]))
|
||||
except requests.ConnectionError as err:
|
||||
raise AssertionError(f"could not establish a connection to the default url. Did you start an instance of the local database at port {port}? Looking for a connection to {url}")
|
||||
user = response.json()
|
||||
token = user.get("token")
|
||||
return locals()
|
||||
|
||||
def test_input_validation_ship_fails_when_length_is_incorrect():
|
||||
with pytest.raises(ValidationError, match=re.escape("Must be greater than 0 and less than 1000.")):
|
||||
post_data = get_stub_valid_ship()
|
||||
post_data["length"] = 0
|
||||
loadedModel = model.ShipSchema().load(data=post_data, many=False, partial=True)
|
||||
|
||||
with pytest.raises(ValidationError, match=re.escape("Must be greater than 0 and less than 1000.")):
|
||||
post_data = get_stub_valid_ship()
|
||||
post_data["length"] = 1000
|
||||
loadedModel = model.ShipSchema().load(data=post_data, many=False, partial=True)
|
||||
|
||||
# success
|
||||
post_data = get_stub_valid_ship()
|
||||
post_data["length"] = 123
|
||||
loadedModel = model.ShipSchema().load(data=post_data, many=False, partial=True)
|
||||
return
|
||||
|
||||
def test_input_validation_ship_fails_when_width_is_incorrect():
|
||||
with pytest.raises(ValidationError, match=re.escape("Must be greater than 0 and less than 100.")):
|
||||
post_data = get_stub_valid_ship()
|
||||
post_data["width"] = 0
|
||||
loadedModel = model.ShipSchema().load(data=post_data, many=False, partial=True)
|
||||
|
||||
with pytest.raises(ValidationError, match=re.escape("Must be greater than 0 and less than 100.")):
|
||||
post_data = get_stub_valid_ship()
|
||||
post_data["width"] = 100
|
||||
loadedModel = model.ShipSchema().load(data=post_data, many=False, partial=True)
|
||||
|
||||
# success
|
||||
post_data = get_stub_valid_ship()
|
||||
post_data["width"] = 12
|
||||
loadedModel = model.ShipSchema().load(data=post_data, many=False, partial=True)
|
||||
return
|
||||
|
||||
def test_input_validation_ship_fails_when_name_is_incorrect():
|
||||
with pytest.raises(ValidationError, match=re.escape("'name' argument should have at max. 63 characters")):
|
||||
post_data = get_stub_valid_ship()
|
||||
post_data["name"] = "0123456789012345678901234567890123456789012345678901234567890123"
|
||||
loadedModel = model.ShipSchema().load(data=post_data, many=False, partial=True)
|
||||
|
||||
with pytest.raises(ValidationError, match=re.escape("'name' argument should not have special characters.")):
|
||||
post_data = get_stub_valid_ship()
|
||||
post_data["name"] = '👽'
|
||||
loadedModel = model.ShipSchema().load(data=post_data, many=False, partial=True)
|
||||
|
||||
post_data = get_stub_valid_ship()
|
||||
post_data["name"] = "012345678901234567890123456789012345678901234567890123456789012"
|
||||
loadedModel = model.ShipSchema().load(data=post_data, many=False, partial=True)
|
||||
return
|
||||
|
||||
def test_input_validation_ship_fails_when_callsign_is_incorrect():
|
||||
with pytest.raises(ValidationError, match=re.escape("'callsign' argument should not have more than 8 characters")):
|
||||
post_data = get_stub_valid_ship()
|
||||
post_data["callsign"] = "123456789"
|
||||
loadedModel = model.ShipSchema().load(data=post_data, many=False, partial=True)
|
||||
|
||||
with pytest.raises(ValidationError, match=re.escape("'callsign' argument should not have special characters.")):
|
||||
post_data = get_stub_valid_ship()
|
||||
post_data["callsign"] = '👽'
|
||||
loadedModel = model.ShipSchema().load(data=post_data, many=False, partial=True)
|
||||
|
||||
# success
|
||||
post_data = get_stub_valid_ship()
|
||||
post_data["callsign"] = 'PBIO'
|
||||
loadedModel = model.ShipSchema().load(data=post_data, many=False, partial=True)
|
||||
|
||||
# success
|
||||
post_data = get_stub_valid_ship()
|
||||
post_data["callsign"] = None
|
||||
loadedModel = model.ShipSchema().load(data=post_data, many=False, partial=True)
|
||||
|
||||
return
|
||||
|
||||
def test_input_validation_ship_fails_when_imo_is_incorrect():
|
||||
# imo must have exactly 7 digits and can't be None
|
||||
with pytest.raises(ValidationError, match=re.escape("'imo' should be a 7-digit number")):
|
||||
post_data = get_stub_valid_ship()
|
||||
post_data["imo"] = 12345678
|
||||
loadedModel = model.ShipSchema().load(data=post_data, many=False, partial=True)
|
||||
|
||||
with pytest.raises(ValidationError, match=re.escape("Field may not be null.")):
|
||||
post_data = get_stub_valid_ship()
|
||||
post_data["imo"] = None
|
||||
loadedModel = model.ShipSchema().load(data=post_data, many=False, partial=True)
|
||||
|
||||
# success
|
||||
post_data = get_stub_valid_ship()
|
||||
post_data["imo"] = 1234567
|
||||
loadedModel = model.ShipSchema().load(data=post_data, many=False, partial=True)
|
||||
|
||||
# success: when there are less than 7-digits, the backend applies trailing zeros
|
||||
post_data = get_stub_valid_ship()
|
||||
post_data["imo"] = 123456
|
||||
loadedModel = model.ShipSchema().load(data=post_data, many=False, partial=True)
|
||||
return
|
||||
|
||||
def test_input_validation_ship_fails_when_bollard_pull_and_tug_values_are_set():
|
||||
ivs = InputValidationShip()
|
||||
|
||||
with pytest.raises(ValidationError, match=re.escape("'bollard_pull' is only allowed, when a ship is a tug ('is_tug').")):
|
||||
content = {'is_tug':0, 'bollard_pull':230}
|
||||
ivs.optionally_evaluate_bollard_pull_value(content)
|
||||
|
||||
with pytest.raises(ValidationError, match=re.escape("'bollard_pull' is only allowed, when a ship is a tug ('is_tug').")):
|
||||
content = {'is_tug':None, 'bollard_pull':230}
|
||||
ivs.optionally_evaluate_bollard_pull_value(content)
|
||||
|
||||
content = {'is_tug':0, 'bollard_pull':None}
|
||||
ivs.optionally_evaluate_bollard_pull_value(content)
|
||||
|
||||
content = {'is_tug':1, 'bollard_pull':None}
|
||||
ivs.optionally_evaluate_bollard_pull_value(content)
|
||||
|
||||
content = {'is_tug':1, 'bollard_pull':125}
|
||||
ivs.optionally_evaluate_bollard_pull_value(content)
|
||||
|
||||
with pytest.raises(ValidationError, match=re.escape("when a ship is a tug, the bollard pull must be 0 < value < 500.")):
|
||||
content = {'is_tug':1, 'bollard_pull':-1}
|
||||
ivs.optionally_evaluate_bollard_pull_value(content)
|
||||
|
||||
with pytest.raises(ValidationError, match=re.escape("when a ship is a tug, the bollard pull must be 0 < value < 500.")):
|
||||
content = {'is_tug':1, 'bollard_pull':0}
|
||||
ivs.optionally_evaluate_bollard_pull_value(content)
|
||||
|
||||
with pytest.raises(ValidationError, match=re.escape("when a ship is a tug, the bollard pull must be 0 < value < 500.")):
|
||||
content = {'is_tug':1, 'bollard_pull':500}
|
||||
ivs.optionally_evaluate_bollard_pull_value(content)
|
||||
|
||||
with pytest.raises(ValidationError, match=re.escape("when a ship is a tug, the bollard pull must be 0 < value < 500.")):
|
||||
content = {'is_tug':1, 'bollard_pull':501}
|
||||
ivs.optionally_evaluate_bollard_pull_value(content)
|
||||
return
|
||||
|
||||
def test_input_validation_ship_post_request_fails_when_ship_imo_already_exists():
|
||||
# get the ships, convert them to a list of JSON dictionaries
|
||||
response, status_code, header = GetShips(token=None)
|
||||
ships = json.loads(response)
|
||||
|
||||
# extract only the 'imo' values
|
||||
ship_imos = [ship.get("imo") for ship in ships]
|
||||
|
||||
post_data = get_stub_valid_ship()
|
||||
post_data["imo"] = ship_imos[-1] # assign one of the IMOs, which already exist
|
||||
loadedModel = get_stub_valid_ship_loaded_model(post_data)
|
||||
content = post_data
|
||||
|
||||
with pytest.raises(ValidationError, match="the provided ship IMO 9186687 already exists. A ship may only be added, if there is no other ship with the same IMO number."):
|
||||
InputValidationShip.check_ship_imo_already_exists(loadedModel)
|
||||
return
|
||||
|
||||
|
||||
|
||||
def test_input_validation_ship_put_request_fails_when_ship_imo_should_be_changed():
|
||||
# get the ships, convert them to a list of JSON dictionaries
|
||||
response, status_code, header = GetShips(token=None)
|
||||
ships = json.loads(response)
|
||||
selected_ship = ships[-1] # select one of the ships; in this case the last one.
|
||||
|
||||
put_data = get_stub_valid_ship()
|
||||
put_data["imo"] = selected_ship.get("imo")+1 # assign one of the IMOs, which already exist
|
||||
|
||||
loadedModel = get_stub_valid_ship_loaded_model(put_data)
|
||||
content = put_data
|
||||
|
||||
with pytest.raises(ValidationError, match=re.escape("The IMO number field may not be changed since it serves the purpose of a primary (matching) key.")):
|
||||
InputValidationShip.put_content_may_not_contain_imo_number(content)
|
||||
return
|
||||
|
||||
|
||||
def test_input_validation_ship_put_request_fails_when_ship_id_is_missing():
|
||||
put_data = get_stub_valid_ship()
|
||||
put_data.pop("id",None) # make sure there is no ID within the put data for this test
|
||||
|
||||
loadedModel = get_stub_valid_ship_loaded_model(put_data)
|
||||
content = put_data
|
||||
|
||||
with pytest.raises(ValidationError, match="The id field is required."):
|
||||
InputValidationShip.content_contains_ship_id(content)
|
||||
return
|
||||
|
||||
def test_input_validation_ship_post_failure_case_20240802():
|
||||
"""Description: https://trello.com/c/DmwLnfbN/260-shipcall-anlegen-bad-format"""
|
||||
|
||||
post_data = {
|
||||
"name": "Testschiff 02",
|
||||
"imo": 1, #0000000001,
|
||||
"length": 100.2,
|
||||
"width": 16.5,
|
||||
"is_tug": 0,
|
||||
"bollard_pull": 42,
|
||||
"callsign": "9992",
|
||||
"participant_id": None,
|
||||
"eni": 1
|
||||
}
|
||||
|
||||
content = post_data
|
||||
loadedModel = model.ShipSchema().load(data=post_data, many=False, partial=True)
|
||||
|
||||
# Fails: not BSMD user
|
||||
with pytest.raises(ValidationError, match="current user does not belong to BSMD. Cannot post, put or delete ships. Found user data"):
|
||||
user = execute_sql_query_standalone(query=SQLQuery.get_user_by_id(), param={"id":9}, command_type="single", model=model.User)
|
||||
user_data = user.__dict__
|
||||
assert user.participant_id == 4
|
||||
|
||||
InputValidationShip.evaluate_post_data(user_data, loadedModel, content)
|
||||
|
||||
# Fails: bollard_pull is set, but ship is not a tug
|
||||
with pytest.raises(ValidationError, match="'bollard_pull' is only allowed, when a ship is a tug"):
|
||||
user = execute_sql_query_standalone(query=SQLQuery.get_user_by_id(), param={"id":5}, command_type="single", model=model.User)
|
||||
user_data = user.__dict__
|
||||
assert user.participant_id == 3
|
||||
|
||||
InputValidationShip.evaluate_post_data(user_data, loadedModel, content)
|
||||
|
||||
# Success
|
||||
post_data["bollard_pull"] = None
|
||||
InputValidationShip.evaluate_post_data(user_data, loadedModel, content)
|
||||
return
|
||||
|
||||
def test_input_validation_ship_post_failure_when_name_empty():
|
||||
# success
|
||||
post_data = get_stub_valid_ship()
|
||||
post_data["name"] = "" # empty name
|
||||
with pytest.raises(ValidationError, match="'name' argument should have at least one character"):
|
||||
loadedModel = model.ShipSchema().load(data=post_data, many=False, partial=True)
|
||||
return
|
||||
935
src/server/tests/validators/test_input_validation_shipcall.py
Normal file
935
src/server/tests/validators/test_input_validation_shipcall.py
Normal file
@ -0,0 +1,935 @@
|
||||
import pytest
|
||||
|
||||
import os
|
||||
import re
|
||||
import jwt
|
||||
import json
|
||||
import requests
|
||||
import datetime
|
||||
import werkzeug
|
||||
from marshmallow import ValidationError
|
||||
|
||||
from BreCal import local_db
|
||||
|
||||
from BreCal.schemas.model import Participant_Assignment, EvaluationType, ShipcallType
|
||||
from BreCal.stubs.shipcall import create_postman_stub_shipcall, get_stub_valid_shipcall_arrival, get_stub_valid_shipcall_departure, get_stub_valid_shipcall_shifting, get_stub_shipcall_arrival_invalid_missing_eta, get_stub_shipcall_shifting_invalid_missing_eta, get_stub_shipcall_shifting_invalid_missing_etd, get_stub_shipcall_arrival_invalid_missing_type, get_stub_shipcall_departure_invalid_missing_etd
|
||||
from BreCal.stubs.participant import get_stub_list_of_valid_participants
|
||||
from BreCal.validators.input_validation import validation_error_default_asserts
|
||||
from BreCal.schemas.model import ParticipantType, ShipcallParticipantMap
|
||||
from BreCal.validators.input_validation_shipcall import InputValidationShipcall
|
||||
from BreCal.database.sql_handler import execute_sql_query_standalone
|
||||
from BreCal.database.sql_queries import SQLQuery
|
||||
from BreCal.schemas import model
|
||||
|
||||
instance_path = os.path.join(os.path.expanduser('~'), "brecal", "src", "server", "instance")
|
||||
local_db.initPool(os.path.dirname(instance_path), connection_filename="connection_data_local.json")
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def get_stub_token():
|
||||
"""
|
||||
performs a login to the user 'maxm' and returns the respective url and the token. The token will be used in
|
||||
further requests in the following format (example of post-request):
|
||||
requests.post(f"{url}/shipcalls", headers={"Content-Type":"text", "Authorization":f"Bearer {token}"}, json=post_data)
|
||||
|
||||
"""
|
||||
port = 9013
|
||||
url = f"http://127.0.0.1:{port}"
|
||||
|
||||
# set the JWT key
|
||||
os.environ['SECRET_KEY'] = 'zdiTz8P3jXOc7jztIQAoelK4zztyuCpJ'
|
||||
|
||||
try:
|
||||
response = requests.post(f"{url}/login", json=jwt.decode("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6Im1heG0iLCJwYXNzd29yZCI6IlN0YXJ0MTIzNCJ9.uIrbz3g-IwwTLz6C1zXELRGtAtRJ_myYJ4J4x0ozjAI", key=os.environ.get("SECRET_KEY"), algorithms=["HS256"]))
|
||||
except requests.ConnectionError as err:
|
||||
raise AssertionError(f"could not establish a connection to the default url. Did you start an instance of the local database at port {port}? Looking for a connection to {url}")
|
||||
user = response.json()
|
||||
token = user.get("token")
|
||||
return locals()
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def get_shipcall_id_after_stub_post_request(get_stub_token):
|
||||
url, token = get_stub_token["url"], get_stub_token["token"]
|
||||
|
||||
post_data = get_stub_valid_shipcall_arrival()
|
||||
response = requests.post(
|
||||
f"{url}/shipcalls", headers={"Content-Type":"text", "Authorization":f"Bearer {token}"}, json=post_data
|
||||
)
|
||||
assert response.status_code==201
|
||||
shipcall_id = response.json().get("id")
|
||||
assert shipcall_id is not None
|
||||
return locals()
|
||||
|
||||
def test_shipcall_post_request_fails_when_ship_id_is_invalid(get_stub_token):
|
||||
url, token = get_stub_token["url"], get_stub_token["token"]
|
||||
|
||||
post_data = get_stub_valid_shipcall_arrival() # create_postman_stub_shipcall()
|
||||
|
||||
post_data["ship_id"] = 1234562
|
||||
response = requests.post(
|
||||
f"{url}/shipcalls", headers={"Content-Type":"text", "Authorization":f"Bearer {token}"}, json=post_data
|
||||
)
|
||||
|
||||
with pytest.raises(ValidationError, match=f"provided an invalid ship id"):
|
||||
assert response.status_code==400
|
||||
raise ValidationError(response.json()) # because the response does not raise a ValidationError, we artifically create it to check the pytest.raises outcome
|
||||
return
|
||||
|
||||
def test_shipcall_post_request_fails_when_arrival_berth_id_is_invalid(get_stub_token):
|
||||
url, token = get_stub_token["url"], get_stub_token["token"]
|
||||
|
||||
post_data = get_stub_valid_shipcall_arrival() # create_postman_stub_shipcall()
|
||||
|
||||
post_data["arrival_berth_id"] = 1234562
|
||||
response = requests.post(
|
||||
f"{url}/shipcalls", headers={"Content-Type":"text", "Authorization":f"Bearer {token}"}, json=post_data
|
||||
)
|
||||
|
||||
with pytest.raises(ValidationError, match=f"provided an invalid arrival berth id"):
|
||||
assert response.status_code==400
|
||||
raise ValidationError(response.json()) # because the response does not raise a ValidationError, we artifically create it to check the pytest.raises outcome
|
||||
return
|
||||
|
||||
def test_shipcall_post_request_fails_when_departure_berth_id_is_invalid(get_stub_token):
|
||||
url, token = get_stub_token["url"], get_stub_token["token"]
|
||||
|
||||
post_data = get_stub_valid_shipcall_arrival() # create_postman_stub_shipcall()
|
||||
|
||||
post_data["departure_berth_id"] = 1234562
|
||||
response = requests.post(
|
||||
f"{url}/shipcalls", headers={"Content-Type":"text", "Authorization":f"Bearer {token}"}, json=post_data
|
||||
)
|
||||
|
||||
with pytest.raises(ValidationError, match=f"provided an invalid departure berth id"):
|
||||
assert response.status_code==400
|
||||
raise ValidationError(response.json()) # because the response does not raise a ValidationError, we artifically create it to check the pytest.raises outcome
|
||||
return
|
||||
|
||||
def test_shipcall_post_request_fails_when_participant_ids_are_invalid(get_stub_token):
|
||||
url, token = get_stub_token["url"], get_stub_token["token"]
|
||||
|
||||
post_data = get_stub_valid_shipcall_arrival() # create_postman_stub_shipcall()
|
||||
|
||||
participant_id = 1234562
|
||||
post_data["participants"] = [Participant_Assignment(participant_id,4).to_json()] # identical to: [{'participant_id': 1234562, 'type': 4}]
|
||||
response = requests.post(
|
||||
f"{url}/shipcalls", headers={"Content-Type":"text", "Authorization":f"Bearer {token}"}, json=post_data
|
||||
)
|
||||
|
||||
with pytest.raises(ValidationError, match=f"the provided participant_id {participant_id} does not exist in the database"):
|
||||
assert response.status_code==400
|
||||
raise ValidationError(response.json()) # because the response does not raise a ValidationError, we artifically create it to check the pytest.raises outcome
|
||||
return
|
||||
|
||||
def test_shipcall_post_request_fails_when_forbidden_keys_are_set(get_stub_token):
|
||||
url, token = get_stub_token["url"], get_stub_token["token"]
|
||||
|
||||
original_post_data = get_stub_valid_shipcall_arrival() # create_postman_stub_shipcall()
|
||||
|
||||
for forbidden_key, forbidden_value in zip(["canceled", "evaluation", "evaluation_message"], [1, EvaluationType.red.name, "random error message"]):
|
||||
post_data = original_post_data.copy()
|
||||
post_data[forbidden_key] = forbidden_value
|
||||
response = requests.post(
|
||||
f"{url}/shipcalls", headers={"Content-Type":"text", "Authorization":f"Bearer {token}"}, json=post_data
|
||||
)
|
||||
assert response.status_code==400
|
||||
with pytest.raises(ValidationError, match=f"may not be set on POST. "):
|
||||
raise ValidationError(response.json()) # because the response does not raise a ValidationError, we artifically create it to check the pytest.raises outcome
|
||||
return
|
||||
|
||||
def test_shipcall_post_request_fails_when_draft_is_out_of_range(get_stub_token):
|
||||
url, token = get_stub_token["url"], get_stub_token["token"]
|
||||
|
||||
post_data = get_stub_valid_shipcall_arrival() # create_postman_stub_shipcall()
|
||||
|
||||
post_data["draft"] = 0
|
||||
|
||||
response = requests.post(f"{url}/shipcalls", headers={"Content-Type":"text", "Authorization":f"Bearer {token}"}, json=post_data)
|
||||
with pytest.raises(ValidationError, match=f"Must be greater than 0 and less than or equal to "):
|
||||
assert response.status_code==400
|
||||
raise ValidationError(response.json()) # because the response does not raise a ValidationError, we artifically create it to check the pytest.raises outcome
|
||||
|
||||
post_data = get_stub_valid_shipcall_arrival() # create_postman_stub_shipcall()
|
||||
|
||||
post_data["draft"] = 21
|
||||
|
||||
response = requests.post(f"{url}/shipcalls", headers={"Content-Type":"text", "Authorization":f"Bearer {token}"}, json=post_data)
|
||||
with pytest.raises(ValidationError, match=f"Must be greater than 0 and less than or equal to "):
|
||||
assert response.status_code==400
|
||||
raise ValidationError(response.json()) # because the response does not raise a ValidationError, we artifically create it to check the pytest.raises outcome
|
||||
|
||||
post_data = get_stub_valid_shipcall_arrival() # create_postman_stub_shipcall()
|
||||
|
||||
post_data["draft"] = 20
|
||||
response = requests.post(f"{url}/shipcalls", headers={"Content-Type":"text", "Authorization":f"Bearer {token}"}, json=post_data)
|
||||
assert response.status_code==201, f"the request should accept 20.0 as a valid 'draft' value"
|
||||
return
|
||||
|
||||
def test_shipcall_post_request_fails_when_recommended_tugs_is_out_of_range(get_stub_token):
|
||||
url, token = get_stub_token["url"], get_stub_token["token"]
|
||||
|
||||
original_post_data = get_stub_valid_shipcall_arrival() # create_postman_stub_shipcall()
|
||||
|
||||
post_data = original_post_data.copy()
|
||||
post_data["recommended_tugs"] = 10
|
||||
response = requests.post(f"{url}/shipcalls", headers={"Content-Type":"text", "Authorization":f"Bearer {token}"}, json=post_data)
|
||||
assert response.status_code == 201
|
||||
|
||||
post_data = original_post_data.copy()
|
||||
post_data["recommended_tugs"] = 0
|
||||
response = requests.post(f"{url}/shipcalls", headers={"Content-Type":"text", "Authorization":f"Bearer {token}"}, json=post_data)
|
||||
assert response.status_code == 201
|
||||
|
||||
post_data = original_post_data.copy()
|
||||
post_data["recommended_tugs"] = 11
|
||||
response = requests.post(f"{url}/shipcalls", headers={"Content-Type":"text", "Authorization":f"Bearer {token}"}, json=post_data)
|
||||
|
||||
with pytest.raises(ValidationError, match=f"Must be greater than or equal to 0 and less than or equal to"):
|
||||
assert response.status_code==400
|
||||
raise ValidationError(response.json()) # because the response does not raise a ValidationError, we artifically create it to check the pytest.raises outcome
|
||||
|
||||
|
||||
def test_shipcall_post_request_fails_when_voyage_string_is_invalid(get_stub_token):
|
||||
url, token = get_stub_token["url"], get_stub_token["token"]
|
||||
|
||||
original_post_data = get_stub_valid_shipcall_arrival() # create_postman_stub_shipcall()
|
||||
|
||||
# Accept
|
||||
post_data = original_post_data.copy()
|
||||
post_data["voyage"] = "abcdefghijklmnop"
|
||||
response = requests.post(f"{url}/shipcalls", headers={"Content-Type":"text", "Authorization":f"Bearer {token}"}, json=post_data)
|
||||
assert response.status_code==201
|
||||
|
||||
# Fail: too long string
|
||||
post_data = original_post_data.copy()
|
||||
post_data["voyage"] = "abcdefghijklmnopq"
|
||||
response = requests.post(f"{url}/shipcalls", headers={"Content-Type":"text", "Authorization":f"Bearer {token}"}, json=post_data)
|
||||
|
||||
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"] = '👽'
|
||||
response = requests.post(f"{url}/shipcalls", headers={"Content-Type":"text", "Authorization":f"Bearer {token}"}, json=post_data)
|
||||
|
||||
with pytest.raises(ValidationError, match="Please use only digits and ASCII letters."):
|
||||
assert response.status_code==400
|
||||
raise ValidationError(response.json())
|
||||
return
|
||||
|
||||
def test_shipcall_post_request_fails_when_type_arrival_and_not_in_future(get_stub_token):
|
||||
url, token = get_stub_token["url"], get_stub_token["token"]
|
||||
|
||||
original_post_data = get_stub_valid_shipcall_arrival() # create_postman_stub_shipcall()
|
||||
|
||||
# accept
|
||||
post_data = original_post_data.copy()
|
||||
post_data["type"] = ShipcallType.arrival.name
|
||||
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(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="is too far in the past. Incorrect datetime provided"):
|
||||
assert response.status_code==400
|
||||
raise ValidationError(response.json())
|
||||
return
|
||||
|
||||
def test_shipcall_post_request_fails_when_type_departure_and_not_in_future(get_stub_token):
|
||||
url, token = get_stub_token["url"], get_stub_token["token"]
|
||||
|
||||
original_post_data = get_stub_valid_shipcall_departure() # create_postman_stub_shipcall()
|
||||
|
||||
# accept
|
||||
post_data = original_post_data.copy()
|
||||
post_data["type"] = ShipcallType.departure.name
|
||||
post_data["etd"] = (datetime.datetime.now() + datetime.timedelta(hours=3)).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.departure.name
|
||||
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="is too far in the past. Incorrect datetime provided"):
|
||||
assert response.status_code==400
|
||||
raise ValidationError(response.json())
|
||||
return
|
||||
|
||||
def test_shipcall_post_request_fails_when_type_shifting_and_not_in_future(get_stub_token):
|
||||
url, token = get_stub_token["url"], get_stub_token["token"]
|
||||
|
||||
original_post_data = get_stub_valid_shipcall_shifting() # create_postman_stub_shipcall()
|
||||
|
||||
# accept
|
||||
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["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)
|
||||
assert response.status_code == 201
|
||||
|
||||
# error
|
||||
post_data = original_post_data.copy()
|
||||
post_data["type"] = ShipcallType.shifting.name
|
||||
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="is too far in the past. Incorrect datetime provided"):
|
||||
assert response.status_code==400
|
||||
raise ValidationError(response.json())
|
||||
return
|
||||
|
||||
def test_shipcall_post_request_fails_when_type_arrival_and_missing_eta(get_stub_token):
|
||||
url, token = get_stub_token["url"], get_stub_token["token"]
|
||||
|
||||
original_post_data = get_stub_valid_shipcall_arrival() # create_postman_stub_shipcall()
|
||||
|
||||
post_data = original_post_data.copy()
|
||||
post_data.pop("eta", None)
|
||||
post_data["type"] = ShipcallType.arrival.name
|
||||
response = requests.post(f"{url}/shipcalls", headers={"Content-Type":"text", "Authorization":f"Bearer {token}"}, json=post_data)
|
||||
|
||||
with pytest.raises(ValidationError, match="Missing key!"):
|
||||
assert response.status_code==400
|
||||
raise ValidationError(response.json())
|
||||
return
|
||||
|
||||
def test_shipcall_post_request_fails_when_type_departure_and_missing_etd(get_stub_token):
|
||||
url, token = get_stub_token["url"], get_stub_token["token"]
|
||||
|
||||
original_post_data = get_stub_valid_shipcall_arrival() # create_postman_stub_shipcall()
|
||||
|
||||
post_data = original_post_data.copy()
|
||||
post_data.pop("etd", None)
|
||||
post_data["type"] = ShipcallType.departure.name
|
||||
response = requests.post(f"{url}/shipcalls", headers={"Content-Type":"text", "Authorization":f"Bearer {token}"}, json=post_data)
|
||||
|
||||
with pytest.raises(ValidationError, match="Missing key!"):
|
||||
assert response.status_code==400
|
||||
raise ValidationError(response.json())
|
||||
return
|
||||
|
||||
def test_shipcall_post_request_fails_when_type_shifting_and_missing_eta(get_stub_token):
|
||||
url, token = get_stub_token["url"], get_stub_token["token"]
|
||||
|
||||
original_post_data = get_stub_valid_shipcall_arrival() # create_postman_stub_shipcall()
|
||||
|
||||
post_data = original_post_data.copy()
|
||||
post_data["type"] = ShipcallType.departure.name
|
||||
post_data.pop("eta", None)
|
||||
post_data["etd"] = (datetime.datetime.now() + datetime.timedelta(hours=3)).isoformat()
|
||||
response = requests.post(f"{url}/shipcalls", headers={"Content-Type":"text", "Authorization":f"Bearer {token}"}, json=post_data)
|
||||
|
||||
with pytest.raises(ValidationError, match="Missing key!"):
|
||||
assert response.status_code==400
|
||||
raise ValidationError(response.json())
|
||||
return
|
||||
|
||||
def test_shipcall_post_request_fails_when_type_shifting_and_missing_etd(get_stub_token):
|
||||
url, token = get_stub_token["url"], get_stub_token["token"]
|
||||
|
||||
original_post_data = get_stub_valid_shipcall_arrival() # create_postman_stub_shipcall()
|
||||
|
||||
post_data = original_post_data.copy()
|
||||
post_data["type"] = ShipcallType.departure.name
|
||||
post_data["eta"] = (datetime.datetime.now() + datetime.timedelta(hours=3)).isoformat()
|
||||
post_data.pop("etd", None)
|
||||
response = requests.post(f"{url}/shipcalls", headers={"Content-Type":"text", "Authorization":f"Bearer {token}"}, json=post_data)
|
||||
|
||||
with pytest.raises(ValidationError, match="Missing key!"):
|
||||
assert response.status_code==400
|
||||
raise ValidationError(response.json())
|
||||
return
|
||||
|
||||
|
||||
|
||||
def test_shipcall_post_invalid_tidal_window_to_smaller_than_tidal_window_from(get_stub_token):
|
||||
url, token = get_stub_token["url"], get_stub_token["token"]
|
||||
|
||||
post_data = get_stub_valid_shipcall_shifting()
|
||||
post_data["tidal_window_to"] = (datetime.datetime.fromisoformat(post_data["tidal_window_from"])-datetime.timedelta(minutes=1)).isoformat()
|
||||
|
||||
response = requests.post(
|
||||
f"{url}/shipcalls", headers={"Content-Type":"text", "Authorization":f"Bearer {token}"}, json=post_data
|
||||
)
|
||||
validation_error_default_asserts(response)
|
||||
assert "\'tidal_window_to\' must take place after \'tidal_window_from\'" in response.json().get("message","")
|
||||
return
|
||||
|
||||
def test_shipcall_post_invalid_tidal_windows_must_be_in_future(get_stub_token):
|
||||
url, token = get_stub_token["url"], get_stub_token["token"]
|
||||
|
||||
post_data = get_stub_valid_shipcall_shifting()
|
||||
post_data["tidal_window_from"] = (datetime.datetime.now()-datetime.timedelta(minutes=1)).isoformat()
|
||||
|
||||
response = requests.post(
|
||||
f"{url}/shipcalls", headers={"Content-Type":"text", "Authorization":f"Bearer {token}"}, json=post_data
|
||||
)
|
||||
validation_error_default_asserts(response)
|
||||
assert "\'tidal_window_from\' must be in the future. " in response.json().get("message","")
|
||||
|
||||
post_data = get_stub_valid_shipcall_shifting()
|
||||
post_data["tidal_window_to"] = (datetime.datetime.now()-datetime.timedelta(minutes=1)).isoformat()
|
||||
|
||||
response = requests.post(
|
||||
f"{url}/shipcalls", headers={"Content-Type":"text", "Authorization":f"Bearer {token}"}, json=post_data
|
||||
)
|
||||
validation_error_default_asserts(response)
|
||||
assert "\'tidal_window_to\' must be in the future. " in response.json().get("message","")
|
||||
return
|
||||
|
||||
def test_shipcall_post_invalid_canceled_must_not_be_set(get_stub_token):
|
||||
url, token = get_stub_token["url"], get_stub_token["token"]
|
||||
stubs = [("canceled", 1), ("evaluation", "green"), ("evaluation_message", "this is an error message")]
|
||||
|
||||
for key, value in stubs:
|
||||
post_data = get_stub_valid_shipcall_shifting()
|
||||
post_data[key] = value
|
||||
|
||||
response = requests.post(
|
||||
f"{url}/shipcalls", headers={"Content-Type":"text", "Authorization":f"Bearer {token}"}, json=post_data
|
||||
)
|
||||
validation_error_default_asserts(response)
|
||||
assert f"\'{key}\' may not be set on POST. Found:" in response.json().get("message","")
|
||||
return
|
||||
|
||||
def test_shipcall_post_invalid_participant_type_listed_multiple_times(get_stub_token):
|
||||
url, token = get_stub_token["url"], get_stub_token["token"]
|
||||
participants = get_stub_list_of_valid_participants()
|
||||
|
||||
# create double entry for 'id' and 'type'. Either of the two should raise an exception.
|
||||
participants.append(participants[0])
|
||||
|
||||
post_data = get_stub_valid_shipcall_shifting()
|
||||
post_data["participants"] = participants
|
||||
|
||||
response = requests.post(
|
||||
f"{url}/shipcalls", headers={"Content-Type":"text", "Authorization":f"Bearer {token}"}, json=post_data
|
||||
)
|
||||
validation_error_default_asserts(response)
|
||||
assert f"every participant id and type should be listed only once. Found multiple entries for one of the participants." in response.json().get("message","")
|
||||
return
|
||||
|
||||
def test_shipcall_post_invalid_participants_missing_agency(get_stub_token):
|
||||
url, token = get_stub_token["url"], get_stub_token["token"]
|
||||
|
||||
post_data = {}
|
||||
response = requests.get(
|
||||
f"{url}/participants", headers={"Content-Type":"text", "Authorization":f"Bearer {token}"}, json=post_data
|
||||
)
|
||||
participants = response.json()
|
||||
participant_id_dict = {item.get("id"):{"participant_id":item.get("id"), "type":item.get("type")} for item in response.json()}
|
||||
|
||||
# e.g., [{'participant_id': 2, 'type': 4}, {'participant_id': 3, 'type': 1}, {'participant_id': 4, 'type': 2}]
|
||||
participants = [participant_id_dict[2], participant_id_dict[3], participant_id_dict[4]]
|
||||
|
||||
post_data = get_stub_valid_shipcall_shifting()
|
||||
post_data["participants"] = participants
|
||||
|
||||
response = requests.post(
|
||||
f"{url}/shipcalls", headers={"Content-Type":"text", "Authorization":f"Bearer {token}"}, json=post_data
|
||||
)
|
||||
validation_error_default_asserts(response)
|
||||
assert "One of the assigned participants *must* be of type \'ParticipantType.AGENCY\'" in response.json().get("message","")
|
||||
return
|
||||
|
||||
def test_shipcall_post_invalid_etd_smaller_than_eta(get_stub_token):
|
||||
url, token = get_stub_token["url"], get_stub_token["token"]
|
||||
|
||||
post_data = get_stub_valid_shipcall_shifting()
|
||||
post_data["eta"] = (datetime.datetime.fromisoformat(post_data["etd"])-datetime.timedelta(minutes=1)).isoformat()
|
||||
|
||||
response = requests.post(
|
||||
f"{url}/shipcalls", headers={"Content-Type":"text", "Authorization":f"Bearer {token}"}, json=post_data
|
||||
)
|
||||
validation_error_default_asserts(response)
|
||||
assert "The estimated time of departure ('etd') must take place *before the estimated time of arrival ('eta')" in response.json().get("message","")
|
||||
#assert "\'etd\' must be larger than \'eta\'. " in response.json().get("message","")
|
||||
return
|
||||
|
||||
def test_shipcall_post_invalid_eta_and_etd_must_be_in_future(get_stub_token):
|
||||
url, token = get_stub_token["url"], get_stub_token["token"]
|
||||
|
||||
post_data = get_stub_valid_shipcall_shifting()
|
||||
post_data["etd"] = (datetime.datetime.now()-datetime.timedelta(minutes=1)).isoformat()
|
||||
|
||||
response = requests.post(
|
||||
f"{url}/shipcalls", headers={"Content-Type":"text", "Authorization":f"Bearer {token}"}, json=post_data
|
||||
)
|
||||
validation_error_default_asserts(response)
|
||||
assert "\'eta\' and \'etd\' must be in the future. " in response.json().get("message","")
|
||||
|
||||
post_data = get_stub_valid_shipcall_shifting()
|
||||
post_data["eta"] = (datetime.datetime.now()-datetime.timedelta(minutes=1)).isoformat()
|
||||
|
||||
response = requests.post(
|
||||
f"{url}/shipcalls", headers={"Content-Type":"text", "Authorization":f"Bearer {token}"}, json=post_data
|
||||
)
|
||||
validation_error_default_asserts(response)
|
||||
assert "\'eta\' and \'etd\' must be in the future. " in response.json().get("message","")
|
||||
return
|
||||
|
||||
def test_shipcall_post_request_missing_mandatory_keys(get_stub_token): # fixture: some sort of local API start in the background
|
||||
"""
|
||||
creates a valid shipcall entry and modifies it, by dropping one of the mandatory keys. This test ensures,
|
||||
that each mandatory key raises a ValidationError, when the key is missing.
|
||||
"""
|
||||
url = get_stub_token.get("url")
|
||||
token = get_stub_token.get("token")
|
||||
|
||||
post_data = get_stub_shipcall_arrival_invalid_missing_eta()
|
||||
response = requests.post(
|
||||
f"{url}/shipcalls", headers={"Content-Type":"text", "Authorization":f"Bearer {token}"}, json=post_data
|
||||
)
|
||||
validation_error_default_asserts(response)
|
||||
assert "providing \'eta\' is mandatory." in response.json().get("message","")
|
||||
|
||||
post_data = get_stub_shipcall_departure_invalid_missing_etd()
|
||||
response = requests.post(
|
||||
f"{url}/shipcalls", headers={"Content-Type":"text", "Authorization":f"Bearer {token}"}, json=post_data
|
||||
)
|
||||
validation_error_default_asserts(response)
|
||||
assert "providing \'etd\' is mandatory." in response.json().get("message","")
|
||||
|
||||
post_data = get_stub_shipcall_shifting_invalid_missing_eta()
|
||||
response = requests.post(
|
||||
f"{url}/shipcalls", headers={"Content-Type":"text", "Authorization":f"Bearer {token}"}, json=post_data
|
||||
)
|
||||
validation_error_default_asserts(response)
|
||||
assert "providing \'eta\' and \'etd\' is mandatory." in response.json().get("message","")
|
||||
|
||||
post_data = get_stub_shipcall_shifting_invalid_missing_etd()
|
||||
response = requests.post(
|
||||
f"{url}/shipcalls", headers={"Content-Type":"text", "Authorization":f"Bearer {token}"}, json=post_data
|
||||
)
|
||||
validation_error_default_asserts(response)
|
||||
assert "providing \'eta\' and \'etd\' is mandatory." in response.json().get("message","")
|
||||
|
||||
# the following keys all share the same logic and will be tested in sequence
|
||||
for KEY in ["eta", "arrival_berth_id", "type", "ship_id"]:
|
||||
|
||||
# artificially remove the KEY from a valid shipcall entry
|
||||
post_data = get_stub_valid_shipcall_arrival()
|
||||
post_data.pop(KEY,None)
|
||||
|
||||
response = requests.post(
|
||||
f"{url}/shipcalls", headers={"Content-Type":"text", "Authorization":f"Bearer {token}"}, json=post_data
|
||||
)
|
||||
validation_error_default_asserts(response)
|
||||
assert f"providing \'{KEY}\' is mandatory." in response.json().get("message","")
|
||||
|
||||
# BERTH ID (arrival or departure, based on type)
|
||||
post_data = get_stub_valid_shipcall_arrival()
|
||||
post_data.pop("arrival_berth_id",None)
|
||||
|
||||
response = requests.post(
|
||||
f"{url}/shipcalls", headers={"Content-Type":"text", "Authorization":f"Bearer {token}"}, json=post_data
|
||||
)
|
||||
validation_error_default_asserts(response)
|
||||
assert "providing \'arrival_berth_id\' is mandatory." in response.json().get("message","")
|
||||
|
||||
|
||||
post_data = get_stub_valid_shipcall_departure()
|
||||
post_data.pop("departure_berth_id",None)
|
||||
|
||||
response = requests.post(
|
||||
f"{url}/shipcalls", headers={"Content-Type":"text", "Authorization":f"Bearer {token}"}, json=post_data
|
||||
)
|
||||
validation_error_default_asserts(response)
|
||||
assert "providing \'departure_berth_id\' is mandatory." in response.json().get("message","")
|
||||
|
||||
|
||||
post_data = get_stub_valid_shipcall_shifting()
|
||||
post_data.pop("arrival_berth_id",None)
|
||||
|
||||
response = requests.post(
|
||||
f"{url}/shipcalls", headers={"Content-Type":"text", "Authorization":f"Bearer {token}"}, json=post_data
|
||||
)
|
||||
validation_error_default_asserts(response)
|
||||
assert "providing \'arrival_berth_id\' & \'departure_berth_id\' is mandatory." in response.json().get("message","")
|
||||
|
||||
|
||||
post_data = get_stub_valid_shipcall_shifting()
|
||||
post_data.pop("departure_berth_id",None)
|
||||
|
||||
response = requests.post(
|
||||
f"{url}/shipcalls", headers={"Content-Type":"text", "Authorization":f"Bearer {token}"}, json=post_data
|
||||
)
|
||||
validation_error_default_asserts(response)
|
||||
assert "providing \'arrival_berth_id\' & \'departure_berth_id\' is mandatory." in response.json().get("message","")
|
||||
|
||||
return
|
||||
|
||||
|
||||
def test_shipcall_post_invalid_agency_missing_participant_list(get_stub_token):
|
||||
url, token = get_stub_token["url"], get_stub_token["token"]
|
||||
|
||||
post_data = get_stub_valid_shipcall_shifting()
|
||||
|
||||
# keep all participants, but drop the agency
|
||||
post_data["participants"] = [
|
||||
participant for participant in post_data.get("participants")
|
||||
if not int(participant.get("type")) == int(ParticipantType.AGENCY)
|
||||
]
|
||||
|
||||
response = requests.post(
|
||||
f"{url}/shipcalls", headers={"Content-Type":"text", "Authorization":f"Bearer {token}"}, json=post_data
|
||||
)
|
||||
validation_error_default_asserts(response)
|
||||
assert "One of the assigned participants *must* be of type \'ParticipantType.AGENCY\'" in response.json().get("message","")
|
||||
return
|
||||
|
||||
def test_shipcall_post_type_is_wrong(get_stub_token):
|
||||
url, token = get_stub_token["url"], get_stub_token["token"]
|
||||
|
||||
post_data = get_stub_valid_shipcall_arrival()
|
||||
|
||||
# type 1 should be successful (201)
|
||||
post_data["type"] = ShipcallType.arrival.name # "arrival"
|
||||
|
||||
response = requests.post(''
|
||||
f"{url}/shipcalls", headers={"Content-Type":"text", "Authorization":f"Bearer {token}"}, json=post_data
|
||||
)
|
||||
assert response.status_code == 201
|
||||
|
||||
# type 51 should not be successful (400 BAD REQUEST)
|
||||
post_data["type"] = "area51"
|
||||
|
||||
response = requests.post(
|
||||
f"{url}/shipcalls", headers={"Content-Type":"text", "Authorization":f"Bearer {token}"}, json=post_data
|
||||
)
|
||||
validation_error_default_asserts(response)
|
||||
return
|
||||
|
||||
def test_shipcall_put_request_fails_when_different_participant_id_is_assigned(get_shipcall_id_after_stub_post_request):
|
||||
url, token, shipcall_id = get_shipcall_id_after_stub_post_request["token"], get_shipcall_id_after_stub_post_request["url"], get_shipcall_id_after_stub_post_request["shipcall_id"]
|
||||
|
||||
post_data = get_stub_valid_shipcall_arrival()
|
||||
post_data["id"] = shipcall_id
|
||||
|
||||
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)
|
||||
|
||||
spm_shipcall_data = [{"id":99112, 'participant_id': 6, 'type': 4},
|
||||
{"id":99113, 'participant_id': 3, 'type': 1},
|
||||
{"id":99114, 'participant_id': 4, 'type': 2},
|
||||
{"id":99115, 'participant_id': 5, 'type': 8}]
|
||||
spm_shipcall_data = [
|
||||
{**{"created":created, "modified":modified, "shipcall_id":shipcall_id}, **spm}
|
||||
for spm in
|
||||
spm_shipcall_data
|
||||
]
|
||||
spm_shipcall_data = [ShipcallParticipantMap(**spm) for spm in spm_shipcall_data]
|
||||
|
||||
|
||||
# agency with different participant id is assigned
|
||||
ivs = InputValidationShipcall()
|
||||
with pytest.raises(werkzeug.exceptions.Forbidden, match=f"PUT Requests for shipcalls can only be issued by an assigned AGENCY or BSMD users"):
|
||||
ivs.check_user_is_authorized_for_put_request(user_data, loadedModel, content, spm_shipcall_data)
|
||||
return
|
||||
|
||||
|
||||
def test_shipcall_put_request_success(get_shipcall_id_after_stub_post_request):
|
||||
url, token, shipcall_id = get_shipcall_id_after_stub_post_request["token"], get_shipcall_id_after_stub_post_request["url"], get_shipcall_id_after_stub_post_request["shipcall_id"]
|
||||
|
||||
post_data = get_stub_valid_shipcall_arrival()
|
||||
post_data["id"] = shipcall_id
|
||||
|
||||
# success happens, when shipcall data is valid, the user is authorized and the assigned spm shipcall data is suitable
|
||||
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)
|
||||
|
||||
spm_shipcall_data = [{"id":99112, 'participant_id': 6, 'type': 8},
|
||||
{"id":99113, 'participant_id': 3, 'type': 1},
|
||||
{"id":99114, 'participant_id': 4, 'type': 2},
|
||||
{"id":99115, 'participant_id': 5, 'type': 8}]
|
||||
spm_shipcall_data = [
|
||||
{**{"created":created, "modified":modified, "shipcall_id":shipcall_id}, **spm}
|
||||
for spm in
|
||||
spm_shipcall_data
|
||||
]
|
||||
spm_shipcall_data = [ShipcallParticipantMap(**spm) for spm in spm_shipcall_data]
|
||||
|
||||
|
||||
# success
|
||||
ivs = InputValidationShipcall()
|
||||
with pytest.raises(Exception, match="deprecated"):
|
||||
ivs.check_agency_in_shipcall_participant_map(user_data, loadedModel, content, spm_shipcall_data)
|
||||
|
||||
# failure: the user is BSMD and the agency (participant_id 6) has set the BSMD flag, but there is more than one agency
|
||||
with pytest.raises(ValidationError, match="Found more than one assigned agency for the shipcall with ID"):
|
||||
ivs.check_user_is_authorized_for_put_request(user_data, loadedModel, content, spm_shipcall_data)
|
||||
return
|
||||
|
||||
def test_shipcall_put_request_fails_when_no_agency_is_assigned(get_shipcall_id_after_stub_post_request):
|
||||
url, token, shipcall_id = get_shipcall_id_after_stub_post_request["token"], get_shipcall_id_after_stub_post_request["url"], get_shipcall_id_after_stub_post_request["shipcall_id"]
|
||||
|
||||
post_data = get_stub_valid_shipcall_arrival()
|
||||
post_data["id"] = shipcall_id
|
||||
|
||||
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)
|
||||
|
||||
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
|
||||
spm_shipcall_data
|
||||
]
|
||||
spm_shipcall_data = [ShipcallParticipantMap(**spm) for spm in spm_shipcall_data]
|
||||
|
||||
|
||||
# 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
|
||||
|
||||
def test_shipcall_put_request_fails_when_user_is_not_authorized(get_shipcall_id_after_stub_post_request):
|
||||
url, token, shipcall_id = get_shipcall_id_after_stub_post_request["token"], get_shipcall_id_after_stub_post_request["url"], get_shipcall_id_after_stub_post_request["shipcall_id"]
|
||||
|
||||
post_data = get_stub_valid_shipcall_arrival()
|
||||
post_data["id"] = shipcall_id
|
||||
|
||||
# user '1' is artificially set as participant 2, which has ParticipantType 4 (pilot), and is not authorized as an agency
|
||||
user_data = {'id':1, 'participant_id':2}
|
||||
loadedModel = post_data
|
||||
content = post_data
|
||||
|
||||
created = datetime.datetime.now()+datetime.timedelta(minutes=1)
|
||||
modified = datetime.datetime.now()+datetime.timedelta(minutes=2)
|
||||
|
||||
spm_shipcall_data = [{"id":99112, 'participant_id': 2, 'type': 4},
|
||||
{"id":99113, 'participant_id': 3, 'type': 1},
|
||||
{"id":99114, 'participant_id': 4, 'type': 8},
|
||||
{"id":99115, 'participant_id': 5, 'type': 4}]
|
||||
spm_shipcall_data = [
|
||||
{**{"created":created, "modified":modified, "shipcall_id":shipcall_id}, **spm}
|
||||
for spm in
|
||||
spm_shipcall_data
|
||||
]
|
||||
spm_shipcall_data = [ShipcallParticipantMap(**spm) for spm in spm_shipcall_data]
|
||||
|
||||
|
||||
# current user is not authorized
|
||||
ivs = InputValidationShipcall()
|
||||
with pytest.raises(werkzeug.exceptions.Forbidden, match=f"PUT Requests for shipcalls can only be issued by an assigned AGENCY or BSMD users"):
|
||||
ivs.check_user_is_authorized_for_put_request(user_data, loadedModel, content, spm_shipcall_data)
|
||||
return
|
||||
|
||||
def test_shipcall_put_request_fails_when_user_tries_self_assignment(get_shipcall_id_after_stub_post_request):
|
||||
url, token, shipcall_id = get_shipcall_id_after_stub_post_request["token"], get_shipcall_id_after_stub_post_request["url"], get_shipcall_id_after_stub_post_request["shipcall_id"]
|
||||
|
||||
post_data = get_stub_valid_shipcall_arrival()
|
||||
post_data["id"] = shipcall_id
|
||||
|
||||
user_data = {'id':1, 'participant_id':6}
|
||||
loadedModel = post_data
|
||||
content = post_data
|
||||
|
||||
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
|
||||
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.
|
||||
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.""
|
||||
# however, self-assignment is no longer possible, because the SPM is verified beforehand.
|
||||
ivs.check_user_is_authorized_for_put_request(user_data, loadedModel, content, spm_shipcall_data)
|
||||
return
|
||||
|
||||
def test_shipcall_put_request_fails_input_validation_shipcall_when_shipcall_is_canceled(get_stub_token):
|
||||
url, token = get_stub_token["url"], get_stub_token["token"]
|
||||
|
||||
# get all shipcalls and grab shipcall with ID 4
|
||||
# #TODO: there must be a better way to accomplish this easily...
|
||||
response = requests.get(f"{url}/shipcalls", headers={"Content-Type":"text", "Authorization":f"Bearer {token}"}, params={"past_days":30000})
|
||||
assert response.status_code==200
|
||||
assert isinstance(response.json(), list)
|
||||
shipcalls = response.json()
|
||||
|
||||
shipcall_id = 4
|
||||
sh4 = [sh for sh in shipcalls if sh.get("id")==shipcall_id][0]
|
||||
put_data = {k:v for k,v in sh4.items() if k in ["eta", "type", "ship_id", "arrival_berth_id", "participants"]}
|
||||
put_data["id"] = shipcall_id
|
||||
|
||||
loadedModel = put_data
|
||||
content = put_data
|
||||
|
||||
# a canceled shipcall cannot be selected
|
||||
with pytest.raises(ValidationError, match="The shipcall with id 'shipcall_id' is canceled. A canceled shipcall may not be changed."):
|
||||
InputValidationShipcall.check_shipcall_is_canceled(loadedModel, content)
|
||||
return
|
||||
|
||||
def test_shipcall_put_request_works_if_most_values_are_null():
|
||||
"""This pytest verifies, that a PUT-request for shipcalls works, even if only a single value is to be modified"""
|
||||
|
||||
user = execute_sql_query_standalone(query=SQLQuery.get_user_by_id(), param={"id":10}, command_type="single", model=model.User)
|
||||
user_data = user.__dict__
|
||||
assert user.participant_id == 5
|
||||
|
||||
shipcall = execute_sql_query_standalone(query=SQLQuery.get_shipcall_by_id(), param={"id":152}, command_type="single", model=model.Shipcall)
|
||||
assert shipcall.id == 152
|
||||
|
||||
put_data = {"id":shipcall.id, "arrival_berth_id":142}
|
||||
loadedModel = content = put_data
|
||||
|
||||
InputValidationShipcall.evaluate_put_data(user_data, loadedModel, content)
|
||||
return
|
||||
|
||||
|
||||
|
||||
|
||||
def test_shipcall_put_request_fails_input_validation_shipcall_when_shipcall_is_canceled(get_stub_token):
|
||||
url, token = get_stub_token["url"], get_stub_token["token"]
|
||||
|
||||
# get all shipcalls and grab shipcall with ID 4
|
||||
# #TODO: there must be a better way to accomplish this easily...
|
||||
response = requests.get(f"{url}/shipcalls", headers={"Content-Type":"text", "Authorization":f"Bearer {token}"}, params={"past_days":30000})
|
||||
assert response.status_code==200
|
||||
assert isinstance(response.json(), list)
|
||||
shipcalls = response.json()
|
||||
|
||||
shipcall_id = 152
|
||||
sh4 = [sh for sh in shipcalls if sh.get("id")==shipcall_id][0]
|
||||
put_data = {k:v for k,v in sh4.items() if k in ["eta", "etd", "type", "ship_id", "arrival_berth_id", "participants"]}
|
||||
put_data["id"] = shipcall_id
|
||||
|
||||
loadedModel = put_data
|
||||
content = put_data
|
||||
|
||||
# eta & etd must be in the future, as the request fails otherwise. The shipcall is 'shifting', so both must be provided.
|
||||
loadedModel["etd"] = datetime.datetime.now()+datetime.timedelta(minutes=1)
|
||||
loadedModel["eta"] = datetime.datetime.now()+datetime.timedelta(minutes=2)
|
||||
|
||||
### FAILS:
|
||||
# user 9 (participant id 4) is *not* assigned to the shipcall
|
||||
user = execute_sql_query_standalone(query=SQLQuery.get_user_by_id(), param={"id":9}, command_type="single", model=model.User)
|
||||
user_data = user.__dict__
|
||||
assert user.participant_id == 4
|
||||
|
||||
#### verification should fail, because participant_id 4 is ParticipantType.PILOT (neither an assigned agency, nor bsmd)
|
||||
with pytest.raises(werkzeug.exceptions.Forbidden, match="PUT Requests for shipcalls can only be issued by an assigned AGENCY or BSMD user"):
|
||||
InputValidationShipcall.evaluate_put_data(user_data, loadedModel, content)
|
||||
|
||||
### PASSES:
|
||||
# user 10 (participant id 5) is assigned to the shipcall
|
||||
user = execute_sql_query_standalone(query=SQLQuery.get_user_by_id(), param={"id":10}, command_type="single", model=model.User)
|
||||
user_data = user.__dict__
|
||||
assert user.participant_id == 5
|
||||
|
||||
### verification should pass
|
||||
InputValidationShipcall.evaluate_put_data(user_data, loadedModel, content)
|
||||
return
|
||||
|
||||
def test_post_data_with_valid_data(get_stub_token):
|
||||
"""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.
|
||||
"""
|
||||
url, token = get_stub_token["url"], get_stub_token["token"]
|
||||
|
||||
post_data = {
|
||||
"arrival_berth_id": 167,
|
||||
"created": "2024-08-08T17:20:00",
|
||||
"departure_berth_id": 167,
|
||||
"eta": "2024-08-18T18:18:09.174",
|
||||
"etd": "2024-08-16T18:18:09.174",
|
||||
"participants": [
|
||||
{
|
||||
"participant_id": 136,
|
||||
"type": 8
|
||||
},
|
||||
{
|
||||
"participant_id": 11,
|
||||
"type": 32
|
||||
},
|
||||
{
|
||||
"participant_id": 1,
|
||||
"type": 1
|
||||
}
|
||||
],
|
||||
"ship_id": 12, # originally used ship_id 18. Had to change to a different ship id, so the ship is known.
|
||||
"type": "shifting"
|
||||
}
|
||||
|
||||
# test 1: shipcall schema
|
||||
model.ShipcallSchema().load(data=post_data, many=False, partial=True)
|
||||
|
||||
# test 2: post request with OKAY status_code
|
||||
response = requests.post(
|
||||
f"{url}/shipcalls", headers={"Content-Type":"text", "Authorization":f"Bearer {token}"}, json=post_data
|
||||
)
|
||||
assert response.status_code==201
|
||||
return
|
||||
|
||||
def test_input_validation_shipcall_put_fails_when_type_differs():
|
||||
from marshmallow import ValidationError
|
||||
from BreCal.stubs.shipcall import get_stub_valid_shipcall_arrival
|
||||
from BreCal.schemas import model
|
||||
from BreCal.database.sql_queries import SQLQuery
|
||||
from BreCal.database.sql_handler import execute_sql_query_standalone
|
||||
|
||||
from BreCal.validators.input_validation_shipcall import InputValidationShipcall
|
||||
|
||||
# stub data
|
||||
put_data = get_stub_valid_shipcall_arrival()
|
||||
put_data["id"] = 3
|
||||
loadedModel = model.ShipcallSchema().load(data=put_data, many=False, partial=True)
|
||||
|
||||
# load data from DB
|
||||
shipcall_id = loadedModel.get("id")
|
||||
query = SQLQuery.get_shipcall_by_id()
|
||||
shipcall = execute_sql_query_standalone(query=query, model=model.Shipcall, param={"id":shipcall_id}, command_type="single")
|
||||
|
||||
# create failure case. Ensures that the type is not the same as in the loaded shipcall from the DB
|
||||
failure_case = 1 if shipcall.type==2 else 2
|
||||
loadedModel["type"] = model.ShipcallType(failure_case)
|
||||
|
||||
# should fail: different 'type'
|
||||
with pytest.raises(ValidationError, match="The shipcall type may only be set in the initial POST-request. Afterwards, changing the shipcall type is not allowed"):
|
||||
InputValidationShipcall.check_shipcall_type_is_unchanged(loadedModel)
|
||||
|
||||
# should pass: same 'type'
|
||||
loadedModel["type"] = shipcall.type
|
||||
InputValidationShipcall.check_shipcall_type_is_unchanged(loadedModel)
|
||||
return
|
||||
|
||||
468
src/server/tests/validators/test_input_validation_times.py
Normal file
468
src/server/tests/validators/test_input_validation_times.py
Normal file
@ -0,0 +1,468 @@
|
||||
import pytest
|
||||
|
||||
import os
|
||||
import random
|
||||
import datetime
|
||||
from marshmallow import ValidationError
|
||||
|
||||
from BreCal import local_db
|
||||
from BreCal.schemas import model
|
||||
|
||||
from BreCal.schemas.model import ParticipantType
|
||||
from BreCal.validators.input_validation_times import InputValidationTimes
|
||||
|
||||
from BreCal.stubs.times_full import get_valid_stub_times, get_valid_stub_for_pytests
|
||||
|
||||
instance_path = os.path.join(os.path.expanduser('~'), "brecal", "src", "server", "instance")
|
||||
local_db.initPool(os.path.dirname(instance_path), connection_filename="connection_data_local.json")
|
||||
|
||||
|
||||
def test_input_validation_times_fails_when_berth_info_exceeds_length_limit():
|
||||
# success
|
||||
post_data = get_valid_stub_times()
|
||||
post_data["berth_info"] = "a"*512 # 512 characters
|
||||
model.TimesSchema().load(data=post_data, many=False, partial=True)
|
||||
|
||||
post_data["berth_info"] = "" # 0 characters
|
||||
model.TimesSchema().load(data=post_data, many=False, partial=True)
|
||||
|
||||
# failure
|
||||
with pytest.raises(ValidationError, match="Longer than maximum length 512."):
|
||||
post_data["berth_info"] = "a"*513 # 513 characters
|
||||
model.TimesSchema().load(data=post_data, many=False, partial=True)
|
||||
return
|
||||
|
||||
def test_input_validation_times_fails_when_remarks_exceeds_length_limit():
|
||||
# success
|
||||
post_data = get_valid_stub_times()
|
||||
post_data["remarks"] = "a"*512 # 512 characters
|
||||
model.TimesSchema().load(data=post_data, many=False, partial=True)
|
||||
|
||||
post_data["remarks"] = "" # 0 characters
|
||||
model.TimesSchema().load(data=post_data, many=False, partial=True)
|
||||
|
||||
# failure
|
||||
with pytest.raises(ValidationError, match="Longer than maximum length 512."):
|
||||
post_data["remarks"] = "a"*513 # 513 characters
|
||||
model.TimesSchema().load(data=post_data, many=False, partial=True)
|
||||
return
|
||||
|
||||
def test_input_validation_times_fails_when_participant_type_is_bsmd():
|
||||
# BSMD -> Failure
|
||||
post_data = get_valid_stub_times()
|
||||
post_data["participant_type"] = int(ParticipantType.BSMD)
|
||||
with pytest.raises(ValidationError, match="the participant_type must not be .BSMD"):
|
||||
model.TimesSchema().load(data=post_data, many=False, partial=True)
|
||||
|
||||
# IntFlag property: BSMD & AGENCY -> Failure
|
||||
post_data = get_valid_stub_times()
|
||||
post_data["participant_type"] = int(ParticipantType(ParticipantType.BSMD+ParticipantType.AGENCY))
|
||||
with pytest.raises(ValidationError, match="the participant_type must not be .BSMD"):
|
||||
model.TimesSchema().load(data=post_data, many=False, partial=True)
|
||||
return
|
||||
|
||||
def test_input_validation_times_fails_when_time_key_is_not_reasonable():
|
||||
"""
|
||||
every time key (e.g., 'eta_berth' or 'zone_entry') must be reasonable. The validation expects
|
||||
these values to be 'in the future' (larger than datetime.datetime.now()) and not 'in the too distant future'
|
||||
(e.g., more than one year from now.)
|
||||
"""
|
||||
for time_key in ["eta_berth", "etd_berth", "lock_time", "zone_entry", "operations_start", "operations_end"]:
|
||||
post_data = get_valid_stub_times()
|
||||
|
||||
# success
|
||||
post_data[time_key] = (datetime.datetime.now() + datetime.timedelta(minutes=11)).isoformat()
|
||||
model.TimesSchema().load(data=post_data, many=False, partial=True)
|
||||
|
||||
# fails
|
||||
with pytest.raises(ValidationError, match="The provided value must be in the future."):
|
||||
post_data[time_key] = (datetime.datetime.now() - datetime.timedelta(minutes=11)).isoformat()
|
||||
model.TimesSchema().load(data=post_data, many=False, partial=True)
|
||||
|
||||
# fails
|
||||
with pytest.raises(ValidationError, match="The provided value is in the too distant future and exceeds a threshold for 'reasonable' entries."):
|
||||
post_data[time_key] = (datetime.datetime.now() + datetime.timedelta(days=367)).isoformat()
|
||||
model.TimesSchema().load(data=post_data, many=False, partial=True)
|
||||
return
|
||||
|
||||
def test_input_validation_times_fails_when_user_is_bsmd_user():
|
||||
# create stub-data for a POST request
|
||||
from BreCal.services.jwt_handler import decode_jwt
|
||||
from BreCal.database.sql_utils import get_user_data_for_id
|
||||
import re
|
||||
|
||||
# user 4 is a BSMD user -> fails
|
||||
user_data, loadedModel, content = get_valid_stub_for_pytests(user_id=4)
|
||||
|
||||
with pytest.raises(ValidationError, match=re.escape("current user belongs to BSMD. Cannot post 'times' datasets.")):
|
||||
InputValidationTimes.check_user_is_not_bsmd_type(user_data)
|
||||
|
||||
# user 13 is not a BSMD user -> passes
|
||||
user_data, loadedModel, content = get_valid_stub_for_pytests(user_id=13)
|
||||
|
||||
# success
|
||||
InputValidationTimes.check_user_is_not_bsmd_type(user_data)
|
||||
return
|
||||
|
||||
def test_input_validation_times_fails_when_participant_type_entry_already_exists():
|
||||
# the participant type already has an entry -> fails
|
||||
with pytest.raises(ValidationError, match="A dataset for the participant type is already present. Participant Type:"):
|
||||
user_data, loadedModel, content = get_valid_stub_for_pytests(user_id=3)
|
||||
loadedModel["participant_type"] = int(ParticipantType.AGENCY)
|
||||
|
||||
# 2.) datasets may only be created, if the respective participant type did not already create one.
|
||||
InputValidationTimes.check_if_entry_already_exists_for_participant_type(user_data, loadedModel, content)
|
||||
return
|
||||
|
||||
def test_input_validation_times_fails_when_participant_type_deviates_from_shipcall_participant_map():
|
||||
# success
|
||||
# user id 3 is assigned as participant_type=4, but the stub assigns participant_type=4
|
||||
user_data, loadedModel, content = get_valid_stub_for_pytests(user_id=3)
|
||||
InputValidationTimes.check_if_user_fits_shipcall_participant_map(user_data, loadedModel, content)
|
||||
|
||||
# fails
|
||||
# user id 4 is assigned as participant_type=1, but the stub assigns participant_type=4
|
||||
with pytest.raises(ValidationError, match="is not assigned to the shipcall"):
|
||||
user_data, loadedModel, content = get_valid_stub_for_pytests(user_id=4)
|
||||
InputValidationTimes.check_if_user_fits_shipcall_participant_map(user_data, loadedModel, content)
|
||||
return
|
||||
|
||||
def test_input_validation_times_fails_when_id_references_do_not_exist():
|
||||
# success: all IDs exist
|
||||
user_data, loadedModel, content = get_valid_stub_for_pytests(user_id=3)
|
||||
InputValidationTimes.check_dataset_references(content)
|
||||
|
||||
# fails: IDs do not exist
|
||||
# iterates once for each, berth_id, shipcall_id, participant_id and generates an artificial, non-existing ID
|
||||
for key in ["berth_id", "shipcall_id", "participant_id"]:
|
||||
user_data, loadedModel, content = get_valid_stub_for_pytests(user_id=3)
|
||||
content[key] = loadedModel[key] = 9912737
|
||||
|
||||
with pytest.raises(ValidationError, match=f"The referenced {key} '{content[key]}' does not exist in the database."):
|
||||
InputValidationTimes.check_dataset_references(content)
|
||||
return
|
||||
|
||||
from BreCal.schemas.model import ParticipantType
|
||||
|
||||
|
||||
def test_input_validation_times_fails_when_missing_required_fields_arrival():
|
||||
"""
|
||||
evaluates every individual combination of arriving shipcalls, where one of the required values is arbitrarily missing
|
||||
randomly selects one of the non-terminal ParticipantTypes, which are reasonable (not .BSMD), and validates. This makes sure,
|
||||
that over time, every possible combination has been tested.
|
||||
"""
|
||||
# arrival + not-terminal
|
||||
non_terminal_list = [ParticipantType.AGENCY, ParticipantType.MOORING, ParticipantType.PILOT, ParticipantType.PORT_ADMINISTRATION, ParticipantType.TUG]
|
||||
for key in ["eta_berth"]+InputValidationTimes.get_post_data_type_independent_fields():
|
||||
random_participant_type_for_unit_test = random.sample(non_terminal_list,k=1)[0]
|
||||
|
||||
# pass: all required fields exist for the current shipcall type (arrival/incoming)
|
||||
user_data, loadedModel, content = get_valid_stub_for_pytests(user_id=3)
|
||||
loadedModel["shipcall_id"] = content["shipcall_id"] = 222
|
||||
loadedModel["participant_type"] = random_participant_type_for_unit_test
|
||||
content["participant_type"] = int(random_participant_type_for_unit_test)
|
||||
InputValidationTimes.check_times_required_fields_post_data(loadedModel, content)
|
||||
|
||||
# fails: iteratively creates stubs, where one of the required keys is missing
|
||||
user_data, loadedModel, content = get_valid_stub_for_pytests(user_id=3)
|
||||
loadedModel["shipcall_id"] = content["shipcall_id"] = 222
|
||||
loadedModel["participant_type"] = random_participant_type_for_unit_test
|
||||
content["participant_type"] = int(random_participant_type_for_unit_test)
|
||||
with pytest.raises(ValidationError, match="At least one of the required fields is missing. Missing:"):
|
||||
loadedModel[key] = content[key] = None
|
||||
InputValidationTimes.check_times_required_fields_post_data(loadedModel, content)
|
||||
|
||||
# arrival + terminal
|
||||
for key in ["operations_start"]+InputValidationTimes.get_post_data_type_independent_fields():
|
||||
# pass: all required fields exist for the current shipcall type (arrival/incoming)
|
||||
user_data, loadedModel, content = get_valid_stub_for_pytests(user_id=3)
|
||||
loadedModel["shipcall_id"] = content["shipcall_id"] = 222
|
||||
loadedModel["participant_type"] = ParticipantType.TERMINAL
|
||||
content["participant_type"] = int(ParticipantType.TERMINAL)
|
||||
InputValidationTimes.check_times_required_fields_post_data(loadedModel, content)
|
||||
|
||||
# fails: iteratively creates stubs, where one of the required keys is missing
|
||||
user_data, loadedModel, content = get_valid_stub_for_pytests(user_id=3)
|
||||
loadedModel["shipcall_id"] = content["shipcall_id"] = 222
|
||||
loadedModel["participant_type"] = ParticipantType.TERMINAL
|
||||
content["participant_type"] = int(ParticipantType.TERMINAL)
|
||||
|
||||
with pytest.raises(ValidationError, match="At least one of the required fields is missing. Missing:"):
|
||||
loadedModel[key] = content[key] = None
|
||||
InputValidationTimes.check_times_required_fields_post_data(loadedModel, content)
|
||||
return
|
||||
|
||||
def test_input_validation_times_fails_when_missing_required_fields_departure():
|
||||
"""
|
||||
evaluates every individual combination of departing shipcalls, where one of the required values is arbitrarily missing
|
||||
randomly selects one of the non-terminal ParticipantTypes, which are reasonable (not .BSMD), and validates. This makes sure,
|
||||
that over time, every possible combination has been tested.
|
||||
"""
|
||||
# departure + not-terminal
|
||||
non_terminal_list = [ParticipantType.AGENCY, ParticipantType.MOORING, ParticipantType.PILOT, ParticipantType.PORT_ADMINISTRATION, ParticipantType.TUG]
|
||||
|
||||
for key in ["etd_berth"]+InputValidationTimes.get_post_data_type_independent_fields():
|
||||
# select a *random* particiipant type, which is reasonable and *not* TERMINAL, and validate the function.
|
||||
random_participant_type_for_unit_test = random.sample(non_terminal_list,k=1)[0]
|
||||
|
||||
# pass
|
||||
user_data, loadedModel, content = get_valid_stub_for_pytests(user_id=3)
|
||||
loadedModel["shipcall_id"] = content["shipcall_id"] = 241
|
||||
loadedModel["participant_type"] = random_participant_type_for_unit_test
|
||||
content["participant_type"] = int(random_participant_type_for_unit_test)
|
||||
InputValidationTimes.check_times_required_fields_post_data(loadedModel, content)
|
||||
|
||||
# fails: iteratively creates stubs, where one of the required keys is missing
|
||||
user_data, loadedModel, content = get_valid_stub_for_pytests(user_id=3)
|
||||
loadedModel["shipcall_id"] = content["shipcall_id"] = 241
|
||||
loadedModel["participant_type"] = random_participant_type_for_unit_test
|
||||
content["participant_type"] = int(random_participant_type_for_unit_test)
|
||||
with pytest.raises(ValidationError, match="At least one of the required fields is missing. Missing:"):
|
||||
loadedModel[key] = content[key] = None
|
||||
InputValidationTimes.check_times_required_fields_post_data(loadedModel, content)
|
||||
|
||||
# departure + terminal
|
||||
for key in ["operations_end"]+InputValidationTimes.get_post_data_type_independent_fields():
|
||||
# pass
|
||||
user_data, loadedModel, content = get_valid_stub_for_pytests(user_id=3)
|
||||
loadedModel["shipcall_id"] = content["shipcall_id"] = 241
|
||||
loadedModel["participant_type"] = ParticipantType.TERMINAL
|
||||
content["participant_type"] = int(ParticipantType.TERMINAL)
|
||||
InputValidationTimes.check_times_required_fields_post_data(loadedModel, content)
|
||||
|
||||
# fails: iteratively creates stubs, where one of the required keys is missing
|
||||
user_data, loadedModel, content = get_valid_stub_for_pytests(user_id=3)
|
||||
loadedModel["shipcall_id"] = content["shipcall_id"] = 241
|
||||
loadedModel["participant_type"] = ParticipantType.TERMINAL
|
||||
content["participant_type"] = int(ParticipantType.TERMINAL)
|
||||
with pytest.raises(ValidationError, match="At least one of the required fields is missing. Missing:"):
|
||||
loadedModel[key] = content[key] = None
|
||||
InputValidationTimes.check_times_required_fields_post_data(loadedModel, content)
|
||||
return
|
||||
|
||||
def test_input_validation_times_fails_when_missing_required_fields_shifting():
|
||||
"""
|
||||
evaluates every individual combination of shifting shipcalls, where one of the required values is arbitrarily missing
|
||||
randomly selects one of the non-terminal ParticipantTypes, which are reasonable (not .BSMD), and validates. This makes sure,
|
||||
that over time, every possible combination has been tested.
|
||||
|
||||
Due to the amount of combinations, this test is rather slow. As off 12.08.2024, the test took 10.585362434387207 [s]
|
||||
"""
|
||||
# shifting + not-terminal
|
||||
non_terminal_list = [ParticipantType.AGENCY, ParticipantType.MOORING, ParticipantType.PILOT, ParticipantType.PORT_ADMINISTRATION, ParticipantType.TUG]
|
||||
for key in ["eta_berth", "etd_berth"]+InputValidationTimes.get_post_data_type_independent_fields():
|
||||
# select a *random* particiipant type, which is reasonable and *not* TERMINAL, and validate the function.
|
||||
random_participant_type_for_unit_test = random.sample(non_terminal_list,k=1)[0]
|
||||
# pass: all required fields exist for the current shipcall type (arrival/incoming)
|
||||
user_data, loadedModel, content = get_valid_stub_for_pytests(user_id=3)
|
||||
loadedModel["shipcall_id"] = content["shipcall_id"] = 189
|
||||
loadedModel["participant_type"] =random_participant_type_for_unit_test
|
||||
content["participant_type"] = int(random_participant_type_for_unit_test)
|
||||
InputValidationTimes.check_times_required_fields_post_data(loadedModel, content)
|
||||
|
||||
# fails: iteratively creates stubs, where one of the required keys is missing
|
||||
user_data, loadedModel, content = get_valid_stub_for_pytests(user_id=3)
|
||||
loadedModel["shipcall_id"] = content["shipcall_id"] = 189
|
||||
loadedModel["participant_type"] = random_participant_type_for_unit_test
|
||||
content["participant_type"] = int(random_participant_type_for_unit_test)
|
||||
with pytest.raises(ValidationError, match="At least one of the required fields is missing. Missing:"):
|
||||
loadedModel[key] = content[key] = None
|
||||
InputValidationTimes.check_times_required_fields_post_data(loadedModel, content)
|
||||
|
||||
# shifting + terminal
|
||||
for key in ["operations_start", "operations_end"]+InputValidationTimes.get_post_data_type_independent_fields():
|
||||
# pass: all required fields exist for the current shipcall type (arrival/incoming)
|
||||
user_data, loadedModel, content = get_valid_stub_for_pytests(user_id=3)
|
||||
loadedModel["shipcall_id"] = content["shipcall_id"] = 189
|
||||
loadedModel["participant_type"] = ParticipantType.TERMINAL
|
||||
content["participant_type"] = int(ParticipantType.TERMINAL)
|
||||
InputValidationTimes.check_times_required_fields_post_data(loadedModel, content)
|
||||
|
||||
# fails: iteratively creates stubs, where one of the required keys is missing
|
||||
user_data, loadedModel, content = get_valid_stub_for_pytests(user_id=3)
|
||||
loadedModel["shipcall_id"] = content["shipcall_id"] = 189
|
||||
loadedModel["participant_type"] = ParticipantType.TERMINAL
|
||||
content["participant_type"] = int(ParticipantType.TERMINAL)
|
||||
with pytest.raises(ValidationError, match="At least one of the required fields is missing. Missing:"):
|
||||
loadedModel[key] = content[key] = None
|
||||
InputValidationTimes.check_times_required_fields_post_data(loadedModel, content)
|
||||
return
|
||||
|
||||
|
||||
|
||||
def test_input_validation_times_fails_when_participant_type_is_not_assigned__or__user_does_not_belong_to_the_same_participant_id():
|
||||
"""
|
||||
There are two failure cases in InputValidationTimes.check_user_belongs_to_same_group_as_dataset_determines
|
||||
1.) when the participant type is simply not assigned
|
||||
2.) when the participant type matches to the user, but the participant_id is not assigned
|
||||
|
||||
Test case:
|
||||
shipcall_id 234 is assigned to the participants
|
||||
DELETE# {"participant_id": 136, "type":2} and {"participant_id": 136, "type":8}
|
||||
{"participant_id": 2, "type":4}
|
||||
{"participant_id": 3, "type":1}
|
||||
{"participant_id": 4, "type":2}
|
||||
{"participant_id": 5, "type":8}
|
||||
|
||||
Case 1:
|
||||
When user_id 27 should be set as participant_type 16, the call fails, because type 16 is not assigned
|
||||
|
||||
Case 2:
|
||||
When user_id 2 (participant_id 1) should be set as participant_type 2, the call fails even though type 2 exists,
|
||||
because participant_id 4 is assigned
|
||||
|
||||
Case 3:
|
||||
When user_id 9 (participant_id 4) is set as participant_type 2, the call passes.
|
||||
"""
|
||||
# fails: participant type 16 does not exist
|
||||
user_data, loadedModel, content = get_valid_stub_for_pytests(user_id=27)
|
||||
participant_type = 16
|
||||
loadedModel["shipcall_id"] = content["shipcall_id"] = 234
|
||||
loadedModel["participant_id"] = content["participant_id"] = 16
|
||||
loadedModel["participant_type"] = content["participant_type"] = participant_type
|
||||
|
||||
with pytest.raises(ValidationError, match=f"Could not find a matching time dataset for the provided participant_type: {participant_type} at shipcall with id"):
|
||||
InputValidationTimes.check_user_belongs_to_same_group_as_dataset_determines(user_data, loadedModel=loadedModel, times_id=None)
|
||||
|
||||
# fails: participant type 2 exists, but user_id 2 is part of the wrong participant_id group
|
||||
user_data, loadedModel, content = get_valid_stub_for_pytests(user_id=2)
|
||||
loadedModel["shipcall_id"] = content["shipcall_id"] = 234
|
||||
participant_type = 2
|
||||
loadedModel["participant_type"] = content["participant_type"] = participant_type
|
||||
with pytest.raises(ValidationError, match="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:"):
|
||||
InputValidationTimes.check_user_belongs_to_same_group_as_dataset_determines(user_data, loadedModel=loadedModel, times_id=None)
|
||||
|
||||
# pass: participant type 2 exists & user_id is part of participant_id group 4, which is correct
|
||||
user_data, loadedModel, content = get_valid_stub_for_pytests(user_id=9)
|
||||
loadedModel["shipcall_id"] = content["shipcall_id"] = 234
|
||||
participant_type = 2
|
||||
loadedModel["participant_type"] = content["participant_type"] = participant_type
|
||||
InputValidationTimes.check_user_belongs_to_same_group_as_dataset_determines(user_data, loadedModel=loadedModel, times_id=None)
|
||||
return
|
||||
|
||||
|
||||
def test_input_validation_times_put_request_fails_when_id_field_is_missing():
|
||||
"""used within PUT-requests. When 'id' is missing, a ValidationError is issued"""
|
||||
# passes: as an 'id' is provided
|
||||
user_data, loadedModel, content = get_valid_stub_for_pytests(user_id=3)
|
||||
content["id"] = 379
|
||||
InputValidationTimes.check_times_required_fields_put_data(content)
|
||||
|
||||
# fails: 'id' field is missing
|
||||
user_data, loadedModel, content = get_valid_stub_for_pytests(user_id=3)
|
||||
content.pop("id",None)
|
||||
with pytest.raises(ValidationError, match="A PUT-request requires an 'id' reference, which was not found."):
|
||||
InputValidationTimes.check_times_required_fields_put_data(content)
|
||||
return
|
||||
|
||||
def test_input_validation_times_delete_request_fails_when_times_id_is_deleted_already():
|
||||
# passes: id exists
|
||||
times_id = 379
|
||||
InputValidationTimes.check_if_entry_is_already_deleted(times_id)
|
||||
|
||||
# passes: id exists
|
||||
times_id = 391
|
||||
InputValidationTimes.check_if_entry_is_already_deleted(times_id)
|
||||
|
||||
# fails
|
||||
times_id = 11
|
||||
with pytest.raises(ValidationError, match=f"The selected time entry is already deleted. ID: {times_id}"):
|
||||
InputValidationTimes.check_if_entry_is_already_deleted(times_id)
|
||||
|
||||
# fails
|
||||
times_id = 4
|
||||
with pytest.raises(ValidationError, match=f"The selected time entry is already deleted. ID: {times_id}"):
|
||||
InputValidationTimes.check_if_entry_is_already_deleted(times_id)
|
||||
return
|
||||
|
||||
def test_input_validation_times_delete_request_fails_when_times_id_does_not_exist_():
|
||||
# passes: times_id exists
|
||||
user_data, loadedModel, content = get_valid_stub_for_pytests(user_id=28)
|
||||
times_id = 392
|
||||
pdata = [{'participant_id': 136, 'participant_type': 8, 'shipcall_id': 154}]
|
||||
InputValidationTimes.check_user_belongs_to_same_group_as_dataset_determines(user_data, loadedModel=None, times_id=times_id, pdata=pdata)
|
||||
|
||||
# fails: times_id does not exist
|
||||
user_data, loadedModel, content = get_valid_stub_for_pytests(user_id=28)
|
||||
times_id = 4
|
||||
with pytest.raises(ValidationError, match=f"Unknown times_id. Could not find a matching entry for ID: {times_id}"):
|
||||
InputValidationTimes.check_user_belongs_to_same_group_as_dataset_determines(user_data, loadedModel=None, times_id=times_id)
|
||||
return
|
||||
|
||||
def test_input_validation_times_delete_request_fails_when_user_belongs_to_wrong_participant_id():
|
||||
# fails: participant_id should be 136, but user_id=3 belongs to participant_id=2
|
||||
user_data, loadedModel, content = get_valid_stub_for_pytests(user_id=3)
|
||||
times_id = 392
|
||||
|
||||
with pytest.raises(ValidationError, match=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:"):
|
||||
InputValidationTimes.check_user_belongs_to_same_group_as_dataset_determines(user_data, loadedModel=None, times_id=times_id)
|
||||
|
||||
# success: the participant_id within the times entry is 136. user_id=28 belongs to participant_id=136, so it matches
|
||||
user_data, loadedModel, content = get_valid_stub_for_pytests(user_id=28)
|
||||
times_id = 392
|
||||
InputValidationTimes.check_user_belongs_to_same_group_as_dataset_determines(user_data, loadedModel=None, times_id=times_id)
|
||||
|
||||
# success: creates an artificial SPM, where the participant_id is '136', so it matches the user_id's (28) participant_id (136)
|
||||
user_data, loadedModel, content = get_valid_stub_for_pytests(user_id=28)
|
||||
times_id = 392
|
||||
pdata = [{'participant_id': 136, 'participant_type': 8, 'shipcall_id': 154}]
|
||||
InputValidationTimes.check_user_belongs_to_same_group_as_dataset_determines(user_data, loadedModel=None, times_id=times_id, pdata=pdata)
|
||||
return
|
||||
|
||||
def test_input_validation_times_check_dataset_values_for_time_intervals():
|
||||
import datetime
|
||||
from BreCal.schemas import model
|
||||
from BreCal.validators.input_validation_times import InputValidationTimes
|
||||
from BreCal.stubs.times import get_schema_model_stub_arrival, get_schema_model_stub_departure
|
||||
from marshmallow import ValidationError
|
||||
|
||||
### ETD (departure)
|
||||
# expected to pass
|
||||
schemaModel = get_schema_model_stub_departure()
|
||||
schemaModel["etd_interval_end"] = schemaModel["etd_berth"] + datetime.timedelta(minutes=2)
|
||||
|
||||
schemaModel["etd_berth"] = schemaModel["etd_berth"].isoformat()
|
||||
schemaModel["etd_interval_end"] = schemaModel["etd_interval_end"].isoformat()
|
||||
content = schemaModel
|
||||
loadedModel = model.TimesSchema().load(data=schemaModel, many=False, partial=True)
|
||||
|
||||
InputValidationTimes.check_dataset_values(user_data={}, loadedModel=loadedModel, content=content)
|
||||
|
||||
# expected to fail: the from-to-interval is incorrectly set.
|
||||
schemaModel = get_schema_model_stub_departure()
|
||||
schemaModel["etd_interval_end"] = schemaModel["etd_berth"] - datetime.timedelta(minutes=2)
|
||||
|
||||
schemaModel["etd_berth"] = schemaModel["etd_berth"].isoformat()
|
||||
schemaModel["etd_interval_end"] = schemaModel["etd_interval_end"].isoformat()
|
||||
content = schemaModel
|
||||
loadedModel = model.TimesSchema().load(data=schemaModel, many=False, partial=True)
|
||||
|
||||
with pytest.raises(ValidationError, match="The provided time interval for the estimated departure time is invalid"):
|
||||
InputValidationTimes.check_dataset_values(user_data={}, loadedModel=loadedModel, content=content)
|
||||
|
||||
|
||||
### ETA (arrival)
|
||||
# expected to pass
|
||||
schemaModel = get_schema_model_stub_arrival()
|
||||
schemaModel["eta_interval_end"] = schemaModel["eta_berth"] + datetime.timedelta(minutes=2)
|
||||
|
||||
schemaModel["eta_berth"] = schemaModel["eta_berth"].isoformat()
|
||||
schemaModel["eta_interval_end"] = schemaModel["eta_interval_end"].isoformat()
|
||||
content = schemaModel
|
||||
loadedModel = model.TimesSchema().load(data=schemaModel, many=False, partial=True)
|
||||
|
||||
InputValidationTimes.check_dataset_values(user_data={}, loadedModel=loadedModel, content=content)
|
||||
|
||||
# expected to fail: the from-to-interval is incorrectly set.
|
||||
schemaModel = get_schema_model_stub_arrival()
|
||||
schemaModel["eta_interval_end"] = schemaModel["eta_berth"] - datetime.timedelta(minutes=2)
|
||||
|
||||
schemaModel["eta_berth"] = schemaModel["eta_berth"].isoformat()
|
||||
schemaModel["eta_interval_end"] = schemaModel["eta_interval_end"].isoformat()
|
||||
content = schemaModel
|
||||
loadedModel = model.TimesSchema().load(data=schemaModel, many=False, partial=True)
|
||||
|
||||
with pytest.raises(ValidationError, match="The provided time interval for the estimated arrival time is invalid"):
|
||||
InputValidationTimes.check_dataset_values(user_data={}, loadedModel=loadedModel, content=content)
|
||||
return
|
||||
|
||||
30
src/server/tests/validators/test_input_validation_utils.py
Normal file
30
src/server/tests/validators/test_input_validation_utils.py
Normal file
@ -0,0 +1,30 @@
|
||||
import pytest
|
||||
|
||||
def test_check_if_participant_id_is_valid_standalone__different_assignments():
|
||||
from BreCal.validators.input_validation_utils import check_if_participant_id_is_valid_standalone
|
||||
from BreCal.schemas.model import ParticipantType
|
||||
# participant id 10 has the ParticipantType 10. This means, the participant is, both, agency and terminal.
|
||||
# upon assignment, the participant can take the role of terminal, agency or theoretically, both.
|
||||
|
||||
participant_id = 10
|
||||
participant_type = ParticipantType(10)
|
||||
assert check_if_participant_id_is_valid_standalone(participant_id, participant_type=participant_type)
|
||||
assert check_if_participant_id_is_valid_standalone(participant_id, participant_type=ParticipantType(2))
|
||||
assert check_if_participant_id_is_valid_standalone(participant_id, participant_type=ParticipantType(8))
|
||||
|
||||
# failure cases: BSMD, PILOT, MOORING, PORT_ADMINISTRATION, TUG
|
||||
with pytest.raises(AssertionError, match="wrong role assignment."):
|
||||
assert check_if_participant_id_is_valid_standalone(participant_id, participant_type=ParticipantType(1)), f"wrong role assignment."
|
||||
|
||||
with pytest.raises(AssertionError, match="wrong role assignment."):
|
||||
assert check_if_participant_id_is_valid_standalone(participant_id, participant_type=ParticipantType(4)), f"wrong role assignment."
|
||||
|
||||
with pytest.raises(AssertionError, match="wrong role assignment."):
|
||||
assert check_if_participant_id_is_valid_standalone(participant_id, participant_type=ParticipantType(16)), f"wrong role assignment."
|
||||
|
||||
with pytest.raises(AssertionError, match="wrong role assignment."):
|
||||
assert check_if_participant_id_is_valid_standalone(participant_id, participant_type=ParticipantType(32)), f"wrong role assignment."
|
||||
|
||||
with pytest.raises(AssertionError, match="wrong role assignment."):
|
||||
assert check_if_participant_id_is_valid_standalone(participant_id, participant_type=ParticipantType(64)), f"wrong role assignment."
|
||||
return
|
||||
19
src/server/tests/validators/test_validation_error.py
Normal file
19
src/server/tests/validators/test_validation_error.py
Normal file
@ -0,0 +1,19 @@
|
||||
import pytest
|
||||
|
||||
def test_create_validation_error_response_is_serializable():
|
||||
from BreCal.stubs.times_full import get_valid_stub_times
|
||||
from BreCal.schemas import model
|
||||
from BreCal.validators.validation_error import create_validation_error_response
|
||||
|
||||
content = get_valid_stub_times()
|
||||
|
||||
import datetime
|
||||
content["operations_end"] = (datetime.datetime.now()-datetime.timedelta(minutes=14)).isoformat()
|
||||
|
||||
content["id"] = 3
|
||||
try:
|
||||
loadedModel = model.TimesSchema().load(data=content, many=False, partial=True)
|
||||
except Exception as ex:
|
||||
my_var = ex
|
||||
create_validation_error_response(ex=ex, status_code=400) # this function initially created errors, as datetime objects were not serializable. Corrected now.
|
||||
return
|
||||
@ -12,7 +12,17 @@ from BreCal.stubs.df_times import get_df_times, random_time_perturbation, get_df
|
||||
@pytest.fixture(scope="session")
|
||||
def build_sql_proxy_connection():
|
||||
import mysql.connector
|
||||
conn_from_pool = mysql.connector.connect(**{'host':'localhost', 'port':3306, 'user':'root', 'password':'HalloWach_2323XXL!!', 'pool_name':'brecal_pool', 'pool_size':20, 'database':'bremen_calling', 'autocommit': True})
|
||||
import os
|
||||
import json
|
||||
connection_data_path = os.path.join(os.path.expanduser("~"),"secure","connection_data_local.json")
|
||||
assert os.path.exists(connection_data_path)
|
||||
|
||||
with open(connection_data_path, "r") as jr:
|
||||
connection_data = json.load(jr)
|
||||
connection_data = {k:v for k,v in connection_data.items() if k in ["host", "port", "user", "password", "pool_size", "pool_name", "database"]}
|
||||
|
||||
conn_from_pool = mysql.connector.connect(**connection_data)
|
||||
#conn_from_pool = mysql.connector.connect(**{'host':'localhost', 'port':3306, 'user':'root', 'password':'HalloWach_2323XXL!!', 'pool_name':'brecal_pool', 'pool_size':20, 'database':'bremen_calling_local', 'autocommit': True})
|
||||
sql_handler = SQLHandler(sql_connection=conn_from_pool, read_all=True)
|
||||
vr = ValidationRules(sql_handler)
|
||||
return locals()
|
||||
@ -654,6 +664,9 @@ def test_validation_rule_fct_missing_time_tug_berth_etd__shipcall_soon_but_parti
|
||||
def test_validation_rule_fct_missing_time_terminal_berth_eta__shipcall_soon_but_participant_estimated_time_undefined(build_sql_proxy_connection):
|
||||
"""0001-L validation_rule_fct_missing_time_terminal_berth_eta"""
|
||||
vr = build_sql_proxy_connection['vr']
|
||||
import copy
|
||||
reset_to_default = copy.deepcopy(vr.ignore_terminal_flag)
|
||||
vr.ignore_terminal_flag = False
|
||||
|
||||
shipcall = get_shipcall_simple()
|
||||
df_times = get_df_times(shipcall)
|
||||
@ -684,6 +697,46 @@ def test_validation_rule_fct_missing_time_terminal_berth_eta__shipcall_soon_but_
|
||||
|
||||
# expectation: green state, no msg
|
||||
assert state==StatusFlags.YELLOW, f"function should return 'yellow', because the participant did not provide a time and the shipcall takes place soon (according to the agency)"
|
||||
vr.ignore_terminal_flag = reset_to_default
|
||||
return
|
||||
|
||||
def test_validation_rule_fct_missing_time_terminal_berth_eta__shipcall_soon_but_participant_estimated_time_undefined__no_violation_because_terminal_flag_is_active(build_sql_proxy_connection):
|
||||
"""0001-L validation_rule_fct_missing_time_terminal_berth_eta"""
|
||||
vr = build_sql_proxy_connection['vr']
|
||||
import copy
|
||||
reset_to_default = copy.deepcopy(vr.ignore_terminal_flag)
|
||||
vr.ignore_terminal_flag = True
|
||||
|
||||
shipcall = get_shipcall_simple()
|
||||
df_times = get_df_times(shipcall)
|
||||
|
||||
# according to the agency, a shipcall takes place soon (ETA/ETD)
|
||||
df_times.loc[df_times["participant_type"]==ParticipantType.AGENCY.value, "eta_berth"] = datetime.datetime.now() + datetime.timedelta(minutes=ParticipantwiseTimeDelta.TERMINAL-10)
|
||||
|
||||
# set times agency to be undetermined
|
||||
df_times.loc[df_times["participant_type"]==ParticipantType.TERMINAL.value, "operations_start"] = None # previously: eta_berth, which does not exist in times_terminal
|
||||
|
||||
# must adapt the shipcall_participant_map, so it suits the test
|
||||
agency_participant_id = df_times.loc[df_times["participant_type"]==ParticipantType.AGENCY.value, "participant_id"].iloc[0]
|
||||
terminal_participant_id = df_times.loc[df_times["participant_type"]==ParticipantType.TERMINAL.value, "participant_id"].iloc[0]
|
||||
|
||||
spm = vr.sql_handler.df_dict["shipcall_participant_map"]
|
||||
df = pd.DataFrame(
|
||||
[
|
||||
{"id":10001, "shipcall_id":shipcall.id, "participant_id":agency_participant_id, "type":ParticipantType.AGENCY.value, "created":pd.Timestamp(datetime.datetime.now().isoformat()), "modified":None},
|
||||
{"id":10002, "shipcall_id":shipcall.id, "participant_id":terminal_participant_id, "type":ParticipantType.TERMINAL.value, "created":pd.Timestamp(datetime.datetime.now().isoformat()), "modified":None}
|
||||
]
|
||||
)
|
||||
df.set_index("id", inplace=True)
|
||||
spm = pd.concat([spm, df], axis=0, ignore_index=True)
|
||||
vr.sql_handler.df_dict["shipcall_participant_map"] = spm
|
||||
|
||||
# apply the validation rule
|
||||
(state, msg) = vr.validation_rule_fct_missing_time_terminal_berth_eta(shipcall=shipcall, df_times=df_times)
|
||||
|
||||
# expectation: green state, no msg
|
||||
assert state==StatusFlags.GREEN, f"function should return 'green', becaues the ignore terminal flag is active. Hence, terminal validation rules are ignored."
|
||||
vr.ignore_terminal_flag = reset_to_default
|
||||
return
|
||||
|
||||
|
||||
@ -691,6 +744,9 @@ def test_validation_rule_fct_missing_time_terminal_berth_eta__shipcall_soon_but_
|
||||
def test_validation_rule_fct_missing_time_terminal_berth_etd__shipcall_soon_but_participant_estimated_time_undefined(build_sql_proxy_connection):
|
||||
"""0001-M validation_rule_fct_missing_time_terminal_berth_etd"""
|
||||
vr = build_sql_proxy_connection['vr']
|
||||
import copy
|
||||
reset_to_default = copy.deepcopy(vr.ignore_terminal_flag)
|
||||
vr.ignore_terminal_flag = False
|
||||
|
||||
shipcall = get_shipcall_simple()
|
||||
shipcall.type = ShipcallType.OUTGOING.value
|
||||
@ -723,6 +779,48 @@ def test_validation_rule_fct_missing_time_terminal_berth_etd__shipcall_soon_but_
|
||||
|
||||
# expectation: green state, no msg
|
||||
assert state==StatusFlags.YELLOW, f"function should return 'yellow', because the participant did not provide a time and the shipcall takes place soon (according to the agency)"
|
||||
vr.ignore_terminal_flag = reset_to_default
|
||||
return
|
||||
|
||||
def test_validation_rule_fct_missing_time_terminal_berth_etd__shipcall_soon_but_participant_estimated_time_undefined__no_violation_because_terminal_flag_is_active(build_sql_proxy_connection):
|
||||
"""0001-M validation_rule_fct_missing_time_terminal_berth_etd"""
|
||||
vr = build_sql_proxy_connection['vr']
|
||||
import copy
|
||||
reset_to_default = copy.deepcopy(vr.ignore_terminal_flag)
|
||||
vr.ignore_terminal_flag = True
|
||||
|
||||
shipcall = get_shipcall_simple()
|
||||
shipcall.type = ShipcallType.OUTGOING.value
|
||||
df_times = get_df_times(shipcall)
|
||||
|
||||
# according to the agency, a shipcall takes place soon (ETA/ETD)
|
||||
df_times.loc[df_times["participant_type"]==ParticipantType.AGENCY.value, "etd_berth"] = datetime.datetime.now() + datetime.timedelta(minutes=ParticipantwiseTimeDelta.TERMINAL-10)
|
||||
|
||||
# set times agency to be undetermined
|
||||
df_times.loc[df_times["participant_type"]==ParticipantType.TERMINAL.value, "operations_end"] = None # previously: etd_berth, which does not exist in times_terminal
|
||||
|
||||
|
||||
# must adapt the shipcall_participant_map, so it suits the test
|
||||
agency_participant_id = df_times.loc[df_times["participant_type"]==ParticipantType.AGENCY.value, "participant_id"].iloc[0]
|
||||
terminal_participant_id = df_times.loc[df_times["participant_type"]==ParticipantType.TERMINAL.value, "participant_id"].iloc[0]
|
||||
|
||||
spm = vr.sql_handler.df_dict["shipcall_participant_map"]
|
||||
df = pd.DataFrame(
|
||||
[
|
||||
{"id":10001, "shipcall_id":shipcall.id, "participant_id":agency_participant_id, "type":ParticipantType.AGENCY.value, "created":pd.Timestamp(datetime.datetime.now().isoformat()), "modified":None},
|
||||
{"id":10002, "shipcall_id":shipcall.id, "participant_id":terminal_participant_id, "type":ParticipantType.TERMINAL.value, "created":pd.Timestamp(datetime.datetime.now().isoformat()), "modified":None}
|
||||
]
|
||||
)
|
||||
df.set_index("id", inplace=True)
|
||||
spm = pd.concat([spm, df], axis=0, ignore_index=True)
|
||||
vr.sql_handler.df_dict["shipcall_participant_map"] = spm
|
||||
|
||||
# apply the validation rule
|
||||
(state, msg) = vr.validation_rule_fct_missing_time_terminal_berth_etd(shipcall=shipcall, df_times=df_times)
|
||||
|
||||
# expectation: green state, no msg
|
||||
assert state==StatusFlags.GREEN, f"function should return 'green', becaues the ignore terminal flag is active. Hence, terminal validation rules are ignored."
|
||||
vr.ignore_terminal_flag = reset_to_default
|
||||
return
|
||||
|
||||
|
||||
@ -912,6 +1010,10 @@ def test_validation_rule_fct_shipcall_shifting_participants_disagree_on_eta_or_e
|
||||
def test_validation_rule_fct_eta_time_not_in_operation_window__times_dont_match(build_sql_proxy_connection):
|
||||
"""0003-A validation_rule_fct_eta_time_not_in_operation_window"""
|
||||
vr = build_sql_proxy_connection['vr']
|
||||
import copy
|
||||
reset_to_default = copy.deepcopy(vr.ignore_terminal_flag)
|
||||
vr.ignore_terminal_flag = False
|
||||
|
||||
shipcall = get_shipcall_simple()
|
||||
df_times = get_df_times(shipcall)
|
||||
|
||||
@ -923,11 +1025,37 @@ def test_validation_rule_fct_eta_time_not_in_operation_window__times_dont_match(
|
||||
|
||||
(code, msg) = vr.validation_rule_fct_eta_time_not_in_operation_window(shipcall, df_times)
|
||||
assert code==StatusFlags.RED, f"status flag should be 'red', because the planned operations start is BEFORE the estimated time of arrival for the shipcall"
|
||||
vr.ignore_terminal_flag = reset_to_default
|
||||
return
|
||||
|
||||
def test_validation_rule_fct_eta_time_not_in_operation_window__times_dont_match__no_violation_because_terminal_flag_is_active(build_sql_proxy_connection):
|
||||
"""0003-A validation_rule_fct_eta_time_not_in_operation_window"""
|
||||
vr = build_sql_proxy_connection['vr']
|
||||
import copy
|
||||
reset_to_default = copy.deepcopy(vr.ignore_terminal_flag)
|
||||
vr.ignore_terminal_flag = True
|
||||
|
||||
shipcall = get_shipcall_simple()
|
||||
df_times = get_df_times(shipcall)
|
||||
|
||||
t0_time = datetime.datetime.now() # reference time for easier readability
|
||||
|
||||
# the planned operations_start is before eta_berth (by one minute in this case)
|
||||
df_times.loc[df_times["participant_type"]==ParticipantType.AGENCY.value, "eta_berth"] = t0_time + datetime.timedelta(minutes=1)
|
||||
df_times.loc[df_times["participant_type"]==ParticipantType.TERMINAL.value, "operations_start"] = t0_time + datetime.timedelta(minutes=0)
|
||||
|
||||
(code, msg) = vr.validation_rule_fct_eta_time_not_in_operation_window(shipcall, df_times)
|
||||
assert code==StatusFlags.GREEN, f"the ignore terminal flag is active, so this validation rule is ignored. There should not be a violation"
|
||||
vr.ignore_terminal_flag = reset_to_default
|
||||
return
|
||||
|
||||
def test_validation_rule_fct_etd_time_not_in_operation_window__times_dont_match(build_sql_proxy_connection):
|
||||
"""0003-B validation_rule_fct_etd_time_not_in_operation_window"""
|
||||
vr = build_sql_proxy_connection['vr']
|
||||
import copy
|
||||
reset_to_default = copy.deepcopy(vr.ignore_terminal_flag)
|
||||
vr.ignore_terminal_flag = False
|
||||
|
||||
shipcall = get_shipcall_simple()
|
||||
shipcall.type = ShipcallType.SHIFTING.value
|
||||
df_times = get_df_times(shipcall)
|
||||
@ -941,6 +1069,30 @@ def test_validation_rule_fct_etd_time_not_in_operation_window__times_dont_match(
|
||||
|
||||
(code, msg) = vr.validation_rule_fct_etd_time_not_in_operation_window(shipcall, df_times)
|
||||
assert code==StatusFlags.RED, f"status flag should be 'red', because the planned operations end is AFTER the estimated time of departure for the shipcall"
|
||||
vr.ignore_terminal_flag = reset_to_default
|
||||
return
|
||||
|
||||
def test_validation_rule_fct_etd_time_not_in_operation_window__times_dont_match__no_violation_because_terminal_flag_is_active(build_sql_proxy_connection):
|
||||
"""0003-B validation_rule_fct_etd_time_not_in_operation_window"""
|
||||
vr = build_sql_proxy_connection['vr']
|
||||
import copy
|
||||
reset_to_default = copy.deepcopy(vr.ignore_terminal_flag)
|
||||
vr.ignore_terminal_flag = True
|
||||
|
||||
shipcall = get_shipcall_simple()
|
||||
shipcall.type = ShipcallType.SHIFTING.value
|
||||
df_times = get_df_times(shipcall)
|
||||
|
||||
t0_time = datetime.datetime.now() # reference time for easier readability
|
||||
|
||||
# the planned operations_end is after etd_berth (by one minute in this case)
|
||||
df_times.loc[df_times["participant_type"]==ParticipantType.AGENCY.value, "etd_berth"] = t0_time + datetime.timedelta(hours=1)
|
||||
df_times.loc[df_times["participant_type"]==ParticipantType.TERMINAL.value, "operations_end"] = t0_time+datetime.timedelta(hours=1, minutes=1)
|
||||
|
||||
|
||||
(code, msg) = vr.validation_rule_fct_etd_time_not_in_operation_window(shipcall, df_times)
|
||||
assert code==StatusFlags.GREEN, f"the ignore terminal flag is active, so this validation rule is ignored. There should not be a violation"
|
||||
vr.ignore_terminal_flag = reset_to_default
|
||||
return
|
||||
|
||||
def test_validation_rule_fct_eta_time_not_in_operation_window_and_validation_rule_fct_etd_time_not_in_operation_window__always_okay(build_sql_proxy_connection):
|
||||
|
||||
Reference in New Issue
Block a user