diff --git a/src/server/BreCal/notifications/notifier.py b/src/server/BreCal/notifications/notifier.py index c4c538b..5e4cba6 100644 --- a/src/server/BreCal/notifications/notifier.py +++ b/src/server/BreCal/notifications/notifier.py @@ -5,12 +5,14 @@ from BreCal.database.sql_queries import SQLQuery from BreCal.schemas import model from BreCal.brecal_utils.time_handling import difference_to_then +from BreCal.services.email_handling import EmailHandler, create_shipcall_evaluation_notification, send_notification +from BreCal.database.enums import ParticipantwiseTimeDelta -from BreCal.schemas.model import ShipcallType +from BreCal.stubs.email_template import get_default_html_email eta_etd_type_dict = { - ShipcallType.arrival : "Ankunft", - ShipcallType.departure : "Abfahrt", - ShipcallType.shifting : "Wechselnd" + model.ShipcallType.arrival : "Ankunft", + model.ShipcallType.departure : "Abfahrt", + model.ShipcallType.shifting : "Wechselnd" } @@ -46,107 +48,39 @@ class Notifier(): - iterate over each remaining shipcall and apply .send_notification - those which are unsent, shall be sent by the respective type """ - raise NotImplementedError("skeleton") - - # get all shipcalls - all_shipcalls = NotImplementedError - - shipcalls = [shipcall for shipcall in all_shipcalls if not shipcall.evaluation_notifications_sent] - for shipcall in shipcalls: - notification_list = Notifier.send_notification(shipcall, is_test=is_test) - - # #TODO: get all notifications - # #TODO: get matching shipcall (based on shipcall_id) - - # #TODO: filter: consider only those, which are not yet sent - - # identify necessity - # #TODO: get the 'evaluation_notifications_sent' field from all shipcalls (based on shipcall_id) - # if not -> return - # USE shipcall.evaluation_notifications_sent - - # #TODO: those which are unsent, shall be created&sent by the respective type -- Note: consider the is_test argument - # iterate over the list of Notifier.build_notification_type_list - # one might use Notifier.create(..., update_database=True) - # use the History (GetHistory -- by shipcall_id) to identify all subscribed users - - # #TODO: update the shipcall dataset ('evaluation_notifications_sent') -- Note: consider the is_test argument - - # #TODO_clarify: how to handle the 'evaluation_notifications_sent', when there is no recipient? - return - - @staticmethod - def send_notification(shipcall:model.Shipcall, is_test:bool=False)->list[model.Notification]: - """ - Complex-function, which is responsible of creating notification messages, issuing them to users and optionally updating - the database. The requirement is, that the notification is required and passes through an internal set of filters. - - Steps: - - get all notifications of shipcall_id - - identify the assigned list of users - - apply all filters. When a filter triggers, exit. If not, create and send a notification. - """ - update_database = False if is_test else True - # #TODO: the concept of old state and new state must be refactored. - # old state: read shipcall_id from notifications and look for the latest finding (if None -> EvaluationType.undefined) - # new state: read shipcall_id from shipcalls and look for the *current* 'evaluation' (-> EvaluationType(value)) - - # get existing notifications by shipcall_id (list) - existing_notifications = Notifier.get_existing_notifications(shipcall_id=shipcall.id) - old_state = NotImplementedError - - new_state = shipcall.evaluation - - # get User by querying all History objects of a shipcall_id - users = Notifier.get_users_via_history(shipcall_id=shipcall.id) - - # identify necessity - # state-check: Did the 'evaluation' shift to a higher level of severity? - severity_bool = Notifier.check_higher_severity(old_state, new_state) - if not severity_bool: - return None + # set a threshold, when alarm event notifications become eligible + time_diff_threshold = float(ParticipantwiseTimeDelta.NOTIFICATION)*60 # m minutes, converted to seconds - - # #TODO: time-based filter. There shall be 'enough' time between the evaluation time and NOW - evaluation_time = shipcall.evaluation_time - # latency_bool = #TODO_DIFFERENCE_FROM_NOW_TO_EVALUATION_TIME____THIS_METHOD_ALREADY_EXISTS(evaluation_time) - # careful: what is True, what is False? - # if latency_booL: - # return None + debug = is_test # if is_test, the Emails will not be issued. Only a print message will be created. + update_database = True if not is_test else False # if_test, the database will not be updated. + time_diff_threshold = time_diff_threshold if not is_test else 0.0 # 0.0 delay when is_test is set. - notification_list = [] - for user in users: - notification = Notifier.create( - shipcall.id, - old_state, - new_state, - user, - update_database=update_database, - is_test=is_test - ) - notification_list.append(notification) - return notification_list - - @staticmethod - def publish(shipcall_id, old_state, new_state, user, update_database:bool=False)->typing.Optional[model.Notification]: - """ - Complex-function, which creates, sends and documents a notification. It serves as a convenience function. - The method does not apply internal filters to identify, whether a notification should be created in the first place. + email_handler = EmailHandler(mail_server='w01d5503.kasserver.com', mail_port=465, mail_address="max.metz@scope-sorting.com") + pwd = b'gAAAAABmqJlkXbtJTL1tFiyQNHhF_Y7sgtVI0xEx07ybwbX70Ro1Vp73CLDq49eFSYG-1SswIDQ2JBSORYlWaR-Vh2kIwPHy_lX8SxkySrRvBRzkyZP5x0I=' - options: - update_database: bool. - # #TODO: instead of update_database, one may also use is_test - """ - # 1.) create - # ... = Notifier.create(shipcall_id, old_state, new_state, user) # e.g., might return a dictionary of dict[model.NotificationType, str], where str is the message + try: + # login in advance, so the email handler uses a shared connection. It disconnects only once at the end of the call. + email_handler.login(interactive=False, pwd=pwd) - # 2.) send - # ... = Notifier.send(...) # should contain internal 'logistics', which user the respective handlers to send notifications + # get candidates: find all eligible shipcalls, where the evaluation state is yellow or red & the notifications are not yet sent + eligible_shipcalls = Notifier.get_eligible_shipcalls() + + # find all notifications, which belong to the shipcall ids of the eligible_shipcall list + # a time_diff_threshold is used to block those notifications, which are still fairly novel + eligible_notifications = Notifier.get_eligible_notifications(eligible_shipcalls, time_diff_threshold) + + for notification in eligible_notifications: + # get all users, which are attached to the shipcall (uses the History dataset) + users = Notifier.get_users_via_history(notification.shipcall_id) + + # filter: only consider the users, which have subscribed to the notification type + users = [user for user in users if Notifier.check_user_is_subscribed_to_notification_type(user,notification_type=notification.type)] + + # obtain the mail address of each respective user + Notifier.create_and_send_email_notification(email_handler, pwd, users, notification, update_database=update_database, debug=debug) + finally: + email_handler.close() - # 3.) document (mysql database) - # if update_database - # ... = Notifier.document(...) - raise NotImplementedError("skeleton") return @staticmethod @@ -422,7 +356,7 @@ class Notifier(): # use ship & shipcall data models to prepare the body ship_name = ship.name eta_etd = Notifier.create_etaetd_string(shipcall.eta, shipcall.etd) - eta_etd_type = eta_etd_type_dict[ShipcallType(shipcall.type)] + eta_etd_type = eta_etd_type_dict[model.ShipcallType(shipcall.type)] evaluation_message = shipcall.evaluation_message return (ship_name, evaluation_message, eta_etd, eta_etd_type) @@ -435,6 +369,25 @@ class Notifier(): schemas = execute_sql_query_standalone(query=query, param=schemaModel, command_type="execute") return + + @staticmethod + def create_and_send_email_notification(email_handler:EmailHandler, pwd:bytes, users:list[model.User], notification:model.Notification, update_database:bool=True, debug:bool=False): + email_tgts = [user.user_email for user in users if user.user_email is not None] + + ship_name, evaluation_message, eta_etd, eta_etd_type = Notifier.prepare_notification_body(notification) + + content = get_default_html_email() + files = [] # optional attachments + msg_multipart,msg_content = create_shipcall_evaluation_notification( + email_handler, ship_name, evaluation_message, eta_etd, eta_etd_type, content, files=files + ) + + # send the messages via smtlib's SSL functions + send_notification(email_handler, email_tgts, msg_multipart, pwd, debug=debug) + + if update_database: + Notifier.shipcall_put_update_evaluation_notifications_sent_flag(notification) + return @staticmethod def check_user_is_subscribed_to_notification_type(user,notification_type): diff --git a/src/server/BreCal/services/email_handling.py b/src/server/BreCal/services/email_handling.py index a1e5a10..dc13272 100644 --- a/src/server/BreCal/services/email_handling.py +++ b/src/server/BreCal/services/email_handling.py @@ -287,7 +287,9 @@ def create_shipcall_evaluation_notification(email_handler, ship_name:str, evalua return (msg_multipart,content) def send_notification(email_handler, email_tgts, msg, pwd, debug=False): - email_handler.login(interactive=False, pwd=pwd) + already_logged_in = email_handler.check_login() + if not already_logged_in: + email_handler.login(interactive=False, pwd=pwd) try: assert email_handler.check_login() @@ -297,5 +299,6 @@ def send_notification(email_handler, email_tgts, msg, pwd, debug=False): print(f"(send_notification INFO): debugging state. Would have sent an Email to: {email_tgts}") finally: - email_handler.close() + if not already_logged_in: + email_handler.close() return