refactoring: provided a more understandable variant of the notifier. Will create a BPMN-diagram to depict the process soon.
This commit is contained in:
parent
267b53aa91
commit
b9df88b34a
@ -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 notification in eligible_notifications:
|
||||
eligible_users = Notifier.get_eligible_users(notification)
|
||||
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])
|
||||
|
||||
# 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)
|
||||
# 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)
|
||||
|
||||
# #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:
|
||||
email_handler.close()
|
||||
return
|
||||
|
||||
@staticmethod
|
||||
def get_eligible_users(notification):
|
||||
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(notification.shipcall_id)
|
||||
users = Notifier.get_users_via_history(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)]
|
||||
# create and store tuples of (shipcall, users) for each eligible shipcall
|
||||
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
|
||||
# #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,20 +313,16 @@ 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())
|
||||
|
||||
|
||||
|
||||
@ -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()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user