(still in development): Creating Notifier. The object is created, many unit tests have been added to verify proper functionality. Not yet finalized. Now totaling 230 unit tests, all passing.

This commit is contained in:
Max Metz 2024-05-29 18:19:34 +02:00
parent 8732a4a13a
commit 843fe607ab
12 changed files with 626 additions and 25 deletions

View File

@ -11,6 +11,10 @@ bp = Blueprint('user', __name__)
@bp.route('/user', methods=['put']) @bp.route('/user', methods=['put'])
@auth_guard() # no restriction by role @auth_guard() # no restriction by role
def PutUser(): def PutUser():
# #TODO: user validation should be extended by the notifications. When someone wants to set
# notify_email = 1, the email must be either present or part of the loadedModel
# notify_whatsapp = 1, there must be a phone number (same for notify_signal)
# notify_push = 1, there must be a phone number (#TODO_determine ... or an app-id? Unclear still)
try: try:
content = request.get_json(force=True) content = request.get_json(force=True)

View File

@ -213,7 +213,7 @@ class SQLQuery():
@staticmethod @staticmethod
def get_history()->str: def get_history()->str:
query = "SELECT id, participant_id, shipcall_id, timestamp, eta, type, operation FROM history WHERE shipcall_id = ?shipcallid?" query = "SELECT id, participant_id, shipcall_id, user_id, timestamp, eta, type, operation FROM history WHERE shipcall_id = ?shipcallid?"
return query return query
@staticmethod @staticmethod
@ -402,14 +402,14 @@ class SQLQuery():
@staticmethod @staticmethod
def get_notification_post()->str: def get_notification_post()->str:
raise NotImplementedError() raise NotImplementedError("skeleton")
# #TODO: this query is wrong and just a proxy for a POST request # #TODO: this query is wrong and just a proxy for a POST request
query = "INSERT INTO shipcall_participant_map (shipcall_id, participant_id, type) VALUES (?shipcall_id?, ?participant_id?, ?type?)" query = "INSERT INTO shipcall_participant_map (shipcall_id, participant_id, type) VALUES (?shipcall_id?, ?participant_id?, ?type?)"
return query return query
@staticmethod @staticmethod
def get_shipcall_put_notification_state()->str: def get_shipcall_put_notification_state()->str:
raise NotImplementedError() raise NotImplementedError("skeleton")
# #TODO: use evaluation_notifications_sent here and consider only the shipcall_id # #TODO: use evaluation_notifications_sent here and consider only the shipcall_id
# #TODO: query # #TODO: query
query = ... query = ...

View File

@ -23,7 +23,7 @@ def GetHistory(options):
if "shipcall_id" in options and options["shipcall_id"]: if "shipcall_id" in options and options["shipcall_id"]:
# query = SQLQuery.get_history() # query = SQLQuery.get_history()
# data = commands.query(query, model=History.from_query_row, param={"shipcallid" : options["shipcall_id"]}) # data = commands.query(query, model=History.from_query_row, param={"shipcallid" : options["shipcall_id"]})
data = commands.query("SELECT id, participant_id, shipcall_id, timestamp, eta, type, operation FROM history WHERE shipcall_id = ?shipcallid?", data = commands.query("SELECT id, participant_id, shipcall_id, user_id, timestamp, eta, type, operation FROM history WHERE shipcall_id = ?shipcallid?",
model=History.from_query_row, model=History.from_query_row,
param={"shipcallid" : options["shipcall_id"]}) param={"shipcallid" : options["shipcall_id"]})

View File

@ -48,6 +48,8 @@ class NotifierFunctions():
returns bool, whether a notification should be triggered returns bool, whether a notification should be triggered
""" """
# #TODO_delete: has been refactored to Notifier.check_higher_severity(old_state:model.EvaluationType, new_state:model.EvaluationType)
# state_old is always considered at least 'Green' (1) # state_old is always considered at least 'Green' (1)
if state_old is None: if state_old is None:
state_old = StatusFlags.NONE.value state_old = StatusFlags.NONE.value
@ -65,6 +67,6 @@ class NotifierFunctions():
return evaluation_notifications_sent return evaluation_notifications_sent
class Notifier(NotifierFunctions): class LegacyNotifier(NotifierFunctions):
def __init__(self): def __init__(self):
super().__init__() super().__init__()

