mirror of
https://gitlab.com/bramw/baserow.git
synced 2025-04-03 04:35:31 +00:00
Periodic data sync deactivated notification
This commit is contained in:
parent
163a788b17
commit
301d4e64d6
15 changed files with 354 additions and 9 deletions
changelog/entries/unreleased/feature
enterprise
backend
src/baserow_enterprise
tests/baserow_enterprise_tests/data_sync
web-frontend/modules/baserow_enterprise
web-frontend/modules/database
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"type": "feature",
|
||||
"message": "Send notification to authorized user when periodic data sync is deactivated.",
|
||||
"issue_number": null,
|
||||
"bullet_points": [],
|
||||
"created_at": "2025-02-13"
|
||||
}
|
|
@ -287,6 +287,15 @@ class BaserowEnterpriseConfig(AppConfig):
|
|||
|
||||
connect_to_post_delete_signals_to_cascade_deletion_to_role_assignments()
|
||||
|
||||
from baserow.core.notifications.registries import notification_type_registry
|
||||
from baserow_enterprise.data_sync.notification_types import (
|
||||
PeriodicDataSyncDeactivatedNotificationType,
|
||||
)
|
||||
|
||||
notification_type_registry.register(
|
||||
PeriodicDataSyncDeactivatedNotificationType()
|
||||
)
|
||||
|
||||
# The signals must always be imported last because they use the registries
|
||||
# which need to be filled first.
|
||||
import baserow_enterprise.audit_log.signals # noqa: F
|
||||
|
|
|
@ -24,6 +24,7 @@ from baserow_enterprise.data_sync.models import (
|
|||
)
|
||||
from baserow_enterprise.features import DATA_SYNC
|
||||
|
||||
from .notification_types import PeriodicDataSyncDeactivatedNotificationType
|
||||
from .tasks import sync_periodic_data_sync
|
||||
|
||||
|
||||
|
@ -216,6 +217,15 @@ class EnterpriseDataSyncHandler:
|
|||
>= settings.BASEROW_ENTERPRISE_MAX_PERIODIC_DATA_SYNC_CONSECUTIVE_ERRORS
|
||||
):
|
||||
periodic_data_sync.automatically_deactivated = True
|
||||
|
||||
# Send a notification to the authorized user that the periodic data
|
||||
# sync was deactivated.
|
||||
transaction.on_commit(
|
||||
lambda: PeriodicDataSyncDeactivatedNotificationType.notify_authorized_user(
|
||||
periodic_data_sync
|
||||
)
|
||||
)
|
||||
|
||||
periodic_data_sync.save()
|
||||
elif periodic_data_sync.consecutive_failed_count > 0:
|
||||
# Once it runs successfully, the consecutive count can be reset because we
|
||||
|
|
|
@ -0,0 +1,77 @@
|
|||
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.notifications.handler import NotificationHandler
|
||||
from baserow.core.notifications.models import NotificationRecipient
|
||||
from baserow.core.notifications.registries import (
|
||||
EmailNotificationTypeMixin,
|
||||
NotificationType,
|
||||
)
|
||||
|
||||
from .models import PeriodicDataSyncInterval
|
||||
|
||||
|
||||
@dataclass
|
||||
class DeactivatedPeriodicDataSyncData:
|
||||
data_sync_id: int
|
||||
table_name: str
|
||||
table_id: int
|
||||
database_id: int
|
||||
|
||||
@classmethod
|
||||
def from_periodic_data_sync(cls, periodic_data_sync: PeriodicDataSyncInterval):
|
||||
return cls(
|
||||
data_sync_id=periodic_data_sync.data_sync_id,
|
||||
table_name=periodic_data_sync.data_sync.table.name,
|
||||
table_id=periodic_data_sync.data_sync.table.id,
|
||||
database_id=periodic_data_sync.data_sync.table.database_id,
|
||||
)
|
||||
|
||||
|
||||
class PeriodicDataSyncDeactivatedNotificationType(
|
||||
EmailNotificationTypeMixin, NotificationType
|
||||
):
|
||||
type = "periodic_data_sync_deactivated"
|
||||
has_web_frontend_route = True
|
||||
|
||||
@classmethod
|
||||
def notify_authorized_user(
|
||||
cls, periodic_data_sync: PeriodicDataSyncInterval
|
||||
) -> Optional[List[NotificationRecipient]]:
|
||||
"""
|
||||
Creates a notification for the authorized user of the periodic data sync
|
||||
that was deactivated.
|
||||
|
||||
:param periodic_data_sync: The periodic data sync that was deactivated.
|
||||
"""
|
||||
|
||||
workspace = periodic_data_sync.data_sync.table.database.workspace
|
||||
return NotificationHandler.create_direct_notification_for_users(
|
||||
notification_type=PeriodicDataSyncDeactivatedNotificationType.type,
|
||||
recipients=[periodic_data_sync.authorized_user],
|
||||
data=asdict(
|
||||
DeactivatedPeriodicDataSyncData.from_periodic_data_sync(
|
||||
periodic_data_sync
|
||||
)
|
||||
),
|
||||
sender=None,
|
||||
workspace=workspace,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def get_notification_title_for_email(cls, notification, context):
|
||||
return _("%(name)s periodic data sync has been deactivated.") % {
|
||||
"name": notification.data["table_name"],
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def get_notification_description_for_email(cls, notification, context):
|
||||
return _(
|
||||
"The periodic data sync failed more than %(max_failures)s consecutive times"
|
||||
"and was therefore deactivated."
|
||||
) % {
|
||||
"max_failures": settings.BASEROW_ENTERPRISE_MAX_PERIODIC_DATA_SYNC_CONSECUTIVE_ERRORS,
|
||||
}
|
|
@ -8,7 +8,7 @@ msgid ""
|
|||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-02-11 17:20+0000\n"
|
||||
"POT-Creation-Date: 2025-02-13 13:04+0000\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
|
@ -71,6 +71,18 @@ msgstr ""
|
|||
msgid "Data sync table \"%(table_name)s\" (%(table_id)s) updated"
|
||||
msgstr ""
|
||||
|
||||
#: src/baserow_enterprise/data_sync/notification_types.py:63
|
||||
#, python-format
|
||||
msgid "%(name)s periodic data sync has been deactivated."
|
||||
msgstr ""
|
||||
|
||||
#: src/baserow_enterprise/data_sync/notification_types.py:70
|
||||
#, python-format
|
||||
msgid ""
|
||||
"The periodic data sync failed more than %(max_failures)s consecutive "
|
||||
"timesand was therefore deactivated."
|
||||
msgstr ""
|
||||
|
||||
#: src/baserow_enterprise/role/actions.py:28
|
||||
msgid "Assign multiple roles"
|
||||
msgstr ""
|
||||
|
|
|
@ -14,8 +14,12 @@ from freezegun.api import freeze_time
|
|||
from baserow.contrib.database.data_sync.handler import DataSyncHandler
|
||||
from baserow.contrib.database.data_sync.models import DataSync
|
||||
from baserow.core.exceptions import UserNotInWorkspace
|
||||
from baserow.core.notifications.models import Notification
|
||||
from baserow_enterprise.data_sync.handler import EnterpriseDataSyncHandler
|
||||
from baserow_enterprise.data_sync.models import PeriodicDataSyncInterval
|
||||
from baserow_enterprise.data_sync.notification_types import (
|
||||
PeriodicDataSyncDeactivatedNotificationType,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
|
@ -611,6 +615,53 @@ def test_sync_periodic_data_sync_deactivated_max_failure(enterprise_data_fixture
|
|||
assert periodic_data_sync.automatically_deactivated is True
|
||||
|
||||
|
||||
@pytest.mark.django_db(transaction=True)
|
||||
@override_settings(DEBUG=True)
|
||||
@responses.activate
|
||||
def test_sync_periodic_data_sync_deactivated_max_failure_notification_send(
|
||||
enterprise_data_fixture,
|
||||
):
|
||||
responses.add(
|
||||
responses.GET,
|
||||
"https://baserow.io/ical.ics",
|
||||
status=404,
|
||||
body="",
|
||||
)
|
||||
|
||||
enterprise_data_fixture.enable_enterprise()
|
||||
user = enterprise_data_fixture.create_user()
|
||||
|
||||
periodic_data_sync = EnterpriseDataSyncHandler.update_periodic_data_sync_interval(
|
||||
user=user,
|
||||
data_sync=enterprise_data_fixture.create_ical_data_sync(user=user),
|
||||
interval="DAILY",
|
||||
when=time(hour=12, minute=10, second=1, microsecond=1),
|
||||
)
|
||||
periodic_data_sync.consecutive_failed_count = 3
|
||||
periodic_data_sync.save()
|
||||
|
||||
with transaction.atomic():
|
||||
EnterpriseDataSyncHandler.sync_periodic_data_sync(periodic_data_sync.id)
|
||||
|
||||
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 == [user.id]
|
||||
assert all_notifications[0].type == PeriodicDataSyncDeactivatedNotificationType.type
|
||||
assert all_notifications[0].broadcast is False
|
||||
assert (
|
||||
all_notifications[0].workspace_id
|
||||
== periodic_data_sync.data_sync.table.database.workspace_id
|
||||
)
|
||||
assert all_notifications[0].sender is None
|
||||
assert all_notifications[0].data == {
|
||||
"data_sync_id": periodic_data_sync.data_sync_id,
|
||||
"table_name": periodic_data_sync.data_sync.table.name,
|
||||
"table_id": periodic_data_sync.data_sync.table.id,
|
||||
"database_id": periodic_data_sync.data_sync.table.database_id,
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@override_settings(DEBUG=True)
|
||||
def test_sync_periodic_data_sync_authorized_user_is_none(enterprise_data_fixture):
|
||||
|
|
|
@ -0,0 +1,47 @@
|
|||
from datetime import time
|
||||
|
||||
from django.test.utils import override_settings
|
||||
|
||||
import pytest
|
||||
|
||||
from baserow_enterprise.data_sync.handler import EnterpriseDataSyncHandler
|
||||
from baserow_enterprise.data_sync.notification_types import (
|
||||
PeriodicDataSyncDeactivatedNotificationType,
|
||||
)
|
||||
|
||||
|
||||
@override_settings(DEBUG=True)
|
||||
@pytest.mark.django_db(transaction=True)
|
||||
def test_webhook_deactivated_notification_can_be_render_as_email(
|
||||
api_client, enterprise_data_fixture
|
||||
):
|
||||
enterprise_data_fixture.enable_enterprise()
|
||||
|
||||
user = enterprise_data_fixture.create_user()
|
||||
periodic_data_sync = EnterpriseDataSyncHandler.update_periodic_data_sync_interval(
|
||||
user=user,
|
||||
data_sync=enterprise_data_fixture.create_ical_data_sync(user=user),
|
||||
interval="DAILY",
|
||||
when=time(hour=12, minute=10, second=1, microsecond=1),
|
||||
)
|
||||
|
||||
notification_recipients = (
|
||||
PeriodicDataSyncDeactivatedNotificationType.notify_authorized_user(
|
||||
periodic_data_sync
|
||||
)
|
||||
)
|
||||
notification = notification_recipients[0].notification
|
||||
|
||||
assert PeriodicDataSyncDeactivatedNotificationType.get_notification_title_for_email(
|
||||
notification, {}
|
||||
) == "%(name)s periodic data sync has been deactivated." % {
|
||||
"name": periodic_data_sync.data_sync.table.name,
|
||||
}
|
||||
|
||||
assert (
|
||||
PeriodicDataSyncDeactivatedNotificationType.get_notification_description_for_email(
|
||||
notification, {}
|
||||
)
|
||||
== "The periodic data sync failed more than 4 consecutive times"
|
||||
"and was therefore deactivated."
|
||||
)
|
|
@ -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="periodicDataSyncDeactivatedNotification.body" tag="span">
|
||||
<template #name>
|
||||
<strong>{{ notification.data.table_name }}</strong>
|
||||
</template>
|
||||
</i18n>
|
||||
</div>
|
||||
</nuxt-link>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import notificationContent from '@baserow/modules/core/mixins/notificationContent'
|
||||
|
||||
export default {
|
||||
name: 'PeriodicDataSyncDeactivatedNotification',
|
||||
mixins: [notificationContent],
|
||||
methods: {
|
||||
handleClick() {
|
||||
this.$emit('close-panel')
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
|
@ -466,5 +466,8 @@
|
|||
"none": "None",
|
||||
"ascending": "Ascending",
|
||||
"descending": "Descending"
|
||||
},
|
||||
"periodicDataSyncDeactivatedNotification": {
|
||||
"body": "{name} periodic data sync has been deactivated because it failed too many times consecutively."
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,29 @@
|
|||
import { NotificationType } from '@baserow/modules/core/notificationTypes'
|
||||
|
||||
import PeriodicDataSyncDeactivatedNotification from '@baserow_enterprise/components/notifications/PeriodicDataSyncDeactivatedNotification'
|
||||
import { PeriodicIntervalFieldsConfigureDataSyncType } from '@baserow_enterprise/configureDataSyncTypes'
|
||||
|
||||
export class PeriodicDataSyncDeactivatedNotificationType extends NotificationType {
|
||||
static getType() {
|
||||
return 'periodic_data_sync_deactivated'
|
||||
}
|
||||
|
||||
getIconComponent() {
|
||||
return null
|
||||
}
|
||||
|
||||
getContentComponent() {
|
||||
return PeriodicDataSyncDeactivatedNotification
|
||||
}
|
||||
|
||||
getRoute(notificationData) {
|
||||
return {
|
||||
name: 'database-table-open-configure-data-sync',
|
||||
params: {
|
||||
databaseId: notificationData.database_id,
|
||||
tableId: notificationData.table_id,
|
||||
selectedPage: PeriodicIntervalFieldsConfigureDataSyncType.getType(),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
|
@ -71,6 +71,8 @@ import {
|
|||
VarianceViewAggregationType,
|
||||
MedianViewAggregationType,
|
||||
} from '@baserow/modules/database/viewAggregationTypes'
|
||||
import { PeriodicDataSyncDeactivatedNotificationType } from '@baserow_enterprise/notificationTypes'
|
||||
|
||||
import {
|
||||
FF_AB_SSO,
|
||||
FF_DASHBOARDS,
|
||||
|
@ -242,6 +244,11 @@ export default (context) => {
|
|||
new UniqueCountViewAggregationType(context)
|
||||
)
|
||||
|
||||
app.$registry.register(
|
||||
'notification',
|
||||
new PeriodicDataSyncDeactivatedNotificationType(context)
|
||||
)
|
||||
|
||||
app.$registry.register(
|
||||
'configureDataSync',
|
||||
new PeriodicIntervalFieldsConfigureDataSyncType(context)
|
||||
|
|
|
@ -1,5 +1,9 @@
|
|||
<template>
|
||||
<Modal :left-sidebar="true" :left-sidebar-scrollable="true">
|
||||
<Modal
|
||||
:left-sidebar="true"
|
||||
:left-sidebar-scrollable="true"
|
||||
@hidden="$emit('hidden')"
|
||||
>
|
||||
<template #sidebar>
|
||||
<div class="modal-sidebar__head">
|
||||
<div class="modal-sidebar__head-name">
|
||||
|
@ -64,6 +68,15 @@ export default {
|
|||
},
|
||||
},
|
||||
methods: {
|
||||
show(selectedPage, ...args) {
|
||||
if (
|
||||
selectedPage &&
|
||||
this.$registry.exists('configureDataSync', selectedPage)
|
||||
) {
|
||||
this.selectedPage = selectedPage
|
||||
}
|
||||
modal.methods.show.bind(this)(...args)
|
||||
},
|
||||
setPage(page) {
|
||||
this.selectedPage = page
|
||||
},
|
||||
|
|
|
@ -116,7 +116,7 @@ export default {
|
|||
* Prepares all the table, field and view data for the provided database, table and
|
||||
* view id.
|
||||
*/
|
||||
async asyncData({ store, params, error, app, redirect, route }) {
|
||||
async asyncData({ store, params, query, error, app, redirect, route }) {
|
||||
// @TODO figure out why the id's aren't converted to an int in the route.
|
||||
const databaseId = parseInt(params.databaseId)
|
||||
const tableId = parseInt(params.tableId)
|
||||
|
@ -155,14 +155,11 @@ export default {
|
|||
const viewToUse = getDefaultView(app, store, workspaceId, rowId !== null)
|
||||
|
||||
if (viewToUse !== undefined) {
|
||||
params.viewId = viewToUse.id
|
||||
return redirect({
|
||||
name: route.name,
|
||||
params: {
|
||||
databaseId,
|
||||
tableId,
|
||||
viewId: viewToUse.id,
|
||||
rowId: params.rowId,
|
||||
},
|
||||
params,
|
||||
query,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,49 @@
|
|||
<template>
|
||||
<div>
|
||||
<ConfigureDataSyncModal
|
||||
ref="configureDataSyncModal"
|
||||
:database="database"
|
||||
:table="table"
|
||||
@hidden="back"
|
||||
></ConfigureDataSyncModal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ConfigureDataSyncModal from '@baserow/modules/database/components/dataSync/ConfigureDataSyncModal'
|
||||
|
||||
export default {
|
||||
components: { ConfigureDataSyncModal },
|
||||
props: {
|
||||
database: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
table: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
fields: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
if (this.table.data_sync === null) {
|
||||
return this.back()
|
||||
}
|
||||
|
||||
this.$nextTick(() => {
|
||||
this.$refs.configureDataSyncModal.show(
|
||||
this.$route.params.selectedPage || undefined
|
||||
)
|
||||
})
|
||||
},
|
||||
methods: {
|
||||
back() {
|
||||
delete this.$route.params.selectedPage
|
||||
this.$router.push({ name: 'database-table', params: this.$route.params })
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
|
@ -28,6 +28,11 @@ export const routes = [
|
|||
name: 'database-table-open-webhooks',
|
||||
component: path.resolve(__dirname, 'pages/table/webhooks.vue'),
|
||||
},
|
||||
{
|
||||
path: 'configure-data-sync/:selectedPage?',
|
||||
name: 'database-table-open-configure-data-sync',
|
||||
component: path.resolve(__dirname, 'pages/table/configureDataSync.vue'),
|
||||
},
|
||||
],
|
||||
},
|
||||
// These redirect exist because the original api docs path was `/api/docs`, but
|
||||
|
|
Loading…
Add table
Reference in a new issue