diff --git a/docs/ApiValidationRules.md b/docs/ApiValidationRules.md
index 0e6b46f..64bf33a 100644
--- a/docs/ApiValidationRules.md
+++ b/docs/ApiValidationRules.md
@@ -147,6 +147,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 +189,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/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/schemas/model.py b/src/server/BreCal/schemas/model.py
index a21fb20..14bceda 100644
--- a/src/server/BreCal/schemas/model.py
+++ b/src/server/BreCal/schemas/model.py
@@ -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
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_ship.py b/src/server/BreCal/validators/input_validation_ship.py
index 47fa918..3a47b2c 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,14 +49,14 @@ 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
@@ -101,11 +103,15 @@ class InputValidationShip():
@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:
+ if put_data_ship_imo != ship.imo:
raise ValidationError(f"The IMO number field may not be changed since it serves the purpose of a primary (matching) key.")
return
diff --git a/src/server/BreCal/validators/input_validation_shipcall.py b/src/server/BreCal/validators/input_validation_shipcall.py
index 729a896..59b8b35 100644
--- a/src/server/BreCal/validators/input_validation_shipcall.py
+++ b/src/server/BreCal/validators/input_validation_shipcall.py
@@ -123,6 +123,9 @@ class InputValidationShipcall():
# time values must use future-dates
InputValidationShipcall.check_times_are_in_future(loadedModel, content)
+ # 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)
+
# some arguments must not be provided
InputValidationShipcall.check_forbidden_arguments(content, forbidden_keys=forbidden_keys)
return
@@ -247,6 +250,16 @@ class InputValidationShipcall():
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.")
+ @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(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"]):
"""
diff --git a/src/server/BreCal/validators/input_validation_times.py b/src/server/BreCal/validators/input_validation_times.py
index 0385220..688c651 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
@@ -167,6 +167,17 @@ class InputValidationTimes():
if ParticipantType.BSMD in loadedModel["participant_type"]:
raise ValidationError(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(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(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
@@ -471,3 +482,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/validation_error.py b/src/server/BreCal/validators/validation_error.py
index cdac103..65ee985 100644
--- a/src/server/BreCal/validators/validation_error.py
+++ b/src/server/BreCal/validators/validation_error.py
@@ -27,8 +27,15 @@ def create_validation_error_response(ex:ValidationError, status_code:int=400)->t
"errors":errors,
"valid_data":valid_data
}
- return (json.dumps(json_response), status_code)
+
+ # 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)
+ return (serialized_response, status_code)
def create_werkzeug_error_response(ex:Forbidden, status_code:int=403)->typing.Tuple[str,int]:
- return json.dumps({"message":ex.description}), status_code
+ # 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({"message":ex.description}, default=str)
+ 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/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