diff --git a/docs/ApiValidationRules.md b/docs/ApiValidationRules.md
index c080ae0..ec18ec9 100644
--- a/docs/ApiValidationRules.md
+++ b/docs/ApiValidationRules.md
@@ -61,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"
}
```
@@ -81,6 +82,10 @@ 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.
+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.
@@ -112,7 +117,7 @@ Usually the "Z" is missing at the end indicating local time.
#### 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)
@@ -123,7 +128,11 @@ Usually the "Z" is missing at the end indicating local time.
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.
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
@@ -147,6 +156,7 @@ The id field is required, missing fields will not be updated.
| 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 |
@@ -188,6 +198,7 @@ shipcall_id, participant_id, participant_type
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
diff --git a/misc/Ampelfunktion.md b/misc/Ampelfunktion.md
index 1274ba7..def1e03 100644
--- a/misc/Ampelfunktion.md
+++ b/misc/Ampelfunktion.md
@@ -32,7 +32,7 @@ ___
| 0002 | Zeiten für einen Eintrag weichen voneinander ab | Bedingungen:
- Header der Zeile ist zugeordnet (Agentur, Festmacher usw. - außer BSMD-Spalte)
- Zeiten ungleich (leere Einträge nicht berücksichtigen => 0001) | |
| 0002 - A | Agentur + Festmacher + Hafenamt + Lotsen + Schlepper / einkommend | Schnittmenge aus:
times_agency:
- ETA Berth
____und____
times_mooring:
- ETA Berth
____und____
times_portauthority:
- ETA Berth
____und____
times_pilot:
- ETA Berth
____und____
times_tug:
- ETA Berth | rot |
| 0002 - B | Agentur + Festmacher + Hafenamt + Lotsen + Schlepper / ausgehend | Schnittmenge aus:
times_agency:
- ETD Berth
____und____
times_mooring:
- ETD Berth
____und____
times_portauthority:
- ETD Berth
____und____
times_pilot:
- ETD Berth
____und____
times_tug:
- ETD Berth | rot |
-| 0002 - C | Agentur + Festmacher + Hafenamt + Lotsen + Schlepper / Verholung | Schnittmenge aus:
times_agency:
- ETA Berth
- ETD Berth
____und____
times_mooring:
- ETA Berth
- ETD Berth
____und____
times_portauthority:
- ETA Berth
- ETD Berth
____und____
times_pilot:
- ETA Berth
- ETD Berth
____und____
times_tug:
- ETA Berth
- ETD Berth | rot |
+| 0002 - C | Agentur + Festmacher + Hafenamt + Lotsen + Schlepper / Verholung | Schnittmenge aus:
times_agency:
- ETD Berth
____und____
times_mooring:
- ETD Berth
____und____
times_portauthority:
- ETD Berth
____und____
times_pilot:
- ETD Berth
____und____
times_tug:
- ETD Berth | rot |
| 0003 | Arbeitszeit überschneidet sich mit Fahrtzeit | Bedingungen:
- Header der Zeile ist zugeordnet (Terminal)
- Zeiten passt nicht zu Ankunft / Abfahrt (leere Einträge nicht berücksichtigen => 0001) | |
| 0003 - A | Terminal / einkommend | times_terminal:
- Operation Start
___vor (kleiner als)____
times_agency:
- ETA Berth | rot, aktuell __deaktiviert__! |
| 0003 - B | Terminal / ausgehend + Verholung | times_terminal:
- Operation Ende
___nach (größer als)____
times_agency:
- ETD Berth | rot, aktuell __deaktiviert__! |
diff --git a/misc/BreCalApi.cs b/misc/BreCalApi.cs
index f5147d3..cd59c45 100644
--- a/misc/BreCalApi.cs
+++ b/misc/BreCalApi.cs
@@ -1,7 +1,7 @@
//----------------------
//
-// Generated REST API Client Code Generator v1.10.9.0 on 29.08.2024 08:26:31
+// Generated REST API Client Code Generator v1.11.0.0 on 10.09.2024 11:10:02
// Using the tool OpenAPI Generator v7.8.0
//
//----------------------
@@ -46,7 +46,7 @@ using System.Threading.Tasks;
*
* Administer DEBRE ship calls, times and notifications
*
- * The version of the OpenAPI document: 1.4.0
+ * The version of the OpenAPI document: 1.5.0
* Contact: info@textbausteine.net
* Generated by: https://github.com/openapitools/openapi-generator.git
*/
@@ -850,7 +850,7 @@ namespace BreCalClient.misc.Api
*
* Administer DEBRE ship calls, times and notifications
*
- * The version of the OpenAPI document: 1.4.0
+ * The version of the OpenAPI document: 1.5.0
* Contact: info@textbausteine.net
* Generated by: https://github.com/openapitools/openapi-generator.git
*/
@@ -1505,7 +1505,7 @@ namespace BreCalClient.misc.Api
*
* Administer DEBRE ship calls, times and notifications
*
- * The version of the OpenAPI document: 1.4.0
+ * The version of the OpenAPI document: 1.5.0
* Contact: info@textbausteine.net
* Generated by: https://github.com/openapitools/openapi-generator.git
*/
@@ -2290,7 +2290,7 @@ namespace BreCalClient.misc.Api
*
* Administer DEBRE ship calls, times and notifications
*
- * The version of the OpenAPI document: 1.4.0
+ * The version of the OpenAPI document: 1.5.0
* Contact: info@textbausteine.net
* Generated by: https://github.com/openapitools/openapi-generator.git
*/
@@ -3109,7 +3109,7 @@ namespace BreCalClient.misc.Api
*
* Administer DEBRE ship calls, times and notifications
*
- * The version of the OpenAPI document: 1.4.0
+ * The version of the OpenAPI document: 1.5.0
* Contact: info@textbausteine.net
* Generated by: https://github.com/openapitools/openapi-generator.git
*/
@@ -3594,7 +3594,7 @@ namespace BreCalClient.misc.Api
*
* Administer DEBRE ship calls, times and notifications
*
- * The version of the OpenAPI document: 1.4.0
+ * The version of the OpenAPI document: 1.5.0
* Contact: info@textbausteine.net
* Generated by: https://github.com/openapitools/openapi-generator.git
*/
@@ -4314,7 +4314,7 @@ namespace BreCalClient.misc.Client
*
* Administer DEBRE ship calls, times and notifications
*
- * The version of the OpenAPI document: 1.4.0
+ * The version of the OpenAPI document: 1.5.0
* Contact: info@textbausteine.net
* Generated by: https://github.com/openapitools/openapi-generator.git
*/
@@ -4374,7 +4374,7 @@ namespace BreCalClient.misc.Client
*
* Administer DEBRE ship calls, times and notifications
*
- * The version of the OpenAPI document: 1.4.0
+ * The version of the OpenAPI document: 1.5.0
* Contact: info@textbausteine.net
* Generated by: https://github.com/openapitools/openapi-generator.git
*/
@@ -4514,7 +4514,7 @@ namespace BreCalClient.misc.Client
*
* Administer DEBRE ship calls, times and notifications
*
- * The version of the OpenAPI document: 1.4.0
+ * The version of the OpenAPI document: 1.5.0
* Contact: info@textbausteine.net
* Generated by: https://github.com/openapitools/openapi-generator.git
*/
@@ -4732,7 +4732,7 @@ namespace BreCalClient.misc.Client
*
* Administer DEBRE ship calls, times and notifications
*
- * The version of the OpenAPI document: 1.4.0
+ * The version of the OpenAPI document: 1.5.0
* Contact: info@textbausteine.net
* Generated by: https://github.com/openapitools/openapi-generator.git
*/
@@ -4817,7 +4817,7 @@ namespace BreCalClient.misc.Client
{
Proxy = null;
UserAgent = WebUtility.UrlEncode("OpenAPI-Generator/1.0.0/csharp");
- BasePath = "https://brecaldevel.bsmd-emswe.eu";
+ BasePath = "https://brecaltest.bsmd-emswe.eu";
DefaultHeaders = new ConcurrentDictionary();
ApiKey = new ConcurrentDictionary();
ApiKeyPrefix = new ConcurrentDictionary();
@@ -4825,7 +4825,7 @@ namespace BreCalClient.misc.Client
{
{
new Dictionary {
- {"url", "https://brecaldevel.bsmd-emswe.eu"},
+ {"url", "https://brecaltest.bsmd-emswe.eu"},
{"description", "Development server hosted on vcup"},
}
}
@@ -4844,7 +4844,7 @@ namespace BreCalClient.misc.Client
IDictionary defaultHeaders,
IDictionary apiKey,
IDictionary apiKeyPrefix,
- string basePath = "https://brecaldevel.bsmd-emswe.eu") : this()
+ string basePath = "https://brecaltest.bsmd-emswe.eu") : this()
{
if (string.IsNullOrWhiteSpace(basePath))
throw new ArgumentException("The provided basePath is invalid.", "basePath");
@@ -5191,7 +5191,7 @@ namespace BreCalClient.misc.Client
string report = "C# SDK (BreCalClient.misc) Debug Report:\n";
report += " OS: " + System.Environment.OSVersion + "\n";
report += " .NET Framework Version: " + System.Environment.Version + "\n";
- report += " Version of the API: 1.4.0\n";
+ report += " Version of the API: 1.5.0\n";
report += " SDK Package Version: 1.0.0\n";
return report;
}
@@ -5260,7 +5260,7 @@ namespace BreCalClient.misc.Client
*
* Administer DEBRE ship calls, times and notifications
*
- * The version of the OpenAPI document: 1.4.0
+ * The version of the OpenAPI document: 1.5.0
* Contact: info@textbausteine.net
* Generated by: https://github.com/openapitools/openapi-generator.git
*/
@@ -5280,7 +5280,7 @@ namespace BreCalClient.misc.Client
*
* Administer DEBRE ship calls, times and notifications
*
- * The version of the OpenAPI document: 1.4.0
+ * The version of the OpenAPI document: 1.5.0
* Contact: info@textbausteine.net
* Generated by: https://github.com/openapitools/openapi-generator.git
*/
@@ -5337,7 +5337,7 @@ namespace BreCalClient.misc.Client
*
* Administer DEBRE ship calls, times and notifications
*
- * The version of the OpenAPI document: 1.4.0
+ * The version of the OpenAPI document: 1.5.0
* Contact: info@textbausteine.net
* Generated by: https://github.com/openapitools/openapi-generator.git
*/
@@ -5370,7 +5370,7 @@ namespace BreCalClient.misc.Client
*
* Administer DEBRE ship calls, times and notifications
*
- * The version of the OpenAPI document: 1.4.0
+ * The version of the OpenAPI document: 1.5.0
* Contact: info@textbausteine.net
* Generated by: https://github.com/openapitools/openapi-generator.git
*/
@@ -5403,7 +5403,7 @@ namespace BreCalClient.misc.Client
*
* Administer DEBRE ship calls, times and notifications
*
- * The version of the OpenAPI document: 1.4.0
+ * The version of the OpenAPI document: 1.5.0
* Contact: info@textbausteine.net
* Generated by: https://github.com/openapitools/openapi-generator.git
*/
@@ -5494,7 +5494,7 @@ namespace BreCalClient.misc.Client
*
* Administer DEBRE ship calls, times and notifications
*
- * The version of the OpenAPI document: 1.4.0
+ * The version of the OpenAPI document: 1.5.0
* Contact: info@textbausteine.net
* Generated by: https://github.com/openapitools/openapi-generator.git
*/
@@ -5611,7 +5611,7 @@ namespace BreCalClient.misc.Client
*
* Administer DEBRE ship calls, times and notifications
*
- * The version of the OpenAPI document: 1.4.0
+ * The version of the OpenAPI document: 1.5.0
* Contact: info@textbausteine.net
* Generated by: https://github.com/openapitools/openapi-generator.git
*/
@@ -5695,7 +5695,7 @@ namespace BreCalClient.misc.Client
*
* Administer DEBRE ship calls, times and notifications
*
- * The version of the OpenAPI document: 1.4.0
+ * The version of the OpenAPI document: 1.5.0
* Contact: info@textbausteine.net
* Generated by: https://github.com/openapitools/openapi-generator.git
*/
@@ -5954,7 +5954,7 @@ namespace BreCalClient.misc.Client
*
* Administer DEBRE ship calls, times and notifications
*
- * The version of the OpenAPI document: 1.4.0
+ * The version of the OpenAPI document: 1.5.0
* Contact: info@textbausteine.net
* Generated by: https://github.com/openapitools/openapi-generator.git
*/
@@ -5982,7 +5982,7 @@ namespace BreCalClient.misc.Client
*
* Administer DEBRE ship calls, times and notifications
*
- * The version of the OpenAPI document: 1.4.0
+ * The version of the OpenAPI document: 1.5.0
* Contact: info@textbausteine.net
* Generated by: https://github.com/openapitools/openapi-generator.git
*/
@@ -6052,7 +6052,7 @@ namespace BreCalClient.misc.Client
*
* Administer DEBRE ship calls, times and notifications
*
- * The version of the OpenAPI document: 1.4.0
+ * The version of the OpenAPI document: 1.5.0
* Contact: info@textbausteine.net
* Generated by: https://github.com/openapitools/openapi-generator.git
*/
@@ -6079,7 +6079,7 @@ namespace BreCalClient.misc.Client
*
* Administer DEBRE ship calls, times and notifications
*
- * The version of the OpenAPI document: 1.4.0
+ * The version of the OpenAPI document: 1.5.0
* Contact: info@textbausteine.net
* Generated by: https://github.com/openapitools/openapi-generator.git
*/
@@ -6146,7 +6146,7 @@ namespace BreCalClient.misc.Model
*
* Administer DEBRE ship calls, times and notifications
*
- * The version of the OpenAPI document: 1.4.0
+ * The version of the OpenAPI document: 1.5.0
* Contact: info@textbausteine.net
* Generated by: https://github.com/openapitools/openapi-generator.git
*/
@@ -6272,7 +6272,7 @@ namespace BreCalClient.misc.Model
*
* Administer DEBRE ship calls, times and notifications
*
- * The version of the OpenAPI document: 1.4.0
+ * The version of the OpenAPI document: 1.5.0
* Contact: info@textbausteine.net
* Generated by: https://github.com/openapitools/openapi-generator.git
*/
@@ -6357,14 +6357,14 @@ namespace BreCalClient.misc.Model
*
* Administer DEBRE ship calls, times and notifications
*
- * The version of the OpenAPI document: 1.4.0
+ * The version of the OpenAPI document: 1.5.0
* Contact: info@textbausteine.net
* Generated by: https://github.com/openapitools/openapi-generator.git
*/
namespace BreCalClient.misc.Model
{
///
- /// Error
+ /// Structure returned when invalid data (bad request) is created.
///
[DataContract(Name = "Error")]
public partial class Error : IValidatableObject
@@ -6378,7 +6378,9 @@ namespace BreCalClient.misc.Model
/// Initializes a new instance of the class.
///
/// A human readable error message (required).
- public Error(string message = default(string))
+ /// A list of errors.
+ /// A dictionary of valid data.
+ public Error(string message = default(string), List errors = default(List), Object validData = default(Object))
{
// to ensure "message" is required (not null)
if (message == null)
@@ -6386,6 +6388,8 @@ namespace BreCalClient.misc.Model
throw new ArgumentNullException("message is a required property for Error and cannot be null");
}
this.Message = message;
+ this.Errors = errors;
+ this.ValidData = validData;
}
///
/// A human readable error message
@@ -6394,6 +6398,18 @@ namespace BreCalClient.misc.Model
[DataMember(Name = "message", IsRequired = true, EmitDefaultValue = true)]
public string Message { get; set; }
///
+ /// A list of errors
+ ///
+ /// A list of errors
+ [DataMember(Name = "errors", EmitDefaultValue = true)]
+ public List Errors { get; set; }
+ ///
+ /// A dictionary of valid data
+ ///
+ /// A dictionary of valid data
+ [DataMember(Name = "valid_data", EmitDefaultValue = true)]
+ public Object ValidData { get; set; }
+ ///
/// Returns the string presentation of the object
///
/// String presentation of the object
@@ -6402,6 +6418,8 @@ namespace BreCalClient.misc.Model
StringBuilder sb = new StringBuilder();
sb.Append("class Error {\n");
sb.Append(" Message: ").Append(Message).Append("\n");
+ sb.Append(" Errors: ").Append(Errors).Append("\n");
+ sb.Append(" ValidData: ").Append(ValidData).Append("\n");
sb.Append("}\n");
return sb.ToString();
}
@@ -6430,7 +6448,7 @@ namespace BreCalClient.misc.Model
*
* Administer DEBRE ship calls, times and notifications
*
- * The version of the OpenAPI document: 1.4.0
+ * The version of the OpenAPI document: 1.5.0
* Contact: info@textbausteine.net
* Generated by: https://github.com/openapitools/openapi-generator.git
*/
@@ -6470,7 +6488,7 @@ namespace BreCalClient.misc.Model
*
* Administer DEBRE ship calls, times and notifications
*
- * The version of the OpenAPI document: 1.4.0
+ * The version of the OpenAPI document: 1.5.0
* Contact: info@textbausteine.net
* Generated by: https://github.com/openapitools/openapi-generator.git
*/
@@ -6580,7 +6598,7 @@ namespace BreCalClient.misc.Model
*
* Administer DEBRE ship calls, times and notifications
*
- * The version of the OpenAPI document: 1.4.0
+ * The version of the OpenAPI document: 1.5.0
* Contact: info@textbausteine.net
* Generated by: https://github.com/openapitools/openapi-generator.git
*/
@@ -6642,7 +6660,7 @@ namespace BreCalClient.misc.Model
*
* Administer DEBRE ship calls, times and notifications
*
- * The version of the OpenAPI document: 1.4.0
+ * The version of the OpenAPI document: 1.5.0
* Contact: info@textbausteine.net
* Generated by: https://github.com/openapitools/openapi-generator.git
*/
@@ -6768,7 +6786,7 @@ namespace BreCalClient.misc.Model
*
* Administer DEBRE ship calls, times and notifications
*
- * The version of the OpenAPI document: 1.4.0
+ * The version of the OpenAPI document: 1.5.0
* Contact: info@textbausteine.net
* Generated by: https://github.com/openapitools/openapi-generator.git
*/
@@ -6872,7 +6890,7 @@ namespace BreCalClient.misc.Model
*
* Administer DEBRE ship calls, times and notifications
*
- * The version of the OpenAPI document: 1.4.0
+ * The version of the OpenAPI document: 1.5.0
* Contact: info@textbausteine.net
* Generated by: https://github.com/openapitools/openapi-generator.git
*/
@@ -6907,7 +6925,7 @@ namespace BreCalClient.misc.Model
*
* Administer DEBRE ship calls, times and notifications
*
- * The version of the OpenAPI document: 1.4.0
+ * The version of the OpenAPI document: 1.5.0
* Contact: info@textbausteine.net
* Generated by: https://github.com/openapitools/openapi-generator.git
*/
@@ -6937,7 +6955,7 @@ namespace BreCalClient.misc.Model
*
* Administer DEBRE ship calls, times and notifications
*
- * The version of the OpenAPI document: 1.4.0
+ * The version of the OpenAPI document: 1.5.0
* Contact: info@textbausteine.net
* Generated by: https://github.com/openapitools/openapi-generator.git
*/
@@ -6977,7 +6995,7 @@ namespace BreCalClient.misc.Model
*
* Administer DEBRE ship calls, times and notifications
*
- * The version of the OpenAPI document: 1.4.0
+ * The version of the OpenAPI document: 1.5.0
* Contact: info@textbausteine.net
* Generated by: https://github.com/openapitools/openapi-generator.git
*/
@@ -7134,7 +7152,7 @@ namespace BreCalClient.misc.Model
*
* Administer DEBRE ship calls, times and notifications
*
- * The version of the OpenAPI document: 1.4.0
+ * The version of the OpenAPI document: 1.5.0
* Contact: info@textbausteine.net
* Generated by: https://github.com/openapitools/openapi-generator.git
*/
@@ -7210,7 +7228,7 @@ namespace BreCalClient.misc.Model
*
* Administer DEBRE ship calls, times and notifications
*
- * The version of the OpenAPI document: 1.4.0
+ * The version of the OpenAPI document: 1.5.0
* Contact: info@textbausteine.net
* Generated by: https://github.com/openapitools/openapi-generator.git
*/
@@ -7380,7 +7398,7 @@ namespace BreCalClient.misc.Model
*
* Administer DEBRE ship calls, times and notifications
*
- * The version of the OpenAPI document: 1.4.0
+ * The version of the OpenAPI document: 1.5.0
* Contact: info@textbausteine.net
* Generated by: https://github.com/openapitools/openapi-generator.git
*/
@@ -7695,7 +7713,7 @@ namespace BreCalClient.misc.Model
*
* Administer DEBRE ship calls, times and notifications
*
- * The version of the OpenAPI document: 1.4.0
+ * The version of the OpenAPI document: 1.5.0
* Contact: info@textbausteine.net
* Generated by: https://github.com/openapitools/openapi-generator.git
*/
@@ -7735,7 +7753,7 @@ namespace BreCalClient.misc.Model
*
* Administer DEBRE ship calls, times and notifications
*
- * The version of the OpenAPI document: 1.4.0
+ * The version of the OpenAPI document: 1.5.0
* Contact: info@textbausteine.net
* Generated by: https://github.com/openapitools/openapi-generator.git
*/
@@ -8017,7 +8035,7 @@ namespace BreCalClient.misc.Model
*
* Administer DEBRE ship calls, times and notifications
*
- * The version of the OpenAPI document: 1.4.0
+ * The version of the OpenAPI document: 1.5.0
* Contact: info@textbausteine.net
* Generated by: https://github.com/openapitools/openapi-generator.git
*/
diff --git a/misc/BreCalApi.yaml b/misc/BreCalApi.yaml
index 2accd25..a55aa05 100644
--- a/misc/BreCalApi.yaml
+++ b/misc/BreCalApi.yaml
@@ -2,7 +2,7 @@ openapi: 3.0.0
x-stoplight:
id: mwv4y8vcnopwr
info:
- version: 1.4.1
+ version: 1.5.0
title: Bremen calling API
description: 'Administer DEBRE ship calls, times and notifications'
termsOfService: 'https://www.bsmd.de/'
@@ -14,7 +14,7 @@ info:
name: Use at your own risk
url: 'https://www.bsmd.de/license'
servers:
- - url: 'https://brecaldevel.bsmd-emswe.eu'
+ - url: 'https://brecaltest.bsmd-emswe.eu'
description: Development server hosted on vcup
tags:
- name: user
@@ -943,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:
diff --git a/src/BreCalClient/App.config b/src/BreCalClient/App.config
index 335a433..9435c06 100644
--- a/src/BreCalClient/App.config
+++ b/src/BreCalClient/App.config
@@ -29,7 +29,7 @@
- #1D751F
+ #751D1F
!!Bremen calling Testversion!!
@@ -38,7 +38,7 @@
https://www.textbausteine.net/
- https://brecaldevel.bsmd-emswe.eu
+ https://brecaltest.bsmd-emswe.eu
diff --git a/src/BreCalClient/BreCalClient.csproj b/src/BreCalClient/BreCalClient.csproj
index 2403c98..8980732 100644
--- a/src/BreCalClient/BreCalClient.csproj
+++ b/src/BreCalClient/BreCalClient.csproj
@@ -8,12 +8,12 @@
True
BreCalClient.App
..\..\misc\brecal.snk
- 1.4.1.0
- 1.4.1.0
+ 1.5.0.4
+ 1.5.0.4
Bremen calling client
A Windows WPF client for the Bremen calling API.
containership.ico
- BreCalDevelClient
+ BreCalTestClient
@@ -116,12 +116,12 @@
-
+
-
-
+
+
diff --git a/src/BreCalClient/EditShipDialog.xaml.cs b/src/BreCalClient/EditShipDialog.xaml.cs
index 741c1f4..5feeb9b 100644
--- a/src/BreCalClient/EditShipDialog.xaml.cs
+++ b/src/BreCalClient/EditShipDialog.xaml.cs
@@ -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();
diff --git a/src/BreCalClient/EditShipcallControl.xaml.cs b/src/BreCalClient/EditShipcallControl.xaml.cs
index 322b534..65ae975 100644
--- a/src/BreCalClient/EditShipcallControl.xaml.cs
+++ b/src/BreCalClient/EditShipcallControl.xaml.cs
@@ -205,15 +205,23 @@ namespace BreCalClient
case ShipcallType.Departure:
isEnabled &= this.comboBoxDepartureBerth.SelectedItem != null;
isEnabled &= this.datePickerETD.Value.HasValue;
+ if(this.datePickerETD.Value.HasValue)
+ isEnabled &= (this.datePickerETD.Value.Value > DateTime.Now);
break;
case ShipcallType.Arrival:
isEnabled &= this.comboBoxArrivalBerth.SelectedItem != null;
isEnabled &= this.datePickerETA.Value.HasValue;
+ if(this.datePickerETA.Value.HasValue)
+ isEnabled &= (this.datePickerETA.Value.Value > DateTime.Now);
break;
case ShipcallType.Shifting:
isEnabled &= ((this.comboBoxDepartureBerth.SelectedItem != null) && (this.comboBoxArrivalBerth.SelectedItem != null));
isEnabled &= this.datePickerETD.Value.HasValue;
isEnabled &= this.datePickerETA.Value.HasValue;
+ if (this.datePickerETD.Value.HasValue)
+ isEnabled &= (this.datePickerETD.Value.Value > DateTime.Now);
+ if (this.datePickerETA.Value.HasValue)
+ isEnabled &= (this.datePickerETA.Value.Value > DateTime.Now);
break;
}
}
diff --git a/src/BreCalClient/EditTimesAgencyIncomingControl.xaml.cs b/src/BreCalClient/EditTimesAgencyIncomingControl.xaml.cs
index 8a38a07..b1eba3b 100644
--- a/src/BreCalClient/EditTimesAgencyIncomingControl.xaml.cs
+++ b/src/BreCalClient/EditTimesAgencyIncomingControl.xaml.cs
@@ -4,6 +4,7 @@
using BreCalClient.misc.Model;
using System;
+using System.Runtime.Serialization;
using System.Windows;
using static BreCalClient.Extensions;
@@ -70,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))
+ {
+ MessageBox.Show(message, "Warning", MessageBoxButton.OK, MessageBoxImage.Warning);
+ }
+ else
+ {
+ this.CopyToModel();
+ this.DialogResult = true;
+ this.Close();
+ }
}
private void buttonCancel_Click(object sender, RoutedEventArgs e)
@@ -85,6 +93,49 @@ namespace BreCalClient
#region private methods
+ private bool CheckValues(out string message)
+ {
+ message = "";
+
+ if (this.datePickerETA.Value.HasValue && (this.datePickerETA.Value.Value < DateTime.Now) && (this.datePickerETA_End.Value == null))
+ {
+ message = BreCalClient.Resources.Resources.textETAInThePast;
+ return false;
+ }
+
+ if(this.datePickerETA_End.Value.HasValue && this.datePickerETA_End.Value < DateTime.Now)
+ {
+ 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.HasValue && (this.datePickerTidalWindowFrom.Value.Value < DateTime.Now) && (this.datePickerTidalWindowTo.Value == null))
+ {
+ message = BreCalClient.Resources.Resources.textTideTimesInThePast;
+ return false;
+ }
+
+ if (this.datePickerTidalWindowTo.Value.HasValue && this.datePickerTidalWindowTo.Value < DateTime.Now)
+ {
+ 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;
+ }
+
+ return true;
+ }
+
private void CopyToModel()
{
if (this.ShipcallModel.Shipcall != null)
diff --git a/src/BreCalClient/EditTimesAgencyOutgoingControl.xaml.cs b/src/BreCalClient/EditTimesAgencyOutgoingControl.xaml.cs
index 0a778fd..7258006 100644
--- a/src/BreCalClient/EditTimesAgencyOutgoingControl.xaml.cs
+++ b/src/BreCalClient/EditTimesAgencyOutgoingControl.xaml.cs
@@ -80,9 +80,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, "Warning", MessageBoxButton.OK, MessageBoxImage.Warning);
+ }
+ else
+ {
+ this.CopyToModel();
+ this.DialogResult = true;
+ this.Close();
+ }
}
private void buttonCancel_Click(object sender, RoutedEventArgs e)
@@ -95,6 +102,50 @@ namespace BreCalClient
#region private methods
+
+ private bool CheckValues(out string message)
+ {
+ message = "";
+
+ if (this.datePickerETD.Value.HasValue && (this.datePickerETD.Value.Value < DateTime.Now) && (this.datePickerETD_End.Value == null))
+ {
+ message = BreCalClient.Resources.Resources.textETDInThePast;
+ return false;
+ }
+
+ if (this.datePickerETD_End.Value.HasValue && this.datePickerETD_End.Value < DateTime.Now)
+ {
+ 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.HasValue && (this.datePickerTidalWindowFrom.Value.Value < DateTime.Now) && (this.datePickerTidalWindowTo.Value == null))
+ {
+ message = BreCalClient.Resources.Resources.textTideTimesInThePast;
+ return false;
+ }
+
+ if (this.datePickerTidalWindowTo.Value.HasValue && this.datePickerTidalWindowTo.Value < DateTime.Now)
+ {
+ 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;
+ }
+
+ return true;
+ }
+
private void CopyToModel()
{
if (this.ShipcallModel.Shipcall != null)
diff --git a/src/BreCalClient/EditTimesAgencyShiftingControl.xaml.cs b/src/BreCalClient/EditTimesAgencyShiftingControl.xaml.cs
index 5fa79ad..e8398ce 100644
--- a/src/BreCalClient/EditTimesAgencyShiftingControl.xaml.cs
+++ b/src/BreCalClient/EditTimesAgencyShiftingControl.xaml.cs
@@ -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, "Warning", MessageBoxButton.OK, MessageBoxImage.Warning);
+ }
+ else
+ {
+ this.CopyToModel();
+ this.DialogResult = true;
+ this.Close();
+ }
}
private void buttonCancel_Click(object sender, RoutedEventArgs e)
@@ -86,6 +93,67 @@ namespace BreCalClient
#region private methods
+ private bool CheckValues(out string message)
+ {
+ message = "";
+
+ if (this.datePickerETA.Value.HasValue && (this.datePickerETA.Value.Value < DateTime.Now) && (this.datePickerETA_End.Value == null))
+ {
+ message = BreCalClient.Resources.Resources.textETAInThePast;
+ return false;
+ }
+
+ if (this.datePickerETA_End.Value.HasValue && this.datePickerETA_End.Value < DateTime.Now)
+ {
+ 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.HasValue && (this.datePickerETD.Value.Value < DateTime.Now) && (this.datePickerETD_End.Value == null))
+ {
+ message = BreCalClient.Resources.Resources.textETDInThePast;
+ return false;
+ }
+
+ if (this.datePickerETD_End.Value.HasValue && this.datePickerETD_End.Value < DateTime.Now)
+ {
+ 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.HasValue && (this.datePickerTidalWindowFrom.Value.Value < DateTime.Now) && (this.datePickerTidalWindowTo.Value == null))
+ {
+ message = BreCalClient.Resources.Resources.textTideTimesInThePast;
+ return false;
+ }
+
+ if (this.datePickerTidalWindowTo.Value.HasValue && this.datePickerTidalWindowTo.Value < DateTime.Now)
+ {
+ 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;
+ }
+
+ return true;
+ }
+
private void CopyToModel()
{
if (this.ShipcallModel.Shipcall != null)
diff --git a/src/BreCalClient/EditTimesControl.xaml b/src/BreCalClient/EditTimesControl.xaml
index ad03ed9..03cac40 100644
--- a/src/BreCalClient/EditTimesControl.xaml
+++ b/src/BreCalClient/EditTimesControl.xaml
@@ -44,7 +44,7 @@
-
+
/// API reference to PUT eidted times
- internal async void UpdateTimesAssignments(TimesApi api)
- {
+ internal async Task 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
diff --git a/src/RoleEditor/App.config b/src/RoleEditor/App.config
index 51c1ce1..bd2b4e4 100644
--- a/src/RoleEditor/App.config
+++ b/src/RoleEditor/App.config
@@ -8,7 +8,7 @@
- Server=localhost;User ID=ds;Password=HalloWach_2323XXL!!;Database=bremen_calling_devel;Port=33306
+ Server=localhost;User ID=ds;Password=HalloWach_2323XXL!!;Database=bremen_calling_test;Port=33306
diff --git a/src/server/BreCal/__init__.py b/src/server/BreCal/__init__.py
index 263203e..ad6b3d0 100644
--- a/src/server/BreCal/__init__.py
+++ b/src/server/BreCal/__init__.py
@@ -66,7 +66,7 @@ def create_app(test_config=None, instance_path=None):
app.register_blueprint(user.bp)
app.register_blueprint(history.bp)
- logging.basicConfig(filename='brecaldevel.log', level=logging.DEBUG, format='%(asctime)s | %(name)s | %(levelname)s | %(message)s')
+ logging.basicConfig(filename='brecaltest.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')
diff --git a/src/server/BreCal/api/berths.py b/src/server/BreCal/api/berths.py
index ba8f4ff..114f893 100644
--- a/src/server/BreCal/api/berths.py
+++ b/src/server/BreCal/api/berths.py
@@ -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)
diff --git a/src/server/BreCal/api/history.py b/src/server/BreCal/api/history.py
index c1fe5ad..098423f 100644
--- a/src/server/BreCal/api/history.py
+++ b/src/server/BreCal/api/history.py
@@ -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)
diff --git a/src/server/BreCal/api/notifications.py b/src/server/BreCal/api/notifications.py
index 48a2a44..4fe4a8e 100644
--- a/src/server/BreCal/api/notifications.py
+++ b/src/server/BreCal/api/notifications.py
@@ -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
\ No newline at end of file
+ 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)
diff --git a/src/server/BreCal/api/participant.py b/src/server/BreCal/api/participant.py
index 65887f9..814a3dd 100644
--- a/src/server/BreCal/api/participant.py
+++ b/src/server/BreCal/api/participant.py
@@ -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)
diff --git a/src/server/BreCal/api/shipcalls.py b/src/server/BreCal/api/shipcalls.py
index e6ac854..b21908e 100644
--- a/src/server/BreCal/api/shipcalls.py
+++ b/src/server/BreCal/api/shipcalls.py
@@ -7,7 +7,7 @@ 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
+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
@@ -20,24 +20,28 @@ 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') # see impl/login to see the token encoding, which is a JWT token.
+ try:
+ if 'Authorization' in request.headers:
+ token = request.headers.get('Authorization') # see impl/login to see the token encoding, which is a JWT token.
- """
- 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'
+ """
+ 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)
+ # 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 json.dumps("not authenticated"), 403
+ 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'])
@@ -55,19 +59,14 @@ def PostShipcalls():
# 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:
- logging.error(ex)
- print(ex)
return create_validation_error_response(ex=ex, status_code=400)
except Exception as ex:
- logging.error(ex)
logging.error(traceback.format_exc())
- print(ex)
- return json.dumps("bad format"), 400
-
- return impl.shipcalls.PostShipcalls(loadedModel)
+ return create_dynamic_exception_response(ex=ex, status_code=400, message="bad format")
@bp.route('/shipcalls', methods=['put'])
@@ -85,20 +84,15 @@ def PutShipcalls():
# 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:
- logging.error(ex)
- print(ex)
return create_validation_error_response(ex=ex, status_code=400)
except werkzeug.exceptions.Forbidden as ex:
- logging.error(ex)
- print(ex)
return create_werkzeug_error_response(ex=ex, status_code=403)
except Exception as ex:
- logging.error(ex)
- print(ex)
- return json.dumps("bad format"), 400
+ logging.error(traceback.format_exc())
+ return create_dynamic_exception_response(ex=None, status_code=400, message="bad format")
- return impl.shipcalls.PutShipcalls(loadedModel)
diff --git a/src/server/BreCal/api/ships.py b/src/server/BreCal/api/ships.py
index 5935402..482474b 100644
--- a/src/server/BreCal/api/ships.py
+++ b/src/server/BreCal/api/ships.py
@@ -6,7 +6,7 @@ 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
+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
@@ -17,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'])
@@ -38,25 +42,20 @@ def PostShip():
# 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(f"current user does not belong to BSMD. Cannot post shipcalls. Found user data: {user_data}")
+ 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)
# validate the request data & user permissions
InputValidationShip.evaluate_post_data(user_data, loadedModel, content)
+ return impl.ships.PostShip(loadedModel)
except ValidationError as ex:
- logging.error(ex)
- print(ex)
return create_validation_error_response(ex=ex, status_code=400)
except Exception as ex:
- logging.error(ex)
- print(ex)
- return json.dumps(repr(ex)), 400
-
- return impl.ships.PostShip(loadedModel)
+ return create_dynamic_exception_response(ex=ex, status_code=400, message=None)
@bp.route('/ships', methods=['put'])
@@ -74,18 +73,13 @@ def PutShip():
# validate the request data & user permissions
InputValidationShip.evaluate_put_data(user_data, loadedModel, content)
+ return impl.ships.PutShip(loadedModel)
except ValidationError as ex:
- logging.error(ex)
- print(ex)
return create_validation_error_response(ex=ex, status_code=400)
except Exception as ex:
- logging.error(ex)
- print(ex)
- return json.dumps(repr(ex)), 400
-
- return impl.ships.PutShip(loadedModel)
+ return create_dynamic_exception_response(ex=ex, status_code=400)
@bp.route('/ships', methods=['delete'])
@@ -97,25 +91,21 @@ def DeleteShip():
# read the user data from the JWT token (set when login is performed)
user_data = check_jwt()
- ship_id = request.args.get("id")
if 'id' in request.args:
options = {}
options["id"] = request.args.get("id")
else:
- return json.dumps("no id provided"), 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:
- logging.error(ex)
- print(ex)
return create_validation_error_response(ex=ex, status_code=400)
except Exception as ex:
- logging.error(ex)
- print(ex)
- return json.dumps(repr(ex)), 400
+ return create_dynamic_exception_response(ex=ex, status_code=400)
- return impl.ships.DeleteShip(options)
diff --git a/src/server/BreCal/api/times.py b/src/server/BreCal/api/times.py
index 65b18ed..ff8e17e 100644
--- a/src/server/BreCal/api/times.py
+++ b/src/server/BreCal/api/times.py
@@ -7,7 +7,7 @@ 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
+from BreCal.validators.validation_error import create_validation_error_response, create_werkzeug_error_response, create_dynamic_exception_response
bp = Blueprint('times', __name__)
@@ -16,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'])
@@ -41,18 +44,14 @@ def PostTimes():
# validate the request
InputValidationTimes.evaluate_post_data(user_data, loadedModel, content)
-
+ return impl.times.PostTimes(loadedModel)
+
except ValidationError as ex:
- logging.error(ex)
- print(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=ex, status_code=400, message="bad format")
- return impl.times.PostTimes(loadedModel)
@bp.route('/times', methods=['put'])
@@ -70,18 +69,14 @@ def PutTimes():
# validate the request
InputValidationTimes.evaluate_put_data(user_data, loadedModel, content)
+ return impl.times.PutTimes(loadedModel)
except ValidationError as ex:
- logging.error(ex)
- print(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=ex, status_code=400, message="bad format")
- return impl.times.PutTimes(loadedModel)
@bp.route('/times', methods=['delete'])
@@ -101,15 +96,10 @@ def DeleteTimes():
return impl.times.DeleteTimes(options)
else:
- logging.warning("Times delete missing id argument")
- return json.dumps("missing argument"), 400
+ return create_dynamic_exception_response(ex=None, status_code=400, message="Times delete missing argument: id")
except ValidationError as ex:
- logging.error(ex)
- print(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=ex, status_code=400, message="bad format")
diff --git a/src/server/BreCal/api/user.py b/src/server/BreCal/api/user.py
index 6416704..b77c12c 100644
--- a/src/server/BreCal/api/user.py
+++ b/src/server/BreCal/api/user.py
@@ -6,6 +6,7 @@ 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__)
@@ -18,16 +19,12 @@ def PutUser():
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:
- logging.error(ex)
- print(ex)
- return json.dumps(f"bad format. \nError Messages: {ex.messages}. \nValid Data: {ex.valid_data}"), 400
+ 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)
diff --git a/src/server/BreCal/database/sql_queries.py b/src/server/BreCal/database/sql_queries.py
index 9888ada..e0e9b7e 100644
--- a/src/server/BreCal/database/sql_queries.py
+++ b/src/server/BreCal/database/sql_queries.py
@@ -266,6 +266,11 @@ class SQLQuery():
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, " + \
diff --git a/src/server/BreCal/impl/shipcalls.py b/src/server/BreCal/impl/shipcalls.py
index 83385cf..65bb000 100644
--- a/src/server/BreCal/impl/shipcalls.py
+++ b/src/server/BreCal/impl/shipcalls.py
@@ -10,6 +10,7 @@ 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):
"""
@@ -152,9 +153,7 @@ def PostShipcalls(schemaModel):
return json.dumps({"id" : new_id}), 201, {'Content-Type': 'application/json; charset=utf-8'}
except ValidationError as ex:
- logging.error(ex)
- print(ex)
- return json.dumps({"message":f"bad format. \nError Messages: {ex.messages}. \nValid Data: {ex.valid_data}"}), 400
+ return create_validation_error_response(ex, status_code=400, create_log=True)
except Exception as ex:
logging.error(traceback.format_exc())
@@ -265,9 +264,7 @@ def PutShipcalls(schemaModel):
return json.dumps({"id" : schemaModel["id"]}), 200
except ValidationError as ex:
- logging.error(ex)
- print(ex)
- return json.dumps({"message":f"bad format. \nError Messages: {ex.messages}. \nValid Data: {ex.valid_data}"}), 400
+ return create_validation_error_response(ex, status_code=400, create_log=True)
except Exception as ex:
logging.error(traceback.format_exc())
diff --git a/src/server/BreCal/local_db.py b/src/server/BreCal/local_db.py
index 330bc13..3d63269 100644
--- a/src/server/BreCal/local_db.py
+++ b/src/server/BreCal/local_db.py
@@ -7,11 +7,11 @@ import sys
config_path = None
-def initPool(instancePath, connection_filename="connection_data_devel.json"):
+def initPool(instancePath, connection_filename="connection_data_test.json"):
try:
global config_path
if(config_path == None):
- config_path = os.path.join(instancePath,f'../../../secure/{connection_filename}') #connection_data_devel.json');
+ config_path = os.path.join(instancePath,f'../../../secure/{connection_filename}') #connection_data_test.json');
print (config_path)
diff --git a/src/server/BreCal/schemas/model.py b/src/server/BreCal/schemas/model.py
index a21fb20..c3eed38 100644
--- a/src/server/BreCal/schemas/model.py
+++ b/src/server/BreCal/schemas/model.py
@@ -177,7 +177,7 @@ class Participant(Schema):
valid_type = 0 <= value < max_int
if not valid_type:
- raise ValidationError(f"the provided integer is not supported for default behaviour of the ParticipantType IntFlag. Your choice: {value}. Supported values are: 0 <= value {max_int}")
+ 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")
@@ -188,7 +188,7 @@ class Participant(Schema):
valid_type = 0 <= value < max_int
if not valid_type:
- raise ValidationError(f"the provided integer is not supported for default behaviour of the ParticipantFlag IntFlag. Your choice: {value}. Supported values are: 0 <= value {max_int}")
+ 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):
@@ -253,7 +253,7 @@ class ShipcallSchema(Schema):
valid_shipcall_type = int(value) in [item.value for item in ShipcallType]
if not valid_shipcall_type:
- raise ValidationError(f"the provided type is not a valid shipcall type.")
+ raise ValidationError({"type":f"the provided type is not a valid shipcall type."})
@dataclass
@@ -393,7 +393,7 @@ class TimesSchema(Schema):
value = ParticipantType(value)
if ParticipantType.BSMD in value:
- raise ValidationError(f"the participant_type must not be .BSMD")
+ raise ValidationError({"participant_type":f"the participant_type must not be .BSMD"})
@validates("eta_berth")
def validate_eta_berth(self, value):
@@ -436,6 +436,21 @@ class TimesSchema(Schema):
# 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
@@ -456,12 +471,12 @@ class UserSchema(Schema):
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(f"one of the phone number values is not valid.")
+ 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(f"invalid email address")
+ raise ValidationError({"user_email":f"invalid email address"})
@dataclass
@@ -550,12 +565,12 @@ class ShipSchema(Schema):
def validate_name(self, value):
character_length = len(str(value))
if character_length<1:
- raise ValidationError(f"'name' argument should have at least one character")
+ raise ValidationError({"name":f"'name' argument should have at least one character"})
elif character_length>=64:
- raise ValidationError(f"'name' argument should have at max. 63 characters")
+ raise ValidationError({"name":f"'name' argument should have at max. 63 characters"})
if check_if_string_has_special_characters(value):
- raise ValidationError(f"'name' argument should not have special characters.")
+ raise ValidationError({"name":f"'name' argument should not have special characters."})
return
@validates("imo")
@@ -563,7 +578,7 @@ class ShipSchema(Schema):
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(f"'imo' should be a 7-digit number")
+ raise ValidationError({"imo":f"'imo' should be a 7-digit number"})
return
@validates("callsign")
@@ -571,10 +586,10 @@ class ShipSchema(Schema):
if value is not None:
callsign_length = len(str(value))
if callsign_length>8:
- raise ValidationError(f"'callsign' argument should not have more than 8 characters")
+ raise ValidationError({"callsign":f"'callsign' argument should not have more than 8 characters"})
if check_if_string_has_special_characters(value):
- raise ValidationError(f"'callsign' argument should not have special characters.")
+ raise ValidationError({"callsign":f"'callsign' argument should not have special characters."})
return
diff --git a/src/server/BreCal/stubs/times.py b/src/server/BreCal/stubs/times.py
new file mode 100644
index 0000000..a26b659
--- /dev/null
+++ b/src/server/BreCal/stubs/times.py
@@ -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
+
diff --git a/src/server/BreCal/validators/input_validation.py b/src/server/BreCal/validators/input_validation.py
index 1fb9649..cc97ccb 100644
--- a/src/server/BreCal/validators/input_validation.py
+++ b/src/server/BreCal/validators/input_validation.py
@@ -35,24 +35,24 @@ def validate_posted_shipcall_data(user_data:dict, loadedModel:dict, content:dict
# 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(f"current user does not belong to BSMD. Cannot post shipcalls. Found user data: {user_data}")
+ 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(f"provided an invalid ship id, which is not found in the database: {loadedModel.get('ship_id', None)}")
+ 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(f"provided an invalid arrival berth id, which is not found in the database: {loadedModel.get('arrival_berth_id', None)}")
+ 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(f"provided an invalid departure berth id, which is not found in the database: {loadedModel.get('departure_berth_id', None)}")
+ 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(f"one of the provided participant ids is invalid. Could not find one of these in the database: {loadedModel.get('participants', None)}")
+ raise ValidationError({"participants":f"one of the provided participant ids is invalid. Could not find one of these in the database: {loadedModel.get('participants', None)}"})
##### Section 3: check content #####
@@ -62,11 +62,11 @@ def validate_posted_shipcall_data(user_data:dict, loadedModel:dict, content:dict
for forbidden_key in ["canceled", "evaluation", "evaluation_message"]:
value = content.get(forbidden_key, None)
if value is not None:
- raise ValidationError(f"'{forbidden_key}' may not be set on POST. Found: {value}")
+ 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(f"there are invalid characters in the 'voyage'-string. Please use only digits and ASCII letters. Allowed: {ascii_letters+digits}. Found: {content.get('voyage')}")
+ raise ValidationError({"voyage":f"there are invalid characters in the 'voyage'-string. Please use only digits and ASCII letters. Allowed: {ascii_letters+digits}. Found: {content.get('voyage')}"})
##### Section 4: check loadedModel & content #####
@@ -77,43 +77,43 @@ def validate_posted_shipcall_data(user_data:dict, loadedModel:dict, content:dict
time_now = datetime.datetime.now()
type_ = loadedModel.get("type", int(ShipcallType.undefined))
if int(type_)==int(ShipcallType.undefined):
- raise ValidationError(f"providing 'type' is mandatory. Missing key!")
+ 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(f"providing 'eta' is mandatory. Missing key!")
+ raise ValidationError({"eta":f"providing 'eta' is mandatory. Missing key!"})
if content.get("arrival_berth_id", None) is None:
- raise ValidationError(f"providing 'arrival_berth_id' is mandatory. Missing key!")
+ raise ValidationError({"arrival_berth_id":f"providing 'arrival_berth_id' is mandatory. Missing key!"})
if not eta >= time_now:
- raise ValidationError(f"'eta' must be in the future. Incorrect datetime provided.")
+ raise ValidationError({"eta":f"'eta' must be in the future. Incorrect datetime provided."})
elif int(type_)==int(ShipcallType.departure):
etd = loadedModel.get("etd")
if (content.get("etd", None) is None):
- raise ValidationError(f"providing 'etd' is mandatory. Missing key!")
+ raise ValidationError({"etd":f"providing 'etd' is mandatory. Missing key!"})
if content.get("departure_berth_id", None) is None:
- raise ValidationError(f"providing 'departure_berth_id' is mandatory. Missing key!")
+ raise ValidationError({"departure_berth_id":f"providing 'departure_berth_id' is mandatory. Missing key!"})
if not etd >= time_now:
- raise ValidationError(f"'etd' must be in the future. Incorrect datetime provided.")
+ raise ValidationError({"etd":f"'etd' must be in the future. 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(f"providing 'eta' and 'etd' is mandatory. Missing one of those keys!")
+ 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(f"providing 'arrival_berth_id' & 'departure_berth_id' is mandatory. Missing key!")
+ 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(f"'eta' and 'etd' must be in the future. Incorrect datetime provided.")
+ raise ValidationError({"eta_or_etd":f"'eta' and 'etd' must be in the future. 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(f"'tidal_window_to' must be in the future. Incorrect datetime provided.")
+ raise ValidationError({"tidal_window_to":f"'tidal_window_to' must be in the future. Incorrect datetime provided."})
if tidal_window_from is not None:
if not tidal_window_from >= time_now:
- raise ValidationError(f"'tidal_window_from' must be in the future. Incorrect datetime provided.")
+ raise ValidationError({"tidal_window_from":f"'tidal_window_from' must be in the future. Incorrect datetime provided."})
# #TODO: assert tidal_window_from > tidal_window_to
diff --git a/src/server/BreCal/validators/input_validation_ship.py b/src/server/BreCal/validators/input_validation_ship.py
index 47fa918..80c6261 100644
--- a/src/server/BreCal/validators/input_validation_ship.py
+++ b/src/server/BreCal/validators/input_validation_ship.py
@@ -6,6 +6,8 @@ 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
@@ -47,18 +49,22 @@ class InputValidationShip():
# 1.) Only users of type BSMD are allowed to PUT
InputValidationShip.check_user_is_bsmd_type(user_data)
- # 2.) The IMO number field may not be changed
+ # 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)
- # 3.) Check for reasonable Values (see BreCal.schemas.model.ShipSchema)
+ # 4.) Check for reasonable Values (see BreCal.schemas.model.ShipSchema)
InputValidationShip.optionally_evaluate_bollard_pull_value(content)
-
- # 4.) ID field is mandatory
- InputValidationShip.content_contains_ship_id(content)
return
@staticmethod
- def evaluate_delete_data(user_data:dict, ship_id:int):
+ 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)
@@ -73,16 +79,16 @@ class InputValidationShip():
if bollard_pull is not None:
if not is_tug:
- raise ValidationError(f"'bollard_pull' is only allowed, when a ship is a tug ('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(f"when a ship is a tug, the bollard pull must be 0 < value < 500. ")
+ 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(f"current user does not belong to BSMD. Cannot post, put or delete ships. Found user data: {user_data}")
+ 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):
@@ -96,47 +102,48 @@ class InputValidationShip():
# 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(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.")
+ 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)
- # #TODO: Aktuelle IMO abfragen und nach Änderung suchen, bevor eine Fehlermeldung erstellt wird
-
- if put_data_ship_imo is not None:
- raise ValidationError(f"The IMO number field may not be changed since it serves the purpose of a primary (matching) key.")
+ 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(f"The id field is required.")
+ raise ValidationError({"id":f"The id field is required."})
return
@staticmethod
- def check_if_entry_is_already_deleted(ship_id:int):
+ 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.
"""
- if ship_id is None:
- raise ValidationError(f"The ship_id must be provided.")
-
response, status_code, header = GetShips(token=None)
ships = json.loads(response)
- existing_database_entries = [ship for ship in ships if ship.get("id")==ship_id]
+ existing_database_entries = [ship for ship in ships if ship.get("id")==int(ship_id)]
if len(existing_database_entries)==0:
- raise ValidationError(f"Could not find a ship with the specified ID. Selected: {ship_id}")
+ 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(f"The selected ship entry is already deleted.")
+ raise ValidationError({"deleted":f"The selected ship entry is already deleted."})
return
diff --git a/src/server/BreCal/validators/input_validation_shipcall.py b/src/server/BreCal/validators/input_validation_shipcall.py
index 729a896..5e498ff 100644
--- a/src/server/BreCal/validators/input_validation_shipcall.py
+++ b/src/server/BreCal/validators/input_validation_shipcall.py
@@ -90,7 +90,7 @@ class InputValidationShipcall():
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"])
+ 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)
@@ -98,12 +98,15 @@ class InputValidationShipcall():
return
@staticmethod
- def check_shipcall_values(loadedModel:dict, content:dict, forbidden_keys:list=["evaluation", "evaluation_message"]):
+ 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.
@@ -113,15 +116,19 @@ class InputValidationShipcall():
# voyage shall not contain special characters
voyage_str_is_invalid = check_if_string_has_special_characters(text=content.get("voyage",""))
if voyage_str_is_invalid:
- raise ValidationError(f"there are invalid characters in the 'voyage'-string. Please use only digits and ASCII letters. Allowed: {ascii_letters+digits}. Found: {content.get('voyage')}")
+ raise ValidationError({"voyage":f"there are invalid characters in the 'voyage'-string. Please use only digits and ASCII letters. Allowed: {ascii_letters+digits}. Found: {content.get('voyage')}"})
# the 'flags' integer must be valid
flags_value = content.get("flags", 0)
if check_if_int_is_valid_flag(flags_value, enum_object=ParticipantFlag):
- raise ValidationError(f"incorrect value provided for 'flags'. Must be a valid combination of the flags.")
+ raise ValidationError({"flags":f"incorrect value provided for 'flags'. Must be a valid combination of the flags."})
- # time values must use future-dates
- InputValidationShipcall.check_times_are_in_future(loadedModel, content)
+ if is_put_data:
+ # the type of a shipcall may not be changed. It can only be set with the initial POST-request.
+ InputValidationShipcall.check_shipcall_type_is_unchanged(loadedModel)
+ else:
+ # time values must use future-dates
+ InputValidationShipcall.check_times_are_in_future(loadedModel, content)
# some arguments must not be provided
InputValidationShipcall.check_forbidden_arguments(content, forbidden_keys=forbidden_keys)
@@ -209,7 +216,7 @@ class InputValidationShipcall():
is_bsmd_or_agency = (is_bsmd) or (is_agency)
if not is_bsmd_or_agency:
- raise ValidationError(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}")
+ 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
@@ -229,23 +236,33 @@ class InputValidationShipcall():
valid_ship_id = check_if_ship_id_is_valid(ship_id=ship_id)
if not valid_ship_id:
- raise ValidationError(f"provided an invalid ship id, which is not found in the database: {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(f"provided an invalid arrival berth id, which is not found in the database: {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(f"provided an invalid departure berth id, which is not found in the database: {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(f"one of the provided participant ids is invalid. Could not find one of these in the database: {participants}")
+ 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(f"every participant id and type should be listed only once. Found multiple entries for one of the participants.")
+ raise ValidationError({"participants":f"every participant id and type should be listed only once. Found multiple entries for one of the participants."})
+
+ @staticmethod
+ def check_shipcall_type_is_unchanged(loadedModel:dict):
+ # the type of a shipcall may only be set on POST requests. Afterwards, shipcall types may not be changed.
+ query = SQLQuery.get_shipcall_by_id()
+ shipcall = execute_sql_query_standalone(query=query, model=Shipcall, param={"id":loadedModel.get("id")}, command_type="single")
+
+ if int(loadedModel["type"]) != int(shipcall.type):
+ raise ValidationError({"type":f"The shipcall type may only be set in the initial POST-request. Afterwards, changing the shipcall type is not allowed."}) # @pytest.raises
+ return
@staticmethod
def check_forbidden_arguments(content:dict, forbidden_keys=["evaluation", "evaluation_message"]):
@@ -258,7 +275,7 @@ class InputValidationShipcall():
for forbidden_key in forbidden_keys:
value = content.get(forbidden_key, None)
if value is not None:
- raise ValidationError(f"'{forbidden_key}' may not be set on POST. Found: {value}")
+ raise ValidationError({"forbidden_key":f"'{forbidden_key}' may not be set on POST. Found: {value}"})
return
@staticmethod
@@ -274,36 +291,36 @@ class InputValidationShipcall():
departure_berth_id = content.get("departure_berth_id", None)
if ship_id is None:
- raise ValidationError(f"providing 'ship_id' is mandatory. Missing key!")
+ raise ValidationError({"ship_id":f"providing 'ship_id' is mandatory. Missing key!"})
if int(type_)==int(ShipcallType.undefined):
- raise ValidationError(f"providing 'type' is mandatory. Missing key!")
+ 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(f"providing 'eta' is mandatory. Missing key!")
+ raise ValidationError({"eta":f"providing 'eta' is mandatory. Missing key!"})
if arrival_berth_id is None:
- raise ValidationError(f"providing 'arrival_berth_id' is mandatory. Missing key!")
+ 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(f"providing 'etd' is mandatory. Missing key!")
+ raise ValidationError({"etd":f"providing 'etd' is mandatory. Missing key!"})
if departure_berth_id is None:
- raise ValidationError(f"providing 'departure_berth_id' is mandatory. Missing key!")
+ 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(f"providing 'eta' and 'etd' is mandatory. Missing one of those keys!")
+ 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(f"providing 'arrival_berth_id' & 'departure_berth_id' is mandatory. Missing key!")
+ raise ValidationError({"arrival_berth_id_or_departure_berth_id":f"providing 'arrival_berth_id' & 'departure_berth_id' is mandatory. Missing key!"})
else:
- raise ValidationError(f"incorrect 'type' provided!")
+ raise ValidationError({"type":f"incorrect 'type' provided!"})
return
@staticmethod
@@ -350,32 +367,32 @@ class InputValidationShipcall():
return
if type_ is None:
- raise ValidationError(f"when providing 'eta' or 'etd', one must provide the type of the shipcall, so the datetimes can be verified.")
+ 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(f"providing 'type' is mandatory. Missing key!")
+ 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_now:
- raise ValidationError(f"'eta' must be in the future. Incorrect datetime provided. Current Time: {time_now}. ETA: {eta}.")
+ raise ValidationError({"eta":f"'eta' must be in the future. Incorrect datetime provided. Current Time: {time_now}. ETA: {eta}."})
if etd is not None:
- raise ValidationError(f"'etd' should not be set when the shipcall type is 'arrival'.")
+ raise ValidationError({"etd":f"'etd' should not be set when the shipcall type is 'arrival'."})
elif int(type_)==int(ShipcallType.departure):
if etd is None: # null values -> no violation
return
if not etd > time_now:
- raise ValidationError(f"'etd' must be in the future. Incorrect datetime provided. Current Time: {time_now}. ETD: {etd}.")
+ raise ValidationError({"etd":f"'etd' must be in the future. Incorrect datetime provided. Current Time: {time_now}. ETD: {etd}."})
if eta is not None:
- raise ValidationError(f"'eta' should not be set when the shipcall type is 'departure'.")
+ raise ValidationError({"eta":f"'eta' should not be set when the shipcall type is 'departure'."})
elif int(type_)==int(ShipcallType.shifting):
if (eta is None) and (etd is None): # null values -> no violation
@@ -384,30 +401,30 @@ class InputValidationShipcall():
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(f"For shifting shipcalls one should always provide, both, eta and etd.")
+ raise ValidationError({"eta_or_etd":f"For shifting shipcalls one should always provide, both, eta and etd."})
if (not eta > time_now) or (not etd > time_now):
- raise ValidationError(f"'eta' and 'etd' must be in the future. Incorrect datetime provided. Current Time: {time_now}. ETA: {eta}. ETD: {etd}")
+ raise ValidationError({"eta_or_etd":f"'eta' and 'etd' must be in the future. Incorrect datetime provided. Current Time: {time_now}. ETA: {eta}. ETD: {etd}"})
if (not etd < eta):
- raise ValidationError(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}")
+ 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(f"'eta' and 'etd' must both be provided when the shipcall type is 'shifting'.")
+ raise ValidationError({"eta_or_etd":f"'eta' and 'etd' must both be provided when the shipcall type is 'shifting'."})
return
@staticmethod
def check_tidal_window_in_future(type_, time_now, tidal_window_from, tidal_window_to):
if tidal_window_to is not None:
if not tidal_window_to >= time_now:
- raise ValidationError(f"'tidal_window_to' must be in the future. Incorrect datetime provided.")
+ raise ValidationError({"tidal_window_to":f"'tidal_window_to' must be in the future. Incorrect datetime provided."})
if tidal_window_from is not None:
if not tidal_window_from >= time_now:
- raise ValidationError(f"'tidal_window_from' must be in the future. Incorrect datetime provided.")
+ raise ValidationError({"tidal_window_from":f"'tidal_window_from' must be in the future. Incorrect datetime provided."})
if (tidal_window_to is not None) and (tidal_window_from is not None):
if tidal_window_to < tidal_window_from:
- raise ValidationError(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}.")
+ 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}."})
return
@staticmethod
@@ -419,7 +436,7 @@ class InputValidationShipcall():
is_agency_participant = [ParticipantType.AGENCY in ParticipantType(participant.get("type")) for participant in participants]
if not any(is_agency_participant):
- raise ValidationError(f"One of the assigned participants *must* be of type 'ParticipantType.AGENCY'. Found list of participants: {participants}")
+ raise ValidationError({"participants":f"One of the assigned participants *must* be of type 'ParticipantType.AGENCY'. Found list of participants: {participants}"})
return
@staticmethod
@@ -435,14 +452,14 @@ class InputValidationShipcall():
# if the *existing* shipcall in the database is canceled, it may not be changed
if shipcall.get("canceled", False):
- raise ValidationError(f"The shipcall with id 'shipcall_id' is canceled. A canceled shipcall may not be changed.")
+ 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(f"A PUT request requires an 'id' to refer to.")
+ raise ValidationError({"id":f"A PUT request requires an 'id' to refer to."})
@staticmethod
def check_shipcall_id_exists(loadedModel):
@@ -451,7 +468,7 @@ class InputValidationShipcall():
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(f"unknown shipcall_id. There are no shipcalls with the ID {shipcall_id}")
+ raise ValidationError({"id":f"unknown shipcall_id. There are no shipcalls with the ID {shipcall_id}"})
return
@staticmethod
@@ -484,7 +501,7 @@ class InputValidationShipcall():
an_agency_is_assigned = len(assigned_agency)==1
if len(assigned_agency)>1:
- raise ValidationError(f"Internal error? Found more than one assigned agency for the shipcall with ID {shipcall_id}. Found: {assigned_agency}")
+ raise ValidationError({"internal_error":f"Internal error? Found more than one assigned agency for the shipcall with ID {shipcall_id}. Found: {assigned_agency}"})
if an_agency_is_assigned:
# Agency assigned? User must belong to the assigned agency or be a BSMD user, in case the flag is set
diff --git a/src/server/BreCal/validators/input_validation_times.py b/src/server/BreCal/validators/input_validation_times.py
index 0385220..6dd1234 100644
--- a/src/server/BreCal/validators/input_validation_times.py
+++ b/src/server/BreCal/validators/input_validation_times.py
@@ -29,32 +29,32 @@ def build_post_data_type_dependent_required_fields_dict()->dict[ShipcallType,dic
ShipcallType.arrival:{
ParticipantType.undefined:[], # should not be set in POST requests
ParticipantType.BSMD:[], # should not be set in POST requests
- ParticipantType.TERMINAL:["operations_start"],
- ParticipantType.AGENCY:["eta_berth"],
- ParticipantType.MOORING:["eta_berth"],
- ParticipantType.PILOT:["eta_berth"],
- ParticipantType.PORT_ADMINISTRATION:["eta_berth"],
- ParticipantType.TUG:["eta_berth"],
+ 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:["operations_end"],
- ParticipantType.AGENCY:["etd_berth"],
- ParticipantType.MOORING:["etd_berth"],
- ParticipantType.PILOT:["etd_berth"],
- ParticipantType.PORT_ADMINISTRATION:["etd_berth"],
- ParticipantType.TUG:["etd_berth"],
+ 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:["operations_start", "operations_end"],
- ParticipantType.AGENCY:["eta_berth", "etd_berth"],
- ParticipantType.MOORING:["eta_berth", "etd_berth"],
- ParticipantType.PILOT:["eta_berth", "etd_berth"],
- ParticipantType.PORT_ADMINISTRATION:["eta_berth", "etd_berth"],
- ParticipantType.TUG:["eta_berth", "etd_berth"],
+ ParticipantType.TERMINAL:[],
+ ParticipantType.AGENCY:[],
+ ParticipantType.MOORING:[],
+ ParticipantType.PILOT:[],
+ ParticipantType.PORT_ADMINISTRATION:[],
+ ParticipantType.TUG:[],
},
}
return post_data_type_dependent_required_fields_dict
@@ -85,13 +85,10 @@ class InputValidationTimes():
# 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.) only users who are *not* of type BSMD may post times datasets.
- InputValidationTimes.check_user_is_not_bsmd_type(user_data)
-
- # 4.) Reference checking
+ # 3.) Reference checking
InputValidationTimes.check_dataset_references(content)
- # 5.) Value checking
+ # 4.) Value checking
InputValidationTimes.check_dataset_values(user_data, loadedModel, content)
return
@@ -111,8 +108,10 @@ class InputValidationTimes():
return
@staticmethod
- def evaluate_delete_data(user_data:dict, times_id:int):
- # #TODO_determine: is times_id always an int or does the request.args call provide a string?
+ 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
@@ -137,7 +136,7 @@ class InputValidationTimes():
pdata = execute_sql_query_standalone(query=query, param={"id":times_id}, pooledConnection=None)
if len(pdata)==0:
- raise ValidationError(f"The selected time entry is already deleted. ID: {times_id}")
+ raise ValidationError({"deleted":f"The selected time entry is already deleted. ID: {times_id}"})
return
@staticmethod
@@ -145,7 +144,7 @@ class InputValidationTimes():
"""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(f"current user belongs to BSMD. Cannot post 'times' datasets. Found user data: {user_data}")
+ raise ValidationError({"participant_type":f"current user belongs to BSMD. Cannot post 'times' datasets. Found user data: {user_data}"})
return
@staticmethod
@@ -166,7 +165,17 @@ class InputValidationTimes():
loadedModel["participant_type"] = ParticipantType(loadedModel["participant_type"])
if ParticipantType.BSMD in loadedModel["participant_type"]:
- raise ValidationError(f"current user belongs to BSMD. Cannot post times datasets. Found user data: {user_data}")
+ raise ValidationError({"participant_type":f"current user belongs to BSMD. Cannot post times datasets. Found user data: {user_data}"})
+
+ if (loadedModel["etd_interval_end"] is not None) and (loadedModel["etd_berth"] is not None):
+ time_end_after_time_start = loadedModel["etd_interval_end"] >= loadedModel["etd_berth"]
+ if not time_end_after_time_start:
+ raise ValidationError({"etd":f"The provided time interval for the estimated departure time is invalid. The interval end takes place before the interval start. Found interval data: {loadedModel['etd_berth']} to {loadedModel['etd_interval_end']}"})
+
+ if (loadedModel["eta_interval_end"] is not None) and (loadedModel["eta_berth"] is not None):
+ 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
@@ -178,19 +187,19 @@ class InputValidationTimes():
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, participant_id, shipcall_id = content.get("berth_id"), content.get("participant_id"), content.get("shipcall_id")
+ 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(f"The referenced berth_id '{berth_id}' does not exist in the database.")
+ 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(f"The referenced shipcall_id '{shipcall_id}' does not exist in the database.")
+ 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(f"The referenced participant_id '{participant_id}' does not exist in the database.")
+ raise ValidationError({"participant_id":f"The referenced participant_id '{participant_id}' does not exist in the database."})
return
@@ -212,7 +221,7 @@ class InputValidationTimes():
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(f"At least one of the required fields is missing. Missing: 'participant_type' or 'shipcall_type'")
+ 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
@@ -229,14 +238,14 @@ class InputValidationTimes():
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(f"At least one of the required fields is missing. Missing: {verbosity_tuple}")
+ 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(f"A PUT-request requires an 'id' reference, which was not found.")
+ raise ValidationError({"id":f"A PUT-request requires an 'id' reference, which was not found."})
return
@staticmethod
@@ -326,7 +335,7 @@ class InputValidationTimes():
]
if not len(matching_spm)>0:
- raise ValidationError(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
+ 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
@@ -345,7 +354,7 @@ class InputValidationTimes():
# 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(f"A dataset for the participant type is already present. Participant Type: {participant_type}. Times Datasets: {times}")
+ raise ValidationError({"participant_type":f"A dataset for the participant type is already present. Participant Type: {participant_type}. Times Datasets: {times}"})
return
@staticmethod
@@ -388,7 +397,7 @@ class InputValidationTimes():
# 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(f"Unknown times_id. Could not find a matching entry for ID: {times_id}")
+ 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")
@@ -404,14 +413,14 @@ class InputValidationTimes():
if special_case__bsmd_may_edit_agency_dataset:
return
else:
- raise ValidationError(f"The dataset may only be changed by a user belonging to the same participant group as the times dataset is referring to. User participant_id: {user_participant_id}; Dataset participant_id: {participant_id_of_times_dataset}")
+ raise ValidationError({"user_participant_type":f"The dataset may only be changed by a user belonging to the same participant group as the times dataset is referring to. User participant_id: {user_participant_id}; Dataset participant_id: {participant_id_of_times_dataset}"})
return
@staticmethod
def get_participant_id_from_shipcall_participant_map(shipcall_id:int, participant_type:int, spm_shipcall_data=None)->int:
"""use shipcall_id and participant_type to identify the matching participant_id"""
if shipcall_id is None:
- raise ValidationError(f"Could not find a referenced shipcall_id within the request.")
+ 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(
@@ -421,7 +430,7 @@ class InputValidationTimes():
# raise an error when there are no matches
if len(spm_shipcall_data)==0:
- raise ValidationError(f"Could not find a matching time dataset for the provided participant_type: {participant_type} at shipcall with id {shipcall_id}.")
+ 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
@@ -471,3 +480,44 @@ class InputValidationTimes():
has_bsmd_flag = ParticipantFlag.BSMD in [ParticipantFlag(participant.get("flags"))]
return has_bsmd_flag
+
+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
diff --git a/src/server/BreCal/validators/input_validation_utils.py b/src/server/BreCal/validators/input_validation_utils.py
index e68f18e..8c2c2ed 100644
--- a/src/server/BreCal/validators/input_validation_utils.py
+++ b/src/server/BreCal/validators/input_validation_utils.py
@@ -158,7 +158,7 @@ def check_if_participant_id_is_valid_standalone(participant_id:int, participant_
if participant_type is not None:
if participant_id not in list(participants.keys()):
- raise ValidationError(f"the provided participant_id {participant_id} does not exist in the database.")
+ 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)))
diff --git a/src/server/BreCal/validators/time_logic.py b/src/server/BreCal/validators/time_logic.py
index 8ebad56..4f8535b 100644
--- a/src/server/BreCal/validators/time_logic.py
+++ b/src/server/BreCal/validators/time_logic.py
@@ -19,9 +19,9 @@ def validate_time_is_in_future(value:datetime.datetime):
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:
"""
- combines two boolean operations. Returns True when both conditions are met.
- a) value is in the future
- b) value is not too distant (e.g., at max. 1 year in the future)
+ 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'.
@@ -31,17 +31,17 @@ def validate_time_is_in_not_too_distant_future(raise_validation_error:bool, valu
if value is None:
return False
- is_in_future = validate_time_is_in_future(value)
+ # 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(f"The provided value must be in the future. Current Time: {datetime.datetime.now()}, Value: {value}")
+ #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(f"The provided value is in the too distant future and exceeds a threshold for 'reasonable' entries. Found: {value}")
+ raise ValidationError({"any_date":f"The provided value is in the too distant future and exceeds a threshold for 'reasonable' entries. Found: {value}"})
- return is_in_future & (not is_too_distant)
+ return (not is_too_distant) # & is_in_future
class TimeLogic():
def __init__(self):
diff --git a/src/server/BreCal/validators/validation_error.py b/src/server/BreCal/validators/validation_error.py
index cdac103..823a924 100644
--- a/src/server/BreCal/validators/validation_error.py
+++ b/src/server/BreCal/validators/validation_error.py
@@ -1,34 +1,87 @@
import logging
import typing
import json
+import sys
from marshmallow import ValidationError
-import werkzeug
from werkzeug.exceptions import Forbidden
-
-def create_validation_error_response(ex:ValidationError, status_code:int=400)->typing.Tuple[str,int]:
- # generate an overview the errors
- #example:
- # {'lock_time': ['The provided value must be in the future. Current Time: 2024-09-02 08:23:32.600791, Value: 2024-09-01 08:20:41.853000']}
- errors = ex.messages
-
- # example:
- # "Valid Data": {
- # "id": 2894,
- # "eta_berth": "datetime.datetime(2024, 9, 2, 11, 11, 43)",
- # "eta_berth_fixed": false
- # }
- valid_data = ex.valid_data
-
-
+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 = {
- "message":"ValidationError",
- "errors":errors,
- "valid_data":valid_data
+ "error_field":error_field,
+ "error_description":error_description
}
- return (json.dumps(json_response), status_code)
+ return json_response
-def create_werkzeug_error_response(ex:Forbidden, status_code:int=403)->typing.Tuple[str,int]:
- return json.dumps({"message":ex.description}), status_code
+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 = "Error Description(s): " + " & ".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)
diff --git a/src/server/BreCal/validators/validation_rule_functions.py b/src/server/BreCal/validators/validation_rule_functions.py
index ae3e3bc..b035345 100644
--- a/src/server/BreCal/validators/validation_rule_functions.py
+++ b/src/server/BreCal/validators/validation_rule_functions.py
@@ -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}",
@@ -723,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,
@@ -746,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()
diff --git a/src/server/flaskapp.wsgi b/src/server/flaskapp.wsgi
index 4a5f543..9dec941 100644
--- a/src/server/flaskapp.wsgi
+++ b/src/server/flaskapp.wsgi
@@ -2,7 +2,7 @@ import os
import sys
import logging
-sys.path.insert(0, '/var/www/brecal_devel/src/server')
+sys.path.insert(0, '/var/www/brecal_test/src/server')
sys.path.insert(0, '/var/www/venv/lib/python3.10/site-packages/')
import schedule
diff --git a/src/server/setup.py b/src/server/setup.py
index 44750aa..5b0baf2 100644
--- a/src/server/setup.py
+++ b/src/server/setup.py
@@ -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,
diff --git a/src/server/tests/database/test_sql_queries.py b/src/server/tests/database/test_sql_queries.py
index fc4267a..9db3d75 100644
--- a/src/server/tests/database/test_sql_queries.py
+++ b/src/server/tests/database/test_sql_queries.py
@@ -87,6 +87,13 @@ def test_sql_get_ships():
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"]})
diff --git a/src/server/tests/validators/test_input_validation_shipcall.py b/src/server/tests/validators/test_input_validation_shipcall.py
index df41f1f..87b3b7f 100644
--- a/src/server/tests/validators/test_input_validation_shipcall.py
+++ b/src/server/tests/validators/test_input_validation_shipcall.py
@@ -901,3 +901,35 @@ def test_post_data_with_valid_data(get_stub_token):
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
+
diff --git a/src/server/tests/validators/test_input_validation_times.py b/src/server/tests/validators/test_input_validation_times.py
index 550387a..7a9b7d5 100644
--- a/src/server/tests/validators/test_input_validation_times.py
+++ b/src/server/tests/validators/test_input_validation_times.py
@@ -409,3 +409,60 @@ def test_input_validation_times_delete_request_fails_when_user_belongs_to_wrong_
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
+
diff --git a/src/server/tests/validators/test_validation_error.py b/src/server/tests/validators/test_validation_error.py
new file mode 100644
index 0000000..a103919
--- /dev/null
+++ b/src/server/tests/validators/test_validation_error.py
@@ -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