diff --git a/src/server/BreCal/services/schedule_routines.py b/src/server/BreCal/services/schedule_routines.py index 29226cd..67a70c4 100644 --- a/src/server/BreCal/services/schedule_routines.py +++ b/src/server/BreCal/services/schedule_routines.py @@ -81,7 +81,7 @@ def SendEmails(email_dict): commands = pydapper.using(pooledConnection) conn = smtplib.SMTP(defs.email_credentials["server"], defs.email_credentials["port"]) - conn.set_debuglevel(1) + conn.set_debuglevel(1) # set this to 0 to disable debug output to log conn.ehlo() conn.starttls() conn.ehlo() @@ -107,7 +107,10 @@ def SendEmails(email_dict): for notification in notifications: - message_type = defs.message_types[notification.type] + message_type = next((x for x in defs.message_types if x["type"] == notification.type), None) + if message_type is None: + logging.error(f"Message type {notification.type} not found") + continue with open(os.path.join(current_path,'../msg/notification_element.html'), mode="r", encoding="utf-8") as file: element = file.read() @@ -185,21 +188,38 @@ def SendNotifications(): for notification in data: - if notification.participant_id not in users_dict: - continue - users = users_dict[notification.participant_id] - for user in users: - # send notification to user - if user.notify_email: - if user not in email_dict: - email_dict[user] = [] - email_dict[user].append(notification) - if user.notify_whatsapp: - # TBD - pass - if user.notify_signal: - # TBD - pass + if not notification.participant_id: # no participant defined, this update goes to all participants of this shipcall + p_query = "SELECT * from shipcall_participant_map where shipcall_id = ?id?" + assigned_participants = commands.query(p_query, model=model.ShipcallParticipantMap, param={"id":notification.shipcall_id}) + for assigned_participant in assigned_participants: + users = users_dict[assigned_participant.participant_id] + for user in users: + # send notification to user + if user.notify_email: + if user not in email_dict: + email_dict[user] = [] + email_dict[user].append(notification) + if user.notify_whatsapp: + # TBD + pass + if user.notify_signal: + # TBD + pass + else: + users = users_dict[notification.participant_id] + for user in users: + # send notification to user + if user.notify_email: + if user not in email_dict: + email_dict[user] = [] + email_dict[user].append(notification) + if user.notify_whatsapp: + # TBD + pass + if user.notify_signal: + # TBD + pass + # mark as sent commands.execute("UPDATE notification SET level = 2 WHERE id = ?id?", param={"id":notification.id}) @@ -273,11 +293,8 @@ def setup_schedule(update_shipcalls_interval_in_minutes:int=60): schedule.every().day.at("09:00").do(eval_next_24_hrs) - SendNotifications() add_function_to_schedule_send_notifications(1) - # TODO: Add schedule function to evaluate all notifications in level 1 and create actions - return diff --git a/src/server/BreCal/validators/validation_rules.py b/src/server/BreCal/validators/validation_rules.py index 03ffc1f..9b33fba 100644 --- a/src/server/BreCal/validators/validation_rules.py +++ b/src/server/BreCal/validators/validation_rules.py @@ -1,5 +1,6 @@ import copy import logging +import pydapper import re import numpy as np import pandas as pd @@ -7,6 +8,7 @@ import datetime from BreCal.database.enums import StatusFlags from BreCal.validators.validation_rule_functions import ValidationRuleFunctions from BreCal.schemas.model import Shipcall +from BreCal.local_db import getPoolConnection class ValidationRules(ValidationRuleFunctions): @@ -76,7 +78,7 @@ class ValidationRules(ValidationRuleFunctions): evaluation_states_old = [state_old if not pd.isna(state_old) else 0 for state_old in evaluation_states_old] results = shipcall_df.apply(lambda x: self.evaluate_shipcall_from_df(x), axis=1).values # returns tuple (state, message) - # unbundle individual results. evaluation_states becomes an integer, violation + # unbundle individual results. evaluation_states becomes an integer, violation evaluation_states_new = [StatusFlags(res[0]).value for res in results] violations = [",\r\n".join(res[1]) if len(res[1])>0 else None for res in results] violations = [self.concise_evaluation_message_if_too_long(violation) for violation in violations] @@ -84,9 +86,49 @@ class ValidationRules(ValidationRuleFunctions): # build the list of evaluation times ('now', as isoformat) #evaluation_time = self.get_notification_times(evaluation_states_new) + send_notification = False + if evaluation_states_old is not None and evaluation_states_new is not None: + if len(evaluation_states_old) == 1 and len(evaluation_states_new) == 1: + if evaluation_states_old[0] != evaluation_states_new[0]: + pooledConnection = getPoolConnection() + commands = pydapper.using(pooledConnection) + if evaluation_states_new[0] == 2: + match evaluation_states_old[0]: + case 0: + send_notification = True + case 1: + send_notification = True + if evaluation_states_new[0] == 3: + match evaluation_states_old[0]: + case 0: + send_notification = True + case 1: + send_notification = True + case 2: + send_notification = True + + if send_notification: + query = "INSERT INTO notification (shipcall_id, type, level, message) VALUES (?shipcall_id?, 3, 0, ?message?)" + commands.execute(query, param={"shipcall_id" : int(shipcall_df.index[0]), "message" : violations[0]}) + + if evaluation_states_new[0] == 1 and evaluation_states_old[0] != 0: # this resolves the conflict + query = "SELECT * from notification where shipcall_id = ?shipcall_id? and type = 3 and level = 0" + existing_notification = commands.query(query, param={"shipcall_id" : int(shipcall_df.index[0])}) + if len(existing_notification) > 0: + query = "DELETE from notification where id = ?id?" + commands.execute(query, param={"id" : existing_notification[0]["id"]}) + else: + query = "INSERT INTO notification (shipcall_id, type, level) VALUES (?shipcall_id?, 4, 0)" + commands.execute(query, param={"shipcall_id" : int(shipcall_df.index[0])}) + + pooledConnection.close() + + # build the list of 'evaluation_notifications_sent'. The value is 'False', when a notification should be created #evaluation_notifications_sent = self.get_notification_states(evaluation_states_old, evaluation_states_new) + # TODO: detect evaluation state changes and create notifications + shipcall_df.loc[:,"evaluation"] = evaluation_states_new shipcall_df.loc[:,"evaluation_message"] = violations #shipcall_df.loc[:,"evaluation_time"] = evaluation_time @@ -107,14 +149,14 @@ class ValidationRules(ValidationRuleFunctions): # e.g.: Evaluation message too long. Violated Rules: ['Rule #0001C', 'Rule #0001H', 'Rule #0001F', 'Rule #0001G', 'Rule #0001L', 'Rule #0001M', 'Rule #0001J', 'Rule #0001K'] violation = f"Evaluation message too long. Violated Rules: {concise}" return violation - + def undefined_method(self) -> str: """this function should apply the ValidationRules to the respective .shipcall, in regards to .times""" return (StatusFlags.GREEN, False) # (state:str, should_notify:bool) - + def determine_notification_state(self, state_old, state_new): """ - this method determines state changes in the notification state. When the state increases, a user is notified about it. + this method determines state changes in the notification state. When the state increases, a user is notified about it. state order: (NONE = GREEN < YELLOW < RED) """ # identify a state increase @@ -123,10 +165,10 @@ class ValidationRules(ValidationRuleFunctions): # when a state increases, a notification must be sent. Thereby, the field should be set to False ({evaluation_notifications_sent}) evaluation_notifications_sent = False if bool(should_notify) else None return evaluation_notifications_sent - + def identify_notification_state_change(self, state_old, state_new) -> bool: """ - determines, whether the observed state change should trigger a notification. + determines, whether the observed state change should trigger a notification. internally, this function maps StatusFlags to an integer and determines, if the successor state is more severe than the predecessor. state changes trigger a notification in the following cases: @@ -136,7 +178,7 @@ class ValidationRules(ValidationRuleFunctions): (none -> yellow) or (none -> red) due to the values in the enumeration objects, the states are mapped to provide this function. - green=1, yellow=2, red=3, none=1. Hence, critical changes can be observed by simply checking with "greater than". + green=1, yellow=2, red=3, none=1. Hence, critical changes can be observed by simply checking with "greater than". returns bool, whether a notification should be triggered """ @@ -145,12 +187,12 @@ class ValidationRules(ValidationRuleFunctions): state_old = StatusFlags.NONE.value state_old = max(int(state_old), StatusFlags.GREEN.value) return int(state_new) > int(state_old) - + def get_notification_times(self, evaluation_states_new)->list[datetime.datetime]: """# build the list of evaluation times ('now', as isoformat)""" evaluation_times = [datetime.datetime.now().isoformat() for _i in range(len(evaluation_states_new))] return evaluation_times - + def get_notification_states(self, evaluation_states_old, evaluation_states_new)->list[bool]: """# build the list of 'evaluation_notifications_sent'. The value is 'False', when a notification should be created""" evaluation_notifications_sent = [self.determine_notification_state(state_old=int(state_old), state_new=int(state_new)) for state_old, state_new in zip(evaluation_states_old, evaluation_states_new)] @@ -160,7 +202,7 @@ class ValidationRules(ValidationRuleFunctions): def inspect_shipcall_evaluation(vr, sql_handler, shipcall_id): """ # debug only! - + a simple debugging function, which serves in inspecting an evaluation function for a single shipcall id. It returns the result and all related data. returns: result, shipcall_df (filtered by shipcall id), shipcall, spm (shipcall participant map, filtered by shipcall id), times_df (filtered by shipcall id) """ @@ -170,7 +212,7 @@ def inspect_shipcall_evaluation(vr, sql_handler, shipcall_id): result = vr.evaluate(shipcall=shipcall) notification_state = vr.identify_notification_state_change(state_old=int(shipcall.evaluation), state_new=int(result[0])) print(f"Previous state: {int(shipcall.evaluation)}, New State: {result[0]}, Notification State: {notification_state}") - + times_df = sql_handler.df_dict.get("times") times_df = times_df.loc[times_df["shipcall_id"]==shipcall_id]