From 1ff972883fd7b66f20c5573e4ace1aa645d688ae Mon Sep 17 00:00:00 2001 From: Max Metz Date: Tue, 3 Sep 2024 13:07:45 +0200 Subject: [PATCH 01/10] adding input validation for time intervals --- docs/ApiValidationRules.md | 1 + src/server/BreCal/schemas/model.py | 15 +++++++++++++++ .../BreCal/validators/input_validation_times.py | 11 +++++++++++ 3 files changed, 27 insertions(+) diff --git a/docs/ApiValidationRules.md b/docs/ApiValidationRules.md index c080ae0..8a38a91 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 | 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/validators/input_validation_times.py b/src/server/BreCal/validators/input_validation_times.py index 0385220..37b70b8 100644 --- a/src/server/BreCal/validators/input_validation_times.py +++ b/src/server/BreCal/validators/input_validation_times.py @@ -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 From 3d2405e8fbbfcb585f4bbc0e29db771cb3a79735 Mon Sep 17 00:00:00 2001 From: Max Metz Date: Wed, 4 Sep 2024 09:41:58 +0200 Subject: [PATCH 02/10] maintenance of API Input Validation (ship & times) --- src/server/BreCal/database/sql_queries.py | 5 ++ src/server/BreCal/stubs/times.py | 12 ++++ .../validators/input_validation_ship.py | 22 ++++--- .../validators/input_validation_times.py | 12 ++-- src/server/tests/database/test_sql_queries.py | 7 +++ .../validators/test_input_validation_times.py | 57 +++++++++++++++++++ 6 files changed, 101 insertions(+), 14 deletions(-) create mode 100644 src/server/BreCal/stubs/times.py 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/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_times.py b/src/server/BreCal/validators/input_validation_times.py index 37b70b8..dbe4b3b 100644 --- a/src/server/BreCal/validators/input_validation_times.py +++ b/src/server/BreCal/validators/input_validation_times.py @@ -49,12 +49,12 @@ def build_post_data_type_dependent_required_fields_dict()->dict[ShipcallType,dic 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:["operations_start"], + 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/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_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 + From 126faff281dae04fb0cf6990ea6e6e4a2a7c46a2 Mon Sep 17 00:00:00 2001 From: Daniel Schick Date: Wed, 4 Sep 2024 11:13:38 +0200 Subject: [PATCH 03/10] Removed ETA as required field from shipcall PUT/POST --- docs/ApiValidationRules.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/ApiValidationRules.md b/docs/ApiValidationRules.md index c080ae0..0e6b46f 100644 --- a/docs/ApiValidationRules.md +++ b/docs/ApiValidationRules.md @@ -112,7 +112,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) From 2c0a73113b16d34f2c705f87db01469977807226 Mon Sep 17 00:00:00 2001 From: Max Metz Date: Wed, 4 Sep 2024 11:17:32 +0200 Subject: [PATCH 04/10] shipcall PUTs may no longer change the shipcall type --- .../validators/input_validation_shipcall.py | 13 ++++++++ .../test_input_validation_shipcall.py | 32 +++++++++++++++++++ 2 files changed, 45 insertions(+) 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/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 + From a8d0356eb739d505db45268bca86205b206fd865 Mon Sep 17 00:00:00 2001 From: Max Metz Date: Wed, 4 Sep 2024 11:19:47 +0200 Subject: [PATCH 05/10] added the shipcall PUT rule to the docs/ApiValidatioNRules.md document --- docs/ApiValidationRules.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/ApiValidationRules.md b/docs/ApiValidationRules.md index 8a38a91..1e41b4f 100644 --- a/docs/ApiValidationRules.md +++ b/docs/ApiValidationRules.md @@ -189,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 From 2f678267c88567ae43fabdb75aab3c2f556b9280 Mon Sep 17 00:00:00 2001 From: Max Metz Date: Wed, 4 Sep 2024 11:41:03 +0200 Subject: [PATCH 06/10] time estimations are no longer dependency on times POST requests. This refers to eta_berth, etd_berth, operations_start, operations_end. --- .../validators/input_validation_times.py | 77 ++++++++++++++----- 1 file changed, 59 insertions(+), 18 deletions(-) diff --git a/src/server/BreCal/validators/input_validation_times.py b/src/server/BreCal/validators/input_validation_times.py index dbe4b3b..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"], - 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:[], }, } return post_data_type_dependent_required_fields_dict @@ -482,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 From e4d0ea23012f89046fb232e9ff06f51ca22fd899 Mon Sep 17 00:00:00 2001 From: Max Metz Date: Wed, 4 Sep 2024 12:11:05 +0200 Subject: [PATCH 07/10] fixed serialization of marshmallow.ValidationErrors. This was caused by the 'valid_data' containing datetime objects, which were not serializable natively. --- .../BreCal/validators/validation_error.py | 11 +++++++++-- .../tests/validators/test_validation_error.py | 19 +++++++++++++++++++ 2 files changed, 28 insertions(+), 2 deletions(-) create mode 100644 src/server/tests/validators/test_validation_error.py 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/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 From c7371a945ab6fc3d93e6bd6ad9359a9fb98f4c14 Mon Sep 17 00:00:00 2001 From: Max Metz Date: Wed, 4 Sep 2024 12:21:22 +0200 Subject: [PATCH 08/10] adapting traffic light validation 0002-C. This no longer requires ETA verification. Adapted the description when the error occurs and renamed the function. --- .../validators/validation_rule_functions.py | 22 +++++-------------- 1 file changed, 6 insertions(+), 16 deletions(-) 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() From facafd09bafb848f06a10fbbcfc1ae3f7d26aad9 Mon Sep 17 00:00:00 2001 From: Max Metz Date: Wed, 4 Sep 2024 12:29:06 +0200 Subject: [PATCH 09/10] adapting the .md documentation for the traffic state rules. --- misc/Ampelfunktion.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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__! | From 1243ebf9e707dbaca18f55da112375a9b1f841bb Mon Sep 17 00:00:00 2001 From: Daniel Schick Date: Thu, 5 Sep 2024 06:49:25 +0200 Subject: [PATCH 10/10] Adjusted error formatting and bumped test version to 1.5.0.1 --- src/BreCalClient/BreCalClient.csproj | 4 ++-- src/BreCalClient/MainWindow.xaml.cs | 16 +++++++++++++++- .../PublishProfiles/ClickOnceTestProfile.pubxml | 2 +- 3 files changed, 18 insertions(+), 4 deletions(-) diff --git a/src/BreCalClient/BreCalClient.csproj b/src/BreCalClient/BreCalClient.csproj index ffda91f..3d00ee0 100644 --- a/src/BreCalClient/BreCalClient.csproj +++ b/src/BreCalClient/BreCalClient.csproj @@ -8,8 +8,8 @@ True BreCalClient.App ..\..\misc\brecal.snk - 1.5.0.0 - 1.5.0.0 + 1.5.0.1 + 1.5.0.1 Bremen calling client A Windows WPF client for the Bremen calling API. containership.ico diff --git a/src/BreCalClient/MainWindow.xaml.cs b/src/BreCalClient/MainWindow.xaml.cs index 2152c96..5b21164 100644 --- a/src/BreCalClient/MainWindow.xaml.cs +++ b/src/BreCalClient/MainWindow.xaml.cs @@ -1072,7 +1072,21 @@ namespace BreCalClient dynamic? msg = JsonConvert.DeserializeObject(m.Value); if (msg != null) { - message = msg.message; + if (msg.message != null) + { + caption = $"{caption}: {msg.message}"; + } + + if((msg.errors != null) && msg.errors.Count > 0) + { + message = ""; + foreach(string error in msg.errors) + { + message += error; + message += Environment.NewLine; + } + } + } } catch (Exception) { } diff --git a/src/BreCalClient/Properties/PublishProfiles/ClickOnceTestProfile.pubxml b/src/BreCalClient/Properties/PublishProfiles/ClickOnceTestProfile.pubxml index 6b9c078..8d71e33 100644 --- a/src/BreCalClient/Properties/PublishProfiles/ClickOnceTestProfile.pubxml +++ b/src/BreCalClient/Properties/PublishProfiles/ClickOnceTestProfile.pubxml @@ -5,7 +5,7 @@ https://go.microsoft.com/fwlink/?LinkID=208121. 0 - 1.5.0.0 + 1.5.0.1 True Debug True