View File

@ -0,0 +1,313 @@
import typing
from BreCal.database.sql_handler import execute_sql_query_standalone
from BreCal.database.sql_queries import SQLQuery
from BreCal.schemas import model
class Notifier():
"""
This class provides quick access to different notification functions.
Each method is callable without initializing the Notifier object.
Example:
Notifier.send_notifications(*args)
The Notifier has three main methods.
Notifier.send_notifications() --- can be called routinely. Identifies all candidates and notifies the users
Notifier.send_notification(shipcall) --- applies filters to identify, whether a notification is desired. If so, notifies the users
Notifer.create(user) --- 'naive' method, which simply creates a message and sends it to the user's preferred choice
# #TODO_determine: it makes sense to go one step finer. .create could produce messages and recipients, whereas .publish may then issue those and .document may update the SQL dataset
## naming idea: Notifier.send_notifications, Notifier.send_notification, Notifier.send (which may contain .create, .publish, .document)
"""
def __init__(self) -> None:
pass
@staticmethod
def send_notifications(is_test:bool=False) -> None:
"""
This method is used in BreCal.services.schedule_routines and will issue notifications, once they are due.
It is purposely defined in a way, where no external dependencies or arguments are required. The only exception is the
'is_test' boolean, as it prevents the notifications from being *actually* sent as part of the pytests.
Steps:
- get all shipcalls
- filter: consider only those, which are not yet sent (uses shipcall.evaluation_notifications_sent)
- 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
# #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
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.
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
# 2.) send
# ... = Notifier.send(...) # should contain internal 'logistics', which user the respective handlers to send notifications
# 3.) document (mysql database)
# if update_database
# ... = Notifier.document(...)
raise NotImplementedError("skeleton")
return
@staticmethod
def create(shipcall_id, old_state, new_state, user, update_database:bool=False)->typing.Optional[model.Notification]:
"""
Standalone function, which creates a Notification for a specific user.
Steps:
- identify a list of notification_types, which shall be issued (based on the user's 'notify_*' settings)
- create messages based on the respective NotificationType, which the user has enabled
- send the messages
- update the shipcall dataset ('evaluation_notifications_sent')
args:
update_database: whether to update the MySQL database by posting the notification.
"""
assert user.id is not None
assert shipcall_id is not None
assert old_state is not None
assert new_state is not None
# get Shipcall by shipcall_id
shipcall = Notifier.get_shipcall(shipcall_id=shipcall_id)
"""
## TODO: this might simply be removed due to incorrect concept
## could also relocate this to the generation function, which identifies the notifications to be created
## should be unnecessary due to shipcall.evaluation_notifications_sent
# a) filter existing notifictions and consider only the dataset, where type (notification_type) and level (new_state) are suitable
notification_exists = Notifier.check_notification_type_and_level_exists(shipcall_id=shipcall_id, notification_type=notification_type, level=new_state, existing_notifications=existing_notifications)
if notification_exists:
return None
"""
# get a list of all subscribed notification types and track the state (success or failure)
successes = {}
notification_type_list = Notifier.build_notification_type_list(user)
for notification_type in notification_type_list:
# generate message based on the notification type
message = Notifier.generate_notification_message_by_type(notification_type, evaluation_message=shipcall.evaluation_message, user=user)
# send the message
success_state = Notifier.send_notification_by_type(notification_type, message)
successes[notification_type] = success_state
raise NotImplementedError("skeleton")
notification = ...
return notification
@staticmethod
def find_latest_notification(notifications:list[model.Notification])->typing.Optional[model.Notification]:
"""given a list of notification objects, this method returns the object, where the .created field corresponds to the *latest* notification object"""
latest_notification = sorted(notifications, key=lambda notification: notification.created, reverse=False)[-1] if len(notifications)>0 else None
return latest_notification
@staticmethod
def get_users_via_history(shipcall_id:int)->list[model.User]:
"""using the History objects, one can infer the user_id, which allows querying the Users"""
histories = execute_sql_query_standalone(query=SQLQuery.get_history(), param={"shipcallid" : shipcall_id}, model=model.History, command_type="query")
user_ids = [
history.user_id
for history in histories
]
users = [Notifier.get_user(user_id) for user_id in user_ids]
return users
@staticmethod
def get_user(user_id:int)->model.User:
"""Given a user_id, this method executes an SQL query to return a User"""
user = execute_sql_query_standalone(query=SQLQuery.get_user_by_id(), param={"id" : user_id}, model=model.User, command_type="single")
return user
@staticmethod
def get_shipcall(shipcall_id:int)->model.Shipcall:
"""Given a shipcall_id, this method executes an SQL query to return a Shipcall"""
shipcall = execute_sql_query_standalone(query=SQLQuery.get_shipcall_by_id(), param={"id" : shipcall_id}, model=model.Shipcall.from_query_row, command_type="single")
return shipcall
@staticmethod
def get_existing_notifications(shipcall_id:int)->list[model.Notification]:
existing_notifications = execute_sql_query_standalone(query=SQLQuery.get_notifications(), param={"scid" : shipcall_id}, model=model.Notification, command_type="query")
return existing_notifications
@staticmethod
def build_notification_type_list(user:model.User)->list[model.NotificationType]:
"""
based on a User, this method generates a list of notification types. These can be used as instructions to
generate the respective Notification datasets.
"""
notification_type_list = []
if user.notify_email:
notification_type_list.append(model.NotificationType.email)
if user.notify_popup:
notification_type_list.append(model.NotificationType.push)
if user.notify_whatsapp:
# currently not defined as a data model. Must be included / changed, once the data model of NotificationType is updated
notification_type_list.append(model.NotificationType.undefined)
if user.notify_signal:
# currently not defined as a data model. Must be included / changed, once the data model of NotificationType is updated
notification_type_list.append(model.NotificationType.undefined)
return notification_type_list
@staticmethod
def check_notification_type_and_level_exists(shipcall_id:int, notification_type:model.NotificationType, level:model.EvaluationType, existing_notifications:list[model.Notification])->bool:
"""This method checks, whether one of the Notification elements in the provided list is a perfect match to the arguments shipcall_id, notification_type and level"""
# #TODO_determine: should a notification be *skipped*, when there already is a dataset with
# identical level and type? ---> currently enabled.
# check, if any of the existing notifications is a perfect match for notification type & level & shipcall
matches = [note for note in existing_notifications if (int(level)==int(note.level)) and (int(note.type)==int(notification_type)) and (shipcall_id==note.shipcall_id)]
# bool: whether there is a perfect match
exists = len(matches)>0
raise Exception("deprecated")
return exists
@staticmethod
def check_higher_severity(old_state:model.EvaluationType, new_state:model.EvaluationType)->bool:
"""
determines, whether the observed state change should trigger a notification.
internally, this function maps StatusFlags to an integer and determines, if the successor state is more severe than the predecessor.
state changes trigger a notification in the following cases:
green -> yellow
green -> red
yellow -> red
(none -> yellow) or (none -> red)
due to the values in the enumeration objects, the states are mapped to provide this function.
green=1, yellow=2, red=3, none=1. Hence, critical changes can be observed by simply checking with "greater than".
returns bool, whether a notification should be triggered
"""
# undefined previous state: .undefined (0)
if old_state is None:
old_state = model.EvaluationType.undefined
# old_state is always considered at least .green (1) (hence, .undefined becomes .green)
old_state = max(int(old_state), model.EvaluationType.green)
# the IntEnum values are correctly sequenced. .red > .yellow > .green > .undefined
# as .undefined becomes .green, an old_state is always *at least* green.
severity_grew = int(new_state) > int(old_state)
return severity_grew
@staticmethod
def generate_notification_message_by_type(notification_type:model.NotificationType, evaluation_message:str, user:model.User):
assert isinstance(user, model.User)
if int(notification_type) == int(model.NotificationType.undefined):
raise NotImplementedError("skeleton")
elif int(notification_type) == int(model.NotificationType.email):
raise NotImplementedError("skeleton")
elif int(notification_type) == int(model.NotificationType.push):
raise NotImplementedError("skeleton")
#elif int(notification_type) == int(model.NotificationType.whatsapp):
#raise NotImplementedError("skeleton")
#elif int(notification_type) == int(model.NotificationType.signal):
#raise NotImplementedError("skeleton")
elif int(notification_type) == int(model.NotificationType.undefined):
raise NotImplementedError("skeleton")
else:
raise ValueError(notification_type)
return
"""# build the list of evaluation times ('now', as isoformat)"""
#evaluation_times = [datetime.datetime.now().isoformat() for _i in range(len(evaluation_states_new))]

View File

@ -85,10 +85,11 @@ class ShipcallType(IntEnum):
@dataclass @dataclass
class History: class History:
def __init__(self, id, participant_id, shipcall_id, timestamp, eta, type, operation): def __init__(self, id, participant_id, shipcall_id, user_id, timestamp, eta, type, operation):
self.id = id self.id = id
self.participant_id = participant_id self.participant_id = participant_id
self.shipcall_id = shipcall_id self.shipcall_id = shipcall_id
self.user_id = user_id
self.timestamp = timestamp self.timestamp = timestamp
self.eta = eta self.eta = eta
self.type = type self.type = type
@ -98,6 +99,7 @@ class History:
id: int id: int
participant_id: int participant_id: int
shipcall_id: int shipcall_id: int
user_id: int
timestamp: datetime timestamp: datetime
eta: datetime eta: datetime
type: ObjectType type: ObjectType
@ -108,6 +110,7 @@ class History:
"id": self.id, "id": self.id,
"participant_id": self.participant_id, "participant_id": self.participant_id,
"shipcall_id": self.shipcall_id, "shipcall_id": self.shipcall_id,
"user_id": self.user_id,
"timestamp": self.timestamp.isoformat() if self.timestamp else "", "timestamp": self.timestamp.isoformat() if self.timestamp else "",
"eta": self.eta.isoformat() if self.eta else "", "eta": self.eta.isoformat() if self.eta else "",
"type": self.type.name, "type": self.type.name,
@ -115,8 +118,8 @@ class History:
} }
@classmethod @classmethod
def from_query_row(self, id, participant_id, shipcall_id, timestamp, eta, type, operation): def from_query_row(self, id, participant_id, shipcall_id, user_id, timestamp, eta, type, operation):
return self(id, participant_id, shipcall_id, timestamp, eta, ObjectType(type), OperationType(operation)) return self(id, participant_id, shipcall_id, user_id, timestamp, eta, ObjectType(type), OperationType(operation))
class Error(Schema): class Error(Schema):
message = fields.String(metadata={'required':True}) message = fields.String(metadata={'required':True})
@ -135,7 +138,7 @@ class Notification:
""" """
id: int id: int
shipcall_id: int # 'shipcall record that caused the notification' shipcall_id: int # 'shipcall record that caused the notification'
level: int # 'severity of the notification' level: int # 'severity of the notification'. #TODO_determine: Should this be identical to EvaluationType?
type: NotificationType # 'type of the notification' type: NotificationType # 'type of the notification'
message: str # 'individual message' message: str # 'individual message'
created: datetime created: datetime

