bugfix for shipcall PUT validation

This commit is contained in:
Daniel Schick 2024-10-16 16:16:54 +02:00
parent fb8b732b1d
commit 11098da25b
2 changed files with 92 additions and 93 deletions

View File

@ -11,7 +11,7 @@ from BreCal.schemas import model
def pandas_series_to_data_model():
return
return
def set_participant_type(x, participant_df)->int:
"""
@ -23,12 +23,12 @@ def set_participant_type(x, participant_df)->int:
participant_id = x["participant_id"]
participant_type = participant_df.loc[participant_id, "type"]
return participant_type
def get_synchronous_shipcall_times_standalone(query_time:pd.Timestamp, all_df_times:pd.DataFrame, delta_threshold=900)->int:
"""
This function counts all entries in {all_df_times}, which have the same timestamp as {query_time}.
It does so by:
1.) selecting all eta_berth & etd_berth entries
1.) selecting all eta_berth & etd_berth entries
2.) measuring the timedelta towards {query_time}
3.) converting the timedelta to total absolute seconds (positive or negative time differences do not matter)
4.) applying a {delta_threshold} to identify, whether two times are too closely together
@ -60,8 +60,8 @@ def get_synchronous_shipcall_times_standalone(query_time:pd.Timestamp, all_df_ti
def execute_sql_query_standalone(query, param={}, pooledConnection=None, model=None, command_type="query"):
"""
execute an arbitrary query with a set of parameters, return the output and convert it to a list.
when the pooled connection is rebuilt, it will be closed at the end of the function.
execute an arbitrary query with a set of parameters, return the output and convert it to a list.
when the pooled connection is rebuilt, it will be closed at the end of the function.
"""
rebuild_pooled_connection = pooledConnection is None
@ -99,13 +99,13 @@ def execute_sql_query_standalone(query, param={}, pooledConnection=None, model=N
# when providing a model, such as model.Shipcall, the dataset is immediately translated into a data model.
schemas = commands.query_single_or_default(query, sentinel, param=param) if model is None else commands.query_single_or_default(query, sentinel, param=param, model=model)
schemas = None if schemas is sentinel else schemas
elif command_type=="execute_scalar":
schemas = commands.execute_scalar(query)
else:
raise ValueError(command_type)
finally: # if needed, ensure that the pooled connection is closed.
if rebuild_pooled_connection:
pooledConnection.close()
@ -114,23 +114,23 @@ def execute_sql_query_standalone(query, param={}, pooledConnection=None, model=N
def get_assigned_participant_of_type(shipcall_id:int, participant_type:typing.Union[int,model.ParticipantType])->typing.Optional[model.Participant]:
"""obtains the ShipcallParticipantMap of a given shipcall and finds the participant id of a desired type. Finally, returns the respective Participant"""
spm_shipcall_data = execute_sql_query_standalone(
query=SQLQuery.get_shipcall_participant_map_by_shipcall_id_and_type(),
param={"id":shipcall_id, "type":participant_type},
query=SQLQuery.get_shipcall_participant_map_by_shipcall_id_and_type(),
param={"id":shipcall_id, "type":int(participant_type)},
command_type="query") # returns a list of matches
if len(spm_shipcall_data)==0:
return None
query = 'SELECT * FROM participant WHERE id=?participant_id?'
assigned_participant = execute_sql_query_standalone(
query=query,
param={"participant_id":spm_shipcall_data[0]["participant_id"]},
query=query,
param={"participant_id":spm_shipcall_data[0]["participant_id"]},
model=model.Participant,
command_type="single_or_none"
) # returns a list of matches
return assigned_participant
class SQLHandler():
"""
An object that reads SQL queries from the sql_connection and stores it in pandas DataFrames. The object can read all available tables
@ -161,7 +161,7 @@ class SQLHandler():
schema = cursor.fetchall()
all_schemas = [schem[0] for schem in schema]
return all_schemas
def build_str_to_model_dict(self):
"""
creates a simple dictionary, which maps a string to a data object
@ -181,7 +181,7 @@ class SQLHandler():
cursor.execute(f"DESCRIBE {table_name}")
cols = cursor.fetchall()
column_names = [col_name[0] for col_name in cols]
# 2.) get the data tuples
cursor.execute(f"SELECT * FROM {table_name}")
data = cursor.fetchall()
@ -192,14 +192,14 @@ class SQLHandler():
# 4.) build a dataframe from the respective data models (which ensures the correct data type)
df = self.build_df_from_data_and_name(data, table_name)
return df
def build_df_from_data_and_name(self, data, table_name):
data_model = self.str_to_model_dict.get(table_name)
if data_model is not None:
df = pd.DataFrame([data_model(**dat) for dat in data], columns=list(data_model.__annotations__.keys()))
else:
df = pd.DataFrame([dat for dat in data])
return df
return df
def mysql_to_df(self, query, table_name):
"""provide an arbitrary sql query that should be read from a mysql server {sql_connection}. returns a pandas DataFrame with the obtained data"""
@ -222,7 +222,7 @@ class SQLHandler():
if 'id' in df.columns:
df = df.set_index('id', inplace=False) # avoid inplace updates, so the raw sql remains unchanged
return df
def read_all(self, all_schemas):
# create a dictionary, which maps every mysql schema to pandas DataFrames
self.df_dict = self.build_full_mysql_df_dict(all_schemas)
@ -242,7 +242,7 @@ class SQLHandler():
query = f"SELECT * FROM {schem}"
mysql_df_dict[schem] = self.mysql_to_df(query, table_name=schem)
return mysql_df_dict
def initialize_shipcall_participant_list(self):
"""
iteratively applies the .get_participants method to each shipcall.
@ -256,10 +256,10 @@ class SQLHandler():
# if the shipcall_id exists, the list contains ids
# otherwise, return a blank list
df['participants'] = df.apply(
lambda x: self.get_participants(x.name),
lambda x: self.get_participants(x.name),
axis=1)
return
def add_participant_type_to_map(self):
"""
applies a lambda function, where the 'type'-column in the shipcall_participant_map is updated by reading the
@ -272,14 +272,14 @@ class SQLHandler():
#spm.loc[:,"type"] = spm.loc[:].apply(lambda x: set_participant_type(x, participant_df=participant_df),axis=1)
#self.df_dict["shipcall_participant_map"] = spm
return
def get_assigned_participants(self, shipcall)->pd.DataFrame:
"""return each participant of a respective shipcall, filtered by the shipcall id"""
# get the shipcall_participant_map
spm = self.df_dict["shipcall_participant_map"]
assigned_participants = spm.loc[spm["shipcall_id"]==shipcall.id]
return assigned_participants
def get_assigned_participants_by_type(self, assigned_participants:pd.DataFrame, participant_type:ParticipantType):
"""filters a dataframe of assigned_participants by the provided type enumerator"""
if isinstance(participant_type,int):
@ -288,7 +288,7 @@ class SQLHandler():
assigned_participants_of_type = assigned_participants.loc[[participant_type in ParticipantType(int(pt_)) for pt_ in list(assigned_participants["type"].values)]]
#assigned_participants_of_type = assigned_participants.loc[assigned_participants["type"]==participant_type.value]
return assigned_participants_of_type
def check_if_any_participant_of_type_is_unassigned(self, shipcall, *args:list[ParticipantType])->bool:
"""
given a list of input arguments, where item is a participant type, the function determines, whether at least one participant
@ -305,13 +305,13 @@ class SQLHandler():
unassignment = len(assignments_of_type)==0 # a participant type does not exist, when there is no match
unassigned.append(unassignment)
return any(unassigned) # returns a single boolean, whether ANY of the types is not assigned
def standardize_model_str(self, model_str:str)->str:
"""check if the 'model_str' is valid and apply lowercasing to the string"""
model_str = model_str.lower()
assert model_str in list(self.df_dict.keys()), f"cannot find the requested 'model_str' in mysql: {model_str}"
return model_str
def get_data(self, id:int, model_str:str):
"""
obtains {id} from the respective mysql database and builds a data model from that.
@ -323,11 +323,11 @@ class SQLHandler():
returns a Shipcall object
"""
model_str = self.standardize_model_str(model_str)
df = self.df_dict.get(model_str)
data = self.df_loc_to_data_model(df, id, model_str)
return data
def get_all(self, model_str:str)->list:
"""
given a model string (e.g., 'shipcall'), return a list of all
@ -341,13 +341,13 @@ class SQLHandler():
for _aid in all_ids
]
return all_data
def df_loc_to_data_model(self, df, id, model_str, loc_type:str="loc"):
if not len(df)>0:
import warnings
warnings.warn(f"empty dataframe in SQLHandler.df_loc_to_data_model for model type: {model_str}\n")
return df
# get a pandas series from the dataframe
series = df.loc[id] if loc_type=="loc" else df.iloc[id]
@ -360,7 +360,7 @@ class SQLHandler():
data = {**{'id':int(id)}, **series.to_dict()} # 'id' must be added manually, as .to_dict does not contain the index, which was set with .set_index
data = data_model(**data)
return data
def filter_df_by_participant_type(self, df, participant_type:typing.Union[int, ParticipantType])->pd.DataFrame:
"""
As ParticipantTypes are Flag objects, a dataframe's integer might resemble multiple participant types simultaneously.
@ -376,7 +376,7 @@ class SQLHandler():
participant_type = ParticipantType(participant_type)
filtered_df = df.loc[[participant_type in ParticipantType(df_pt) for df_pt in list(df["participant_type"].values)]]
return filtered_df
def get_times_for_participant_type(self, df_times, participant_type:int):
filtered_series = self.filter_df_by_participant_type(df_times, participant_type)
#filtered_series = df_times.loc[df_times["participant_type"]==participant_type]
@ -385,14 +385,14 @@ class SQLHandler():
return None
if not len(filtered_series)<=1:
# correcting the error: ERROR:root:found multiple results
# correcting the error: ERROR:root:found multiple results
# however, a warning will still be issued
import warnings
warnings.warn(f"found multiple results in function SQLHandler.get_times_for_participant_type\nConsidering only the first match!\nAffected Times Indexes: {filtered_series.index}")
times = self.df_loc_to_data_model(filtered_series, id=0, model_str='times', loc_type="iloc") # use iloc! to retrieve the first result
return times
def dataframe_to_data_model_list(self, df, model_str)->list:
model_str = self.standardize_model_str(model_str)
@ -413,22 +413,22 @@ class SQLHandler():
df = self.df_dict.get("shipcall_participant_map")
if 'shipcall_id' in list(df.columns):
df = df.set_index('shipcall_id', inplace=False)
# the 'if' call is needed to ensure, that no Exception is raised, when the shipcall_id is not present in the df
participant_id_list = df.loc[shipcall_id, "participant_id"].tolist() if shipcall_id in list(df.index) else []
if not isinstance(participant_id_list,list):
participant_id_list = [participant_id_list]
return participant_id_list
def get_times_of_shipcall(self, shipcall)->pd.DataFrame:
df_times = self.df_dict.get('times') # -> pd.DataFrame
df_times = df_times.loc[df_times["shipcall_id"]==shipcall.id]
return df_times
def get_times_for_agency(self, non_null_column=None)->pd.DataFrame:
"""
options:
non_null_column:
non_null_column:
None or str. If provided, the 'non_null_column'-column of the dataframe will be filtered,
so only entries with provided values are returned (filters all NaN and NaT entries)
"""
@ -437,8 +437,8 @@ class SQLHandler():
# filter out all NaN and NaT entries
if non_null_column is not None:
# in the Pandas documentation, it says for .isnull():
# "This function takes a scalar or array-like object and indicates whether values are missing
# in the Pandas documentation, it says for .isnull():
# "This function takes a scalar or array-like object and indicates whether values are missing
# (NaN in numeric arrays, None or NaN in object arrays, NaT in datetimelike)."
df_times = df_times.loc[~df_times[non_null_column].isnull()] # NOT null filter
@ -446,10 +446,10 @@ class SQLHandler():
times_agency = self.filter_df_by_participant_type(df_times, ParticipantType.AGENCY.value)
#times_agency = df_times.loc[df_times["participant_type"]==ParticipantType.AGENCY.value]
return times_agency
def filter_df_by_key_value(self, df, key, value)->pd.DataFrame:
return df.loc[df[key]==value]
def get_unique_ship_counts(self, all_df_times:pd.DataFrame, times_agency:pd.DataFrame, query:str, rounding:str="min", maximum_threshold=3):
"""given a dataframe of all agency times, get all unique ship counts, their values (datetime) and the string tags. returns a tuple (values,unique,counts)"""
# #deprecated!

View File

@ -29,7 +29,7 @@ class InputValidationShipcall():
Example:
InputValidationShipcall.evaluate(user_data, loadedModel, content)
When the data violates one of the rules, a marshmallow.ValidationError is raised, which details the issues.
When the data violates one of the rules, a marshmallow.ValidationError is raised, which details the issues.
"""
def __init__(self) -> None:
@ -39,7 +39,7 @@ class InputValidationShipcall():
def evaluate_post_data(user_data:dict, loadedModel:dict, content:dict):
"""
this function combines multiple validation functions to verify data, which is sent to the API as a shipcall's POST-request
checks:
1. permission: only participants that belong to the BSMD or AGENCY groups are allowed to POST shipcalls
2. reference checks: all refered objects within the Shipcall must exist
@ -61,16 +61,16 @@ class InputValidationShipcall():
# check for reasonable values in the shipcall fields
InputValidationShipcall.check_shipcall_values(loadedModel, content, forbidden_keys=["evaluation", "evaluation_message"]) # "canceled"
return
@staticmethod
def evaluate_put_data(user_data:dict, loadedModel:dict, content:dict):
"""
this function combines multiple validation functions to verify data, which is sent to the API as a shipcall's PUT-request
checks:
1. user's authority:
a) whether the user's participant is assigned to the shipcall (via shipcall-participant-map)
b) whether the user is either an AGENCY (assigned) or the BSMD, in case the AGENCY allows the BSMD to edit their shipcalls
b) whether the user is either an AGENCY (assigned) or the BSMD, in case the AGENCY allows the BSMD to edit their shipcalls
2. existance of required fields
3. all value-rules of the POST evaluation
4. a canceled shipcall may not be changed
@ -90,24 +90,24 @@ class InputValidationShipcall():
# check the referenced IDs
InputValidationShipcall.check_referenced_ids(loadedModel)
# check for reasonable values in the shipcall fields and checks for forbidden keys.
# check for reasonable values in the shipcall fields and checks for forbidden keys.
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)
InputValidationShipcall.check_shipcall_is_canceled(loadedModel, content)
return
@staticmethod
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.
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.
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.
@ -117,13 +117,13 @@ 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({"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')}"})
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({"flags":f"incorrect value provided for 'flags'. Must be a valid combination of the flags."})
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)
@ -134,7 +134,7 @@ class InputValidationShipcall():
# some arguments must not be provided
InputValidationShipcall.check_forbidden_arguments(content, forbidden_keys=forbidden_keys)
return
@staticmethod
def check_agency_in_shipcall_participant_map(user_data:dict, loadedModel:dict, content:dict, spm_shipcall_data:typing.Optional[list]=None):
"""
@ -145,11 +145,11 @@ class InputValidationShipcall():
Upon violation, this method issues 'Forbidden'-Exceptions with HTTP status code 403. There are four reasons for violations:
a) an agency tries to self-assign for a shipcall
b) there is no assigned agency for the current shipcall
c) an agency is assigned, but the current agency-user belongs to a different participant_id
c) an agency is assigned, but the current agency-user belongs to a different participant_id
d) the user must be of ParticipantType BSMD or AGENCY
args:
spm_shipcall_data:
spm_shipcall_data:
a list of entries obtained from the ShipcallParticipantMap. These are deserialized dictionaries.
e.g., [{'participant_id': 136, 'type': 8}, ]
"""
@ -158,11 +158,11 @@ class InputValidationShipcall():
# read the ShipcallParticipantMap entry of the current shipcall_id. This is used within the input validation of a PUT request
spm_shipcall_data = execute_sql_query_standalone(
# #TODO_refactor: place this within the SQLQuery object
query = "SELECT participant_id, type FROM shipcall_participant_map WHERE shipcall_id=?shipcall_id?",
query = "SELECT participant_id, type FROM shipcall_participant_map WHERE shipcall_id=?shipcall_id?",
param={"shipcall_id":loadedModel["id"]},
pooledConnection=None
)
# which role should be set by the PUT request? If the agency is about to be set, an error will be created
# read the user data from the JWT token (set when login is performed)
user_type = get_participant_type_from_user_data(user_data) # decode JWT -> get 'type' value (guarantees to convert user type into an IntFlag)
@ -180,7 +180,7 @@ class InputValidationShipcall():
# user not AGENCY or BSMD
raise werkzeug.exceptions.Forbidden(f"PUT Requests for shipcalls can only be issued by AGENCY or BSMD users.") # Forbidden: 403
# Placeholder: when a user is an AGENCY,
# Placeholder: when a user is an AGENCY,
if (ParticipantType.AGENCY in user_type) & (any_type_is_agency):
# self-assignment: agency sets agency participant
@ -189,7 +189,7 @@ class InputValidationShipcall():
if len(agency_entries)>0:
# agency participant exists: participant id must be the same as shipcall participant map entry
matching_spm_entry = [spm_entry for spm_entry in spm_shipcall_data if (spm_entry.get("participant_id")==user_data["id"]) & (int(spm_entry.get("type"))==int(ParticipantType.AGENCY))]
if len(matching_spm_entry)==0:
# An AGENCY was found, but a different participant_id is assigned to that AGENCY
raise werkzeug.exceptions.Forbidden(f"A different participant_id is assigned as the AGENCY of this shipcall. Provided ID: {user_data.get('id')}, Assigned ShipcallParticipantMap: {agency_entries}") # Forbidden: 403
@ -201,7 +201,7 @@ class InputValidationShipcall():
# agency participant does not exist: there is no assigned agency role for the shipcall {shipcall_id}
raise werkzeug.exceptions.Forbidden(f"There is no assigned agency for this shipcall. Shipcall ID: {loadedModel['id']}") # Forbidden: 403
return
@staticmethod
def check_user_is_bsmd_or_agent_type(user_data):
"""
@ -226,7 +226,7 @@ class InputValidationShipcall():
check, whether the referenced entries exist (e.g., when a Ship ID is referenced, but does not exist, the validation fails)
"""
# #TODO: arrival and departure berth id should be coupled with the shipcall type. One shall not provide
# arrival berth id when the shipcall type is departure or vise versa.
# arrival berth id when the shipcall type is departure or vise versa.
# a similar logic has already been implemented to the eta/etd or for the operation windows
# get all IDs from the loadedModel
@ -238,23 +238,23 @@ class InputValidationShipcall():
valid_ship_id = check_if_ship_id_is_valid(ship_id=ship_id)
if not valid_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({"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({"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({"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({"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.
@ -264,20 +264,20 @@ class InputValidationShipcall():
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"]):
"""
a post-request must not contain the arguments 'canceled', 'evaluation', 'evaluation_message'.
a put-request must not contain the arguments 'evaluation', 'evaluation_message'
"""
# the following keys should not be set in a POST-request.
for forbidden_key in forbidden_keys:
value = content.get(forbidden_key, None)
if value is not None:
raise ValidationError({"forbidden_key":f"'{forbidden_key}' may not be set on POST. Found: {value}"})
return
return
@staticmethod
def check_required_fields_exist_based_on_type(loadedModel:dict, content:dict):
@ -296,30 +296,30 @@ class InputValidationShipcall():
if int(type_)==int(ShipcallType.undefined):
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({"eta":f"providing 'eta' is mandatory. Missing key!"})
if arrival_berth_id is None:
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({"etd":f"providing 'etd' is mandatory. Missing key!"})
if departure_berth_id is None:
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({"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({"arrival_berth_id_or_departure_berth_id":f"providing 'arrival_berth_id' & 'departure_berth_id' is mandatory. Missing key!"})
else:
raise ValidationError({"type":f"incorrect 'type' provided!"})
return
@ -328,7 +328,7 @@ class InputValidationShipcall():
def check_times_are_in_future(loadedModel:dict, content:dict):
"""
Dates should be in the future. Depending on the ShipcallType, specific values should be checked
Perfornms datetime checks in the loadedModel (datetime.datetime objects).
Perfornms datetime checks in the loadedModel (datetime.datetime objects).
"""
# obtain the current datetime to check, whether the provided values are in the future
time_now = datetime.datetime.now()
@ -350,11 +350,11 @@ class InputValidationShipcall():
# Estimated arrival or departure times
InputValidationShipcall.check_times_in_future_based_on_type(type_, time_now, eta, etd)
# Tidal Window
InputValidationShipcall.check_tidal_window_in_future(type_, time_now, tidal_window_from, tidal_window_to)
return
@staticmethod
def check_times_in_future_based_on_type(type_, time_now, eta, etd):
"""
@ -366,7 +366,7 @@ class InputValidationShipcall():
"""
if (eta is None) and (etd is None):
return
if type_ is None:
raise ValidationError({"type":f"when providing 'eta' or 'etd', one must provide the type of the shipcall, so the datetimes can be verified."})
@ -412,7 +412,7 @@ class InputValidationShipcall():
if (eta is not None and etd is None) or (eta is None and etd is not None):
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:
@ -422,12 +422,12 @@ class InputValidationShipcall():
if tidal_window_from is not None:
if not tidal_window_from >= time_now:
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({"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
def check_participant_list_not_empty_when_user_is_agency(loadedModel):
"""
@ -436,10 +436,10 @@ class InputValidationShipcall():
participants = loadedModel.get("participants", [])
is_agency_participant = [ParticipantType.AGENCY in ParticipantType(participant.get("type")) for participant in participants]
if not any(is_agency_participant):
if not any(is_agency_participant):
raise ValidationError({"participants":f"One of the assigned participants *must* be of type 'ParticipantType.AGENCY'. Found list of participants: {participants}"})
return
@staticmethod
def check_shipcall_is_canceled(loadedModel, content):
# read the shipcall_id from the PUT data
@ -455,13 +455,13 @@ class InputValidationShipcall():
if shipcall.get("canceled", False):
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({"id":f"A PUT request requires an 'id' to refer to."})
@staticmethod
def check_shipcall_id_exists(loadedModel):
"""simply checks, whether the defined shipcall ID exists in the database. Otherwise, a PUT-request must fail."""
@ -483,8 +483,8 @@ class InputValidationShipcall():
a) belong to the ASSIGNED agency participant group
b) belong to a BSMD participant, if the assigned agency has enabled the bit flag
When there is not yet an assigned agency for the respective shipcall, only BSMD users are authorized.
This mechanism prevents self-assignment of an agency to arbitrary shipcalls.
When there is not yet an assigned agency for the respective shipcall, only BSMD users are authorized.
This mechanism prevents self-assignment of an agency to arbitrary shipcalls.
"""
### preparation ###
# use the decoded JWT token and extract the participant type & participant id
@ -521,7 +521,7 @@ class InputValidationShipcall():
### USER authority ###
# determine, whether the user is a) the assigned agency or b) a BSMD participant
user_is_assigned_agency = (user_participant_id == assigned_agency.participant_id)
user_is_assigned_agency = (user_participant_id == assigned_agency.id)
# when the BSMD flag is set: the user must be either BSMD or the assigned agency
# when the BSMD flag is not set: the user must be the assigned agency
@ -529,7 +529,7 @@ class InputValidationShipcall():
if not user_is_authorized:
raise werkzeug.exceptions.Forbidden(f"PUT Requests for shipcalls can only be issued by an assigned AGENCY or BSMD users (if the special-flag is enabled). Assigned Agency: {assigned_agency} with Flags: {assigned_agency.flags}") # Forbidden: 403
else:
# when there is no assigned agency, only BSMD users can update the shipcall
if not user_is_bsmd:
@ -537,4 +537,3 @@ class InputValidationShipcall():
return