mirror of
https://gitlab.com/bramw/baserow.git
synced 2025-04-14 00:59:06 +00:00
Send notification when webhook is deactivated
This commit is contained in:
parent
966ecdb801
commit
fa9048f584
21 changed files with 348 additions and 38 deletions
backend
src/baserow
contrib
builder/locale/en/LC_MESSAGES
database
core
locale/en/LC_MESSAGES
templates/baserow/core
tests/baserow/contrib/database/webhooks
changelog/entries/unreleased/feature
enterprise/backend/src/baserow_enterprise/locale/en/LC_MESSAGES
web-frontend
|
@ -8,7 +8,7 @@ msgid ""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: PACKAGE VERSION\n"
|
"Project-Id-Version: PACKAGE VERSION\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2025-01-15 11:59+0000\n"
|
"POT-Creation-Date: 2025-02-11 17:20+0000\n"
|
||||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||||
|
@ -46,7 +46,7 @@ msgstr ""
|
||||||
msgid "Last name"
|
msgid "Last name"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/baserow/contrib/builder/data_providers/data_provider_types.py:452
|
#: src/baserow/contrib/builder/data_providers/data_provider_types.py:555
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "%(user_source_name)s member"
|
msgid "%(user_source_name)s member"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
|
@ -946,6 +946,9 @@ class DatabaseConfig(AppConfig):
|
||||||
from baserow.contrib.database.views.notification_types import (
|
from baserow.contrib.database.views.notification_types import (
|
||||||
FormSubmittedNotificationType,
|
FormSubmittedNotificationType,
|
||||||
)
|
)
|
||||||
|
from baserow.contrib.database.webhooks.notification_types import (
|
||||||
|
WebhookDeactivatedNotificationType,
|
||||||
|
)
|
||||||
from baserow.core.notifications.registries import notification_type_registry
|
from baserow.core.notifications.registries import notification_type_registry
|
||||||
|
|
||||||
notification_type_registry.register(CollaboratorAddedToRowNotificationType())
|
notification_type_registry.register(CollaboratorAddedToRowNotificationType())
|
||||||
|
@ -953,6 +956,7 @@ class DatabaseConfig(AppConfig):
|
||||||
UserMentionInRichTextFieldNotificationType()
|
UserMentionInRichTextFieldNotificationType()
|
||||||
)
|
)
|
||||||
notification_type_registry.register(FormSubmittedNotificationType())
|
notification_type_registry.register(FormSubmittedNotificationType())
|
||||||
|
notification_type_registry.register(WebhookDeactivatedNotificationType())
|
||||||
|
|
||||||
# The signals must always be imported last because they use the registries
|
# The signals must always be imported last because they use the registries
|
||||||
# which need to be filled first.
|
# which need to be filled first.
|
||||||
|
|
|
@ -8,7 +8,7 @@ msgid ""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: PACKAGE VERSION\n"
|
"Project-Id-Version: PACKAGE VERSION\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2025-01-15 11:59+0000\n"
|
"POT-Creation-Date: 2025-02-11 17:20+0000\n"
|
||||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||||
|
@ -38,11 +38,11 @@ msgid ""
|
||||||
"\"%(database_name)s\" (%(database_id)s)."
|
"\"%(database_name)s\" (%(database_id)s)."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/baserow/contrib/database/airtable/actions.py:22
|
#: src/baserow/contrib/database/airtable/actions.py:23
|
||||||
msgid "Import database from Airtable"
|
msgid "Import database from Airtable"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/baserow/contrib/database/airtable/actions.py:24
|
#: src/baserow/contrib/database/airtable/actions.py:25
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid ""
|
msgid ""
|
||||||
"Imported database "
|
"Imported database "
|
||||||
|
@ -80,7 +80,7 @@ msgstr ""
|
||||||
msgid "The data sync synchronized"
|
msgid "The data sync synchronized"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/baserow/contrib/database/data_sync/handler.py:186
|
#: src/baserow/contrib/database/data_sync/handler.py:187
|
||||||
#: src/baserow/contrib/database/table/handler.py:548
|
#: src/baserow/contrib/database/table/handler.py:548
|
||||||
msgid "Grid"
|
msgid "Grid"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
@ -148,8 +148,8 @@ msgid ""
|
||||||
"%(new_primary_field_name)s"
|
"%(new_primary_field_name)s"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/baserow/contrib/database/fields/models.py:415
|
#: src/baserow/contrib/database/fields/models.py:453
|
||||||
#: src/baserow/contrib/database/fields/models.py:594
|
#: src/baserow/contrib/database/fields/models.py:632
|
||||||
msgid "The format of the duration."
|
msgid "The format of the duration."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -601,12 +601,12 @@ msgstr ""
|
||||||
msgid "Row (%(row_id)s) created via form submission"
|
msgid "Row (%(row_id)s) created via form submission"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/baserow/contrib/database/views/notification_types.py:84
|
#: src/baserow/contrib/database/views/notification_types.py:86
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "%(form_name)s has been submitted in %(table_name)s"
|
msgid "%(form_name)s has been submitted in %(table_name)s"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/baserow/contrib/database/views/notification_types.py:101
|
#: src/baserow/contrib/database/views/notification_types.py:103
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "and 1 more field"
|
msgid "and 1 more field"
|
||||||
msgid_plural "and %(count)s more fields"
|
msgid_plural "and %(count)s more fields"
|
||||||
|
@ -645,3 +645,15 @@ msgid ""
|
||||||
"Webhook \"%(webhook_name)s\" (%(webhook_id)s) as %(webhook_request_method)s "
|
"Webhook \"%(webhook_name)s\" (%(webhook_id)s) as %(webhook_request_method)s "
|
||||||
"to %(webhook_url)s\" updated"
|
"to %(webhook_url)s\" updated"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/baserow/contrib/database/webhooks/notification_types.py:70
|
||||||
|
#, python-format
|
||||||
|
msgid "%(name)s webhook has been deactivated."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/baserow/contrib/database/webhooks/notification_types.py:77
|
||||||
|
#, python-format
|
||||||
|
msgid ""
|
||||||
|
"The webhook failed more than %(max_failures)s consecutive times and was "
|
||||||
|
"therefore deactivated."
|
||||||
|
msgstr ""
|
||||||
|
|
|
@ -0,0 +1,81 @@
|
||||||
|
from dataclasses import asdict, dataclass
|
||||||
|
from typing import List, Optional
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.utils.translation import gettext as _
|
||||||
|
|
||||||
|
from baserow.core.models import WORKSPACE_USER_PERMISSION_ADMIN, WorkspaceUser
|
||||||
|
from baserow.core.notifications.handler import NotificationHandler
|
||||||
|
from baserow.core.notifications.models import NotificationRecipient
|
||||||
|
from baserow.core.notifications.registries import (
|
||||||
|
EmailNotificationTypeMixin,
|
||||||
|
NotificationType,
|
||||||
|
)
|
||||||
|
|
||||||
|
from .models import TableWebhook
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class DeactivatedWebhookData:
|
||||||
|
webhook_id: int
|
||||||
|
table_id: int
|
||||||
|
database_id: int
|
||||||
|
webhook_name: str
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_webhook(cls, webhook):
|
||||||
|
return cls(
|
||||||
|
webhook_id=webhook.id,
|
||||||
|
table_id=webhook.table_id,
|
||||||
|
database_id=webhook.table.database_id,
|
||||||
|
webhook_name=webhook.name,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class WebhookDeactivatedNotificationType(EmailNotificationTypeMixin, NotificationType):
|
||||||
|
type = "webhook_deactivated"
|
||||||
|
has_web_frontend_route = True
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def notify_admins_in_workspace(
|
||||||
|
cls, webhook: TableWebhook
|
||||||
|
) -> Optional[List[NotificationRecipient]]:
|
||||||
|
"""
|
||||||
|
Creates a notification for each user that is subscribed to receive comments on
|
||||||
|
the row on which the comment was created.
|
||||||
|
|
||||||
|
:param webhook: The comment that was created.
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
|
||||||
|
workspace = webhook.table.database.workspace
|
||||||
|
admins_workspace_users = WorkspaceUser.objects.filter(
|
||||||
|
workspace=workspace,
|
||||||
|
permissions=WORKSPACE_USER_PERMISSION_ADMIN,
|
||||||
|
user__profile__to_be_deleted=False,
|
||||||
|
user__is_active=True,
|
||||||
|
).select_related("user")
|
||||||
|
admins_in_workspace = [admin.user for admin in admins_workspace_users]
|
||||||
|
|
||||||
|
return NotificationHandler.create_direct_notification_for_users(
|
||||||
|
notification_type=WebhookDeactivatedNotificationType.type,
|
||||||
|
recipients=admins_in_workspace,
|
||||||
|
data=asdict(DeactivatedWebhookData.from_webhook(webhook)),
|
||||||
|
sender=None,
|
||||||
|
workspace=webhook.table.database.workspace,
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_notification_title_for_email(cls, notification, context):
|
||||||
|
return _("%(name)s webhook has been deactivated.") % {
|
||||||
|
"name": notification.data["webhook_name"],
|
||||||
|
}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_notification_description_for_email(cls, notification, context):
|
||||||
|
return _(
|
||||||
|
"The webhook failed more than %(max_failures)s consecutive times and "
|
||||||
|
"was therefore deactivated."
|
||||||
|
) % {
|
||||||
|
"max_failures": settings.BASEROW_WEBHOOKS_MAX_CONSECUTIVE_TRIGGER_FAILURES,
|
||||||
|
}
|
|
@ -88,6 +88,7 @@ def call_webhook(
|
||||||
|
|
||||||
from .handler import WebhookHandler
|
from .handler import WebhookHandler
|
||||||
from .models import TableWebhook, TableWebhookCall
|
from .models import TableWebhook, TableWebhookCall
|
||||||
|
from .notification_types import WebhookDeactivatedNotificationType
|
||||||
|
|
||||||
if self.request.retries > retries:
|
if self.request.retries > retries:
|
||||||
retries = self.request.retries
|
retries = self.request.retries
|
||||||
|
@ -187,6 +188,14 @@ def call_webhook(
|
||||||
webhook.active = False
|
webhook.active = False
|
||||||
webhook.save()
|
webhook.save()
|
||||||
|
|
||||||
|
# Send a notification to the workspace admins that the webhook was
|
||||||
|
# deactivated.
|
||||||
|
transaction.on_commit(
|
||||||
|
lambda: WebhookDeactivatedNotificationType.notify_admins_in_workspace(
|
||||||
|
webhook
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
# After the transaction successfully commits we can delay the next call
|
# After the transaction successfully commits we can delay the next call
|
||||||
# in the queue, so that only one call is triggered concurrently.
|
# in the queue, so that only one call is triggered concurrently.
|
||||||
transaction.on_commit(lambda: schedule_next_task_in_queue(webhook_id))
|
transaction.on_commit(lambda: schedule_next_task_in_queue(webhook_id))
|
||||||
|
|
|
@ -8,7 +8,7 @@ msgid ""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: PACKAGE VERSION\n"
|
"Project-Id-Version: PACKAGE VERSION\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2025-01-15 11:59+0000\n"
|
"POT-Creation-Date: 2025-02-11 17:20+0000\n"
|
||||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||||
|
@ -242,7 +242,7 @@ msgstr ""
|
||||||
msgid "Decimal number"
|
msgid "Decimal number"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/baserow/core/handler.py:2109 src/baserow/core/user/handler.py:267
|
#: src/baserow/core/handler.py:2122 src/baserow/core/user/handler.py:267
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "%(name)s's workspace"
|
msgid "%(name)s's workspace"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
|
@ -230,24 +230,6 @@
|
||||||
</tr>
|
</tr>
|
||||||
<!-- htmlmin:ignore -->{% endif %}
|
<!-- htmlmin:ignore -->{% endif %}
|
||||||
<!-- htmlmin:ignore -->
|
<!-- htmlmin:ignore -->
|
||||||
<tr>
|
|
||||||
<td align="left" vertical-align="middle" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
|
||||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-collapse:separate;line-height:100%;">
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td align="center" bgcolor="#5190ef" role="presentation" style="border:none;border-radius:4px;cursor:auto;mso-padding-alt:12px 30px;background:#5190ef;" valign="middle">
|
|
||||||
<a href="{{ baserow_embedded_share_url }}" style="display:inline-block;background:#5190ef;color:#ffffff;font-family:Inter,sans-serif;font-size:15px;font-weight:600;line-height:120%;margin:0;text-decoration:none;text-transform:none;padding:12px 30px;mso-padding-alt:0px;border-radius:4px;" target="_blank"> {% trans "View in Baserow" %} </a>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
|
||||||
<div style="font-family:Inter,sans-serif;font-size:12px;line-height:1;text-align:left;color:#9c9c9f;">{{ baserow_embedded_share_url }}</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
<tr>
|
||||||
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||||
<div style="font-family:Inter,sans-serif;font-size:13px;line-height:170%;text-align:left;color:#070810;">{% blocktrans trimmed %} Baserow is an open source no-code database tool which allows you to collaborate on projects, customers and more. It gives you the powers of a developer without leaving your browser. {% endblocktrans %}</div>
|
<div style="font-family:Inter,sans-serif;font-size:13px;line-height:170%;text-align:left;color:#070810;">{% blocktrans trimmed %} Baserow is an open source no-code database tool which allows you to collaborate on projects, customers and more. It gives you the powers of a developer without leaving your browser. {% endblocktrans %}</div>
|
||||||
|
|
|
@ -31,12 +31,6 @@
|
||||||
{% endblocktrans %}
|
{% endblocktrans %}
|
||||||
</mj-text>
|
</mj-text>
|
||||||
<mj-raw><!-- htmlmin:ignore -->{% endif %}<!-- htmlmin:ignore --></mj-raw>
|
<mj-raw><!-- htmlmin:ignore -->{% endif %}<!-- htmlmin:ignore --></mj-raw>
|
||||||
<mj-button mj-class="button mt-20" href="{{ baserow_embedded_share_url }}">
|
|
||||||
{% trans "View in Baserow" %}
|
|
||||||
</mj-button>
|
|
||||||
<mj-text mj-class="button-url">
|
|
||||||
{{ baserow_embedded_share_url }}
|
|
||||||
</mj-text>
|
|
||||||
<mj-text mj-class="text">
|
<mj-text mj-class="text">
|
||||||
{% blocktrans trimmed %}
|
{% blocktrans trimmed %}
|
||||||
Baserow is an open source no-code database tool which allows you to collaborate
|
Baserow is an open source no-code database tool which allows you to collaborate
|
||||||
|
|
|
@ -0,0 +1,37 @@
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from baserow.contrib.database.webhooks.notification_types import (
|
||||||
|
WebhookDeactivatedNotificationType,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db(transaction=True)
|
||||||
|
def test_webhook_deactivated_notification_can_be_render_as_email(
|
||||||
|
api_client, data_fixture
|
||||||
|
):
|
||||||
|
user = data_fixture.create_user()
|
||||||
|
workspace = data_fixture.create_workspace(user=user)
|
||||||
|
database = data_fixture.create_database_application(workspace=workspace)
|
||||||
|
table = data_fixture.create_database_table(database=database)
|
||||||
|
webhook = data_fixture.create_table_webhook(
|
||||||
|
table=table, active=True, failed_triggers=1
|
||||||
|
)
|
||||||
|
|
||||||
|
notification_recipients = (
|
||||||
|
WebhookDeactivatedNotificationType.notify_admins_in_workspace(webhook)
|
||||||
|
)
|
||||||
|
notification = notification_recipients[0].notification
|
||||||
|
|
||||||
|
assert WebhookDeactivatedNotificationType.get_notification_title_for_email(
|
||||||
|
notification, {}
|
||||||
|
) == "%(name)s webhook has been deactivated." % {
|
||||||
|
"name": notification.data["webhook_name"],
|
||||||
|
}
|
||||||
|
|
||||||
|
assert (
|
||||||
|
WebhookDeactivatedNotificationType.get_notification_description_for_email(
|
||||||
|
notification, {}
|
||||||
|
)
|
||||||
|
== "The webhook failed more than 8 consecutive times and "
|
||||||
|
"was therefore deactivated."
|
||||||
|
)
|
|
@ -10,7 +10,12 @@ import responses
|
||||||
from celery.exceptions import Retry
|
from celery.exceptions import Retry
|
||||||
|
|
||||||
from baserow.contrib.database.webhooks.models import TableWebhook, TableWebhookCall
|
from baserow.contrib.database.webhooks.models import TableWebhook, TableWebhookCall
|
||||||
|
from baserow.contrib.database.webhooks.notification_types import (
|
||||||
|
WebhookDeactivatedNotificationType,
|
||||||
|
)
|
||||||
from baserow.contrib.database.webhooks.tasks import call_webhook
|
from baserow.contrib.database.webhooks.tasks import call_webhook
|
||||||
|
from baserow.core.models import WorkspaceUser
|
||||||
|
from baserow.core.notifications.models import Notification
|
||||||
from baserow.core.redis import RedisQueue
|
from baserow.core.redis import RedisQueue
|
||||||
from baserow.test_utils.helpers import stub_getaddrinfo
|
from baserow.test_utils.helpers import stub_getaddrinfo
|
||||||
|
|
||||||
|
@ -361,3 +366,67 @@ def test_can_call_webhook_to_localhost_when_private_addresses_allowed(
|
||||||
assert not call.error
|
assert not call.error
|
||||||
assert call.response_status == 201
|
assert call.response_status == 201
|
||||||
assert webhook.active
|
assert webhook.active
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db(transaction=True)
|
||||||
|
@responses.activate
|
||||||
|
@override_settings(
|
||||||
|
BASEROW_WEBHOOKS_MAX_RETRIES_PER_CALL=1,
|
||||||
|
BASEROW_WEBHOOKS_MAX_CONSECUTIVE_TRIGGER_FAILURES=1,
|
||||||
|
)
|
||||||
|
@patch("baserow.contrib.database.webhooks.tasks.RedisQueue", MemoryQueue)
|
||||||
|
@patch("baserow.contrib.database.webhooks.tasks.cache", MagicMock())
|
||||||
|
@patch("baserow.ws.tasks.broadcast_to_users.apply")
|
||||||
|
def test_call_webhook_failed_reached_notification_send(
|
||||||
|
mocked_broadcast_to_users, data_fixture
|
||||||
|
):
|
||||||
|
user_1 = data_fixture.create_user()
|
||||||
|
user_2 = data_fixture.create_user()
|
||||||
|
admin_1 = data_fixture.create_user()
|
||||||
|
admin_2 = data_fixture.create_user()
|
||||||
|
workspace = data_fixture.create_workspace()
|
||||||
|
|
||||||
|
WorkspaceUser.objects.create(
|
||||||
|
user=user_1, workspace=workspace, order=1, permissions="MEMBER"
|
||||||
|
)
|
||||||
|
WorkspaceUser.objects.create(
|
||||||
|
user=user_2, workspace=workspace, order=2, permissions="MEMBER"
|
||||||
|
)
|
||||||
|
WorkspaceUser.objects.create(
|
||||||
|
user=admin_1, workspace=workspace, order=3, permissions="ADMIN"
|
||||||
|
)
|
||||||
|
WorkspaceUser.objects.create(
|
||||||
|
user=admin_2, workspace=workspace, order=4, permissions="ADMIN"
|
||||||
|
)
|
||||||
|
|
||||||
|
database = data_fixture.create_database_application(workspace=workspace)
|
||||||
|
table = data_fixture.create_database_table(database=database)
|
||||||
|
webhook = data_fixture.create_table_webhook(
|
||||||
|
table=table, active=True, failed_triggers=1
|
||||||
|
)
|
||||||
|
|
||||||
|
call_webhook.push_request(retries=1)
|
||||||
|
call_webhook.run(
|
||||||
|
webhook_id=webhook.id,
|
||||||
|
event_id="00000000-0000-0000-0000-000000000000",
|
||||||
|
event_type="rows.created",
|
||||||
|
method="POST",
|
||||||
|
url="http://localhost/",
|
||||||
|
headers={"Baserow-header-1": "Value 1"},
|
||||||
|
payload={"type": "rows.created"},
|
||||||
|
)
|
||||||
|
|
||||||
|
all_notifications = list(Notification.objects.all())
|
||||||
|
assert len(all_notifications) == 1
|
||||||
|
recipient_ids = [r.id for r in all_notifications[0].recipients.all()]
|
||||||
|
assert recipient_ids == [admin_1.id, admin_2.id]
|
||||||
|
assert all_notifications[0].type == WebhookDeactivatedNotificationType.type
|
||||||
|
assert all_notifications[0].broadcast is False
|
||||||
|
assert all_notifications[0].workspace_id == workspace.id
|
||||||
|
assert all_notifications[0].sender is None
|
||||||
|
assert all_notifications[0].data == {
|
||||||
|
"database_id": database.id,
|
||||||
|
"table_id": table.id,
|
||||||
|
"webhook_id": webhook.id,
|
||||||
|
"webhook_name": webhook.name,
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
{
|
||||||
|
"type": "feature",
|
||||||
|
"message": "Send notification to all admins when a webhook is deactivated.",
|
||||||
|
"issue_number": null,
|
||||||
|
"bullet_points": [],
|
||||||
|
"created_at": "2025-02-11"
|
||||||
|
}
|
|
@ -8,7 +8,7 @@ msgid ""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: PACKAGE VERSION\n"
|
"Project-Id-Version: PACKAGE VERSION\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2024-12-17 08:48+0000\n"
|
"POT-Creation-Date: 2025-02-11 17:20+0000\n"
|
||||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||||
|
@ -62,6 +62,15 @@ msgstr ""
|
||||||
msgid "REDONE"
|
msgid "REDONE"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/baserow_enterprise/data_sync/actions.py:21
|
||||||
|
msgid "Update periodic data sync interval"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/baserow_enterprise/data_sync/actions.py:22
|
||||||
|
#, python-format
|
||||||
|
msgid "Data sync table \"%(table_name)s\" (%(table_id)s) updated"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/baserow_enterprise/role/actions.py:28
|
#: src/baserow_enterprise/role/actions.py:28
|
||||||
msgid "Assign multiple roles"
|
msgid "Assign multiple roles"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
|
@ -0,0 +1,29 @@
|
||||||
|
<template>
|
||||||
|
<nuxt-link
|
||||||
|
class="notification-panel__notification-link"
|
||||||
|
:to="route"
|
||||||
|
@click.native="markAsReadAndHandleClick"
|
||||||
|
>
|
||||||
|
<div class="notification-panel__notification-content-title">
|
||||||
|
<i18n path="webhookDeactivatedNotification.body" tag="span">
|
||||||
|
<template #name>
|
||||||
|
<strong>{{ notification.data.webhook_name }}</strong>
|
||||||
|
</template>
|
||||||
|
</i18n>
|
||||||
|
</div>
|
||||||
|
</nuxt-link>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import notificationContent from '@baserow/modules/core/mixins/notificationContent'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'WebhookDeactivatedNotification',
|
||||||
|
mixins: [notificationContent],
|
||||||
|
methods: {
|
||||||
|
handleClick() {
|
||||||
|
this.$emit('close-panel')
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<Modal>
|
<Modal @hidden="$emit('hidden')">
|
||||||
<h2 class="box__title">
|
<h2 class="box__title">
|
||||||
{{ $t('webhookModal.title', { name: table.name }) }}
|
{{ $t('webhookModal.title', { name: table.name }) }}
|
||||||
</h2>
|
</h2>
|
||||||
|
|
|
@ -1054,5 +1054,8 @@
|
||||||
"configureDataSyncSettings": {
|
"configureDataSyncSettings": {
|
||||||
"title": "Change data sync",
|
"title": "Change data sync",
|
||||||
"syncTable": "Sync when save"
|
"syncTable": "Sync when save"
|
||||||
|
},
|
||||||
|
"webhookDeactivatedNotification": {
|
||||||
|
"body": "{name} webhook has been deactivated because it failed too many times consecutively."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,7 @@ import NotificationSenderInitialsIcon from '@baserow/modules/core/components/not
|
||||||
import CollaboratorAddedToRowNotification from '@baserow/modules/database/components/notifications/CollaboratorAddedToRowNotification'
|
import CollaboratorAddedToRowNotification from '@baserow/modules/database/components/notifications/CollaboratorAddedToRowNotification'
|
||||||
import UserMentionInRichTextFieldNotification from '@baserow/modules/database/components/notifications/UserMentionInRichTextFieldNotification'
|
import UserMentionInRichTextFieldNotification from '@baserow/modules/database/components/notifications/UserMentionInRichTextFieldNotification'
|
||||||
import FormSubmittedNotification from '@baserow/modules/database/components/notifications/FormSubmittedNotification'
|
import FormSubmittedNotification from '@baserow/modules/database/components/notifications/FormSubmittedNotification'
|
||||||
|
import WebhookDeactivatedNotification from '@baserow/modules/database/components/notifications/WebhookDeactivatedNotification'
|
||||||
|
|
||||||
export class CollaboratorAddedToRowNotificationType extends NotificationType {
|
export class CollaboratorAddedToRowNotificationType extends NotificationType {
|
||||||
static getType() {
|
static getType() {
|
||||||
|
@ -78,3 +79,27 @@ export class UserMentionInRichTextFieldNotificationType extends NotificationType
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class WebhookDeactivatedNotificationType extends NotificationType {
|
||||||
|
static getType() {
|
||||||
|
return 'webhook_deactivated'
|
||||||
|
}
|
||||||
|
|
||||||
|
getIconComponent() {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
getContentComponent() {
|
||||||
|
return WebhookDeactivatedNotification
|
||||||
|
}
|
||||||
|
|
||||||
|
getRoute(notificationData) {
|
||||||
|
return {
|
||||||
|
name: 'database-table-open-webhooks',
|
||||||
|
params: {
|
||||||
|
databaseId: notificationData.database_id,
|
||||||
|
tableId: notificationData.table_id,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -17,6 +17,7 @@
|
||||||
(row, activeSearchTerm) => setAdjacentRow(false, row, activeSearchTerm)
|
(row, activeSearchTerm) => setAdjacentRow(false, row, activeSearchTerm)
|
||||||
"
|
"
|
||||||
></Table>
|
></Table>
|
||||||
|
<NuxtChild :database="database" :table="table" :fields="fields" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
37
web-frontend/modules/database/pages/table/webhooks.vue
Normal file
37
web-frontend/modules/database/pages/table/webhooks.vue
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<WebhookModal
|
||||||
|
ref="webhookModal"
|
||||||
|
:database="database"
|
||||||
|
:table="table"
|
||||||
|
:fields="fields"
|
||||||
|
@hidden="$router.push({ name: 'database-table', params: $route.params })"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
import WebhookModal from '@baserow/modules/database/components/webhook/WebhookModal'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: { WebhookModal },
|
||||||
|
props: {
|
||||||
|
database: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
table: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
fields: {
|
||||||
|
type: Array,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.$refs.webhookModal.show()
|
||||||
|
})
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
|
@ -319,6 +319,7 @@ import {
|
||||||
CollaboratorAddedToRowNotificationType,
|
CollaboratorAddedToRowNotificationType,
|
||||||
FormSubmittedNotificationType,
|
FormSubmittedNotificationType,
|
||||||
UserMentionInRichTextFieldNotificationType,
|
UserMentionInRichTextFieldNotificationType,
|
||||||
|
WebhookDeactivatedNotificationType,
|
||||||
} from '@baserow/modules/database/notificationTypes'
|
} from '@baserow/modules/database/notificationTypes'
|
||||||
import { HistoryRowModalSidebarType } from '@baserow/modules/database/rowModalSidebarTypes'
|
import { HistoryRowModalSidebarType } from '@baserow/modules/database/rowModalSidebarTypes'
|
||||||
import { FieldsDataProviderType } from '@baserow/modules/database/dataProviderTypes'
|
import { FieldsDataProviderType } from '@baserow/modules/database/dataProviderTypes'
|
||||||
|
@ -1019,6 +1020,10 @@ export default (context) => {
|
||||||
'notification',
|
'notification',
|
||||||
new UserMentionInRichTextFieldNotificationType(context)
|
new UserMentionInRichTextFieldNotificationType(context)
|
||||||
)
|
)
|
||||||
|
app.$registry.register(
|
||||||
|
'notification',
|
||||||
|
new WebhookDeactivatedNotificationType(context)
|
||||||
|
)
|
||||||
|
|
||||||
app.$registry.register(
|
app.$registry.register(
|
||||||
'rowModalSidebar',
|
'rowModalSidebar',
|
||||||
|
|
|
@ -23,6 +23,11 @@ export const routes = [
|
||||||
path: 'row/:rowId',
|
path: 'row/:rowId',
|
||||||
name: 'database-table-row',
|
name: 'database-table-row',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'webhooks',
|
||||||
|
name: 'database-table-open-webhooks',
|
||||||
|
component: path.resolve(__dirname, 'pages/table/webhooks.vue'),
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
// These redirect exist because the original api docs path was `/api/docs`, but
|
// These redirect exist because the original api docs path was `/api/docs`, but
|
||||||
|
|
|
@ -39,6 +39,7 @@ export const bootstrapVueContext = (configureContext) => {
|
||||||
jest.isolateModules(() => {
|
jest.isolateModules(() => {
|
||||||
context.vueTestUtils = require('@vue/test-utils')
|
context.vueTestUtils = require('@vue/test-utils')
|
||||||
context.vueTestUtils.config.stubs.nuxt = { template: '<div />' }
|
context.vueTestUtils.config.stubs.nuxt = { template: '<div />' }
|
||||||
|
context.vueTestUtils.config.stubs.NuxtChild = { template: '<div />' }
|
||||||
context.vueTestUtils.config.stubs['nuxt-link'] = {
|
context.vueTestUtils.config.stubs['nuxt-link'] = {
|
||||||
template: '<a><slot /></a>',
|
template: '<a><slot /></a>',
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue