refactoring: provided a more understandable variant of the notifier. Will create a BPMN-diagram to depict the process soon.

This commit is contained in:
Max Metz 2024-07-31 18:37:51 +02:00
parent 267b53aa91
commit b9df88b34a
2 changed files with 103 additions and 45 deletions

View File

@ -4,6 +4,7 @@ from BreCal.database.sql_handler import execute_sql_query_standalone
from BreCal.database.sql_queries import SQLQuery from BreCal.database.sql_queries import SQLQuery
from BreCal.schemas import model from BreCal.schemas import model
from BreCal.brecal_utils.time_handling import difference_to_then from BreCal.brecal_utils.time_handling import difference_to_then
from BreCal.notifications.accounts import mail_server, mail_port, mail_address, mail_pwd
from BreCal.services.email_handling import EmailHandler, create_shipcall_evaluation_notification, send_notification, get_default_html_email from BreCal.services.email_handling import EmailHandler, create_shipcall_evaluation_notification, send_notification, get_default_html_email
from BreCal.database.enums import ParticipantwiseTimeDelta from BreCal.database.enums import ParticipantwiseTimeDelta
@ -14,7 +15,6 @@ eta_etd_type_dict = {
model.ShipcallType.shifting : "Wechselnd" model.ShipcallType.shifting : "Wechselnd"
} }
class Notifier(): class Notifier():
""" """
This class provides quick access to different notification functions. This class provides quick access to different notification functions.
@ -54,42 +54,64 @@ class Notifier():
update_database = True if not is_test else False # if_test, the database will not be updated. 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. time_diff_threshold = time_diff_threshold if not is_test else 0.0 # 0.0 delay when is_test is set.
from BreCal.notifications.accounts import mail_server, mail_port, mail_address, mail_pwd
email_handler = EmailHandler(mail_server=mail_server, mail_port=mail_port, mail_address=mail_address) email_handler = EmailHandler(mail_server=mail_server, mail_port=mail_port, mail_address=mail_address)
# get candidates: find all eligible shipcalls, where the evaluation state is yellow or red & the notifications are not yet sent # 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() eligible_shipcalls = Notifier.get_eligible_shipcalls(time_diff_threshold)
# find all notifications, which belong to the shipcall ids of the eligible_shipcall list # get a list of tuples, where (shipcall, users) are combined for easier iterations.
# a time_diff_threshold is used to block those notifications, which are still fairly novel notification_instructions = Notifier.build_notification_tuples(eligible_shipcalls)
eligible_notifications = Notifier.get_eligible_notifications(eligible_shipcalls, time_diff_threshold)
if len(eligible_notifications) > 0: # only perform a login when there are eligible notifications if len(notification_instructions)>0: # only perform a login when there are any notification_instructions
try: try:
# login in advance, so the email handler uses a shared connection. It disconnects only once at the end of the call. # 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=mail_pwd) email_handler.login(interactive=False, pwd=mail_pwd)
for notification in eligible_notifications: for shipcall, users in notification_instructions:
eligible_users = Notifier.get_eligible_users(notification) assert isinstance(shipcall, model.Shipcall)
assert isinstance(users,list)
assert all([isinstance(user,model.User) for user in users])
# create an Email and send it to each eligible_user. # iterate over each notification type
# #TODO: this method must be a distributor. It should send emails for those, who want emails, and provide placeholders for other types of notifications for notification_type in Notifier.get_all_notification_types():
Notifier.create_and_send_email_notification(email_handler, mail_pwd, eligible_users, notification, update_database=update_database, debug=debug) # consider only those users, which have subscribed to the respective notification type
eligible_users = Notifier.get_eligible_users(users, notification_type)
# #TODO: except... logging? if len(eligible_users)>0:
Notifier.create_and_send_notification_mapper(notification_type, shipcall, eligible_users, email_handler, mail_pwd, debug=debug)
if not debug:
# populate notifications within the database to keep track
Notifier.generate_notification(shipcall, notification_type)
# update the database entries, so notifications are only sent once.
if update_database:
Notifier.shipcall_put_update_evaluation_notifications_sent_flag(shipcall)
# #TODO: except... create log?
finally: finally:
email_handler.close() email_handler.close()
return return
@staticmethod @staticmethod
def get_eligible_users(notification): def build_notification_tuples(eligible_shipcalls)->list[typing.Tuple[model.Shipcall,list[model.User]]]:
# get all users, which are attached to the shipcall (uses the History dataset) """
users = Notifier.get_users_via_history(notification.shipcall_id) creates tuples, where shipcall and the list of attached users are grouped. One can iterate over the tuples
to perform actions, such as issueing notifications
"""
notification_instructions = []
for shipcall in eligible_shipcalls:
# get all users, which are attached to the shipcall (uses the History dataset)
users = Notifier.get_users_via_history(shipcall.id)
# filter: only consider the users, which have subscribed to the notification type # create and store tuples of (shipcall, users) for each eligible shipcall
eligible_users = [user for user in users if Notifier.check_user_is_subscribed_to_notification_type(user,notification_type=notification.type)] notification_instructions.append((shipcall, users))
return notification_instructions
@staticmethod
def get_eligible_users(users, notification_type):
eligible_users = [user for user in users if Notifier.check_user_is_subscribed_to_notification_type(user,notification_type=notification_type)]
# filter: consider only those users, where an Email is set # filter: consider only those users, where an Email is set
# #TODO: this is Email-specific and should not be a filter for other notifications # #TODO: this is Email-specific and should not be a filter for other notifications
@ -267,7 +289,7 @@ class Notifier():
return return
@staticmethod @staticmethod
def get_eligible_shipcalls(): def get_eligible_shipcalls(time_diff_threshold:float):
""" """
get all eligible shipcalls, which do not have a sent notification yet get all eligible shipcalls, which do not have a sent notification yet
criterion a) notification shall not be sent yet (evaluation_notifications_sent = 0) criterion a) notification shall not be sent yet (evaluation_notifications_sent = 0)
@ -275,7 +297,15 @@ class Notifier():
""" """
query = 'SELECT * FROM shipcall WHERE (evaluation_notifications_sent = ?evaluation_notifications_sent?) AND (evaluation = 2 OR evaluation = 3)' query = 'SELECT * FROM shipcall WHERE (evaluation_notifications_sent = ?evaluation_notifications_sent?) AND (evaluation = 2 OR evaluation = 3)'
evaluation_notifications_sent = 0 evaluation_notifications_sent = 0
# filter out the shipcalls, where the notifications were already sent (ignores null values, which is as expected)
eligible_shipcalls = execute_sql_query_standalone(query=query, model=model.Shipcall, param={"evaluation_notifications_sent" : evaluation_notifications_sent}) eligible_shipcalls = execute_sql_query_standalone(query=query, model=model.Shipcall, param={"evaluation_notifications_sent" : evaluation_notifications_sent})
# filter out the shipcalls, where evaluation_time is not set
eligible_shipcalls = [shipcall for shipcall in eligible_shipcalls if shipcall.evaluation_time is not None]
# filter out the shipcalls, where the evaluation_time is too recent. We expect a minimum difference, so user input errors do not cause notifications
eligible_shipcalls = [shipcall for shipcall in eligible_shipcalls if Notifier.check_shipcall_evaluation_time_exceeds_minimum_time_difference(shipcall, time_diff_threshold)]
return eligible_shipcalls return eligible_shipcalls
@staticmethod @staticmethod
@ -283,20 +313,16 @@ class Notifier():
"""obtain all notifications, which belong to the shipcall id""" """obtain all notifications, which belong to the shipcall id"""
query = SQLQuery.get_notifications() query = SQLQuery.get_notifications()
eligible_notifications = execute_sql_query_standalone(query=query, model=model.Notification, param={"scid" : shipcall.id}) eligible_notifications = execute_sql_query_standalone(query=query, model=model.Notification, param={"scid" : shipcall.id})
eligible_notifications = [notification for notification in eligible_notifications if Notifier.check_notification_exceeds_minimum_time_difference(notification, time_diff_threshold)]
eligible_notifications = [notification for notification in eligible_notifications if Notifier.check_notification_level_matches_shipcall_entry(notification, shipcall)] eligible_notifications = [notification for notification in eligible_notifications if Notifier.check_notification_level_matches_shipcall_entry(notification, shipcall)]
return eligible_notifications return eligible_notifications
@staticmethod @staticmethod
def check_notification_exceeds_minimum_time_difference(notification:model.Notification, time_diff_threshold:float): def check_shipcall_evaluation_time_exceeds_minimum_time_difference(shipcall:model.Shipcall, time_diff_threshold:float):
""" """
a notification may only be sent, when the created notification has been created or modified {time_diff_threshold} seconds ago. a notification may only be sent, when the created notification has been created or modified {time_diff_threshold} seconds ago.
""" """
assert (notification.created is not None) or (notification.modified is not None), f"must provide either 'created' or 'modified'" assert (shipcall.evaluation_time is not None), f"must provide 'evaluation_time'"
if notification.modified is not None: return difference_to_then(shipcall.evaluation_time)>=time_diff_threshold
return difference_to_then(notification.modified)>time_diff_threshold
else:
return difference_to_then(notification.created)>time_diff_threshold
@staticmethod @staticmethod
def check_notification_level_matches_shipcall_entry(notification, shipcall): def check_notification_level_matches_shipcall_entry(notification, shipcall):
@ -307,16 +333,18 @@ class Notifier():
return int(shipcall.evaluation) == int(notification.level) return int(shipcall.evaluation) == int(notification.level)
@staticmethod @staticmethod
def get_eligible_notifications(shipcalls:list[model.Shipcall], time_diff_threshold:float): def get_eligible_notifications(shipcalls:list[model.Shipcall]):
"""obtain a list of all notifications of each element of the shipcall list.""" """obtain a list of all notifications of each element of the shipcall list."""
eligible_notifications = [] eligible_notifications = []
for shipcall in shipcalls: for shipcall in shipcalls:
eligible_notification = Notifier.get_eligible_notifications_of_shipcall(shipcall, time_diff_threshold) eligible_notification = Notifier.get_eligible_notifications_of_shipcall(shipcall)
eligible_notifications.extend(eligible_notification) eligible_notifications.extend(eligible_notification)
raise NotImplementedError("refactoring!")
return eligible_notifications return eligible_notifications
@staticmethod @staticmethod
def create_notifications_for_user_list(shipcall, users:list[model.User]): def create_notifications_for_user_list(shipcall, users:list[model.User]):
raise NotImplementedError("deprecated")
notification_type_list = [] notification_type_list = []
for user in users: for user in users:
user_notification_type_list = Notifier.build_notification_type_list(user) user_notification_type_list = Notifier.build_notification_type_list(user)
@ -331,8 +359,26 @@ class Notifier():
return return
@staticmethod @staticmethod
def generate_notifications(shipcall_id): def generate_notification(shipcall:model.Shipcall, notification_type:model.NotificationType):
""" """
This one-line method creates a notification for the provided shipcall and notification type
"""
schemaModel = dict(
shipcall_id = shipcall.id,
level = int(shipcall.evaluation),
type = notification_type,
message = "", # #TODO_messsage: what should be stored here? The HTML-template appears to be too long.
created = datetime.datetime.now(),
modified=None)
query = SQLQuery.get_notifications_post(schemaModel)
schemas = execute_sql_query_standalone(query=query, param=schemaModel, command_type="execute")
return
@staticmethod
def DEPRECATED_generate_notifications(shipcall_id):
"""
# #TODO_delete: this method can be safely removed after 15.08.2024
This one-line method creates all notifications for the provided shipcall id. It does so by obtaining the shipcall, This one-line method creates all notifications for the provided shipcall id. It does so by obtaining the shipcall,
looking up its history, and finding all attached users. looking up its history, and finding all attached users.
For each user, a notification will be created for each subscribed notification type (e.g., Email) For each user, a notification will be created for each subscribed notification type (e.g., Email)
@ -385,9 +431,9 @@ class Notifier():
return eta_etd return eta_etd
@staticmethod @staticmethod
def prepare_notification_body(notification): def prepare_notification_body(shipcall:model.Shipcall):
# obtain the respective shipcall and ship # obtain the respective shipcall and ship
shipcall = execute_sql_query_standalone(query=SQLQuery.get_shipcall_by_id(), model=model.Shipcall, param={"id" : notification.shipcall_id}, command_type="single") shipcall = execute_sql_query_standalone(query=SQLQuery.get_shipcall_by_id(), model=model.Shipcall, param={"id" : shipcall.id}, command_type="single")
ship = execute_sql_query_standalone(query=SQLQuery.get_ship_by_id(), model=model.Ship, param={"id" : shipcall.ship_id}, command_type="single") ship = execute_sql_query_standalone(query=SQLQuery.get_ship_by_id(), model=model.Ship, param={"id" : shipcall.ship_id}, command_type="single")
# use ship & shipcall data models to prepare the body # use ship & shipcall data models to prepare the body
@ -398,10 +444,10 @@ class Notifier():
return (ship_name, evaluation_message, eta_etd, eta_etd_type) return (ship_name, evaluation_message, eta_etd, eta_etd_type)
@staticmethod @staticmethod
def shipcall_put_update_evaluation_notifications_sent_flag(notification): def shipcall_put_update_evaluation_notifications_sent_flag(shipcall):
# change the 'evaluation_notifications_sent' flag # change the 'evaluation_notifications_sent' flag to 1
evaluation_notifications_sent = 1 evaluation_notifications_sent = 1
schemaModel = {"id":notification.shipcall_id, "evaluation_notifications_sent":evaluation_notifications_sent} schemaModel = {"id":shipcall.id, "evaluation_notifications_sent":evaluation_notifications_sent}
query = SQLQuery.get_shipcall_put(schemaModel) query = SQLQuery.get_shipcall_put(schemaModel)
schemas = execute_sql_query_standalone(query=query, param=schemaModel, command_type="execute") schemas = execute_sql_query_standalone(query=query, param=schemaModel, command_type="execute")
@ -423,7 +469,20 @@ class Notifier():
return email_tgts return email_tgts
@staticmethod @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): def create_and_send_notification_mapper(notification_type:model.NotificationType, shipcall:model.Shipcall, eligible_users:list[model.User], email_handler:EmailHandler, mail_pwd:bytes, debug:bool=False):
# #TODO_refactor: instead create a method, which contains the 'distribution-logic' for all notification types
if int(notification_type)==int(model.NotificationType.email):
# create an Email and send it to each eligible_user.
# #TODO: this method must be a distributor. It should send emails for those, who want emails, and provide placeholders for other types of notifications
Notifier.create_and_send_email_notification(email_handler, mail_pwd, eligible_users, shipcall, debug=debug)
elif int(notification_type)==int(model.NotificationType.undefined): pass
elif int(notification_type)==int(model.NotificationType.push): pass
else: pass
return
@staticmethod
def create_and_send_email_notification(email_handler:EmailHandler, pwd:bytes, users:list[model.User], shipcall:model.Shipcall, debug:bool=False):
""" """
# #TODO_rename: when there is more than one type of notification, this should be renamed. This method refers to a validation-state notification # #TODO_rename: when there is more than one type of notification, this should be renamed. This method refers to a validation-state notification
@ -436,7 +495,7 @@ class Notifier():
# prepare and build the Email content # prepare and build the Email content
content = get_default_html_email() content = get_default_html_email()
files = [] # optional attachments files = [] # optional attachments
ship_name, evaluation_message, eta_etd, eta_etd_type = Notifier.prepare_notification_body(notification) ship_name, evaluation_message, eta_etd, eta_etd_type = Notifier.prepare_notification_body(shipcall)
msg_multipart,msg_content = create_shipcall_evaluation_notification( msg_multipart,msg_content = create_shipcall_evaluation_notification(
email_handler, ship_name, evaluation_message, eta_etd, eta_etd_type, content, files=files email_handler, ship_name, evaluation_message, eta_etd, eta_etd_type, content, files=files
@ -444,11 +503,6 @@ class Notifier():
# send the messages via smtlib's SSL functions # send the messages via smtlib's SSL functions
send_notification(email_handler, email_tgts, msg_multipart, pwd, debug=debug) send_notification(email_handler, email_tgts, msg_multipart, pwd, debug=debug)
# #TODO_refactor: when there are multiple notification types, it makes sense to decouple updating the database
# from this method. Hence, an update would be done after *all* notifications are sent
if update_database:
Notifier.shipcall_put_update_evaluation_notifications_sent_flag(notification)
return return
@staticmethod @staticmethod
@ -473,4 +527,9 @@ class Notifier():
else: # placeholder: whatsapp/signal else: # placeholder: whatsapp/signal
raise NotImplementedError(notification_type) raise NotImplementedError(notification_type)
@staticmethod
def get_all_notification_types():
from BreCal.schemas import model
return list(model.NotificationType._member_map_.values())

View File

@ -39,7 +39,6 @@ def UpdateShipcalls(options:dict = {'past_days':2}):
for shipcall_id in shipcall_ids: for shipcall_id in shipcall_ids:
# apply 'Traffic Light' evaluation to obtain 'GREEN', 'YELLOW' or 'RED' evaluation state. The function internally updates the mysql database # apply 'Traffic Light' evaluation to obtain 'GREEN', 'YELLOW' or 'RED' evaluation state. The function internally updates the mysql database
evaluate_shipcall_state(mysql_connector_instance=pooledConnection, shipcall_id=shipcall_id) # new_id (last insert id) refers to the shipcall id evaluate_shipcall_state(mysql_connector_instance=pooledConnection, shipcall_id=shipcall_id) # new_id (last insert id) refers to the shipcall id
Notifier.generate_notifications(shipcall_id)
pooledConnection.close() pooledConnection.close()