This repository has been archived on 2025-02-17. You can view files and clone it, but cannot push or open issues or pull requests.
BreCal/src/server/BreCal/validators/input_validation_ship.py

164 lines
7.1 KiB
Python

import typing
import json
import datetime
from abc import ABC, abstractmethod
from marshmallow import ValidationError
from string import ascii_letters, digits
from BreCal.schemas.model import Ship, Shipcall, Berth, User, Participant, ShipcallType
from BreCal.database.sql_handler import execute_sql_query_standalone
from BreCal.database.sql_queries import SQLQuery
from BreCal.database.sql_utils import get_ship_data_for_id
from BreCal.impl.participant import GetParticipant
from BreCal.impl.ships import GetShips
from BreCal.impl.berths import GetBerths
from BreCal.database.enums import ParticipantType, ParticipantFlag
from BreCal.validators.input_validation_utils import check_if_user_is_bsmd_type, check_if_ship_id_is_valid, check_if_berth_id_is_valid, check_if_participant_ids_are_valid, check_if_participant_ids_and_types_are_valid, get_shipcall_id_dictionary, get_participant_type_from_user_data
from BreCal.database.sql_handler import execute_sql_query_standalone
from BreCal.validators.validation_base_utils import check_if_int_is_valid_flag
from BreCal.validators.validation_base_utils import check_if_string_has_special_characters
import werkzeug
class InputValidationShip():
"""
This class combines a complex set of individual input validation functions into a joint object.
It uses static methods, so the object does not need to be instantiated, but functions can be called immediately.
Example:
InputValidationShip.evaluate(user_data, loadedModel, content)
When the data violates one of the rules, a marshmallow.ValidationError is raised, which details the issues.
"""
def __init__(self) -> None:
pass
@staticmethod
def exists_ship_by_dict(model:dict):
ship_id = model.get("id")
if(ship_id is not None):
return get_ship_data_for_id(ship_id) is not None
return False
@staticmethod
def exists_ship_by_id(id:int):
return get_ship_data_for_id(id) is not None
@staticmethod
def evaluate_post_data(user_data:dict, loadedModel:dict, content:dict):
# 1.) Only users of type BSMD are allowed to POST
InputValidationShip.check_user_is_bsmd_type(user_data)
# 2.) The ship IMOs are used as matching keys. They must be unique in the database.
InputValidationShip.check_ship_imo_already_exists(loadedModel)
# 3.) Check for reasonable Values (see BreCal.schemas.model.ShipSchema)
InputValidationShip.optionally_evaluate_bollard_pull_value(content)
return
@staticmethod
def evaluate_put_data(user_data:dict, loadedModel:dict, content:dict):
# 1.) Only users of type BSMD are allowed to PUT
InputValidationShip.check_user_is_bsmd_type(user_data)
# 2.) ID field is mandatory
InputValidationShip.content_contains_ship_id(content)
# 3.) The IMO number field may not be changed
InputValidationShip.put_content_may_not_contain_imo_number(content)
# 4.) Check for reasonable Values (see BreCal.schemas.model.ShipSchema)
InputValidationShip.optionally_evaluate_bollard_pull_value(content)
return
@staticmethod
def evaluate_delete_data(user_data:dict, ship_id:typing.Optional[int]):
if ship_id is None:
raise ValidationError({"id":f"The ship id must be provided."})
ship_id = int(ship_id)
# 1.) Only users of type BSMD are allowed to PUT
InputValidationShip.check_user_is_bsmd_type(user_data)
# 2.) The dataset entry may not be deleted already
InputValidationShip.check_if_entry_is_already_deleted(ship_id)
return
@staticmethod
def optionally_evaluate_bollard_pull_value(content:dict):
bollard_pull = content.get("bollard_pull",None)
is_tug = content.get("is_tug", False) # default to 'False', so the bollard pull entry fails unless the property is also actively set
if bollard_pull is not None:
if not is_tug:
raise ValidationError({"bollard_pull":f"'bollard_pull' is only allowed, when a ship is a tug ('is_tug')."})
if (not (0 < bollard_pull < 500)) & (is_tug):
raise ValidationError({"bollard_pull":f"when a ship is a tug, the bollard pull must be 0 < value < 500. "})
@staticmethod
def check_user_is_bsmd_type(user_data:dict):
is_bsmd = check_if_user_is_bsmd_type(user_data)
if not is_bsmd:
raise ValidationError({"participant_type":f"current user does not belong to BSMD. Cannot post, put or delete ships. Found user data: {user_data}"})
@staticmethod
def check_ship_imo_already_exists(loadedModel:dict):
# get the ships, convert them to a list of JSON dictionaries
response, status_code, header = GetShips(token=None)
ships = json.loads(response)
# extract only the 'imo' values
ship_imos = [ship.get("imo") for ship in ships if not ship.deleted]
# check, if the imo in the POST-request already exists in the list
imo_already_exists = loadedModel.get("imo") in ship_imos
if imo_already_exists:
raise ValidationError({"imo":f"the provided ship IMO {loadedModel.get('imo')} already exists. A ship may only be added, if there is no other ship with the same IMO number."})
return
@staticmethod
def put_content_may_not_contain_imo_number(content:dict):
# IMO is a required field, so it will never be None outside of tests. If so, there is no violation
put_data_ship_imo = content.get("imo",None)
if put_data_ship_imo is None:
return
# grab the ship by its ID and compare, whether the IMO is unchanged
ship = execute_sql_query_standalone(SQLQuery.get_ship_by_id(), param={"id":content.get("id")}, command_type="single", model=Ship)
if put_data_ship_imo != ship.imo:
raise ValidationError({"imo":f"The IMO number field may not be changed since it serves the purpose of a primary (matching) key."})
return
@staticmethod
def content_contains_ship_id(content:dict):
put_data_ship_id = content.get('id',None)
if put_data_ship_id is None:
raise ValidationError({"id":f"The id field is required."})
return
@staticmethod
def check_if_entry_is_already_deleted(ship_id:typing.Optional[int]):
"""
When calling a delete request for ships, the dataset may not be deleted already. This method
makes sure, that the request contains and ID, has a matching entry in the database, and the
database entry may not have a deletion state already.
"""
response, status_code, header = GetShips(token=None)
ships = json.loads(response)
existing_database_entries = [ship for ship in ships if ship.get("id")==int(ship_id)]
if len(existing_database_entries)==0:
raise ValidationError({"id":f"Could not find a ship with the specified ID. Selected: {ship_id}"})
existing_database_entry = existing_database_entries[0]
deletion_state = existing_database_entry.get("deleted",None)
if deletion_state:
raise ValidationError({"deleted":f"The selected ship entry is already deleted."})
return