Skip to content

Commit

Permalink
Merge pull request galaxyproject#17914 from davelopez/add_email_notif…
Browse files Browse the repository at this point in the history
…ications_channel

Add `email` notifications channel
  • Loading branch information
jdavcs authored Apr 16, 2024
2 parents 0a68b5d + 23164de commit b60bbbd
Show file tree
Hide file tree
Showing 34 changed files with 1,031 additions and 147 deletions.
15 changes: 11 additions & 4 deletions client/src/api/notifications.preferences.ts
Original file line number Diff line number Diff line change
@@ -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<UserNotificationPreferencesExtended> {
const { data, headers } = await getNotificationsPreferences({});
return {
...data,
supportedChannels: headers.get("supported-channels")?.split(",") ?? [],
};
}

type UpdateUserNotificationPreferencesRequest = components["schemas"]["UpdateUserNotificationPreferencesRequest"];
Expand Down
2 changes: 1 addition & 1 deletion client/src/api/notifications.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export type NewSharedItemNotificationContentItemType =

type UserNotificationUpdateRequest = components["schemas"]["UserNotificationUpdateRequest"];

type NotificationCreateRequest = components["schemas"]["NotificationCreateRequest"];
export type NotificationCreateRequest = components["schemas"]["NotificationCreateRequest"];

type NotificationResponse = components["schemas"]["NotificationResponse"];

