mirror of
https://gitlab.com/bramw/baserow.git
synced 2025-04-15 01:28:30 +00:00
Resolve "Email notifications should have links to redirect the user directly to the specific comment, field, etc."
This commit is contained in:
parent
a704135b07
commit
e643ac84ab
34 changed files with 321 additions and 163 deletions
backend
src/baserow
tests/baserow
contrib/database/field
core/notifications
changelog/entries/unreleased/feature
premium
backend
src/baserow_premium/row_comments
tests/baserow_premium_tests/row_comments
web-frontend/modules/baserow_premium
components/row_comments
notificationTypes.jsweb-frontend/modules
core
components/notifications
BaserowVersionUpgradeNotification.vueWorkspaceInvitationAcceptedNotification.vueWorkspaceInvitationCreatedNotification.vueWorkspaceInvitationRejectedNotification.vue
mixins
notificationTypes.jspages
routes.jsdatabase
components/notifications
notificationTypes.js
|
@ -45,6 +45,7 @@ class CollaboratorAddedToRowNotificationType(
|
|||
EmailNotificationTypeMixin, NotificationType
|
||||
):
|
||||
type = "collaborator_added_to_row"
|
||||
has_web_frontend_route = True
|
||||
|
||||
@classmethod
|
||||
def get_notification_title_for_email(cls, notification, context):
|
||||
|
@ -172,6 +173,7 @@ class UserMentionInRichTextFieldNotificationType(
|
|||
EmailNotificationTypeMixin, NotificationType
|
||||
):
|
||||
type = "user_mention_in_rich_text_field"
|
||||
has_web_frontend_route = True
|
||||
|
||||
@classmethod
|
||||
def get_notification_title_for_email(cls, notification, context):
|
||||
|
|
|
@ -30,6 +30,7 @@ class FormSubmittedNotificationData:
|
|||
|
||||
class FormSubmittedNotificationType(EmailNotificationTypeMixin, NotificationType):
|
||||
type = "form_submitted"
|
||||
has_web_frontend_route = True
|
||||
|
||||
@classmethod
|
||||
def create_form_submitted_notification(
|
||||
|
|
|
@ -160,10 +160,12 @@ class NotificationsSummaryEmail(BaseEmailMessage):
|
|||
notification, context
|
||||
)
|
||||
)
|
||||
email_url = notification_type.get_web_frontend_url(notification)
|
||||
rendered_notifications.append(
|
||||
{
|
||||
"title": email_title,
|
||||
"description": email_description,
|
||||
"url": email_url,
|
||||
}
|
||||
)
|
||||
unlisted_notifications_count = self.new_notifications_count - len(
|
||||
|
|
|
@ -78,7 +78,7 @@ class Command(BaseCommand):
|
|||
timestamp = options["timestamp"]
|
||||
|
||||
if user_id is not None and not frequency:
|
||||
result = NotificationHandler.send_new_notifications_to_users_matching_filters_by_email(
|
||||
result = NotificationHandler.send_unread_notifications_by_email_to_users_matching_filters(
|
||||
Q(id=user_id), max_emails=max_emails
|
||||
)
|
||||
logger.info(
|
||||
|
|
|
@ -752,7 +752,7 @@ class NotificationHandler:
|
|||
|
||||
@classmethod
|
||||
@baserow_trace(tracer)
|
||||
def send_new_notifications_to_users_matching_filters_by_email(
|
||||
def send_unread_notifications_by_email_to_users_matching_filters(
|
||||
cls, user_filters_q: Q, max_emails: Optional[int] = None
|
||||
) -> UserWithScheduledEmailNotifications:
|
||||
"""
|
||||
|
|
|
@ -55,6 +55,13 @@ class Notification(models.Model):
|
|||
)
|
||||
data = models.JSONField(default=dict, help_text="The data of the notification.")
|
||||
|
||||
@property
|
||||
def web_frontend_url(self):
|
||||
from .registries import notification_type_registry
|
||||
|
||||
notification_type = notification_type_registry.get(self.type)
|
||||
return notification_type.get_web_frontend_url(self)
|
||||
|
||||
class Meta:
|
||||
ordering = ["-created_on"]
|
||||
indexes = [
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
from abc import ABCMeta, abstractmethod
|
||||
from typing import Optional
|
||||
from urllib.parse import urljoin
|
||||
|
||||
from django.conf import settings
|
||||
|
||||
from baserow.core.exceptions import (
|
||||
InstanceTypeAlreadyRegistered,
|
||||
|
@ -20,6 +23,18 @@ class NotificationType(MapAPIExceptionsInstanceMixin, Instance):
|
|||
model_class = Notification
|
||||
include_in_notifications_email = False
|
||||
|
||||
def get_web_frontend_url(self, notification: Notification) -> Optional[str]:
|
||||
"""
|
||||
Can optionally return a public URL related to the notification. This is
|
||||
typically used when the user wants to get more information about the
|
||||
notification.
|
||||
|
||||
:param notification: The notification for which we want to generate the URL.
|
||||
:return: The generated URL as string, or None if not compatible.
|
||||
"""
|
||||
|
||||
return None
|
||||
|
||||
|
||||
class EmailNotificationTypeMixin(metaclass=ABCMeta):
|
||||
"""
|
||||
|
@ -29,6 +44,12 @@ class EmailNotificationTypeMixin(metaclass=ABCMeta):
|
|||
|
||||
include_in_notifications_email = True
|
||||
|
||||
has_web_frontend_route = False
|
||||
"""
|
||||
If `True`, then the notification will be clickable in the email. Note that this
|
||||
will only work if a route is defined in the web-frontend.
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
@abstractmethod
|
||||
def get_notification_title_for_email(cls, notification, context) -> str:
|
||||
|
@ -46,6 +67,16 @@ class EmailNotificationTypeMixin(metaclass=ABCMeta):
|
|||
and context.
|
||||
"""
|
||||
|
||||
def get_web_frontend_url(self, notification):
|
||||
if not self.has_web_frontend_route:
|
||||
return None
|
||||
|
||||
base_url = settings.BASEROW_EMBEDDED_SHARE_URL
|
||||
# This path must match the one defined in web-frontend/modules/core/routes.js
|
||||
path = f"/notification/{notification.workspace_id}/{notification.id}"
|
||||
|
||||
return urljoin(base_url, path)
|
||||
|
||||
|
||||
class CliNotificationTypeMixin(metaclass=ABCMeta):
|
||||
@classmethod
|
||||
|
|
|
@ -8,6 +8,7 @@ from baserow.core.notifications.handler import NotificationHandler
|
|||
from baserow.core.notifications.models import NotificationRecipient
|
||||
from baserow.core.registries import OperationType
|
||||
|
||||
from .exceptions import NotificationDoesNotExist
|
||||
from .operations import (
|
||||
ListNotificationsOperationType,
|
||||
MarkNotificationAsReadOperationType,
|
||||
|
@ -36,6 +37,35 @@ class NotificationService:
|
|||
|
||||
return NotificationHandler.list_notifications(user, workspace)
|
||||
|
||||
@classmethod
|
||||
def get_notification(
|
||||
cls,
|
||||
user: AbstractUser,
|
||||
workspace_id: int,
|
||||
notification_id: int,
|
||||
) -> NotificationRecipient:
|
||||
"""
|
||||
Get notification
|
||||
|
||||
:param user: The user on whose behalf the request is made.
|
||||
:param workspace_id: The workspace id to get the notification for.
|
||||
:param notification_id: The notification id to get.
|
||||
"""
|
||||
|
||||
workspace = cls.get_workspace_if_has_permissions_or_raise(
|
||||
user, workspace_id, ListNotificationsOperationType
|
||||
)
|
||||
|
||||
try:
|
||||
notification = NotificationHandler.all_notifications_for_user(
|
||||
user, workspace
|
||||
).get(notification_id=notification_id)
|
||||
except NotificationRecipient.DoesNotExist:
|
||||
raise NotificationDoesNotExist(
|
||||
f"Notification {notification_id} is not found."
|
||||
)
|
||||
return notification
|
||||
|
||||
@classmethod
|
||||
def mark_notification_as_read(
|
||||
cls,
|
||||
|
|
|
@ -128,11 +128,9 @@ def send_instant_notifications_email_to_users():
|
|||
)
|
||||
max_emails = settings.EMAIL_NOTIFICATIONS_LIMIT_PER_TASK[notifications_frequency]
|
||||
|
||||
return (
|
||||
NotificationHandler.send_new_notifications_to_users_matching_filters_by_email(
|
||||
Q(profile__email_notification_frequency=notifications_frequency),
|
||||
max_emails,
|
||||
)
|
||||
return NotificationHandler.send_unread_notifications_by_email_to_users_matching_filters(
|
||||
Q(profile__email_notification_frequency=notifications_frequency),
|
||||
max_emails,
|
||||
)
|
||||
|
||||
|
||||
|
@ -177,7 +175,7 @@ def send_daily_notifications_email_to_users(now: Optional[datetime] = None):
|
|||
notifications_frequency = UserProfile.EmailNotificationFrequencyOptions.DAILY.value
|
||||
max_emails = settings.EMAIL_NOTIFICATIONS_LIMIT_PER_TASK[notifications_frequency]
|
||||
|
||||
return handler.send_new_notifications_to_users_matching_filters_by_email(
|
||||
return handler.send_unread_notifications_by_email_to_users_matching_filters(
|
||||
Q(
|
||||
profile__email_notification_frequency=notifications_frequency,
|
||||
profile__timezone__in=timezones_to_send_notifications,
|
||||
|
@ -209,7 +207,7 @@ def send_weekly_notifications_email_to_users(now: Optional[datetime] = None):
|
|||
notifications_frequency = UserProfile.EmailNotificationFrequencyOptions.WEEKLY.value
|
||||
max_emails = settings.EMAIL_NOTIFICATIONS_LIMIT_PER_TASK[notifications_frequency]
|
||||
|
||||
return handler.send_new_notifications_to_users_matching_filters_by_email(
|
||||
return handler.send_unread_notifications_by_email_to_users_matching_filters(
|
||||
Q(
|
||||
profile__email_notification_frequency=notifications_frequency,
|
||||
profile__timezone__in=timezones_to_send_notifications,
|
||||
|
|
|
@ -119,7 +119,7 @@ class SnapshotHandler:
|
|||
mark_for_deletion=False,
|
||||
)
|
||||
.select_related("created_by")
|
||||
.order_by("-created_at")
|
||||
.order_by("-created_at", "-id")
|
||||
)
|
||||
|
||||
def create(self, application_id: int, performed_by: User, name: str):
|
||||
|
|
|
@ -64,6 +64,13 @@
|
|||
color="#9c9c9f"
|
||||
font-family="Inter,sans-serif"
|
||||
/>
|
||||
<mj-class
|
||||
name="notification-title"
|
||||
font-size="14px"
|
||||
color="#070810"
|
||||
font-family="Inter,sans-serif"
|
||||
line-height="170%"
|
||||
/>
|
||||
<mj-class
|
||||
name="notification-description"
|
||||
font-size="12px"
|
||||
|
|
|
@ -59,10 +59,8 @@
|
|||
</style>
|
||||
<![endif]-->
|
||||
<!--[if !mso]><!-->
|
||||
<link href="https://fonts.googleapis.com/css?family=Ubuntu:300,400,500,700" rel="stylesheet" type="text/css">
|
||||
<link href="https://fonts.googleapis.com/css?family=Inter:400,600" rel="stylesheet" type="text/css">
|
||||
<style type="text/css">
|
||||
@import url(https://fonts.googleapis.com/css?family=Ubuntu:300,400,500,700);
|
||||
@import url(https://fonts.googleapis.com/css?family=Inter:400,600);
|
||||
</style>
|
||||
<!--<![endif]-->
|
||||
|
@ -156,11 +154,22 @@
|
|||
</tr>
|
||||
<!-- htmlmin:ignore -->{% for notification in notifications %}
|
||||
<!-- htmlmin:ignore -->
|
||||
<!-- htmlmin:ignore -->{% if notification.url %}
|
||||
<!-- htmlmin:ignore -->
|
||||
<tr>
|
||||
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div style="font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:13px;line-height:1;text-align:left;color:#000000;">{{ notification.title }}</div>
|
||||
<div style="font-family:Inter,sans-serif;font-size:14px;line-height:170%;text-align:left;color:#070810;"><a style="font-family:Inter,sans-serif;font-size:14px;line-height:170%;text-align:left;color:#070810;" href="{{ notification.url }}">{{ notification.title }}</a></div>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- htmlmin:ignore -->{% else %}
|
||||
<!-- htmlmin:ignore -->
|
||||
<tr>
|
||||
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div style="font-family:Inter,sans-serif;font-size:14px;line-height:170%;text-align:left;color:#070810;">{{ notification.title }}</div>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- htmlmin:ignore -->{% endif %}
|
||||
<!-- htmlmin:ignore -->
|
||||
<!-- htmlmin:ignore -->{% if notification.description %}
|
||||
<!-- htmlmin:ignore -->
|
||||
<tr>
|
||||
|
|
|
@ -11,7 +11,12 @@
|
|||
</mj-text>
|
||||
<mj-divider border-width="1px" border-style="dashed" border-color="lightgrey" />
|
||||
<mj-raw><!-- htmlmin:ignore -->{% for notification in notifications %}<!-- htmlmin:ignore --></mj-raw>
|
||||
<mj-text mj-class="notification-title">{{ notification.title }}</mj-text>
|
||||
{{ notification.url }}
|
||||
<mj-raw><!-- htmlmin:ignore -->{% if notification.url %}<!-- htmlmin:ignore --></mj-raw>
|
||||
<mj-text mj-class="notification-title"><a style="font-family:Inter,sans-serif;font-size:14px;line-height:170%;text-align:left;color:#070810;" href="{{ notification.url }}">{{ notification.title }}</a></mj-text>
|
||||
<mj-raw><!-- htmlmin:ignore -->{% else %}<!-- htmlmin:ignore --></mj-raw>
|
||||
<mj-text mj-class="notification-title">{{ notification.title }}</mj-text>
|
||||
<mj-raw><!-- htmlmin:ignore -->{% endif %}<!-- htmlmin:ignore --></mj-raw>
|
||||
<mj-raw><!-- htmlmin:ignore -->{% if notification.description %}<!-- htmlmin:ignore --></mj-raw>
|
||||
<mj-text mj-class="notification-description">{{ notification.description|linebreaksbr }}</mj-text>
|
||||
<mj-raw><!-- htmlmin:ignore -->{% endif %}<!-- htmlmin:ignore --></mj-raw>
|
||||
|
|
|
@ -464,7 +464,7 @@ def test_email_notifications_are_created_correctly_for_collaborators_added(
|
|||
assert response.status_code == HTTP_200_OK
|
||||
|
||||
# Force to send the notifications
|
||||
res = NotificationHandler.send_new_notifications_to_users_matching_filters_by_email(
|
||||
res = NotificationHandler.send_unread_notifications_by_email_to_users_matching_filters(
|
||||
Q(pk=user_2.pk)
|
||||
)
|
||||
assert res.users_with_notifications == [user_2]
|
||||
|
@ -479,12 +479,16 @@ def test_email_notifications_are_created_correctly_for_collaborators_added(
|
|||
assert user_2_summary_email.to == [user_2.email]
|
||||
assert user_2_summary_email.get_subject() == "You have 1 new notification - Baserow"
|
||||
|
||||
notif = NotificationRecipient.objects.get(recipient=user_2)
|
||||
notification_url = f"http://localhost:3000/notification/{notif.workspace_id}/{notif.notification_id}"
|
||||
|
||||
expected_context = {
|
||||
"notifications": [
|
||||
{
|
||||
"title": f"User 1 assigned you to Collaborator 1 in row unnamed row"
|
||||
f" {row.id} in Example.",
|
||||
"description": None,
|
||||
"url": notification_url,
|
||||
}
|
||||
],
|
||||
"new_notifications_count": 1,
|
||||
|
@ -1024,7 +1028,7 @@ def test_email_notifications_are_created_correctly_for_mentions_in_rich_text_fie
|
|||
)
|
||||
|
||||
# Force to send the notifications
|
||||
res = NotificationHandler.send_new_notifications_to_users_matching_filters_by_email(
|
||||
res = NotificationHandler.send_unread_notifications_by_email_to_users_matching_filters(
|
||||
Q(pk=user_2.pk)
|
||||
)
|
||||
assert res.users_with_notifications == [user_2]
|
||||
|
@ -1039,6 +1043,9 @@ def test_email_notifications_are_created_correctly_for_mentions_in_rich_text_fie
|
|||
assert user_2_summary_email.to == [user_2.email]
|
||||
assert user_2_summary_email.get_subject() == "You have 1 new notification - Baserow"
|
||||
|
||||
notif = NotificationRecipient.objects.get(recipient=user_2)
|
||||
notification_url = f"http://localhost:3000/notification/{notif.workspace_id}/{notif.notification_id}"
|
||||
|
||||
expected_context = {
|
||||
"notifications": [
|
||||
{
|
||||
|
@ -1046,6 +1053,7 @@ def test_email_notifications_are_created_correctly_for_mentions_in_rich_text_fie
|
|||
f"Lisa Smith mentioned you in RichTextField in row unnamed row {row.id} in Example."
|
||||
),
|
||||
"description": None,
|
||||
"url": notification_url,
|
||||
}
|
||||
],
|
||||
"new_notifications_count": 1,
|
||||
|
|
|
@ -0,0 +1,37 @@
|
|||
import pytest
|
||||
|
||||
from baserow.core.models import Notification
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_get_web_frontend_url(data_fixture):
|
||||
user = data_fixture.create_user()
|
||||
workspace = data_fixture.create_workspace(user=user)
|
||||
|
||||
notification = data_fixture.create_workspace_notification_for_users(
|
||||
recipients=[user], workspace=workspace
|
||||
)
|
||||
|
||||
assert notification.web_frontend_url is None
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_get_web_frontend_url_with_notification_that_has_url(data_fixture):
|
||||
workspace = data_fixture.create_workspace()
|
||||
notification = Notification.objects.create(
|
||||
type="form_submitted",
|
||||
workspace_id=workspace.id,
|
||||
data={
|
||||
"row_id": 1,
|
||||
"values": [["Name", "1"]],
|
||||
"form_id": 1,
|
||||
"table_id": 2,
|
||||
"form_name": "Form",
|
||||
"table_name": "Table",
|
||||
"database_id": 3,
|
||||
},
|
||||
)
|
||||
|
||||
assert notification.web_frontend_url == (
|
||||
f"http://localhost:3000/notification/{workspace.id}/{notification.id}"
|
||||
)
|
|
@ -373,19 +373,19 @@ def test_not_all_notification_types_are_included_in_the_email_notification_summa
|
|||
notification_type=ExcludedFromEmailTestNotification.type,
|
||||
)
|
||||
|
||||
res = NotificationHandler.send_new_notifications_to_users_matching_filters_by_email(
|
||||
res = NotificationHandler.send_unread_notifications_by_email_to_users_matching_filters(
|
||||
Q(profile__email_notification_frequency=options.DAILY)
|
||||
)
|
||||
assert res.users_with_notifications == []
|
||||
assert res.remaining_users_to_notify_count == 0
|
||||
|
||||
res = NotificationHandler.send_new_notifications_to_users_matching_filters_by_email(
|
||||
res = NotificationHandler.send_unread_notifications_by_email_to_users_matching_filters(
|
||||
Q(profile__email_notification_frequency=options.WEEKLY)
|
||||
)
|
||||
assert res.users_with_notifications == []
|
||||
assert res.remaining_users_to_notify_count == 0
|
||||
|
||||
res = NotificationHandler.send_new_notifications_to_users_matching_filters_by_email(
|
||||
res = NotificationHandler.send_unread_notifications_by_email_to_users_matching_filters(
|
||||
Q(profile__email_notification_frequency=options.INSTANT)
|
||||
)
|
||||
assert res.users_with_notifications == [user_1]
|
||||
|
@ -407,6 +407,7 @@ def test_not_all_notification_types_are_included_in_the_email_notification_summa
|
|||
{
|
||||
"title": "Test notification",
|
||||
"description": None,
|
||||
"url": None,
|
||||
}
|
||||
],
|
||||
"new_notifications_count": 1,
|
||||
|
@ -434,19 +435,19 @@ def test_no_email_without_renderable_notifications(
|
|||
notification_type=ExcludedFromEmailTestNotification.type,
|
||||
)
|
||||
|
||||
res = NotificationHandler.send_new_notifications_to_users_matching_filters_by_email(
|
||||
res = NotificationHandler.send_unread_notifications_by_email_to_users_matching_filters(
|
||||
Q(profile__email_notification_frequency=options.DAILY)
|
||||
)
|
||||
assert res.users_with_notifications == []
|
||||
assert res.remaining_users_to_notify_count == 0
|
||||
|
||||
res = NotificationHandler.send_new_notifications_to_users_matching_filters_by_email(
|
||||
res = NotificationHandler.send_unread_notifications_by_email_to_users_matching_filters(
|
||||
Q(profile__email_notification_frequency=options.WEEKLY)
|
||||
)
|
||||
assert res.users_with_notifications == []
|
||||
assert res.remaining_users_to_notify_count == 0
|
||||
|
||||
res = NotificationHandler.send_new_notifications_to_users_matching_filters_by_email(
|
||||
res = NotificationHandler.send_unread_notifications_by_email_to_users_matching_filters(
|
||||
Q(profile__email_notification_frequency=options.INSTANT)
|
||||
)
|
||||
assert res.users_with_notifications == []
|
||||
|
@ -487,7 +488,7 @@ def test_user_with_daily_email_notification_frequency_settings(
|
|||
notification_type=ExcludedFromEmailTestNotification.type,
|
||||
)
|
||||
|
||||
res = NotificationHandler.send_new_notifications_to_users_matching_filters_by_email(
|
||||
res = NotificationHandler.send_unread_notifications_by_email_to_users_matching_filters(
|
||||
Q(profile__email_notification_frequency=email_notification_frequency)
|
||||
)
|
||||
assert res.users_with_notifications == [user_1]
|
||||
|
@ -509,6 +510,7 @@ def test_user_with_daily_email_notification_frequency_settings(
|
|||
{
|
||||
"title": "Test notification",
|
||||
"description": None,
|
||||
"url": None,
|
||||
}
|
||||
],
|
||||
"new_notifications_count": 1,
|
||||
|
@ -539,7 +541,7 @@ def test_email_notifications_are_sent_only_after_setting_is_activated(
|
|||
user_1 = UserHandler().update_user(
|
||||
user_1, email_notification_frequency=options.INSTANT
|
||||
)
|
||||
res = NotificationHandler.send_new_notifications_to_users_matching_filters_by_email(
|
||||
res = NotificationHandler.send_unread_notifications_by_email_to_users_matching_filters(
|
||||
Q(pk=user_1.pk)
|
||||
)
|
||||
assert res.users_with_notifications == []
|
||||
|
@ -549,7 +551,7 @@ def test_email_notifications_are_sent_only_after_setting_is_activated(
|
|||
recipients=[user_1], notification_type=TestNotification.type
|
||||
)
|
||||
|
||||
res = NotificationHandler.send_new_notifications_to_users_matching_filters_by_email(
|
||||
res = NotificationHandler.send_unread_notifications_by_email_to_users_matching_filters(
|
||||
Q(pk=user_1.pk)
|
||||
)
|
||||
|
||||
|
@ -572,6 +574,7 @@ def test_email_notifications_are_sent_only_after_setting_is_activated(
|
|||
{
|
||||
"title": "Test notification",
|
||||
"description": None,
|
||||
"url": None,
|
||||
}
|
||||
],
|
||||
"new_notifications_count": 1,
|
||||
|
@ -601,7 +604,7 @@ def test_email_notifications_are_included_up_to_email_limit(
|
|||
recipients=[user_1], notification_type=TestNotification.type
|
||||
)
|
||||
|
||||
res = NotificationHandler.send_new_notifications_to_users_matching_filters_by_email(
|
||||
res = NotificationHandler.send_unread_notifications_by_email_to_users_matching_filters(
|
||||
Q(pk=user_1.pk)
|
||||
)
|
||||
assert res.users_with_notifications == [user_1]
|
||||
|
@ -624,6 +627,7 @@ def test_email_notifications_are_included_up_to_email_limit(
|
|||
{
|
||||
"title": "Test notification",
|
||||
"description": None,
|
||||
"url": None,
|
||||
}
|
||||
for _ in range(limit)
|
||||
],
|
||||
|
@ -656,7 +660,7 @@ def test_email_notifications_are_sent_just_once(
|
|||
)
|
||||
|
||||
assert NotificationRecipient.objects.filter(email_scheduled=True).count() == 1
|
||||
res = NotificationHandler.send_new_notifications_to_users_matching_filters_by_email(
|
||||
res = NotificationHandler.send_unread_notifications_by_email_to_users_matching_filters(
|
||||
Q(profile__email_notification_frequency=options.INSTANT)
|
||||
)
|
||||
assert res.users_with_notifications == [user_1]
|
||||
|
@ -664,7 +668,7 @@ def test_email_notifications_are_sent_just_once(
|
|||
assert res.remaining_users_to_notify_count == 0
|
||||
|
||||
assert NotificationRecipient.objects.filter(email_scheduled=True).count() == 0
|
||||
res = NotificationHandler.send_new_notifications_to_users_matching_filters_by_email(
|
||||
res = NotificationHandler.send_unread_notifications_by_email_to_users_matching_filters(
|
||||
Q(profile__email_notification_frequency=options.INSTANT)
|
||||
)
|
||||
assert res.users_with_notifications == []
|
||||
|
@ -683,7 +687,7 @@ def test_broadcast_notifications_are_not_sent_by_email(
|
|||
|
||||
user_1 = data_fixture.create_user()
|
||||
|
||||
res = NotificationHandler.send_new_notifications_to_users_matching_filters_by_email(
|
||||
res = NotificationHandler.send_unread_notifications_by_email_to_users_matching_filters(
|
||||
Q(pk=user_1.pk)
|
||||
)
|
||||
assert res.users_with_notifications == []
|
||||
|
@ -708,7 +712,7 @@ def test_email_notifications_are_not_sent_if_global_setting_is_disabled(
|
|||
|
||||
assert NotificationRecipient.objects.filter(email_scheduled=True).count() == 1
|
||||
|
||||
res = NotificationHandler.send_new_notifications_to_users_matching_filters_by_email(
|
||||
res = NotificationHandler.send_unread_notifications_by_email_to_users_matching_filters(
|
||||
Q(pk=user_1.pk)
|
||||
)
|
||||
assert res.users_with_notifications == [user_1]
|
||||
|
@ -745,7 +749,7 @@ def test_email_notifications_are_not_sent_if_already_read_by_user(
|
|||
|
||||
assert NotificationRecipient.objects.filter(email_scheduled=True).count() == 0
|
||||
|
||||
res = NotificationHandler.send_new_notifications_to_users_matching_filters_by_email(
|
||||
res = NotificationHandler.send_unread_notifications_by_email_to_users_matching_filters(
|
||||
Q(pk=user_1.pk)
|
||||
)
|
||||
assert res.users_with_notifications == []
|
||||
|
@ -765,7 +769,7 @@ def test_email_notifications_are_not_sent_if_already_read_by_user(
|
|||
|
||||
assert NotificationRecipient.objects.filter(email_scheduled=True).count() == 0
|
||||
|
||||
res = NotificationHandler.send_new_notifications_to_users_matching_filters_by_email(
|
||||
res = NotificationHandler.send_unread_notifications_by_email_to_users_matching_filters(
|
||||
Q(pk=user_1.pk)
|
||||
)
|
||||
assert res.users_with_notifications == []
|
||||
|
@ -798,7 +802,7 @@ def test_email_notifications_are_not_sent_if_already_cleared_by_user(
|
|||
|
||||
assert NotificationRecipient.objects.filter(email_scheduled=True).count() == 0
|
||||
|
||||
res = NotificationHandler.send_new_notifications_to_users_matching_filters_by_email(
|
||||
res = NotificationHandler.send_unread_notifications_by_email_to_users_matching_filters(
|
||||
Q(pk=user_1.pk)
|
||||
)
|
||||
assert res.users_with_notifications == []
|
||||
|
|
|
@ -83,6 +83,7 @@ def test_daily_report_is_sent_at_correct_time_according_to_user_timezone(
|
|||
{
|
||||
"title": "Test notification",
|
||||
"description": None,
|
||||
"url": None,
|
||||
}
|
||||
],
|
||||
"new_notifications_count": 1,
|
||||
|
@ -125,10 +126,12 @@ def test_daily_report_is_sent_at_correct_time_according_to_user_timezone(
|
|||
{
|
||||
"title": "Test notification",
|
||||
"description": None,
|
||||
"url": None,
|
||||
},
|
||||
{
|
||||
"title": "Test notification",
|
||||
"description": None,
|
||||
"url": None,
|
||||
},
|
||||
],
|
||||
"new_notifications_count": 2,
|
||||
|
@ -207,6 +210,7 @@ def test_weekly_report_is_sent_at_correct_date_and_time_according_to_user_timezo
|
|||
{
|
||||
"title": "Test notification",
|
||||
"description": None,
|
||||
"url": None,
|
||||
}
|
||||
],
|
||||
"new_notifications_count": 1,
|
||||
|
@ -249,10 +253,12 @@ def test_weekly_report_is_sent_at_correct_date_and_time_according_to_user_timezo
|
|||
{
|
||||
"title": "Test notification",
|
||||
"description": None,
|
||||
"url": None,
|
||||
},
|
||||
{
|
||||
"title": "Test notification",
|
||||
"description": None,
|
||||
"url": None,
|
||||
},
|
||||
],
|
||||
"new_notifications_count": 2,
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"type": "feature",
|
||||
"message": "Made email notifications clickable and link to related row.",
|
||||
"issue_number": 1881,
|
||||
"bullet_points": [],
|
||||
"created_at": "2024-03-28"
|
||||
}
|
|
@ -48,6 +48,7 @@ class RowCommentNotificationData:
|
|||
|
||||
class RowCommentMentionNotificationType(EmailNotificationTypeMixin, NotificationType):
|
||||
type = "row_comment_mention"
|
||||
has_web_frontend_route = True
|
||||
|
||||
@classmethod
|
||||
def notify_mentioned_users(cls, row_comment, row, mentions):
|
||||
|
@ -93,6 +94,7 @@ class RowCommentNotificationType(EmailNotificationTypeMixin, NotificationType):
|
|||
"""
|
||||
|
||||
type = "row_comment"
|
||||
has_web_frontend_route = True
|
||||
|
||||
@classmethod
|
||||
def notify_subscribed_users(
|
||||
|
|
|
@ -232,7 +232,7 @@ def test_email_notifications_are_created_correctly(
|
|||
assert response.status_code == HTTP_200_OK
|
||||
|
||||
# Force to send the notifications
|
||||
res = NotificationHandler.send_new_notifications_to_users_matching_filters_by_email(
|
||||
res = NotificationHandler.send_unread_notifications_by_email_to_users_matching_filters(
|
||||
Q(pk=user_2.pk)
|
||||
)
|
||||
assert res.users_with_notifications == [user_2]
|
||||
|
@ -247,11 +247,15 @@ def test_email_notifications_are_created_correctly(
|
|||
assert user_2_summary_email.to == [user_2.email]
|
||||
assert user_2_summary_email.get_subject() == "You have 1 new notification - Baserow"
|
||||
|
||||
notif = NotificationRecipient.objects.get(recipient=user_2)
|
||||
notification_url = f"http://localhost:3000/notification/{notif.workspace_id}/{notif.notification_id}"
|
||||
|
||||
expected_context = {
|
||||
"notifications": [
|
||||
{
|
||||
"title": f"User 1 mentioned you in row {str(row)} in {table.name}.",
|
||||
"description": "@User 2",
|
||||
"url": notification_url,
|
||||
}
|
||||
],
|
||||
"new_notifications_count": 1,
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<template>
|
||||
<nuxt-link
|
||||
class="notification-panel__notification-link"
|
||||
:to="url"
|
||||
:to="route"
|
||||
@click.native="markAsReadAndHandleClick"
|
||||
>
|
||||
<div class="notification-panel__notification-content-title">
|
||||
|
@ -42,33 +42,6 @@ export default {
|
|||
RichTextEditor,
|
||||
},
|
||||
mixins: [notificationContent],
|
||||
props: {
|
||||
notification: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
workspace: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
params() {
|
||||
const data = this.notification.data
|
||||
|
||||
return {
|
||||
databaseId: data.database_id,
|
||||
tableId: data.table_id,
|
||||
rowId: data.row_id,
|
||||
}
|
||||
},
|
||||
url() {
|
||||
return {
|
||||
name: 'database-table-row',
|
||||
params: this.params,
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
handleClick(evt) {
|
||||
this.$emit('close-panel')
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<template>
|
||||
<nuxt-link
|
||||
class="notification-panel__notification-link"
|
||||
:to="url"
|
||||
:to="route"
|
||||
@click.native="markAsReadAndHandleClick"
|
||||
>
|
||||
<div class="notification-panel__notification-content-title">
|
||||
|
@ -40,40 +40,6 @@ export default {
|
|||
RichTextEditor,
|
||||
},
|
||||
mixins: [notificationContent],
|
||||
props: {
|
||||
notification: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
params() {
|
||||
const data = this.notification.data
|
||||
let viewId = null
|
||||
|
||||
if (
|
||||
['database-table-row', 'database-table'].includes(
|
||||
this.$nuxt.$route.name
|
||||
) &&
|
||||
this.$nuxt.$route.params.tableId === this.notification.data.table_id
|
||||
) {
|
||||
viewId = this.$nuxt.$route.params.viewId
|
||||
}
|
||||
|
||||
return {
|
||||
databaseId: data.database_id,
|
||||
tableId: data.table_id,
|
||||
rowId: data.row_id,
|
||||
viewId,
|
||||
}
|
||||
},
|
||||
url() {
|
||||
return {
|
||||
name: 'database-table-row',
|
||||
params: this.params,
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
handleClick(evt) {
|
||||
this.$emit('close-panel')
|
||||
|
|
|
@ -15,6 +15,17 @@ export class RowCommentMentionNotificationType extends NotificationType {
|
|||
getContentComponent() {
|
||||
return RowCommentMentionNotification
|
||||
}
|
||||
|
||||
getRoute(notificationData) {
|
||||
return {
|
||||
name: 'database-table-row',
|
||||
params: {
|
||||
databaseId: notificationData.database_id,
|
||||
tableId: notificationData.table_id,
|
||||
rowId: notificationData.row_id,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class RowCommentNotificationType extends NotificationType {
|
||||
|
@ -29,4 +40,15 @@ export class RowCommentNotificationType extends NotificationType {
|
|||
getContentComponent() {
|
||||
return RowCommentNotification
|
||||
}
|
||||
|
||||
getRoute(notificationData) {
|
||||
return {
|
||||
name: 'database-table-row',
|
||||
params: {
|
||||
databaseId: notificationData.database_id,
|
||||
tableId: notificationData.table_id,
|
||||
rowId: notificationData.row_id,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -22,11 +22,5 @@ import notificationContent from '@baserow/modules/core/mixins/notificationConten
|
|||
export default {
|
||||
name: 'BaserowVersionUpgradeNotification',
|
||||
mixins: [notificationContent],
|
||||
props: {
|
||||
notification: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -28,11 +28,5 @@ import notificationContent from '@baserow/modules/core/mixins/notificationConten
|
|||
export default {
|
||||
name: 'WorkspaceInvitationAcceptedNotification',
|
||||
mixins: [notificationContent],
|
||||
props: {
|
||||
notification: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -28,12 +28,6 @@ import notificationContent from '@baserow/modules/core/mixins/notificationConten
|
|||
export default {
|
||||
name: 'WorkspaceInvitationCreatedNotification',
|
||||
mixins: [notificationContent],
|
||||
props: {
|
||||
notification: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
handleClick() {
|
||||
this.$emit('close-panel')
|
||||
|
|
|
@ -28,11 +28,5 @@ import notificationContent from '@baserow/modules/core/mixins/notificationConten
|
|||
export default {
|
||||
name: 'WorkspaceInvitationRejectedNotification',
|
||||
mixins: [notificationContent],
|
||||
props: {
|
||||
notification: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -1,7 +1,22 @@
|
|||
import { notifyIf } from '@baserow/modules/core/utils/error'
|
||||
|
||||
export default {
|
||||
props: {
|
||||
workspace: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
notification: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
route() {
|
||||
return this.$registry
|
||||
.get('notification', this.notification.type)
|
||||
.getRoute(this.notification.data)
|
||||
},
|
||||
sender() {
|
||||
return this.notification.sender?.first_name
|
||||
},
|
||||
|
|
|
@ -19,6 +19,17 @@ export class NotificationType extends Registerable {
|
|||
getIconComponentProps() {
|
||||
return {}
|
||||
}
|
||||
|
||||
/**
|
||||
* Should return the nuxt route of the page where to redirect to if the user clicks
|
||||
* on the notification. Note that the backend also uses this to create a link in
|
||||
* external communication like the email, and if anything changes in this method,
|
||||
* the `safe_route_data_parameters` then might need to be updated as well. If
|
||||
* `null` is returned, it means that the notification is not clickable.
|
||||
*/
|
||||
getRoute(notificationData) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export class WorkspaceInvitationCreatedNotificationType extends NotificationType {
|
||||
|
|
37
web-frontend/modules/core/pages/notificationRedirect.vue
Normal file
37
web-frontend/modules/core/pages/notificationRedirect.vue
Normal file
|
@ -0,0 +1,37 @@
|
|||
<script>
|
||||
import notificationService from '@baserow/modules/core/services/notification'
|
||||
|
||||
/**
|
||||
* This page functions as a never changing path in the web-frontend that will redirect
|
||||
* the visitor to the correct page related to the provided notification type and ID.
|
||||
* The reason we have this is so that the backend doesn't need to know about the paths
|
||||
* available in the web-frontend, and won't break behavior if they change.
|
||||
*/
|
||||
export default {
|
||||
async asyncData({ route, redirect, app, error, store }) {
|
||||
let notification
|
||||
|
||||
try {
|
||||
const { data } = await notificationService(app.$client).markAsRead(
|
||||
route.params.workspaceId,
|
||||
route.params.notificationId
|
||||
)
|
||||
notification = data
|
||||
} catch {
|
||||
return error({ statusCode: 404, message: 'Notification not found.' })
|
||||
}
|
||||
|
||||
const notificationType = app.$registry.get(
|
||||
'notification',
|
||||
notification.type
|
||||
)
|
||||
const redirectParams = notificationType.getRoute(notification.data)
|
||||
|
||||
if (!redirectParams) {
|
||||
return error({ statusCode: 404, message: 'Notification has no route.' })
|
||||
}
|
||||
|
||||
return redirect(redirectParams)
|
||||
},
|
||||
}
|
||||
</script>
|
|
@ -83,6 +83,11 @@ export const routes = [
|
|||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'notification-redirect',
|
||||
path: '/notification/:workspaceId/:notificationId',
|
||||
component: path.resolve(__dirname, 'pages/notificationRedirect.vue'),
|
||||
},
|
||||
]
|
||||
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<template>
|
||||
<nuxt-link
|
||||
class="notification-panel__notification-link"
|
||||
:to="url"
|
||||
:to="route"
|
||||
@click.native="markAsReadAndHandleClick"
|
||||
>
|
||||
<div class="notification-panel__notification-content-title">
|
||||
|
@ -36,27 +36,6 @@ import notificationContent from '@baserow/modules/core/mixins/notificationConten
|
|||
export default {
|
||||
name: 'CollaboratorAddedToRowNotification',
|
||||
mixins: [notificationContent],
|
||||
props: {
|
||||
notification: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
params() {
|
||||
return {
|
||||
databaseId: this.notification.data.database_id,
|
||||
tableId: this.notification.data.table_id,
|
||||
rowId: this.notification.data.row_id,
|
||||
}
|
||||
},
|
||||
url() {
|
||||
return {
|
||||
name: 'database-table-row',
|
||||
params: this.params,
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
handleClick() {
|
||||
this.$emit('close-panel')
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<template>
|
||||
<nuxt-link
|
||||
class="notification-panel__notification-link"
|
||||
:to="url"
|
||||
:to="route"
|
||||
@click.native="markAsReadAndHandleClick"
|
||||
>
|
||||
<div class="notification-panel__notification-content-title">
|
||||
|
@ -37,31 +37,12 @@ import notificationContent from '@baserow/modules/core/mixins/notificationConten
|
|||
export default {
|
||||
name: 'FormSubmittedNotification',
|
||||
mixins: [notificationContent],
|
||||
props: {
|
||||
notification: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
limitValues: 3, // only the first 3 elements to keep it short
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
params() {
|
||||
return {
|
||||
databaseId: this.notification.data.database_id,
|
||||
tableId: this.notification.data.table_id,
|
||||
rowId: this.notification.data.row_id,
|
||||
}
|
||||
},
|
||||
url() {
|
||||
return {
|
||||
name: 'database-table-row',
|
||||
params: this.params,
|
||||
}
|
||||
},
|
||||
submittedValuesSummary() {
|
||||
return this.notification.data.values
|
||||
.slice(0, this.limitValues)
|
||||
|
|
|
@ -16,6 +16,17 @@ export class CollaboratorAddedToRowNotificationType extends NotificationType {
|
|||
getContentComponent() {
|
||||
return CollaboratorAddedToRowNotification
|
||||
}
|
||||
|
||||
getRoute(notificationData) {
|
||||
return {
|
||||
name: 'database-table-row',
|
||||
params: {
|
||||
databaseId: notificationData.database_id,
|
||||
tableId: notificationData.table_id,
|
||||
rowId: notificationData.row_id,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class FormSubmittedNotificationType extends NotificationType {
|
||||
|
@ -30,6 +41,17 @@ export class FormSubmittedNotificationType extends NotificationType {
|
|||
getContentComponent() {
|
||||
return FormSubmittedNotification
|
||||
}
|
||||
|
||||
getRoute(notificationData) {
|
||||
return {
|
||||
name: 'database-table-row',
|
||||
params: {
|
||||
databaseId: notificationData.database_id,
|
||||
tableId: notificationData.table_id,
|
||||
rowId: notificationData.row_id,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class UserMentionInRichTextFieldNotificationType extends NotificationType {
|
||||
|
@ -44,4 +66,15 @@ export class UserMentionInRichTextFieldNotificationType extends NotificationType
|
|||
getContentComponent() {
|
||||
return UserMentionInRichTextFieldNotification
|
||||
}
|
||||
|
||||
getRoute(notificationData) {
|
||||
return {
|
||||
name: 'database-table-row',
|
||||
params: {
|
||||
databaseId: notificationData.database_id,
|
||||
tableId: notificationData.table_id,
|
||||
rowId: notificationData.row_id,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue