bugfix for shipcall PUT validation
This commit is contained in:
parent
fb8b732b1d
commit
11098da25b
@ -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!
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user