Expand Down
31 changes: 20 additions & 11 deletions client/src/api/schema/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"];
/**
Expand Down Expand Up @@ -9564,6 +9567,7 @@ export interface components {
* Channels
* @description The channels that the user wants to receive notifications from for this category.
* @default {
* "email": true,
* "push": true
* }
*/
Expand All @@ -9580,6 +9584,12 @@ export interface components {
* @description The settings for each channel of a notification category.
*/
NotificationChannelSettings: {
/**
* Email
* @description Whether the user wants to receive email notifications for this category. This setting will be ignored unless the server supports asynchronous tasks.
* @default true
*/
email?: boolean;
/**
* Push
* @description Whether the user wants to receive push notifications in the browser for this category.
Expand Down Expand Up @@ -9628,10 +9638,7 @@ export interface components {
*/
variant: components["schemas"]["NotificationVariant"];
};
/**
* NotificationCreateRequest
* @description Contains the recipients and the notification to create.
*/
/** NotificationCreateRequest */
NotificationCreateRequest: {
/**
* Notification
Expand All @@ -9642,7 +9649,7 @@ export interface components {
* Recipients
* @description The recipients of the notification. Can be a combination of users, groups and roles.
*/
recipients: components["schemas"]["NotificationRecipients"];
recipients: components["schemas"]["NotificationRecipientsRequest"];
};
/** NotificationCreatedResponse */
NotificationCreatedResponse: {
Expand All @@ -9657,11 +9664,8 @@ export interface components {
*/
total_notifications_sent: number;
};
/**
* NotificationRecipients
* @description The recipients of a notification. Can be a combination of users, groups and roles.
*/
NotificationRecipients: {
/** NotificationRecipientsRequest */
NotificationRecipientsRequest: {
/**
* Group IDs
* @description The list of encoded group IDs of the groups that should receive the notification.
Expand Down Expand Up @@ -19983,7 +19987,9 @@ export interface operations {
/** @description Successful Response */
200: {
content: {
"application/json": components["schemas"]["NotificationCreatedResponse"];
"application/json":
| components["schemas"]["NotificationCreatedResponse"]
| components["schemas"]["AsyncTaskResultSummary"];
};
};
/** @description Validation Error */
Expand Down Expand Up @@ -20155,6 +20161,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. */
Expand Down
10 changes: 9 additions & 1 deletion client/src/components/Notifications/NotificationsList.vue
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,17 @@ library.add(faCog, faHourglassHalf, faRetweet);
const notificationsStore = useNotificationsStore();
const { notifications, loadingNotifications } = storeToRefs(notificationsStore);
interface Props {
shouldOpenPreferences?: boolean;
}
const props = withDefaults(defineProps<Props>(), {
shouldOpenPreferences: false,
});
const showUnread = ref(false);
const showShared = ref(false);
const preferencesOpen = ref(false);
const preferencesOpen = ref(props.shouldOpenPreferences);
const selectedNotificationIds = ref<string[]>([]);
const haveSelected = computed(() => selectedNotificationIds.value.length > 0);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -39,14 +39,15 @@ const { config } = useConfig(true);
const loading = ref(false);
const errorMessage = ref<string | null>(null);
const pushNotificationsGranted = ref(pushNotificationsEnabled());
const notificationsPreferences = ref<UserNotificationPreferences["preferences"]>({});
const notificationsPreferences = ref<UserNotificationPreferencesExtended["preferences"]>({});
const supportedChannels = ref<string[]>([]);
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<string, string> = {
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.",
};
Expand All @@ -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) => {
Expand Down Expand Up @@ -148,10 +150,7 @@ watch(
switch />
</div>

<div
v-for="channel in Object.keys(notificationsPreferences[category].channels)"
:key="channel"
class="category-channel">
<div v-for="channel in supportedChannels" :key="channel" class="category-channel">
<BFormCheckbox
v-model="notificationsPreferences[category].channels[channel]"
v-localize
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { computed, type Ref, ref } from "vue";
import { useRouter } from "vue-router/composables";
import { getAllGroups } from "@/api/groups";
import { sendNotification } from "@/api/notifications";
import { NotificationCreateRequest, sendNotification } from "@/api/notifications";
import { getAllRoles } from "@/api/roles";
import { type components } from "@/api/schema";
import { getAllUsers } from "@/api/users";
Expand All @@ -25,7 +25,6 @@ library.add(faInfoCircle);
type SelectOption = [string, string];
type NotificationCreateData = components["schemas"]["NotificationCreateData"];
type NotificationCreateRequest = components["schemas"]["NotificationCreateRequest"];
interface MessageNotificationCreateData extends NotificationCreateData {
category: "message";
Expand Down
3 changes: 3 additions & 0 deletions client/src/entry/analysis/router.js
Original file line number Diff line number Diff line change
Expand Up @@ -450,6 +450,9 @@ export function getRouter(Galaxy) {
path: "user/notifications",
component: NotificationsList,
redirect: redirectIf(!Galaxy.config.enable_notification_system, "/") || redirectAnon(),
props: (route) => ({
shouldOpenPreferences: Boolean(route.query.preferences),
}),
},
{
path: "user/notifications/preferences",
Expand Down
11 changes: 11 additions & 0 deletions doc/source/admin/galaxy_options.rst
Original file line number Diff line number Diff line change
Expand Up @@ -5463,6 +5463,17 @@
:Type: int


~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
``dispatch_notifications_interval``
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

:Description:
The interval in seconds between attempts to dispatch notifications
to users (every 10 minutes by default). Runs in a Celery task.
:Default: ``600``
:Type: int


~~~~~~~~~~~~~~~~~~~~~~
``help_forum_api_url``
~~~~~~~~~~~~~~~~~~~~~~
Expand Down
5 changes: 4 additions & 1 deletion lib/galaxy/celery/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -238,7 +238,10 @@ def schedule_task(task, interval):
beat_schedule: Dict[str, Dict[str, Any]] = {}
schedule_task("prune_history_audit_table", config.history_audit_table_prune_interval)
schedule_task("cleanup_short_term_storage", config.short_term_storage_cleanup_interval)
schedule_task("cleanup_expired_notifications", config.expired_notifications_cleanup_interval)

if config.enable_notification_system:
schedule_task("cleanup_expired_notifications", config.expired_notifications_cleanup_interval)
schedule_task("dispatch_pending_notifications", config.dispatch_notifications_interval)

if config.object_store_cache_monitor_driver in ["auto", "celery"]:
schedule_task("clean_object_store_caches", config.object_store_cache_monitor_interval)
Expand Down
19 changes: 19 additions & 0 deletions lib/galaxy/celery/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
from galaxy.objectstore import BaseObjectStore
from galaxy.objectstore.caching import check_caches
from galaxy.queue_worker import GalaxyQueueWorker
from galaxy.schema.notifications import NotificationCreateRequest
from galaxy.schema.tasks import (
ComputeDatasetHashTaskRequest,
GenerateHistoryContentDownload,
Expand Down Expand Up @@ -483,3 +484,21 @@ def cleanup_expired_notifications(notification_manager: NotificationManager):
@galaxy_task(action="prune object store cache directories")
def clean_object_store_caches(object_store: BaseObjectStore):
check_caches(object_store.cache_targets())


@galaxy_task(action="send notifications to all recipients")
def send_notification_to_recipients_async(
request: NotificationCreateRequest, notification_manager: NotificationManager
):
"""Send a notification to a list of users."""
_, notifications_sent = notification_manager.send_notification_to_recipients(request=request)

log.info(f"Successfully sent {notifications_sent} notifications.")


@galaxy_task(action="dispatch pending notifications")
def dispatch_pending_notifications(notification_manager: NotificationManager):
"""Dispatch pending notifications."""
count = notification_manager.dispatch_pending_notifications_via_channels()
if count:
log.info(f"Successfully dispatched {count} notifications.")
4 changes: 4 additions & 0 deletions lib/galaxy/config/sample/galaxy.yml.sample
Original file line number Diff line number Diff line change
Expand Up @@ -2911,6 +2911,10 @@ galaxy:
# a Celery task.
#expired_notifications_cleanup_interval: 86400

# The interval in seconds between attempts to dispatch notifications
# to users (every 10 minutes by default). Runs in a Celery task.
#dispatch_notifications_interval: 600

# The URL pointing to the Galaxy Help Forum API base URL. The API must
# be compatible with Discourse API (https://docs.discourse.org/).
#help_forum_api_url: https://help.galaxyproject.org/
Expand Down
7 changes: 7 additions & 0 deletions lib/galaxy/config/schemas/config_schema.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3990,6 +3990,13 @@ mapping:
desc: |
The interval in seconds between attempts to delete all expired notifications from the database (every 24 hours by default). Runs in a Celery task.
dispatch_notifications_interval:
type: int
required: false
default: 600
desc: |
The interval in seconds between attempts to dispatch notifications to users (every 10 minutes by default). Runs in a Celery task.
help_forum_api_url:
type: str
required: false
Expand Down
84 changes: 84 additions & 0 deletions lib/galaxy/config/templates/mail/notifications/message-email.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
Use this template to customize the HTML-formatted email your users will receive
when a new notification of category "message" is sent to them.
Copy the file to {{ templates_dir }}/mail/notifications/message-email.html and modify as required.

If you are adding URLs, remember that only absolute URLs (with
a domain name) make sense in email! They can be served from any stable
location, including your Galaxy server or GitHub.

The following variables are available for inserting into the HTML with Jinja2
syntax, like {{ variable_name }}. They will be rendered into the text before
the email is sent:

- name The user's name
- user_email The user's email
- date Date and time of the notification
- hostname Your galaxy's hostname (i.e. usegalaxy.* or the value in `server_name` from the galaxy config file)
- contact_email Your galaxy's contact email
- notification_settings_url The URL to the user's notification settings to manage their subscriptions
- content The message payload
- subject The message subject
- content The message content in HTML (converted from Markdown)
- galaxy_url The URL to the Galaxy instance (i.e. https://usegalaxy.*)

Template begins here >>>>>>
<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
<meta charset="utf-8">
<title>[Galaxy] New message received: {{ content['subject'] }}</title>
<link href="https://fonts.googleapis.com/css2?family=Roboto&display=swap" rel="stylesheet">
</head>
<body style="font-family: 'Roboto', sans-serif;">

<p style="font-size: 12pt;">
Hello {{ name }},<br><br>

You have received a new message on <b>{{ date }}</b> from the Galaxy Team at <b>{{ hostname }}</b>, here are the details:
<br><br>
</p>

<p style="font-size: 12pt;">
<strong>Subject:</strong>
<br>
{{ content['subject'] }}
<br><br>
<strong>Message:</strong>
<br>
{{ content['message'] }}
<br><br>
</p>

<p style="font-size: 12pt;">
Thank you for using Galaxy!
</p>


<p style="font-size: 12pt;">
Regards,<br>
Your Galaxy Team at <a href="{{ galaxy_url }}">{{ hostname }}</a>
</p>

<p style="font-size: 10pt;">
You received this email because you are subscribed to receive notifications from the Galaxy Team.
{% if notification_settings_url %}
You can manage your notification settings <a href="{{ notification_settings_url }}">here</a>.
{% endif %}

<br>

{% if contact_email %}
This is an automated email. If you have any questions or concerns, please do not reply to this email, instead, contact us at <a href="mailto:{{ contact_email }}">{{ contact_email }}</a>.
{% endif %}
</p>

<img
style="width: 130px; height: auto; margin: 15px 0;"
src="https://galaxyproject.org/images/galaxy-logos/galaxy_project_logo_square.png"
alt="Galaxy project logo"
>

<br>

</body>
</html>
Loading

0 comments on commit b60bbbd

Please sign in to comment.