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.schemas import model
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.database.enums import ParticipantwiseTimeDelta
@ -14,7 +15,6 @@ eta_etd_type_dict = {
model.ShipcallType.shifting : "Wechselnd"
}
class Notifier():
"""
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.
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)
# 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
# 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)
# get a list of tuples, where (shipcall, users) are combined for easier iterations.
notification_instructions = Notifier.build_notification_tuples(eligible_shipcalls)
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:
# 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)
for shipcall, users in notification_instructions:
assert isinstance(shipcall, model.Shipcall)
assert isinstance(users,list)
assert all([isinstance(user,model.User) for user in users])
for notification in eligible_notifications:
eligible_users = Notifier.get_eligible_users(notification)
# iterate over each notification type
for notification_type in Notifier.get_all_notification_types():
# consider only those users, which have subscribed to the respective notification type
eligible_users = Notifier.get_eligible_users(users, notification_type)
# 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, notification, update_database=update_database, debug=debug)
if len(eligible_users)>0:
Notifier.create_and_send_notification_mapper(notification_type, shipcall, eligible_users, email_handler, mail_pwd, debug=debug)
# #TODO: except... logging?
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:
email_handler.close()
return
@staticmethod
def build_notification_tuples(eligible_shipcalls)->list[typing.Tuple[model.Shipcall,list[model.User]]]:
"""
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)
# create and store tuples of (shipcall, users) for each eligible shipcall
notification_instructions.append((shipcall, users))
return notification_instructions
@staticmethod
def get_eligible_users(notification):
# 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
eligible_users = [user for user in users if Notifier.check_user_is_subscribed_to_notification_type(user,notification_type=notification.type)]
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
# #TODO: this is Email-specific and should not be a filter for other notifications
@ -267,7 +289,7 @@ class Notifier():
return
@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
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)'
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})
# 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
@staticmethod
@ -283,21 +313,17 @@ class Notifier():
"""obtain all notifications, which belong to the shipcall id"""
query = SQLQuery.get_notifications()
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)]
return eligible_notifications
@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.
"""
assert (notification.created is not None) or (notification.modified is not None), f"must provide either 'created' or 'modified'"
if notification.modified is not None:
return difference_to_then(notification.modified)>time_diff_threshold
else:
return difference_to_then(notification.created)>time_diff_threshold
assert (shipcall.evaluation_time is not None), f"must provide 'evaluation_time'"
return difference_to_then(shipcall.evaluation_time)>=time_diff_threshold
@staticmethod
def check_notification_level_matches_shipcall_entry(notification, shipcall):
"""
@ -307,16 +333,18 @@ class Notifier():
return int(shipcall.evaluation) == int(notification.level)
@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."""
eligible_notifications = []
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)
raise NotImplementedError("refactoring!")
return eligible_notifications
@staticmethod
def create_notifications_for_user_list(shipcall, users:list[model.User]):
raise NotImplementedError("deprecated")
notification_type_list = []
for user in users:
user_notification_type_list = Notifier.build_notification_type_list(user)
@ -331,8 +359,26 @@ class Notifier():
return
@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,
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)
@ -385,9 +431,9 @@ class Notifier():
return eta_etd
@staticmethod
def prepare_notification_body(notification):
def prepare_notification_body(shipcall:model.Shipcall):
# 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")
# 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)
@staticmethod
def shipcall_put_update_evaluation_notifications_sent_flag(notification):
# change the 'evaluation_notifications_sent' flag
def shipcall_put_update_evaluation_notifications_sent_flag(shipcall):
# change the 'evaluation_notifications_sent' flag to 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)
schemas = execute_sql_query_standalone(query=query, param=schemaModel, command_type="execute")
@ -423,7 +469,20 @@ class Notifier():
return email_tgts
@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
@ -436,7 +495,7 @@ class Notifier():
# prepare and build the Email content
content = get_default_html_email()
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(
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_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
@staticmethod
@ -473,4 +527,9 @@ class Notifier():
else: # placeholder: whatsapp/signal
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:
# 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
Notifier.generate_notifications(shipcall_id)
pooledConnection.close()