284 lines
14 KiB
Python
284 lines
14 KiB
Python
|
|
####################################### InputValidation #######################################
|
|
|
|
import json
|
|
import datetime
|
|
from abc import ABC, abstractmethod
|
|
from marshmallow import ValidationError
|
|
from string import ascii_letters, digits
|
|
|
|
from BreCal.schemas.model import Ship, Shipcall, Berth, User, Participant, ShipcallType
|
|
from BreCal.impl.participant import GetParticipant
|
|
from BreCal.impl.ships import GetShips
|
|
from BreCal.impl.berths import GetBerths
|
|
|
|
from BreCal.database.enums import ParticipantType
|
|
|
|
|
|
from BreCal.validators.input_validation_utils import check_if_user_is_bsmd_type, get_participant_id_dictionary, check_if_ship_id_is_valid, check_if_berth_id_is_valid, check_if_participant_ids_are_valid, get_berth_id_dictionary, get_ship_id_dictionary
|
|
from BreCal.validators.validation_base_utils import check_if_string_has_special_characters
|
|
|
|
def validation_error_default_asserts(response):
|
|
"""creates assertions, when the response does not fail as expected. This function is extensively used in the input validation pytests"""
|
|
assert response.status_code == 400
|
|
assert 'message' in list(response.json().keys())
|
|
return
|
|
|
|
|
|
def validate_posted_shipcall_data(user_data:dict, loadedModel:dict, content:dict):
|
|
"""this function applies more complex validation functions to data, which is sent to a post-request of shipcalls"""
|
|
# DEPRECATED: this function has been refactored into InputValidationShipcall (see methods for POST and PUT evaluation)
|
|
# #TODO_refactor: this function is pretty complex. One may instead build an object, which calls the methods separately.
|
|
|
|
##### Section 1: check user_data #####
|
|
# check, whether the user belongs to a participant, which is of type ParticipantType.BSMD
|
|
# as ParticipantType is an IntFlag, a user belonging to multiple groups is properly evaluated.
|
|
is_bsmd = check_if_user_is_bsmd_type(user_data)
|
|
if not is_bsmd:
|
|
raise ValidationError(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)}")
|
|
|
|
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)}")
|
|
|
|
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)}")
|
|
|
|
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)}")
|
|
|
|
|
|
##### Section 3: check content #####
|
|
# loadedModel fills missing values, sometimes using optional values. Hence, check content
|
|
|
|
# the following keys should not be set in a POST-request.
|
|
for forbidden_key in ["canceled", "evaluation", "evaluation_message"]:
|
|
value = content.get(forbidden_key, None)
|
|
if value is not None:
|
|
raise ValidationError(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')}")
|
|
|
|
|
|
##### Section 4: check loadedModel & content #####
|
|
# #TODO_refactor: these methods should be placed in separate locations
|
|
|
|
# existance checks in content
|
|
# datetime checks in loadedModel (datetime.datetime objects). Dates should be in the future.
|
|
time_now = datetime.datetime.now()
|
|
type_ = loadedModel.get("type", int(ShipcallType.undefined))
|
|
if int(type_)==int(ShipcallType.undefined):
|
|
raise ValidationError(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!")
|
|
if content.get("arrival_berth_id", None) is None:
|
|
raise ValidationError(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.")
|
|
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!")
|
|
if content.get("departure_berth_id", None) is None:
|
|
raise ValidationError(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.")
|
|
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!")
|
|
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!")
|
|
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.")
|
|
|
|
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.")
|
|
|
|
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.")
|
|
|
|
# #TODO: assert tidal_window_from > tidal_window_to
|
|
|
|
# #TODO: len of participants > 0, if agency
|
|
# * assigned participant for agency
|
|
return
|
|
|
|
|
|
class InputValidation():
|
|
def __init__(self):
|
|
self.build_supported_models_dictionary()
|
|
return
|
|
|
|
def build_supported_models_dictionary(self):
|
|
self.supported_models = {
|
|
Ship:ShipValidation(),
|
|
Shipcall:ShipcallValidation(),
|
|
Berth:BerthValidation(),
|
|
User:UserValidation(),
|
|
Participant:ParticipantValidation(),
|
|
}
|
|
return
|
|
|
|
def assert_if_not_supported(self, dataclass_object):
|
|
assert type(dataclass_object) in self.supported_models, f"unsupported model. Found: {type(dataclass_object)}"
|
|
return
|
|
|
|
def verify(self, dataclass_object):
|
|
self.assert_if_not_supported(dataclass_object)
|
|
|
|
# determine the type of the dataclass object. The internal dictionary 'supported_models' matches the dataclass object
|
|
# to the respective validation protocol
|
|
validator = self.supported_models.get(type(dataclass_object))
|
|
|
|
# check the object based on the rules within the matched validator
|
|
input_validation_state = validator.check(dataclass_object)
|
|
return input_validation_state
|
|
|
|
|
|
class DataclassValidation(ABC):
|
|
"""parent class of dataclas validators, which determines the outline of every object"""
|
|
def __init__(self):
|
|
return
|
|
|
|
def check(self, dataclass_object) -> (list, bool):
|
|
"""
|
|
the 'check' method provides a default style, how each dataclass object is validated. It returns a list of violations
|
|
and a boolean, which determines, whether the check is passed successfully
|
|
"""
|
|
all_rules = self.apply_all_rules(dataclass_object)
|
|
violations = self.filter_violations(all_rules)
|
|
input_validation_state = self.evaluate(violations)
|
|
return (violations, input_validation_state)
|
|
|
|
@abstractmethod
|
|
def apply_all_rules(self, dataclass_object) -> list:
|
|
"""
|
|
the 'apply_all_rules' method is mandatory for any dataclass validation object. It should execute all validation rules and
|
|
return a list of tuples, where each element is (output_boolean, validation_name)
|
|
"""
|
|
all_rules = [(True, 'blank_validation_rule')]
|
|
return all_rules
|
|
|
|
def filter_violations(self, all_rules):
|
|
"""input: all_rules, a list of tuples, where each element is (output, validation_name), which are (bool, str). """
|
|
# if output is False, a violation is observed
|
|
violations = [result[1] for result in all_rules if not result[0]]
|
|
return violations
|
|
|
|
def evaluate(self, violations) -> bool:
|
|
input_validation_state = len(violations)==0
|
|
return input_validation_state
|
|
|
|
|
|
|
|
class ShipcallValidation(DataclassValidation):
|
|
"""an object that validates a Shipcall dataclass object"""
|
|
def __init__(self):
|
|
super().__init__()
|
|
return
|
|
|
|
def apply_all_rules(self, dataclass_object) -> list:
|
|
"""apply all input validation rules to determine, whether there are violations. returns a list of tuples (output, validation_name)"""
|
|
raise NotImplementedError()
|
|
return all_rules
|
|
|
|
|
|
from BreCal.validators.schema_validation import ship_bollard_pull_is_defined_or_is_not_tug, ship_bollard_pull_is_none_or_in_range, ship_callsign_len_is_seven_at_maximum, ship_eni_len_is_eight, ship_imo_len_is_seven, ship_length_in_range, ship_participant_id_is_defined_or_is_not_tug, ship_participant_id_is_none_or_int, ship_width_in_range
|
|
# skip: ship_max_draft_is_defined_or_is_not_tug, ship_max_draft_is_none_or_in_range,
|
|
class ShipValidation(DataclassValidation):
|
|
"""an object that validates a Ship dataclass object"""
|
|
def __init__(self):
|
|
super().__init__()
|
|
return
|
|
|
|
def apply_all_rules(self, dataclass_object) -> list:
|
|
"""apply all input validation rules to determine, whether there are violations. returns a list of tuples (output, validation_name)"""
|
|
# skip: ship_max_draft_is_defined_or_is_not_tug, ship_max_draft_is_none_or_in_range,
|
|
"""
|
|
#TODO_ship_max_draft
|
|
with pytest.raises(AttributeError, match="'Ship' object has no attribute 'max_draft'"):
|
|
assert ship_max_draft_in_range(ship)[0], f"max draft of a ship must be between 0 and 20 meters"
|
|
assert ship_max_draft_is_none_or_in_range(ship)[0], f"the max_draft should either be undefined or between 0 and 20 meters"
|
|
"""
|
|
|
|
# list comprehension: every function becomes part of the loop and will be executed. Each function is wrapped and provides (output, validation_name)
|
|
all_rules = [
|
|
# tuple: (output, validation_name)
|
|
check_rule(dataclass_object)
|
|
|
|
for check_rule in [
|
|
ship_bollard_pull_is_defined_or_is_not_tug,
|
|
ship_bollard_pull_is_none_or_in_range,
|
|
ship_callsign_len_is_seven_at_maximum,
|
|
ship_eni_len_is_eight,
|
|
ship_imo_len_is_seven,
|
|
ship_length_in_range,
|
|
ship_participant_id_is_defined_or_is_not_tug,
|
|
ship_participant_id_is_none_or_int,
|
|
ship_width_in_range
|
|
]
|
|
]
|
|
return all_rules
|
|
|
|
class BerthValidation(DataclassValidation):
|
|
"""an object that validates a Berth dataclass object"""
|
|
def __init__(self):
|
|
super().__init__()
|
|
return
|
|
|
|
def apply_all_rules(self, dataclass_object) -> list:
|
|
"""apply all input validation rules to determine, whether there are violations. returns a list of tuples (output, validation_name)"""
|
|
raise NotImplementedError()
|
|
return all_rules
|
|
|
|
class UserValidation(DataclassValidation):
|
|
"""an object that validates a User dataclass object"""
|
|
def __init__(self):
|
|
super().__init__()
|
|
return
|
|
|
|
def apply_all_rules(self, dataclass_object) -> list:
|
|
"""apply all input validation rules to determine, whether there are violations. returns a list of tuples (output, validation_name)"""
|
|
raise NotImplementedError()
|
|
return all_rules
|
|
|
|
from BreCal.validators.schema_validation import participant_postal_code_len_is_five
|
|
class ParticipantValidation(DataclassValidation):
|
|
"""an object that validates a Participant dataclass object"""
|
|
def __init__(self):
|
|
super().__init__()
|
|
return
|
|
|
|
def apply_all_rules(self, dataclass_object) -> list:
|
|
"""apply all input validation rules to determine, whether there are violations. returns a list of tuples (output, validation_name)"""
|
|
|
|
# list comprehension: every function becomes part of the loop and will be executed. Each function is wrapped and provides (output, validation_name)
|
|
all_rules = [
|
|
# tuple: (output, validation_name)
|
|
check_rule(dataclass_object)
|
|
|
|
for check_rule in [
|
|
participant_postal_code_len_is_five,
|
|
]
|
|
]
|
|
return all_rules
|
|
|