View File

@ -4,6 +4,7 @@ from BreCal.schemas import model
from BreCal.local_db import getPoolConnection from BreCal.local_db import getPoolConnection
from BreCal.database.update_database import evaluate_shipcall_state from BreCal.database.update_database import evaluate_shipcall_state
from BreCal.database.sql_queries import create_sql_query_shipcall_get from BreCal.database.sql_queries import create_sql_query_shipcall_get
from BreCal.notifications.notifier import Notifier
import threading import threading
import schedule import schedule
@ -51,7 +52,7 @@ def add_function_to_schedule__update_shipcalls(interval_in_minutes:int, options:
return return
def add_function_to_schedule__send_notifications(vr, interval_in_minutes:int=10): def add_function_to_schedule__send_notifications(vr, interval_in_minutes:int=10):
schedule.every(interval_in_minutes).minutes.do(vr.notifier.send_notifications) schedule.every(interval_in_minutes).minutes.do(Notifier.send_notifications)
return return

View File

@ -1,21 +1,21 @@
import datetime import datetime
from BreCal.stubs import generate_uuid1_int from BreCal.stubs import generate_uuid1_int
from BreCal.schemas.model import Notification from BreCal.schemas.model import Notification, NotificationType
def get_notification_simple(): def get_notification_simple():
"""creates a default notification, where 'created' is now, and modified is now+10 seconds""" """creates a default notification, where 'created' is now, and modified is now+10 seconds"""
notification_id = generate_uuid1_int() # uid? notification_id = generate_uuid1_int() # uid?
times_id = generate_uuid1_int() # uid? shipcall_id = 85
level = 10 level = 2
type = 0 type = NotificationType.email
message = "hello world" message = "hello world"
created = datetime.datetime.now() created = datetime.datetime.now()
modified = created+datetime.timedelta(seconds=10) modified = created+datetime.timedelta(seconds=10)
notification = Notification( notification = Notification(
notification_id, notification_id,
times_id, shipcall_id,
level, level,
type, type,
message, message,

View File

@ -7,7 +7,7 @@ import datetime
from BreCal.database.enums import StatusFlags from BreCal.database.enums import StatusFlags
from BreCal.validators.validation_rule_functions import ValidationRuleFunctions from BreCal.validators.validation_rule_functions import ValidationRuleFunctions
from BreCal.schemas.model import Shipcall from BreCal.schemas.model import Shipcall
from BreCal.notifications.notification_functions import Notifier from BreCal.notifications.notification_functions import LegacyNotifier
class ValidationRules(ValidationRuleFunctions): class ValidationRules(ValidationRuleFunctions):
@ -19,7 +19,7 @@ class ValidationRules(ValidationRuleFunctions):
""" """
def __init__(self, sql_handler): # use the entire data that is provided for this query (e.g., json input) def __init__(self, sql_handler): # use the entire data that is provided for this query (e.g., json input)
super().__init__(sql_handler) super().__init__(sql_handler)
self.notifier = Notifier() self.notifier = LegacyNotifier()
return return
def evaluate(self, shipcall): def evaluate(self, shipcall):

View File

@ -3,12 +3,12 @@ import pytest
import os import os
import bcrypt import bcrypt
import pydapper import pydapper
from BreCal import local_db
from BreCal.database.sql_handler import execute_sql_query_standalone 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.stubs.user import get_user_simple from BreCal.stubs.user import get_user_simple
from BreCal import local_db
instance_path = os.path.join(os.path.expanduser('~'), "brecal", "src", "server", "instance", "instance") instance_path = os.path.join(os.path.expanduser('~'), "brecal", "src", "server", "instance", "instance")
local_db.initPool(os.path.dirname(instance_path), connection_filename="connection_data_local.json") local_db.initPool(os.path.dirname(instance_path), connection_filename="connection_data_local.json")
@ -36,13 +36,10 @@ def test_sql_query_get_user():
return return
def test_sql_get_notifications(): def test_sql_get_notifications():
import mysql.connector
# unfortunately, there currently is *no* notification in the database. # unfortunately, there currently is *no* notification in the database.
with pytest.raises(mysql.connector.errors.ProgrammingError, match="Unknown column 'shipcall_id' in 'field list'"): options = {"shipcall_id":222}
options = {"shipcall_id":222} notifications = execute_sql_query_standalone(query=SQLQuery.get_notifications(), param={"scid" : options["shipcall_id"]}, model=model.Notification.from_query_row)
notifications = execute_sql_query_standalone(query=SQLQuery.get_notifications(), param={"scid" : options["shipcall_id"]}, model=model.Notification.from_query_row) assert all([isinstance(notification,model.Notification) for notification in notifications])
assert all([isinstance(notification,model.Notification) for notification in notifications])
return return
def test_sql_get_participants(): def test_sql_get_participants():
@ -458,7 +455,12 @@ def test_sql__shipcall_post__get_last_insert_id__get_spm__update_participants__v
### proxy data ### ### proxy data ###
# loop across passed participant ids, creating entries for those not present in pdata # loop across passed participant ids, creating entries for those not present in pdata
schemaModel = {'id': new_id, "participants":[{'id': 128, 'participant_id': 2, 'type': 4}, {'id': 129, 'participant_id': 3, 'type': 1}, {'id': 130, 'participant_id': 4, 'type': 2}, {'id': 131, 'participant_id': 6, 'type': 8}]} schemaModel = {'id': new_id,
"participants":[{'id': 128, 'participant_id': 2, 'type': 4},
{'id': 129, 'participant_id': 3, 'type': 1},
{'id': 130, 'participant_id': 4, 'type': 2},
{'id': 131, 'participant_id': 5, 'type': 8},
{'id': 131, 'participant_id': 6, 'type': 8}]}
# 4.) assign the participants # 4.) assign the participants
for participant_assignment in schemaModel["participants"]: for participant_assignment in schemaModel["participants"]:
@ -522,6 +524,26 @@ def test_sql_query_put_ship_is_identical_to_legacy_query():
assert query_refactored == query_legacy, f"the refactored code to generate the query must be absolutely identical to the legacy version" assert query_refactored == query_legacy, f"the refactored code to generate the query must be absolutely identical to the legacy version"
return return
def test_sql_query_notification_post():
with pytest.raises(NotImplementedError, match="skeleton"):
# not yet defined. Test also not created yet.
SQLQuery.get_notification_post()
return
def test_sql_query_notification_put():
with pytest.raises(NotImplementedError, match="skeleton"):
# not yet defined. Test also not created yet.
SQLQuery.get_shipcall_put_notification_state()
return
def test_sql_query_notifications_get():
shipcall_id = 85
existing_notifications = execute_sql_query_standalone(query=SQLQuery.get_notifications(), param={"scid" : shipcall_id}, model=model.Notification, command_type="query")
if len(existing_notifications)>0:
assert isinstance(existing_notifications[0], model.Notification), f"returned an incorrect data model. Should have been model.Notification. Found: {type(existing_notifications)} --- {existing_notifications}"
return
#schemas = execute_sql_query_standalone(query=SQLQuery.get_berth(), param={}) #schemas = execute_sql_query_standalone(query=SQLQuery.get_berth(), param={})

View File

@ -0,0 +1,256 @@
import pytest
from BreCal.notifications.notifier import Notifier
from BreCal.schemas.model import Notification
from BreCal.database.sql_queries import SQLQuery
from BreCal.database.sql_handler import execute_sql_query_standalone
from BreCal.schemas import model
from BreCal.stubs.user import get_user_simple
import os
from BreCal import local_db
instance_path = os.path.join(os.path.expanduser('~'), "brecal", "src", "server", "instance", "instance")
local_db.initPool(os.path.dirname(instance_path), connection_filename="connection_data_local.json")
@pytest.fixture(scope="session")
def default_ids():
# #TODO: use a shipcall_id, where a notification actually exists. Otherwise, this is not testable..
shipcall_id = 85
user_id = 29
return locals()
def test_notifier_send_notifications():
"""
the Notifier's .send_notifications() method will be called by the schedule module as part of a routine.
The method shall work without issues or outside arguments.
"""
with pytest.raises(NotImplementedError, match="skeleton"):
Notifier.send_notifications(is_test=True)
return
def test_notifier_create_notification(default_ids):
shipcall_id, user_id = default_ids["shipcall_id"], default_ids["user_id"]
old_state, new_state = model.EvaluationType(1),model.EvaluationType(3)
user = Notifier.get_user(user_id)
user.notify_email = 1
user.notify_popup = 1
user.notify_signal = 1
user.notify_whatsapp = 1
with pytest.raises(NotImplementedError, match="skeleton"):
notification = Notifier.publish(shipcall_id, old_state, new_state, user, update_database=False)
assert isinstance(notification, Notification)
return
def test_notifier_create_notification_update_database(default_ids):
# generate a shipcall
# get the shipcall
# create notifications
shipcall_id, user_id = default_ids["shipcall_id"], default_ids["user_id"]
old_state, new_state = model.EvaluationType(1),model.EvaluationType(3)
user = Notifier.get_user(user_id)
user.notify_email = 1
user.notify_popup = 1
user.notify_signal = 1
user.notify_whatsapp = 1
with pytest.raises(NotImplementedError, match="skeleton"):
notification = Notifier.publish(shipcall_id, old_state, new_state, user, update_database=True)
# #TODO: apply GetNotifications(*args) and identify, whether the notification exists now. Use an obvious 'message' to identify pytest creations.
return
def test_notifier_get_user(default_ids):
user_id = default_ids["user_id"]
user_data = get_user_simple().__dict__
user_data["id"] = user_id
user = Notifier.get_user(user_data["id"])
assert hasattr(user, "notify_email")
assert hasattr(user, "notify_popup")
assert hasattr(user, "notify_whatsapp")
assert hasattr(user, "notify_signal")
return
def test_notification_type():
"""this pytest serves as a warning method to inform the developers, once novel NotificationType s have been included."""
members = [key for key in model.NotificationType.__members__]
expected_member_count = 3
assert len(members)==expected_member_count, f"the BreCal.model.NotificationType has been changed. Initially, there were only {expected_member_count} members. Now that there are more members, the Notifier.build_notification_type_list and its pytest can be updated. This pytest shall also be updated!"
return
def test_notifier_build_notification_type_list(default_ids):
user_id = default_ids["user_id"]
user = Notifier.get_user(user_id)
user.notify_email = 1
user.notify_popup = 1
user.notify_whatsapp = 1
user.notify_signal = 1
notification_type_list = Notifier.build_notification_type_list(user)
assert len(notification_type_list) == 4, f"there must be n=4 notifications, as there are fours types of notifications."
assert len([ntype for ntype in notification_type_list if ntype==model.NotificationType.email])==1, f"the EMAIL notification instruction was not created"
assert len([ntype for ntype in notification_type_list if ntype==model.NotificationType.push])==1, f"the POPUP/PUSH notification instruction was not created"
assert len([ntype for ntype in notification_type_list if ntype==model.NotificationType.undefined])==2, f"there must currently be two notifications, which have an undefined type (SIGNAL and WHATSAPP)"
return
def test_notifier_build_notification_type_list__behaves_as_expected(default_ids):
user_id = default_ids["user_id"]
user = Notifier.get_user(user_id)
# only email
user.notify_email = 1
user.notify_popup = 0
user.notify_whatsapp = 0
user.notify_signal = 0
notification_type_list = Notifier.build_notification_type_list(user)
assert len(notification_type_list) == 1
assert int(notification_type_list[0])==int(model.NotificationType.email), f"should've returned email, but did not"
# only popup
user.notify_email = 0
user.notify_popup = 1
user.notify_whatsapp = 0
user.notify_signal = 0
notification_type_list = Notifier.build_notification_type_list(user)
assert len(notification_type_list) == 1
assert int(notification_type_list[0])==int(model.NotificationType.push), f"should've returned push, but did not"
# only whatsapp
user.notify_email = 0
user.notify_popup = 0
user.notify_whatsapp = 1
user.notify_signal = 0
notification_type_list = Notifier.build_notification_type_list(user)
assert len(notification_type_list) == 1
# #TODO: adapt the NotificationType.undefined once a data model exists for whatsapp
assert int(notification_type_list[0])==int(model.NotificationType.undefined), f"should've returned whatsapp, but did not"
# only signal
user.notify_email = 0
user.notify_popup = 0
user.notify_whatsapp = 0
user.notify_signal = 1
notification_type_list = Notifier.build_notification_type_list(user)
assert len(notification_type_list) == 1
# #TODO: adapt the NotificationType.undefined once a data model exists for signal
assert int(notification_type_list[0])==int(model.NotificationType.undefined), f"should've returned signal, but did not"
return
def test_notifier_get_shipcall():
shipcall_id = 85
shipcall = Notifier.get_shipcall(shipcall_id=shipcall_id)
assert shipcall.id==85
assert shipcall.arrival_berth_id == 194
return
def test_notifier_get_existing_notifications(default_ids):
shipcall_id = default_ids["shipcall_id"]
existing_notifications = Notifier.get_existing_notifications(shipcall_id)
assert isinstance(existing_notifications, list)
if len(existing_notifications)>0:
assert all([isinstance(existing_notification, model.Notification) for existing_notification in existing_notifications])
return
def test_notifier_check_notification_type_and_level_exists(default_ids):
"""this method has been deprecated, because the conceptual-logic does not add up."""
with pytest.raises(Exception, match="deprecated"):
shipcall_id = default_ids["shipcall_id"]
from BreCal.stubs.notification import get_notification_simple
notification = get_notification_simple().__dict__
notification["shipcall_id"] = shipcall_id
level = 2
existing_notifications = Notifier.get_existing_notifications(shipcall_id=notification.get("shipcall_id"))
exists = Notifier.check_notification_type_and_level_exists(shipcall_id=shipcall_id, notification_type=notification.get("type"), level=level, existing_notifications=existing_notifications)
# assert exists==True, f"for the given notification, the method should return, that the notification already exists."
return
def test_notifier_check_higher_severity():
"""
Checks all possible combinations of model.EvaluationType within Notifier.check_higher_severity
It should prove, that a boolean True is only returned, when the severity grows. In particular,
undefined or green -> yellow
undefined or green -> red
yellow -> red
"""
# [FALSE] cases: should not trigger the creation of a Notification
old_state, new_state = model.EvaluationType.green, model.EvaluationType.undefined
assert not Notifier.check_higher_severity(old_state, new_state), f"this case should not create a Notification!"
old_state, new_state = model.EvaluationType.yellow, model.EvaluationType.green
assert not Notifier.check_higher_severity(old_state, new_state), f"this case should not create a Notification!"
old_state, new_state = model.EvaluationType.yellow, model.EvaluationType.undefined
assert not Notifier.check_higher_severity(old_state, new_state), f"this case should not create a Notification!"
old_state, new_state = model.EvaluationType.red, model.EvaluationType.yellow
assert not Notifier.check_higher_severity(old_state, new_state), f"this case should not create a Notification!"
old_state, new_state = model.EvaluationType.red, model.EvaluationType.green
assert not Notifier.check_higher_severity(old_state, new_state), f"this case should not create a Notification!"
old_state, new_state = model.EvaluationType.red, model.EvaluationType.undefined
assert not Notifier.check_higher_severity(old_state, new_state), f"this case should not create a Notification!"
old_state, new_state = model.EvaluationType.undefined, model.EvaluationType.green
assert not Notifier.check_higher_severity(old_state, new_state), f"this case should not create a Notification!"
# [FALSE] SAME EVALUATION as before
old_state, new_state = model.EvaluationType.undefined, model.EvaluationType.undefined
assert not Notifier.check_higher_severity(old_state, new_state), f"this case should not create a Notification!"
old_state, new_state = model.EvaluationType.green, model.EvaluationType.green
assert not Notifier.check_higher_severity(old_state, new_state), f"this case should not create a Notification!"
old_state, new_state = model.EvaluationType.yellow, model.EvaluationType.yellow
assert not Notifier.check_higher_severity(old_state, new_state), f"this case should not create a Notification!"
old_state, new_state = model.EvaluationType.red, model.EvaluationType.red
assert not Notifier.check_higher_severity(old_state, new_state), f"this case should not create a Notification!"
# [TRUE] cases: should then trigger the creation of a Notification
old_state, new_state = model.EvaluationType.green, model.EvaluationType.yellow
assert Notifier.check_higher_severity(old_state, new_state), f"this case should create a Notification, but the method did not recognize it."
old_state, new_state = model.EvaluationType.yellow, model.EvaluationType.red
assert Notifier.check_higher_severity(old_state, new_state), f"this case should create a Notification, but the method did not recognize it."
old_state, new_state = model.EvaluationType.green, model.EvaluationType.red
assert Notifier.check_higher_severity(old_state, new_state), f"this case should create a Notification, but the method did not recognize it."
old_state, new_state = model.EvaluationType.undefined, model.EvaluationType.yellow
assert Notifier.check_higher_severity(old_state, new_state), f"this case should create a Notification, but the method did not recognize it."
old_state, new_state = model.EvaluationType.undefined, model.EvaluationType.red
assert Notifier.check_higher_severity(old_state, new_state), f"this case should create a Notification, but the method did not recognize it."
return
def test_notifier_loop_generate_notification_message_by_type(default_ids):
user_id = default_ids["user_id"]
user = execute_sql_query_standalone(query=SQLQuery.get_user_by_id(), param={"id" : user_id}, model=model.User, command_type="single")
# enable all notification types
user.notify_email=1
user.notify_whatsapp=1
user.notify_signal=1
user.notify_popup=1
evaluation_message = "abc"
with pytest.raises(NotImplementedError, match="skeleton"):
# create a list of notification types and create a message for each of them
notification_type_list = Notifier.build_notification_type_list(user)
for notification_type in notification_type_list:
message = Notifier.generate_notification_message_by_type(notification_type, evaluation_message, user)
assert isinstance(message, str), f"Message must be a string. Found: {type(message)} --- {message}"
assert not evaluation_message=="abc", f"#TODO_metz: lazy development. have to update the message body"
return