From 11098da25b7ca1f4f0627593105063ada027d5be Mon Sep 17 00:00:00 2001 From: Daniel Schick Date: Wed, 16 Oct 2024 16:16:54 +0200 Subject: [PATCH] bugfix for shipcall PUT validation --- src/server/BreCal/database/sql_handler.py | 84 +++++++-------- .../validators/input_validation_shipcall.py | 101 +++++++++--------- 2 files changed, 92 insertions(+), 93 deletions(-) diff --git a/src/server/BreCal/database/sql_handler.py b/src/server/BreCal/database/sql_handler.py index 2efbf18..d8f74da 100644 --- a/src/server/BreCal/database/sql_handler.py +++ b/src/server/BreCal/database/sql_handler.py @@ -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! diff --git a/src/server/BreCal/validators/input_validation_shipcall.py b/src/server/BreCal/validators/input_validation_shipcall.py index bcfb655..40932e0 100644 --- a/src/server/BreCal/validators/input_validation_shipcall.py +++ b/src/server/BreCal/validators/input_validation_shipcall.py @@ -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 - \ No newline at end of file