From f1c5bd3cd85301e7fedca096111367afcdd969c5 Mon Sep 17 00:00:00 2001 From: Daniel Schick Date: Wed, 5 Feb 2025 19:24:07 +0100 Subject: [PATCH] Added event type evaluation and storage of selection bitflag. Fixed some details in the UI --- src/BreCalClient/AboutDialog.xaml | 2 +- src/BreCalClient/AboutDialog.xaml.cs | 6 +- src/BreCalClient/AppNotification.cs | 26 ++++----- src/BreCalClient/MainWindow.xaml.cs | 58 ++++++++++--------- src/BreCalClient/Resources/Resources.de.resx | 3 + src/server/BreCal/api/user.py | 7 +-- src/server/BreCal/database/sql_queries.py | 2 +- src/server/BreCal/impl/login.py | 6 +- src/server/BreCal/impl/user.py | 11 +++- src/server/BreCal/schemas/model.py | 26 ++++++++- .../BreCal/services/schedule_routines.py | 6 +- src/server/BreCal/stubs/user.py | 13 +++-- 12 files changed, 99 insertions(+), 67 deletions(-) diff --git a/src/BreCalClient/AboutDialog.xaml b/src/BreCalClient/AboutDialog.xaml index 76a4af8..f43da10 100644 --- a/src/BreCalClient/AboutDialog.xaml +++ b/src/BreCalClient/AboutDialog.xaml @@ -21,12 +21,12 @@ - + diff --git a/src/BreCalClient/AboutDialog.xaml.cs b/src/BreCalClient/AboutDialog.xaml.cs index d2def00..005d6fb 100644 --- a/src/BreCalClient/AboutDialog.xaml.cs +++ b/src/BreCalClient/AboutDialog.xaml.cs @@ -57,11 +57,11 @@ namespace BreCalClient this.LoginResult.UserPhone = this.textBoxUserPhone.Text.Trim(); this.LoginResult.UserEmail = this.textBoxUserEmail.Text.Trim(); this.LoginResult.NotifyEmail = this.checkboxEMailNotify.IsChecked ?? false; - this.LoginResult.NotifyPopup = this.checkboxPushNotify.IsChecked ?? false; - this.LoginResult.NotifyOn.Clear(); + this.LoginResult.NotifyPopup = this.checkboxPushNotify.IsChecked ?? false; if ((this.checkListBoxEventSelection.SelectedItems.Count > 0) && (this.LoginResult.NotifyOn == null)) this.LoginResult.NotifyOn = new(); - foreach(NotificationType nt in this.checkListBoxEventSelection.SelectedItems) + this.LoginResult.NotifyOn.Clear(); + foreach (NotificationType nt in this.checkListBoxEventSelection.SelectedItems) this.LoginResult.NotifyOn.Add(nt); this.ChangeUserSettingsRequested?.Invoke(); } diff --git a/src/BreCalClient/AppNotification.cs b/src/BreCalClient/AppNotification.cs index 46faaa5..7db4d5c 100644 --- a/src/BreCalClient/AppNotification.cs +++ b/src/BreCalClient/AppNotification.cs @@ -8,24 +8,17 @@ using System.Collections.Generic; using System.Collections.ObjectModel; using ToastNotifications.Core; using BreCalClient.misc.Model; -using System.Runtime.CompilerServices; namespace BreCalClient { - internal class AppNotification + internal class AppNotification(int id) { - private static readonly Dictionary _notifications = new(); - private readonly int _id; - private static readonly ObservableCollection _notificationsCollection = new(); - - public AppNotification(int id) - { - _id = id; - } + private static readonly Dictionary _notifications = []; + private static readonly ObservableCollection _notificationsCollection = []; #region Properties - public int Id { get { return _id; } } + public int Id { get { return id; } } public string? NotificationType { @@ -95,7 +88,7 @@ namespace BreCalClient SaveNotifications(); } - internal static bool UpdateNotifications(List notifications, System.Collections.Concurrent.ConcurrentDictionary currentShipcalls, ToastViewModel vm) + internal static bool UpdateNotifications(List notifications, System.Collections.Concurrent.ConcurrentDictionary currentShipcalls, ToastViewModel vm, LoginResult loginResult) { bool result = false; @@ -121,6 +114,10 @@ namespace BreCalClient if (!iAmAssigned) continue; } + // filter out notifications the user is not interested in + if((notification.Type != null) && !loginResult.NotifyOn.Contains(notification.Type.Value)) + continue; + if (!_notificationsCollection.Where(x => x.Id == notification.Id).Any()) { List newList = new(_notificationsCollection); @@ -176,9 +173,8 @@ namespace BreCalClient if (!string.IsNullOrEmpty(ap.Message)) toastText += $" \n{ap.Message}"; - if (!_notifications.ContainsKey(notification.Id)) + if (_notifications.TryAdd(notification.Id, ap)) { - _notifications.Add(notification.Id, ap); App.Current.Dispatcher.Invoke(() => { vm.ShowAppNotification(toastText, options); @@ -197,7 +193,7 @@ namespace BreCalClient internal static void SaveNotifications() { if (Properties.Settings.Default.Notifications == null) - Properties.Settings.Default.Notifications = new(); + Properties.Settings.Default.Notifications = []; else Properties.Settings.Default.Notifications.Clear(); foreach (int notification_id in _notifications.Keys) diff --git a/src/BreCalClient/MainWindow.xaml.cs b/src/BreCalClient/MainWindow.xaml.cs index 724f984..ed94257 100644 --- a/src/BreCalClient/MainWindow.xaml.cs +++ b/src/BreCalClient/MainWindow.xaml.cs @@ -52,7 +52,7 @@ namespace BreCalClient private readonly ConcurrentDictionary _allShipcallsDict = new(); private readonly ConcurrentDictionary _allShipCallsControlDict = new(); - private readonly List _visibleControlModels = new(); + private readonly List _visibleControlModels = []; private readonly ShipcallApi _shipcallApi; private readonly UserApi _userApi; @@ -195,7 +195,7 @@ namespace BreCalClient } catch (ApiException ex) { - if ((ex.ErrorContent != null && ((string)ex.ErrorContent).StartsWith("{"))) { + if ((ex.ErrorContent != null && ((string)ex.ErrorContent).StartsWith('{'))) { Error? anError = JsonConvert.DeserializeObject((string)ex.ErrorContent); if ((anError != null) && anError.ErrorField.Equals("invalid credentials")) this.labelLoginResult.Content = BreCalClient.Resources.Resources.textWrongCredentials; @@ -309,7 +309,7 @@ namespace BreCalClient scmOut.Shipcall.DepartureBerthId = esc.ShipcallModel.Shipcall?.ArrivalBerthId; if (esc.ShipcallModel.Shipcall != null) { - scmOut.Shipcall.Participants = new(); + scmOut.Shipcall.Participants = []; scmOut.Shipcall.Participants.AddRange(esc.ShipcallModel.Shipcall.Participants); foreach(ParticipantType pType in esc.ShipcallModel.AssignedParticipants.Keys) scmOut.AssignedParticipants[pType] = esc.ShipcallModel.AssignedParticipants[pType]; @@ -418,7 +418,7 @@ namespace BreCalClient { this.searchFilterControl.SearchFilter.Ports.Clear(); - List berths = new(); + List berths = []; foreach (Port port in comboBoxPorts.SelectedItems) { this.searchFilterControl.SearchFilter.Ports.Add(port.Id); @@ -449,8 +449,8 @@ namespace BreCalClient _historyDialog.Closed += (sender, e) => { this._historyDialog = null; }; _historyDialog.HistoryItemSelected += (x) => { - if(_allShipCallsControlDict.ContainsKey(x)) - _allShipCallsControlDict[x].BringIntoView(); + if(_allShipCallsControlDict.TryGetValue(x, out ShipcallControl? value)) + value.BringIntoView(); }; _historyDialog.Show(); } @@ -514,14 +514,14 @@ namespace BreCalClient SearchFilterModel? currentFilter = null; if (SearchFilterModel.filterMap != null) { - if((_loginResult != null) && SearchFilterModel.filterMap.ContainsKey(_loginResult.Id)) + if((_loginResult != null) && SearchFilterModel.filterMap.TryGetValue(_loginResult.Id, out SearchFilterModel? value)) { - currentFilter = SearchFilterModel.filterMap[_loginResult.Id]; + currentFilter = value; } } else { - SearchFilterModel.filterMap = new(); + SearchFilterModel.filterMap = []; } if (currentFilter == null) { @@ -607,7 +607,7 @@ namespace BreCalClient // load times for each shipcall List currentTimes = await _timesApi.TimesGetAsync(shipcall.Id); - if (!_allShipcallsDict.ContainsKey(shipcall.Id)) + if (!_allShipcallsDict.TryGetValue(shipcall.Id, out ShipcallControlModel? value)) { // add entry ShipcallControlModel scm = new() @@ -619,10 +619,9 @@ namespace BreCalClient } else { - // update entry - _allShipcallsDict[shipcall.Id].Shipcall = shipcall; - _allShipcallsDict[shipcall.Id].Times = currentTimes; - UpdateShipcall(_allShipcallsDict[shipcall.Id]); + value.Shipcall = shipcall; + value.Times = currentTimes; + UpdateShipcall(value); } } @@ -674,7 +673,7 @@ namespace BreCalClient if (_loginResult?.NotifyPopup ?? false) { List notifications = await _staticApi.NotificationsGetAsync(); - AppNotification.UpdateNotifications(notifications, _allShipcallsDict, _vm); + AppNotification.UpdateNotifications(notifications, _allShipcallsDict, _vm, _loginResult); } } } @@ -689,8 +688,8 @@ namespace BreCalClient _allShipcallsDict[scm.Shipcall.Id] = scm; Shipcall shipcall = scm.Shipcall; - if (BreCalLists.ShipLookupDict.ContainsKey(shipcall.ShipId)) - scm.Ship = BreCalLists.ShipLookupDict[shipcall.ShipId].Ship; + if (BreCalLists.ShipLookupDict.TryGetValue(shipcall.ShipId, out ShipModel? value)) + scm.Ship = value.Ship; if (shipcall.Type == ShipcallType.Arrival) { @@ -723,8 +722,8 @@ namespace BreCalClient { if(scm.Shipcall == null) return; Shipcall shipcall = scm.Shipcall; - if (BreCalLists.ShipLookupDict.ContainsKey(shipcall.ShipId)) - scm.Ship = BreCalLists.ShipLookupDict[shipcall.ShipId].Ship; + if (BreCalLists.ShipLookupDict.TryGetValue(shipcall.ShipId, out ShipModel? value)) + scm.Ship = value.Ship; if (shipcall.Type == ShipcallType.Arrival) { @@ -969,10 +968,10 @@ namespace BreCalClient foreach (ShipcallControlModel visibleModel in this._visibleControlModels) { if (visibleModel.Shipcall == null) continue; // should not happen - if (this._allShipCallsControlDict.ContainsKey(visibleModel.Shipcall.Id)) + if (this._allShipCallsControlDict.TryGetValue(visibleModel.Shipcall.Id, out ShipcallControl? value)) { - this._allShipCallsControlDict[visibleModel.Shipcall.Id].RefreshData(); - this.stackPanel.Children.Add(this._allShipCallsControlDict[visibleModel.Shipcall.Id]); + value.RefreshData(); + this.stackPanel.Children.Add(value); } } } @@ -1122,8 +1121,8 @@ namespace BreCalClient } else { - if(editControl.ShipcallModel.AssignedParticipants.ContainsKey(ParticipantType.AGENCY)) - editControl.Times.ParticipantId = editControl.ShipcallModel.AssignedParticipants[ParticipantType.AGENCY].ParticipantId; + if(editControl.ShipcallModel.AssignedParticipants.TryGetValue(ParticipantType.AGENCY, out ParticipantAssignment? value)) + editControl.Times.ParticipantId = value.ParticipantId; } editControl.Times.ParticipantType = (int)ParticipantType.AGENCY; if(editControl.ShowDialog() ?? false) @@ -1139,9 +1138,9 @@ namespace BreCalClient } // always try to be the agent, even if we are BSMD - if (editControl.ShipcallModel.AssignedParticipants.ContainsKey(ParticipantType.AGENCY)) + if (editControl.ShipcallModel.AssignedParticipants.TryGetValue(ParticipantType.AGENCY, out ParticipantAssignment? value)) { - editControl.Times.ParticipantId = editControl.ShipcallModel.AssignedParticipants[ParticipantType.AGENCY].ParticipantId; + editControl.Times.ParticipantId = value.ParticipantId; } else { @@ -1190,7 +1189,7 @@ namespace BreCalClient // (if the special-flag is enabled). Assigned Agency: ShipcallParticipantMap(id=628, shipcall_id=115, participant_id=10, // type=8, created=datetime.datetime(2024, 8, 28, 15, 13, 14), modified=None) with Flags: 42\"} - Match m = Regex.Match(message, "\\{(.*)\\}"); + Match m = ErrorRegex().Match(message); if ((m != null) && m.Success) { try @@ -1235,7 +1234,10 @@ namespace BreCalClient e.Handled = true; } + [GeneratedRegex("\\{(.*)\\}")] + private static partial Regex ErrorRegex(); + #endregion - + } } diff --git a/src/BreCalClient/Resources/Resources.de.resx b/src/BreCalClient/Resources/Resources.de.resx index 834875f..5c2755a 100644 --- a/src/BreCalClient/Resources/Resources.de.resx +++ b/src/BreCalClient/Resources/Resources.de.resx @@ -595,4 +595,7 @@ Der Anlauf wurde storniert + + Benachrichtigung bei + \ No newline at end of file diff --git a/src/server/BreCal/api/user.py b/src/server/BreCal/api/user.py index b77c12c..27d7436 100644 --- a/src/server/BreCal/api/user.py +++ b/src/server/BreCal/api/user.py @@ -2,8 +2,7 @@ from flask import Blueprint, request from ..schemas import model from .. import impl from ..services.auth_guard import auth_guard -import json -import logging + from marshmallow import ValidationError from . import verify_if_request_is_json from BreCal.validators.validation_error import create_dynamic_exception_response, create_validation_error_response @@ -16,11 +15,11 @@ def PutUser(): try: verify_if_request_is_json(request) - + content = request.get_json(force=True) loadedModel = model.UserSchema().load(data=content, many=False, partial=True) return impl.user.PutUser(loadedModel) - + except ValidationError as ex: return create_validation_error_response(ex=ex, status_code=400) diff --git a/src/server/BreCal/database/sql_queries.py b/src/server/BreCal/database/sql_queries.py index ade5b26..47e4bfa 100644 --- a/src/server/BreCal/database/sql_queries.py +++ b/src/server/BreCal/database/sql_queries.py @@ -237,7 +237,7 @@ class SQLQuery(): @staticmethod def get_user()->str: query = "SELECT id, participant_id, first_name, last_name, user_name, user_email, user_phone, password_hash, " +\ - "api_key, notify_email, notify_whatsapp, notify_signal, notify_popup, created, modified FROM user " +\ + "api_key, notify_email, notify_whatsapp, notify_signal, notify_popup, notify_event, created, modified FROM user " +\ "WHERE user_name = ?username? OR user_email = ?username?" return query diff --git a/src/server/BreCal/impl/login.py b/src/server/BreCal/impl/login.py index c65d630..2810b6a 100644 --- a/src/server/BreCal/impl/login.py +++ b/src/server/BreCal/impl/login.py @@ -8,6 +8,7 @@ from .. import local_db from ..services import jwt_handler from BreCal.database.sql_queries import SQLQuery + def GetUser(options): try: @@ -18,7 +19,7 @@ def GetUser(options): # query = SQLQuery.get_user() # data = commands.query(query, model=model.User, param={"username" : options["username"]}) data = commands.query("SELECT id, participant_id, first_name, last_name, user_name, user_email, user_phone, password_hash, " + - "api_key, notify_email, notify_whatsapp, notify_signal, notify_popup, created, modified FROM user " + + "api_key, notify_email, notify_whatsapp, notify_signal, notify_popup, notify_event, created, modified FROM user " + "WHERE user_name = ?username? OR user_email = ?username?", model=model.User, param={"username" : options["username"]}) @@ -35,7 +36,8 @@ def GetUser(options): "notify_email": data[0].notify_email, "notify_whatsapp": data[0].notify_whatsapp, "notify_signal": data[0].notify_signal, - "notify_popup": data[0].notify_popup + "notify_popup": data[0].notify_popup, + "notify_on": model.bitflag_to_list(data[0].notify_event) } token = jwt_handler.generate_jwt(payload=result, lifetime=120) # generate token valid 60 mins result["token"] = token # add token to user data diff --git a/src/server/BreCal/impl/user.py b/src/server/BreCal/impl/user.py index 6229bc8..6d7fd45 100644 --- a/src/server/BreCal/impl/user.py +++ b/src/server/BreCal/impl/user.py @@ -35,7 +35,7 @@ def PutUser(schemaModel): # should this be refactored? # Also, what about the 'user_name'? # 'participant_id' would also not trigger an update in isolation - if "first_name" in schemaModel or "last_name" in schemaModel or "user_phone" in schemaModel or "user_email" in schemaModel or "notify_email" in schemaModel or "notify_whatsapp" in schemaModel or "notify_signal" in schemaModel or "notify_popup" in schemaModel: + if "first_name" in schemaModel or "last_name" in schemaModel or "user_phone" in schemaModel or "user_email" in schemaModel or "notify_email" in schemaModel or "notify_whatsapp" in schemaModel or "notify_signal" in schemaModel or "notify_popup" in schemaModel or "notify_on" in schemaModel: # query = SQLQuery.get_user_put(schemaModel) query = "UPDATE user SET " isNotFirst = False @@ -49,7 +49,14 @@ def PutUser(schemaModel): if isNotFirst: query += ", " isNotFirst = True - query += key + " = ?" + key + "? " + + if key != "notify_on": + query += key + " = ?" + key + "? " + else: + flag_value = model.list_to_bitflag(schemaModel["notify_on"]) + query += "notify_event = " + str(flag_value) + " " + + query += "WHERE id = ?id?" affected_rows = commands.execute(query, param=schemaModel) diff --git a/src/server/BreCal/schemas/model.py b/src/server/BreCal/schemas/model.py index 4a75daa..5dd51ad 100644 --- a/src/server/BreCal/schemas/model.py +++ b/src/server/BreCal/schemas/model.py @@ -10,11 +10,12 @@ from typing import List import json import re import datetime + from BreCal.validators.time_logic import validate_time_is_in_not_too_distant_future from BreCal.validators.validation_base_utils import check_if_string_has_special_characters from BreCal.database.enums import ParticipantType, ParticipantFlag -# from BreCal. ... import check_if_user_is_bsmd_type + def obj_dict(obj): if isinstance(obj, datetime.datetime): @@ -84,6 +85,21 @@ class NotificationType(IntEnum): def _missing_(cls, value): return cls.undefined +def bitflag_to_list(bitflag: int) -> list[NotificationType]: + if bitflag is None: + return [] + """Converts an integer bitflag to a list of NotificationType enums.""" + return [nt for nt in NotificationType if bitflag & (1 << (nt.value - 1))] + +def list_to_bitflag(notifications: fields.List) -> int: + """Converts a list of NotificationType enums to an integer bitflag.""" + try: + iter(notifications) + return sum(1 << (nt.value - 1) for nt in notifications) + except TypeError as te: + return 0 + + class ShipcallType(IntEnum): undefined = 0 arrival = 1 @@ -497,6 +513,7 @@ class UserSchema(Schema): notify_whatsapp = fields.Bool(allow_none=True, required=False) notify_signal = fields.Bool(allow_none=True, required=False) notify_popup = fields.Bool(allow_none=True, required=False) + notify_on = fields.List(fields.Enum(NotificationType), required=False, allow_none=True) @validates("user_phone") def validate_user_phone(self, value): @@ -507,7 +524,7 @@ class UserSchema(Schema): @validates("user_email") def validate_user_email(self, value): - if value and not re.match(r"[^@]+@[^@]+\.[^@]+", value) in value: + if value and not re.match(r"[^@]+@[^@]+\.[^@]+", value): raise ValidationError({"user_email":f"invalid email address"}) @@ -556,10 +573,15 @@ class User: notify_popup: bool created: datetime modified: datetime + ports: List[NotificationType] = field(default_factory=list) + notify_event: List[NotificationType] = field(default_factory=list) def __hash__(self): return hash(id) + def wants_notification(self, notification_type: NotificationType): + return notification_type in self.notify_event + @dataclass class Ship: id: int diff --git a/src/server/BreCal/services/schedule_routines.py b/src/server/BreCal/services/schedule_routines.py index 0ac3c8f..9971fe2 100644 --- a/src/server/BreCal/services/schedule_routines.py +++ b/src/server/BreCal/services/schedule_routines.py @@ -229,14 +229,14 @@ def SendNotifications(): users = users_dict[notification.participant_id] for user in users: # send notification to user - if user.notify_email: + if user.notify_email and user.wants_notifications(notification.type): if user not in email_dict: email_dict[user] = [] email_dict[user].append(notification) - if user.notify_whatsapp: + if user.notify_whatsapp and user.wants_notifications(notification.type): # TBD pass - if user.notify_signal: + if user.notify_signal and user.wants_notifications(notification.type): # TBD pass diff --git a/src/server/BreCal/stubs/user.py b/src/server/BreCal/stubs/user.py index 908f512..8e6c6e3 100644 --- a/src/server/BreCal/stubs/user.py +++ b/src/server/BreCal/stubs/user.py @@ -18,18 +18,19 @@ def get_user_simple(): created = datetime.datetime.now() modified = created+datetime.timedelta(seconds=10) - + notify_email = True notify_whatsapp = True notify_signal = True notify_popup = True + user = User( - user_id, - participant_id, - first_name, - last_name, - user_name, + user_id, + participant_id, + first_name, + last_name, + user_name, user_email, user_phone, password_hash,