diff --git a/src/server/BreCal/notifications/notifier.py b/src/server/BreCal/notifications/notifier.py index 5e4cba6..41b89f4 100644 --- a/src/server/BreCal/notifications/notifier.py +++ b/src/server/BreCal/notifications/notifier.py @@ -5,10 +5,9 @@ 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.services.email_handling import EmailHandler, create_shipcall_evaluation_notification, send_notification, get_default_html_email from BreCal.database.enums import ParticipantwiseTimeDelta -from BreCal.stubs.email_template import get_default_html_email eta_etd_type_dict = { model.ShipcallType.arrival : "Ankunft", model.ShipcallType.departure : "Abfahrt", @@ -58,30 +57,40 @@ class Notifier(): 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=' - 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) + # 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() - # 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) - # 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) + if len(eligible_notifications) > 0: # only perform a login when there are eligible notifications + 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) - 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() + for notification in eligible_notifications: + eligible_users = Notifier.get_eligible_users(notification) + # 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, pwd, eligible_users, notification, update_database=update_database, debug=debug) + finally: + email_handler.close() return + + @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)] + + # filter: consider only those users, where an Email is set + # #TODO: this is Email-specific and should not be a filter for other notifications + eligible_users = [user for user in eligible_users if user.user_email is not None] + return eligible_users @staticmethod def create(shipcall_id, old_state, new_state, user, update_database:bool=False)->typing.Optional[model.Notification]: @@ -372,6 +381,12 @@ class Notifier(): @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): + """ + # #TODO_rename: when there is more than one type of notification, this should be renamed. This method refers to a validation-state notification + + this 'naive' method creates a message and simply sends it to all users in a list of users. + Afterwards, the database will be updated, so the shipcall no longer requires a notification. + """ 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) @@ -385,6 +400,8 @@ 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 @@ -412,6 +429,3 @@ class Notifier(): raise NotImplementedError(notification_type) -"""# build the list of evaluation times ('now', as isoformat)""" -#evaluation_times = [datetime.datetime.now().isoformat() for _i in range(len(evaluation_states_new))] - diff --git a/src/server/BreCal/resources/__init__.py b/src/server/BreCal/resources/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/server/BreCal/resources/logo_bremen_calling.png b/src/server/BreCal/resources/logo_bremen_calling.png new file mode 100644 index 0000000..bb97bf5 Binary files /dev/null and b/src/server/BreCal/resources/logo_bremen_calling.png differ diff --git a/src/server/BreCal/stubs/default_email_template.txt b/src/server/BreCal/resources/warning_notification_email_template.txt similarity index 100% rename from src/server/BreCal/stubs/default_email_template.txt rename to src/server/BreCal/resources/warning_notification_email_template.txt diff --git a/src/server/BreCal/services/email_handling.py b/src/server/BreCal/services/email_handling.py index f091cf8..923085f 100644 --- a/src/server/BreCal/services/email_handling.py +++ b/src/server/BreCal/services/email_handling.py @@ -237,6 +237,48 @@ import typing from email.mime.application import MIMEApplication import mimetypes +import os + +def find_warning_notification_email_template()->str: + """ + dynamically finds the 'default_email_template.txt' file within the module. + """ + # __file__ is BreCal/stubs/email_template.py + # parent of email_template.py is stubs + # parent of stubs is BreCal + brecal_root_folder = os.path.dirname(os.path.dirname(__file__)) # .../BreCal + resource_root_folder = os.path.join(brecal_root_folder, "resources") # .../BreCal/resources + html_filepath = os.path.join(resource_root_folder,"warning_notification_email_template.txt") # .../BreCal/resources/warning_notification_email_template.txt + assert os.path.exists(html_filepath), f"could not find default email template file at path: {html_filepath}" + return html_filepath + +def get_default_html_email()->str: + """ + dynamically finds the 'default_email_template.txt' file within the module. It opens the file and returns the content. + + __file__ returns to the file, where this function is stored (e.g., within BreCal.stubs.email_template) + using the dirname refers to the directory, where __file__ is stored. + finally, the 'default_email_template.txt' is stored within that folder + """ + html_filepath = find_warning_notification_email_template() + with open(html_filepath,"r", encoding="utf-8") as file: # encoding = "utf-8" allows for German Umlaute + content = file.read() + return content + +def find_bremen_calling_logo(): + """ + find the path towards the logo file (located at 'brecal\src\BreCalClient\Resources\logo_bremen_calling.png') + """ + # __file__ is services/email_handling.py + # parent of __file__ is services + # parent of services is BreCal + src_root_folder = os.path.dirname(os.path.dirname(__file__)) # .../BreCal + resource_root_folder = os.path.join(src_root_folder, "resources") + + path = os.path.join(resource_root_folder, "logo_bremen_calling.png") + assert os.path.exists(path), f"cannot find logo of bremen calling at path: {os.path.abspath(path)}" + return path + def add_bremen_calling_logo(msg_multipart): """ The image is not attached automatically when it is embedded to the content. To circumvent this, @@ -244,12 +286,7 @@ def add_bremen_calling_logo(msg_multipart): The content body refers to 'LogoBremenCalling', which the 'Content-ID' of the logo is assigned as. """ - # find the path towards the logo file (located at 'brecal\src\BreCalClient\Resources\logo_bremen_calling.png') - src_root_folder = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(__file__)))) - resource_root_folder = os.path.join(src_root_folder, "BreCalClient", "Resources") - - path = os.path.join(resource_root_folder, "logo_bremen_calling.png") - assert os.path.exists(path), f"cannot find logo of bremen calling at path: {os.path.abspath(path)}" + path = find_bremen_calling_logo() with open(path, 'rb') as file: attachment = MIMEApplication(file.read(), _subtype=mimetypes.MimeTypes().guess_type(path), Name="bremen_calling.png") diff --git a/src/server/BreCal/stubs/email_template.py b/src/server/BreCal/stubs/email_template.py deleted file mode 100644 index f47ca00..0000000 --- a/src/server/BreCal/stubs/email_template.py +++ /dev/null @@ -1,15 +0,0 @@ -import os - -def get_default_html_email(): - """ - dynamically finds the 'default_email_template.txt' file within the module. It opens the file and returns the content. - - __file__ returns to the file, where this function is stored (e.g., within BreCal.stubs.email_template) - using the dirname refers to the directory, where __file__ is stored. - finally, the 'default_email_template.txt' is stored within that folder - """ - html_filepath = os.path.join(os.path.dirname(__file__),"default_email_template.txt") - assert os.path.exists(html_filepath), f"could not find default email template file at path: {html_filepath}" - with open(html_filepath,"r", encoding="utf-8") as file: # encoding = "utf-8" allows for German Umlaute - content = file.read() - return content diff --git a/src/server/tests/resources/__init__.py b/src/server/tests/resources/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/server/tests/resources/test_find_defaults.py b/src/server/tests/resources/test_find_defaults.py new file mode 100644 index 0000000..214f0d7 --- /dev/null +++ b/src/server/tests/resources/test_find_defaults.py @@ -0,0 +1,14 @@ +import pytest +import os + +def test_find_bremen_calling_logo(): + from BreCal.services.email_handling import find_bremen_calling_logo + path = find_bremen_calling_logo() + assert os.path.exists(path), f"cannot find the bremen calling logo file, which is needed for notifications (e.g., Email). Searched at path: \n\t{path}" + return + +def test_find_warning_notification_email_template(): + from BreCal.services.email_handling import find_warning_notification_email_template + path = find_warning_notification_email_template() + assert os.path.exists(path), f"cannot find the required email template, which is needed for warning notifications. Searched at path: \n\t{path}" + return