From f91263d1ad7987aa9b2e3501fba9e44bdd6e808c Mon Sep 17 00:00:00 2001 From: davelopez <46503462+davelopez@users.noreply.github.com> Date: Fri, 5 Apr 2024 15:52:19 +0200 Subject: [PATCH] Handle channel settings depending on server config External notification channels (like email) are processed in Celery tasks. When Celery is not enabled in the server, we cannot show those channels to the user and we have to indicate in the API that those will be ignored. --- client/src/api/notifications.preferences.ts | 15 +++-- client/src/api/schema/schema.ts | 6 ++ .../NotificationsPreferences.vue | 13 ++-- lib/galaxy/managers/notification.py | 66 ++++++++++++------- lib/galaxy/schema/notifications.py | 5 +- .../webapps/galaxy/api/notifications.py | 14 +++- 6 files changed, 80 insertions(+), 39 deletions(-) diff --git a/client/src/api/notifications.preferences.ts b/client/src/api/notifications.preferences.ts index 59b910d8c346..8509d7cd2691 100644 --- a/client/src/api/notifications.preferences.ts +++ b/client/src/api/notifications.preferences.ts @@ -1,11 +1,18 @@ import { type components, fetcher } from "@/api/schema"; -export type UserNotificationPreferences = components["schemas"]["UserNotificationPreferences"]; +type UserNotificationPreferences = components["schemas"]["UserNotificationPreferences"]; + +export interface UserNotificationPreferencesExtended extends UserNotificationPreferences { + supportedChannels: string[]; +} const getNotificationsPreferences = fetcher.path("/api/notifications/preferences").method("get").create(); -export async function getNotificationsPreferencesFromServer() { - const { data } = await getNotificationsPreferences({}); - return data; +export async function getNotificationsPreferencesFromServer(): Promise { + const { data, headers } = await getNotificationsPreferences({}); + return { + ...data, + supportedChannels: headers.get("supported-channels")?.split(",") ?? [], + }; } type UpdateUserNotificationPreferencesRequest = components["schemas"]["UpdateUserNotificationPreferencesRequest"]; diff --git a/client/src/api/schema/schema.ts b/client/src/api/schema/schema.ts index dbc4567a868c..8786ebbb1e4e 100644 --- a/client/src/api/schema/schema.ts +++ b/client/src/api/schema/schema.ts @@ -1255,6 +1255,9 @@ export interface paths { /** * Returns the current user's preferences for notifications. * @description Anonymous users cannot have notification preferences. They will receive only broadcasted notifications. + * + * - The settings will contain all possible channels, but the client should only show the ones that are really supported by the server. + * The supported channels are returned in the `supported-channels` header. */ get: operations["get_notification_preferences_api_notifications_preferences_get"]; /** @@ -20160,6 +20163,9 @@ export interface operations { /** * Returns the current user's preferences for notifications. * @description Anonymous users cannot have notification preferences. They will receive only broadcasted notifications. + * + * - The settings will contain all possible channels, but the client should only show the ones that are really supported by the server. + * The supported channels are returned in the `supported-channels` header. */ parameters?: { /** @description The user ID that will be used to effectively make this API call. Only admins and designated users can make API calls on behalf of other users. */ diff --git a/client/src/components/User/Notifications/NotificationsPreferences.vue b/client/src/components/User/Notifications/NotificationsPreferences.vue index 5d6f0c3ca017..48487103bcbd 100644 --- a/client/src/components/User/Notifications/NotificationsPreferences.vue +++ b/client/src/components/User/Notifications/NotificationsPreferences.vue @@ -8,7 +8,7 @@ import { computed, ref, watch } from "vue"; import { getNotificationsPreferencesFromServer, updateNotificationsPreferencesOnServer, - UserNotificationPreferences, + UserNotificationPreferencesExtended, } from "@/api/notifications.preferences"; import { useConfig } from "@/composables/config"; import { Toast } from "@/composables/toast"; @@ -39,14 +39,15 @@ const { config } = useConfig(true); const loading = ref(false); const errorMessage = ref(null); const pushNotificationsGranted = ref(pushNotificationsEnabled()); -const notificationsPreferences = ref({}); +const notificationsPreferences = ref({}); +const supportedChannels = ref([]); const categories = computed(() => Object.keys(notificationsPreferences.value)); const showPreferences = computed(() => { return !loading.value && config.value.enable_notification_system && notificationsPreferences.value; }); -const categoryDescriptionMap = { +const categoryDescriptionMap: Record = { message: "You will receive notifications when someone sends you a message.", new_shared_item: "You will receive notifications when someone shares an item with you.", }; @@ -55,6 +56,7 @@ async function getNotificationsPreferences() { loading.value = true; await getNotificationsPreferencesFromServer() .then((data) => { + supportedChannels.value = data.supportedChannels; notificationsPreferences.value = data.preferences; }) .catch((error: any) => { @@ -148,10 +150,7 @@ watch( switch /> -
+
List[User]: + pass + + +class NotificationChannelPlugin(Protocol): + config: GalaxyAppConfiguration + + def __init__(self, config: GalaxyAppConfiguration): + self.config = config + + def send(self, notification: Notification, user: User): + raise NotImplementedError + + class NotificationManager: """Manager class to interact with the database models related with Notifications.""" @@ -113,11 +128,7 @@ def __init__(self, sa_session: galaxy_scoped_session, config: GalaxyAppConfigura Notification.expiration_time, Notification.content, ] - # Register the supported notification channels here - self.channel_plugins: Dict[str, NotificationChannelPlugin] = { - "push": NoOpNotificationChannelPlugin(self.config), # Push notifications are handled by the client - "email": EmailNotificationChannelPlugin(self.config), - } + self.channel_plugins = self._register_supported_channels() @property def notifications_enabled(self): @@ -407,14 +418,30 @@ def update_user_notification_preferences( self, user: User, request: UpdateUserNotificationPreferencesRequest ) -> UserNotificationPreferences: """Updates the user's notification preferences with the requested changes.""" - notification_preferences = self.get_user_notification_preferences(user) - notification_preferences.update(request.preferences) - user.preferences[NOTIFICATION_PREFERENCES_SECTION_NAME] = ( - notification_preferences.model_dump_json() - ) # type:ignore[index] + preferences = self.get_user_notification_preferences(user) + preferences.update(request.preferences) + user.preferences[NOTIFICATION_PREFERENCES_SECTION_NAME] = preferences.model_dump_json() # type:ignore[index] with transaction(self.sa_session): self.sa_session.commit() - return notification_preferences + return preferences + + def _register_supported_channels(self) -> Dict[str, NotificationChannelPlugin]: + """Registers the supported notification channels in this server.""" + supported_channels: Dict[str, NotificationChannelPlugin] = { + # Push notifications are handled client-side so no real plugin is needed + "push": NoOpNotificationChannelPlugin(self.config), + } + + if self.can_send_notifications_async: + # Most additional channels require asynchronous processing and will be + # handled by Celery tasks. Add their plugins here. + supported_channels["email"] = EmailNotificationChannelPlugin(self.config) + + return supported_channels + + def get_supported_channels(self) -> Set[str]: + """Returns the set of supported notification channels in this server.""" + return set(self.channel_plugins.keys()) def cleanup_expired_notifications(self) -> CleanupResultSummary: """ @@ -492,9 +519,8 @@ def _broadcasted_notifications_query(self, since: Optional[datetime] = None, act return stmt -class NotificationRecipientResolverStrategy(Protocol): - def resolve_users(self, recipients: NotificationRecipients) -> List[User]: - pass +# -------------------------------------- +# Notification Recipients Resolver Implementations class NotificationRecipientResolver: @@ -603,17 +629,7 @@ def resolve_users(self, recipients: NotificationRecipients) -> List[User]: # -------------------------------------- -# Notification Channel Plugins - - -class NotificationChannelPlugin(Protocol): - config: GalaxyAppConfiguration - - def __init__(self, config: GalaxyAppConfiguration): - self.config = config - - def send(self, notification: Notification, user: User): - raise NotImplementedError +# Notification Channel Plugins Implementations class NoOpNotificationChannelPlugin(NotificationChannelPlugin): diff --git a/lib/galaxy/schema/notifications.py b/lib/galaxy/schema/notifications.py index bbf19a9bc39d..f78bc0954480 100644 --- a/lib/galaxy/schema/notifications.py +++ b/lib/galaxy/schema/notifications.py @@ -421,7 +421,10 @@ class NotificationChannelSettings(Model): email: bool = Field( default=True, title="Email", - description="Whether the user wants to receive email notifications for this category.", + description=( + "Whether the user wants to receive email notifications for this category. " + "This setting will be ignored unless the server supports asynchronous tasks." + ), ) # TODO: Add more channels here and implement the corresponding plugin in lib/galaxy/managers/notification.py # matrix: bool # Possible future Matrix.org integration? diff --git a/lib/galaxy/webapps/galaxy/api/notifications.py b/lib/galaxy/webapps/galaxy/api/notifications.py index dd23fa050311..f1bfbcab88a3 100644 --- a/lib/galaxy/webapps/galaxy/api/notifications.py +++ b/lib/galaxy/webapps/galaxy/api/notifications.py @@ -70,10 +70,20 @@ def get_notifications_status( ) def get_notification_preferences( self, + response: Response, trans: ProvidesUserContext = DependsOnTrans, ) -> UserNotificationPreferences: - """Anonymous users cannot have notification preferences. They will receive only broadcasted notifications.""" - return self.service.get_user_notification_preferences(trans) + """Anonymous users cannot have notification preferences. They will receive only broadcasted notifications. + + - The settings will contain all possible channels, but the client should only show the ones that are really supported by the server. + The supported channels are returned in the `supported-channels` header. + """ + result = self.service.get_user_notification_preferences(trans) + # Inform the client which channels are really supported by the server since the settings will contain all possible channels. + response.headers["supported-channels"] = str.join( + ",", self.service.notification_manager.get_supported_channels() + ) + return result @router.put( "/api/notifications/preferences",