mirror of
https://gitlab.com/bramw/baserow.git
synced 2025-04-06 22:08:52 +00:00
Periodic data sync
This commit is contained in:
parent
6ceee4e155
commit
25378704a5
30 changed files with 2090 additions and 71 deletions
backend/src/baserow/contrib/database/data_sync
changelog/entries/unreleased/feature
docker-compose.ymldocs/installation
enterprise
backend
src/baserow_enterprise
tests/baserow_enterprise_tests
web-frontend/modules/baserow_enterprise
web-frontend/modules
|
@ -50,6 +50,7 @@ class DataSyncHandler:
|
|||
Returns the data sync matching the provided ID.
|
||||
|
||||
:param data_sync_id: The data sync ID to fetch.
|
||||
:param base_queryset: Optionally change the default queryset.
|
||||
:return: The fetched data sync object.
|
||||
"""
|
||||
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"type": "feature",
|
||||
"message": "Periodic data sync",
|
||||
"issue_number": 3071,
|
||||
"bullet_points": [],
|
||||
"created_at": "2025-01-15"
|
||||
}
|
|
@ -196,6 +196,8 @@ x-backend-variables: &backend-variables
|
|||
BASEROW_ASGI_HTTP_MAX_CONCURRENCY: ${BASEROW_ASGI_HTTP_MAX_CONCURRENCY:-}
|
||||
BASEROW_MAX_WEBHOOK_CALLS_IN_QUEUE_PER_WEBHOOK:
|
||||
BASEROW_MAX_HEALTHY_CELERY_QUEUE_SIZE:
|
||||
BASEROW_ENTERPRISE_PERIODIC_DATA_SYNC_CHECK_INTERVAL_MINUTES:
|
||||
BASEROW_ENTERPRISE_MAX_PERIODIC_DATA_SYNC_CONSECUTIVE_ERRORS:
|
||||
|
||||
|
||||
services:
|
||||
|
|
|
@ -65,7 +65,7 @@ The installation methods referred to in the variable descriptions are:
|
|||
|
||||
### Backend Database Configuration
|
||||
| Name | Description | Defaults |
|
||||
|---------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
|--------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| DATABASE\_HOST | The hostname of the postgres database Baserow will use to store its data in. | Defaults to db in the standalone and compose installs. If not provided in the \`baserow/baserow\` install then the embedded Postgres will be setup and used. |
|
||||
| DATABASE\_USER | The username of the database user Baserow will use to connect to the database at DATABASE\_HOST | baserow |
|
||||
| | | |
|
||||
|
@ -82,7 +82,7 @@ The installation methods referred to in the variable descriptions are:
|
|||
| SYNC\_TEMPLATES\_ON\_STARTUP | **Deprecated please use BASEROW\_TRIGGER\_SYNC\_TEMPLATES\_AFTER\_MIGRATION** If provided has the same effect of BASEROW\_TRIGGER\_SYNC\_TEMPLATES\_AFTER\_MIGRATION for backwards compatibility reasons. If BASEROW\_TRIGGER\_SYNC\_TEMPLATES\_AFTER\_MIGRATION is set it will override this value. | true |
|
||||
| DONT\_UPDATE\_FORMULAS\_AFTER\_MIGRATION | Baserow’s formulas have an internal version number. When upgrading Baserow if the formula language has also changed then after the database migration has run Baserow will also automatically recalculate all formulas if they have a different version. Set this to any non empty value to disable this automatic update if you would prefer to run the update\_formulas management command manually yourself. Formulas might break if you forget to do so after an upgrade of Baserow until and so it is recommended to leave this empty. | |
|
||||
| POSTGRES\_STARTUP\_CHECK\_ATTEMPTS | When Baserow's Backend service starts up it first checks to see if the postgres database is available. It checks 5 times by default, after which if it still has not connected it will crash. | 5 |
|
||||
| BASEROW\_PREVENT\_POSTGRESQL\_DATA\_SYNC\_CONNECTION\_\TO\_DATABASE | If true, then it's impossible to connect to the Baserow PostgreSQL database using the PostgreSQL data sync. | true |
|
||||
| BASEROW\_PREVENT\_POSTGRESQL\_DATA\_SYNC\_CONNECTION\_TO\_DATABASE | If true, then it's impossible to connect to the Baserow PostgreSQL database using the PostgreSQL data sync. | true |
|
||||
| BASEROW\_POSTGRESQL\_DATA\_SYNC\_BLACKLIST | Optionally provide a comma separated list of hostnames that the Baserow PostgreSQL data sync can't connect to. (e.g. "localhost,baserow.io") | |
|
||||
|
||||
### Redis Configuration
|
||||
|
@ -140,7 +140,7 @@ The installation methods referred to in the variable descriptions are:
|
|||
|
||||
### Backend Misc Configuration
|
||||
| Name | Description | Defaults |
|
||||
|------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|------------------------|
|
||||
|---------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|------------------------|
|
||||
| BASEROW\_ENABLE\_SECURE\_PROXY\_SSL\_HEADER | Set to any non-empty value to ensure Baserow generates https:// next links provided by paginated API endpoints. Baserow will still work correctly if not enabled, this is purely for giving the correct https url for clients of the API. If you have setup Baserow to use Caddy's auto HTTPS or you have put Baserow behind<br>a reverse proxy which:<br>* Handles HTTPS<br>* Strips the X-Forwarded-Proto header from all incoming requests.<br>* Sets the X-Forwarded-Proto header and sends it to Baserow.<br>Then you can safely set BASEROW\_ENABLE\_SECURE\_PROXY\_SSL\_HEADER=yes to ensure Baserow<br>generates https links for pagination correctly.<br> | |
|
||||
| ADDITIONAL\_APPS | A comma separated list of additional django applications to add to the INSTALLED\_APPS django setting | |
|
||||
| HOURS\_UNTIL\_TRASH\_PERMANENTLY\_DELETED | Items from the trash will be permanently deleted after this number of hours. | |
|
||||
|
@ -167,7 +167,8 @@ The installation methods referred to in the variable descriptions are:
|
|||
| BASEROW\_ICAL\_VIEW\_MAX\_EVENTS | The maximum number of events returned from ical feed endpoint. Empty value means no limit. | |
|
||||
| BASEROW\_ENTERPRISE\_AUDIT\_LOG\_CLEANUP\_INTERVAL_MINUTES | Sets the interval for periodic clean up check of the enterprise audit log in minutes. | 30 |
|
||||
| BASEROW\_ENTERPRISE\_AUDIT\_LOG\_RETENTION\_DAYS | The number of days that the enterprise audit log will be kept. | 365 |
|
||||
|
||||
| BASEROW\_ENTERPRISE\_PERIODIC\_DATA_SYNC\_CHECK\_INTERVAL\_MINUTES | The number of minutes that an async task is run to check if there are periodic data syncs that must run. It's safe to run this task frequently because it works in a non blocking way. | 1 |
|
||||
| BASEROW\_ENTERPRISE\_MAX\_PERIODIC\_DATA\_SYNC\_CONSECUTIVE\_ERRORS | The maximum number of consecutive periodic data sync error before it's disabled. | 4 |
|
||||
|
||||
|
||||
### Backend Application Builder Configuration
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
from rest_framework import serializers
|
||||
|
||||
from baserow_enterprise.data_sync.models import PeriodicDataSyncInterval
|
||||
|
||||
|
||||
class PeriodicDataSyncIntervalSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = PeriodicDataSyncInterval
|
||||
fields = (
|
||||
"interval",
|
||||
"when",
|
||||
"automatically_deactivated",
|
||||
)
|
||||
extra_kwargs = {"automatically_deactivated": {"read_only": True}}
|
|
@ -0,0 +1,13 @@
|
|||
from django.urls import re_path
|
||||
|
||||
from .views import PeriodicDataSyncIntervalView
|
||||
|
||||
app_name = "baserow_enterprise.api.data_sync"
|
||||
|
||||
urlpatterns = [
|
||||
re_path(
|
||||
r"(?P<data_sync_id>[0-9]+)/periodic-interval/$",
|
||||
PeriodicDataSyncIntervalView.as_view(),
|
||||
name="periodic_interval",
|
||||
),
|
||||
]
|
142
enterprise/backend/src/baserow_enterprise/api/data_sync/views.py
Normal file
142
enterprise/backend/src/baserow_enterprise/api/data_sync/views.py
Normal file
|
@ -0,0 +1,142 @@
|
|||
from django.db import transaction
|
||||
|
||||
from baserow_premium.license.handler import LicenseHandler
|
||||
from drf_spectacular.openapi import OpenApiParameter, OpenApiTypes
|
||||
from drf_spectacular.utils import extend_schema
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
|
||||
from baserow.api.decorators import map_exceptions, validate_body
|
||||
from baserow.api.errors import ERROR_USER_NOT_IN_GROUP
|
||||
from baserow.api.schemas import get_error_schema
|
||||
from baserow.contrib.database.api.data_sync.errors import ERROR_DATA_SYNC_DOES_NOT_EXIST
|
||||
from baserow.contrib.database.data_sync.exceptions import DataSyncDoesNotExist
|
||||
from baserow.contrib.database.data_sync.handler import DataSyncHandler
|
||||
from baserow.contrib.database.data_sync.models import DataSync
|
||||
from baserow.contrib.database.data_sync.operations import (
|
||||
GetIncludingPublicValuesOperationType,
|
||||
)
|
||||
from baserow.core.action.registries import action_type_registry
|
||||
from baserow.core.exceptions import UserNotInWorkspace
|
||||
from baserow.core.handler import CoreHandler
|
||||
from baserow_enterprise.data_sync.actions import (
|
||||
UpdatePeriodicDataSyncIntervalActionType,
|
||||
)
|
||||
from baserow_enterprise.data_sync.models import DATA_SYNC_INTERVAL_MANUAL
|
||||
|
||||
from ...features import DATA_SYNC
|
||||
from .serializers import PeriodicDataSyncIntervalSerializer
|
||||
|
||||
|
||||
class PeriodicDataSyncIntervalView(APIView):
|
||||
permission_classes = (IsAuthenticated,)
|
||||
|
||||
@extend_schema(
|
||||
parameters=[
|
||||
OpenApiParameter(
|
||||
name="data_sync_id",
|
||||
location=OpenApiParameter.PATH,
|
||||
type=OpenApiTypes.INT,
|
||||
description="The data sync where to fetch the periodic settings for.",
|
||||
),
|
||||
],
|
||||
tags=["Database tables"],
|
||||
operation_id="get_periodic_data_sync_interval",
|
||||
description=(
|
||||
"Responds with the periodic data sync interval data, if the user has the "
|
||||
"right permissions."
|
||||
"\nThis is an **enterprise** feature."
|
||||
),
|
||||
responses={
|
||||
200: PeriodicDataSyncIntervalSerializer,
|
||||
400: get_error_schema(["ERROR_USER_NOT_IN_GROUP"]),
|
||||
404: get_error_schema(["ERROR_DATA_SYNC_DOES_NOT_EXIST"]),
|
||||
},
|
||||
)
|
||||
@map_exceptions(
|
||||
{
|
||||
DataSyncDoesNotExist: ERROR_DATA_SYNC_DOES_NOT_EXIST,
|
||||
UserNotInWorkspace: ERROR_USER_NOT_IN_GROUP,
|
||||
}
|
||||
)
|
||||
def get(self, request, data_sync_id):
|
||||
"""Responds with the periodic data sync interval."""
|
||||
|
||||
data_sync = DataSyncHandler().get_data_sync(
|
||||
data_sync_id,
|
||||
base_queryset=DataSync.objects.select_related(
|
||||
"periodic_interval", "table__database__workspace"
|
||||
),
|
||||
)
|
||||
|
||||
LicenseHandler.raise_if_workspace_doesnt_have_feature(
|
||||
DATA_SYNC, data_sync.table.database.workspace
|
||||
)
|
||||
|
||||
CoreHandler().check_permissions(
|
||||
request.user,
|
||||
GetIncludingPublicValuesOperationType.type,
|
||||
workspace=data_sync.table.database.workspace,
|
||||
context=data_sync.table,
|
||||
)
|
||||
|
||||
if not hasattr(data_sync, "periodic_interval"):
|
||||
periodic_interval = {
|
||||
"interval": DATA_SYNC_INTERVAL_MANUAL,
|
||||
"when": None,
|
||||
"automatically_deactivated": False,
|
||||
}
|
||||
else:
|
||||
periodic_interval = data_sync.periodic_interval
|
||||
|
||||
serializer = PeriodicDataSyncIntervalSerializer(periodic_interval)
|
||||
return Response(serializer.data)
|
||||
|
||||
@extend_schema(
|
||||
parameters=[
|
||||
OpenApiParameter(
|
||||
name="data_sync_id",
|
||||
location=OpenApiParameter.PATH,
|
||||
type=OpenApiTypes.INT,
|
||||
description="Updates the data sync related to the provided value.",
|
||||
),
|
||||
],
|
||||
tags=["Database tables"],
|
||||
operation_id="update_periodic_data_sync_interval",
|
||||
description=(
|
||||
"Updates the periodic data sync interval, if the user has "
|
||||
"the right permissions."
|
||||
"\nThis is an **enterprise** feature."
|
||||
),
|
||||
request=PeriodicDataSyncIntervalSerializer,
|
||||
responses={
|
||||
200: PeriodicDataSyncIntervalSerializer,
|
||||
400: get_error_schema(["ERROR_USER_NOT_IN_GROUP"]),
|
||||
404: get_error_schema(["ERROR_DATA_SYNC_DOES_NOT_EXIST"]),
|
||||
},
|
||||
)
|
||||
@transaction.atomic
|
||||
@map_exceptions(
|
||||
{
|
||||
DataSyncDoesNotExist: ERROR_DATA_SYNC_DOES_NOT_EXIST,
|
||||
UserNotInWorkspace: ERROR_USER_NOT_IN_GROUP,
|
||||
}
|
||||
)
|
||||
@validate_body(PeriodicDataSyncIntervalSerializer, return_validated=True)
|
||||
def patch(self, request, data_sync_id, data):
|
||||
"""Updates the periodic data sync interval."""
|
||||
|
||||
data_sync = DataSyncHandler().get_data_sync(
|
||||
data_sync_id,
|
||||
base_queryset=DataSync.objects.select_for_update(
|
||||
of=("self",)
|
||||
).select_related("table__database__workspace"),
|
||||
)
|
||||
|
||||
periodic_interval = action_type_registry.get_by_type(
|
||||
UpdatePeriodicDataSyncIntervalActionType
|
||||
).do(user=request.user, data_sync=data_sync, **data)
|
||||
|
||||
serializer = PeriodicDataSyncIntervalSerializer(periodic_interval)
|
||||
return Response(serializer.data)
|
|
@ -2,6 +2,7 @@ from django.urls import include, path
|
|||
|
||||
from .admin import urls as admin_urls
|
||||
from .audit_log import urls as audit_log_urls
|
||||
from .data_sync import urls as data_sync_urls
|
||||
from .role import urls as role_urls
|
||||
from .secure_file_serve import urls as secure_file_serve_urls
|
||||
from .teams import urls as teams_urls
|
||||
|
@ -14,4 +15,5 @@ urlpatterns = [
|
|||
path("admin/", include(admin_urls, namespace="admin")),
|
||||
path("audit-log/", include(audit_log_urls, namespace="audit_log")),
|
||||
path("files/", include(secure_file_serve_urls, namespace="files")),
|
||||
path("data-sync/", include(data_sync_urls, namespace="data_sync")),
|
||||
]
|
||||
|
|
|
@ -204,6 +204,12 @@ class BaserowEnterpriseConfig(AppConfig):
|
|||
data_sync_type_registry.register(GitLabIssuesDataSyncType())
|
||||
data_sync_type_registry.register(HubspotContactsDataSyncType())
|
||||
|
||||
from baserow_enterprise.data_sync.actions import (
|
||||
UpdatePeriodicDataSyncIntervalActionType,
|
||||
)
|
||||
|
||||
action_type_registry.register(UpdatePeriodicDataSyncIntervalActionType())
|
||||
|
||||
# Create default roles
|
||||
post_migrate.connect(sync_default_roles_after_migrate, sender=self)
|
||||
|
||||
|
|
|
@ -66,3 +66,12 @@ def setup(settings):
|
|||
] = "baserow_enterprise.secure_file_serve.storage.EnterpriseFileStorage"
|
||||
|
||||
settings.BASEROW_SERVE_FILES_THROUGH_BACKEND = serve_files_through_backend
|
||||
|
||||
settings.BASEROW_ENTERPRISE_PERIODIC_DATA_SYNC_CHECK_INTERVAL_MINUTES = int(
|
||||
os.getenv("BASEROW_ENTERPRISE_PERIODIC_DATA_SYNC_CHECK_INTERVAL_MINUTES", "")
|
||||
or 1
|
||||
)
|
||||
settings.BASEROW_ENTERPRISE_MAX_PERIODIC_DATA_SYNC_CONSECUTIVE_ERRORS = int(
|
||||
os.getenv("BASEROW_ENTERPRISE_MAX_PERIODIC_DATA_SYNC_CONSECUTIVE_ERRORS", "")
|
||||
or 4
|
||||
)
|
||||
|
|
|
@ -0,0 +1,80 @@
|
|||
import dataclasses
|
||||
from datetime import time
|
||||
|
||||
from django.contrib.auth.models import AbstractUser
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from baserow.contrib.database.action.scopes import DATABASE_ACTION_CONTEXT
|
||||
from baserow.contrib.database.data_sync.models import DataSync
|
||||
from baserow.core.action.registries import (
|
||||
ActionScopeStr,
|
||||
ActionType,
|
||||
ActionTypeDescription,
|
||||
)
|
||||
from baserow.core.action.scopes import ApplicationActionScopeType
|
||||
from baserow_enterprise.data_sync.handler import EnterpriseDataSyncHandler
|
||||
|
||||
|
||||
class UpdatePeriodicDataSyncIntervalActionType(ActionType):
|
||||
type = "update_periodic_data_sync_interval"
|
||||
description = ActionTypeDescription(
|
||||
_("Update periodic data sync interval"),
|
||||
_('Data sync table "%(table_name)s" (%(table_id)s) updated'),
|
||||
DATABASE_ACTION_CONTEXT,
|
||||
)
|
||||
analytics_params = [
|
||||
"database_id",
|
||||
"table_id",
|
||||
"data_sync_id",
|
||||
"interval",
|
||||
"when",
|
||||
]
|
||||
|
||||
@dataclasses.dataclass
|
||||
class Params:
|
||||
database_id: int
|
||||
database_name: str
|
||||
table_id: int
|
||||
table_name: str
|
||||
data_sync_id: int
|
||||
interval: str
|
||||
when: str
|
||||
|
||||
@classmethod
|
||||
def do(
|
||||
cls,
|
||||
user: AbstractUser,
|
||||
data_sync: DataSync,
|
||||
interval: str,
|
||||
when: time,
|
||||
) -> DataSync:
|
||||
data_sync = data_sync.specific
|
||||
|
||||
periodic_interval = (
|
||||
EnterpriseDataSyncHandler.update_periodic_data_sync_interval(
|
||||
user=user,
|
||||
data_sync=data_sync,
|
||||
interval=interval,
|
||||
when=when,
|
||||
)
|
||||
)
|
||||
|
||||
table = data_sync.table
|
||||
database = table.database
|
||||
workspace = database.workspace
|
||||
params = cls.Params(
|
||||
database.id,
|
||||
database.name,
|
||||
table.id,
|
||||
table.name,
|
||||
data_sync.id,
|
||||
interval,
|
||||
when.strftime("%H:%M:%S"),
|
||||
)
|
||||
cls.register_action(user, params, cls.scope(database.id), workspace=workspace)
|
||||
|
||||
return periodic_interval
|
||||
|
||||
@classmethod
|
||||
def scope(cls, database_id) -> ActionScopeStr:
|
||||
return ApplicationActionScopeType.value(database_id)
|
226
enterprise/backend/src/baserow_enterprise/data_sync/handler.py
Normal file
226
enterprise/backend/src/baserow_enterprise/data_sync/handler.py
Normal file
|
@ -0,0 +1,226 @@
|
|||
from datetime import datetime, time
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import AbstractUser
|
||||
from django.core.cache import cache
|
||||
from django.db import transaction
|
||||
from django.db.models import Q
|
||||
from django.utils import timezone
|
||||
|
||||
from baserow_premium.license.handler import LicenseHandler
|
||||
from loguru import logger
|
||||
|
||||
from baserow.contrib.database.data_sync.exceptions import (
|
||||
SyncDataSyncTableAlreadyRunning,
|
||||
)
|
||||
from baserow.contrib.database.data_sync.handler import DataSyncHandler
|
||||
from baserow.contrib.database.data_sync.models import DataSync
|
||||
from baserow.contrib.database.data_sync.operations import SyncTableOperationType
|
||||
from baserow.core.handler import CoreHandler
|
||||
from baserow_enterprise.data_sync.models import (
|
||||
DATA_SYNC_INTERVAL_DAILY,
|
||||
DATA_SYNC_INTERVAL_HOURLY,
|
||||
PeriodicDataSyncInterval,
|
||||
)
|
||||
from baserow_enterprise.features import DATA_SYNC
|
||||
|
||||
from .tasks import sync_periodic_data_sync
|
||||
|
||||
|
||||
class EnterpriseDataSyncHandler:
|
||||
@classmethod
|
||||
def update_periodic_data_sync_interval(
|
||||
cls,
|
||||
user: AbstractUser,
|
||||
data_sync: DataSync,
|
||||
interval: str,
|
||||
when: time,
|
||||
) -> PeriodicDataSyncInterval:
|
||||
"""
|
||||
Updates the periodic configuration of a data sync.
|
||||
|
||||
:param user: The user on whose behalf the periodic configuration is updated.
|
||||
This user is saved on the object, and is used when syncing the data sync.
|
||||
:param data_sync: The data sync where the periodic configuration must be
|
||||
updated for.
|
||||
:param interval: Accepts either `DATA_SYNC_INTERVAL_DAILY` or
|
||||
`DATA_SYNC_INTERVAL_DAILY` indicating how frequently the data sync must be
|
||||
updated.
|
||||
:param when: Indicates when the data sync must periodically be synced.
|
||||
:return: The created or updated periodic data sync object.
|
||||
"""
|
||||
|
||||
LicenseHandler.raise_if_workspace_doesnt_have_feature(
|
||||
DATA_SYNC, data_sync.table.database.workspace
|
||||
)
|
||||
|
||||
CoreHandler().check_permissions(
|
||||
user,
|
||||
SyncTableOperationType.type,
|
||||
workspace=data_sync.table.database.workspace,
|
||||
context=data_sync.table,
|
||||
)
|
||||
|
||||
periodic_data_sync, _ = PeriodicDataSyncInterval.objects.update_or_create(
|
||||
data_sync=data_sync,
|
||||
defaults={
|
||||
"interval": interval,
|
||||
"when": when,
|
||||
"authorized_user": user,
|
||||
"automatically_deactivated": False,
|
||||
},
|
||||
)
|
||||
|
||||
return periodic_data_sync
|
||||
|
||||
@classmethod
|
||||
def call_periodic_data_sync_syncs_that_are_due(cls):
|
||||
"""
|
||||
This method is typically called by an async task. It loops over all daily and
|
||||
hourly periodic data sync that are due to the synced, and fires a task for each
|
||||
to sync it.
|
||||
"""
|
||||
|
||||
now = timezone.now()
|
||||
now_time = time(
|
||||
now.hour, now.minute, now.second, now.microsecond, tzinfo=now.tzinfo
|
||||
)
|
||||
beginning_of_day = datetime(
|
||||
now.year, now.month, now.day, 0, 0, 0, 0, tzinfo=now.tzinfo
|
||||
)
|
||||
beginning_of_hour = datetime(
|
||||
now.year, now.month, now.day, now.hour, 0, 0, 0, tzinfo=now.tzinfo
|
||||
)
|
||||
|
||||
is_null = Q(last_periodic_sync__isnull=True)
|
||||
all_to_trigger = (
|
||||
PeriodicDataSyncInterval.objects.filter(
|
||||
Q(
|
||||
# If the interval is daily, the last periodic sync timestamp must be
|
||||
# yesterday or None meaning it hasn't been executed yet.
|
||||
is_null | Q(last_periodic_sync__lt=beginning_of_day),
|
||||
interval=DATA_SYNC_INTERVAL_DAILY,
|
||||
)
|
||||
| Q(
|
||||
# If the interval is hourly, the last periodic data sync timestamp
|
||||
# must be at least an hour ago or None meaning it hasn't been
|
||||
# executed yet.
|
||||
is_null | Q(last_periodic_sync__lt=beginning_of_hour),
|
||||
interval=DATA_SYNC_INTERVAL_HOURLY,
|
||||
),
|
||||
# Skip deactivated periodic data sync because they're not working
|
||||
# anymore.
|
||||
automatically_deactivated=False,
|
||||
# The now time must be higher than the now time because the data sync
|
||||
# must be triggered at the desired the of the user.
|
||||
when__lte=now_time,
|
||||
).select_related("data_sync__table__database__workspace")
|
||||
# Take a lock on the periodic data sync because the `last_periodic_sync`
|
||||
# must be updated immediately. This will make sure that if this method is
|
||||
# called frequently, it doesn't trigger the same. If self or `data_sync` is
|
||||
# locked, then we can skip the sync for now because the data sync is already
|
||||
# being updated. It doesn't matter if we skip it because it will then be
|
||||
# picked up the next time this method is called.
|
||||
.select_for_update(
|
||||
of=(
|
||||
"self",
|
||||
"data_sync",
|
||||
),
|
||||
skip_locked=True,
|
||||
)
|
||||
)
|
||||
|
||||
updated_periodic_data_sync = []
|
||||
for periodic_data_sync in all_to_trigger:
|
||||
workspace_has_feature = LicenseHandler.workspace_has_feature(
|
||||
DATA_SYNC, periodic_data_sync.data_sync.table.database.workspace
|
||||
)
|
||||
if workspace_has_feature:
|
||||
lock_key = DataSyncHandler().get_table_sync_lock_key(
|
||||
periodic_data_sync.data_sync_id
|
||||
)
|
||||
sync_is_running = cache.get(lock_key) is not None
|
||||
|
||||
periodic_data_sync.last_periodic_sync = now
|
||||
updated_periodic_data_sync.append(periodic_data_sync)
|
||||
|
||||
# If the sync is already running because the lock exists,
|
||||
# then nothing sohuld happen because the sync has already happened
|
||||
# within the correct periodic timeframe. We do want to update the
|
||||
# `last_periodic_sync`, so that it doesn't try again on the next run.
|
||||
if sync_is_running:
|
||||
logger.info(
|
||||
f"Skipping periodic data sync of data sync "
|
||||
f"{periodic_data_sync.data_sync_id} because the sync already "
|
||||
f"running."
|
||||
)
|
||||
else:
|
||||
transaction.on_commit(
|
||||
lambda: sync_periodic_data_sync.delay(periodic_data_sync.id)
|
||||
)
|
||||
|
||||
# Update the last periodic sync so the periodic sync won't be triggerd the next
|
||||
# time this method is called.
|
||||
if len(updated_periodic_data_sync) > 0:
|
||||
PeriodicDataSyncInterval.objects.bulk_update(
|
||||
all_to_trigger, fields=["last_periodic_sync"]
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def sync_periodic_data_sync(cls, periodic_data_sync_id):
|
||||
"""
|
||||
Syncs the data sync of a periodic data sync. This is typically executed by the
|
||||
async task `sync_periodic_data_sync`.
|
||||
|
||||
:param periodic_data_sync_id: The ID of the periodic data sync object that must
|
||||
be synced. Note that this not equal to the data sync ID.
|
||||
:return: True if the data sync ran, even if it wasn't successful. False if it
|
||||
never ran.
|
||||
"""
|
||||
|
||||
try:
|
||||
periodic_data_sync = (
|
||||
PeriodicDataSyncInterval.objects.select_related("data_sync")
|
||||
.select_for_update(of=("self",))
|
||||
.get(id=periodic_data_sync_id, automatically_deactivated=False)
|
||||
)
|
||||
except PeriodicDataSyncInterval.DoesNotExist:
|
||||
logger.info(
|
||||
f"Skipping periodic data sync {periodic_data_sync_id} because it "
|
||||
f"doesn't exist or has been deactivated."
|
||||
)
|
||||
return False
|
||||
|
||||
try:
|
||||
data_sync = DataSyncHandler().sync_data_sync_table(
|
||||
periodic_data_sync.authorized_user,
|
||||
periodic_data_sync.data_sync.specific,
|
||||
)
|
||||
except SyncDataSyncTableAlreadyRunning:
|
||||
# If the sync has started in the meantime, then we don't want to do
|
||||
# anything because the sync already ran.
|
||||
logger.info(
|
||||
f"Skipping periodic data sync of data sync "
|
||||
f"{periodic_data_sync.data_sync_id} because the sync is running."
|
||||
)
|
||||
return False
|
||||
|
||||
if data_sync.last_error:
|
||||
# If the data sync has an error, then something went wrong during execution,
|
||||
# and we need to increase the consecutive count so that when the max errors
|
||||
# is reached, we can deactivate it. This to protect the system from
|
||||
# periodically syncing a data sync that doesn't work anyway.
|
||||
periodic_data_sync.consecutive_failed_count += 1
|
||||
if (
|
||||
periodic_data_sync.consecutive_failed_count
|
||||
>= settings.BASEROW_ENTERPRISE_MAX_PERIODIC_DATA_SYNC_CONSECUTIVE_ERRORS
|
||||
):
|
||||
periodic_data_sync.automatically_deactivated = True
|
||||
periodic_data_sync.save()
|
||||
elif periodic_data_sync.consecutive_failed_count > 0:
|
||||
# Once it runs successfully, the consecutive count can be reset because we
|
||||
# now know it actually works, and it doesn't have to be deactivated anymore.
|
||||
periodic_data_sync.consecutive_failed_count = 0
|
||||
periodic_data_sync.save()
|
||||
|
||||
return True
|
|
@ -8,6 +8,48 @@ from baserow.contrib.database.views.models import View
|
|||
User = get_user_model()
|
||||
|
||||
|
||||
DATA_SYNC_INTERVAL_MANUAL = "MANUAL"
|
||||
DATA_SYNC_INTERVAL_DAILY = "DAILY"
|
||||
DATA_SYNC_INTERVAL_HOURLY = "HOURLY"
|
||||
|
||||
|
||||
class PeriodicDataSyncInterval(models.Model):
|
||||
data_sync = models.OneToOneField(
|
||||
DataSync,
|
||||
on_delete=models.CASCADE,
|
||||
help_text="The periodic data sync.",
|
||||
related_name="periodic_interval",
|
||||
)
|
||||
last_periodic_sync = models.DateTimeField(
|
||||
null=True, help_text="Timestamp when the table was last periodically synced."
|
||||
)
|
||||
interval = models.CharField(
|
||||
choices=(
|
||||
(DATA_SYNC_INTERVAL_MANUAL, DATA_SYNC_INTERVAL_MANUAL),
|
||||
(DATA_SYNC_INTERVAL_DAILY, DATA_SYNC_INTERVAL_DAILY),
|
||||
(DATA_SYNC_INTERVAL_HOURLY, DATA_SYNC_INTERVAL_HOURLY),
|
||||
),
|
||||
default=DATA_SYNC_INTERVAL_MANUAL,
|
||||
)
|
||||
when = models.TimeField()
|
||||
automatically_deactivated = models.BooleanField(
|
||||
default=False,
|
||||
help_text="Indicates whether the periodic data sync has been deactivated.",
|
||||
)
|
||||
consecutive_failed_count = models.SmallIntegerField(
|
||||
default=0,
|
||||
help_text="The number of failed sync data sync operations that have failed. "
|
||||
"This is used to deactivate the periodic sync if it keeps failing.",
|
||||
)
|
||||
authorized_user = models.ForeignKey(
|
||||
User,
|
||||
on_delete=models.SET_NULL,
|
||||
null=True,
|
||||
help_text="The user on whose behalf the data is periodically synced."
|
||||
"Automatically set when the interval changes.",
|
||||
)
|
||||
|
||||
|
||||
class LocalBaserowTableDataSync(DataSync):
|
||||
source_table = models.ForeignKey(
|
||||
Table,
|
||||
|
|
31
enterprise/backend/src/baserow_enterprise/data_sync/tasks.py
Normal file
31
enterprise/backend/src/baserow_enterprise/data_sync/tasks.py
Normal file
|
@ -0,0 +1,31 @@
|
|||
from datetime import timedelta
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import transaction
|
||||
|
||||
from baserow.config.celery import app
|
||||
|
||||
|
||||
@app.task(bind=True, queue="export")
|
||||
def call_periodic_data_sync_syncs_that_are_due(self):
|
||||
from baserow_enterprise.data_sync.handler import EnterpriseDataSyncHandler
|
||||
|
||||
with transaction.atomic():
|
||||
EnterpriseDataSyncHandler().call_periodic_data_sync_syncs_that_are_due()
|
||||
|
||||
|
||||
@app.on_after_finalize.connect
|
||||
def setup_periodic_enterprise_data_sync_tasks(sender, **kwargs):
|
||||
every = timedelta(
|
||||
minutes=settings.BASEROW_ENTERPRISE_PERIODIC_DATA_SYNC_CHECK_INTERVAL_MINUTES
|
||||
)
|
||||
|
||||
sender.add_periodic_task(every, call_periodic_data_sync_syncs_that_are_due.s())
|
||||
|
||||
|
||||
@app.task(bind=True, queue="export")
|
||||
def sync_periodic_data_sync(self, periodic_data_sync_id):
|
||||
from baserow_enterprise.data_sync.handler import EnterpriseDataSyncHandler
|
||||
|
||||
with transaction.atomic():
|
||||
EnterpriseDataSyncHandler().sync_periodic_data_sync(periodic_data_sync_id)
|
|
@ -0,0 +1,81 @@
|
|||
# Generated by Django 5.0.9 on 2025-01-14 20:35
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("baserow_enterprise", "0036_localbaserowtabledatasync_source_table_view_id"),
|
||||
("database", "0175_formviewfieldoptions_include_all_select_options_and_more"),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="PeriodicDataSyncInterval",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.AutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
(
|
||||
"last_periodic_sync",
|
||||
models.DateTimeField(
|
||||
help_text="Timestamp when the table was last periodically synced.",
|
||||
null=True,
|
||||
),
|
||||
),
|
||||
(
|
||||
"interval",
|
||||
models.CharField(
|
||||
choices=[
|
||||
("MANUAL", "MANUAL"),
|
||||
("DAILY", "DAILY"),
|
||||
("HOURLY", "HOURLY"),
|
||||
],
|
||||
default="MANUAL",
|
||||
),
|
||||
),
|
||||
("when", models.TimeField()),
|
||||
(
|
||||
"automatically_deactivated",
|
||||
models.BooleanField(
|
||||
default=False,
|
||||
help_text="Indicates whether the periodic data sync has been deactivated.",
|
||||
),
|
||||
),
|
||||
(
|
||||
"consecutive_failed_count",
|
||||
models.SmallIntegerField(
|
||||
default=0,
|
||||
help_text="The number of failed sync data sync operations that have failed. This is used to deactivate the periodic sync if it keeps failing.",
|
||||
),
|
||||
),
|
||||
(
|
||||
"authorized_user",
|
||||
models.ForeignKey(
|
||||
help_text="The user on whose behalf the data is periodically synced.Automatically set when the interval changes.",
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
(
|
||||
"data_sync",
|
||||
models.OneToOneField(
|
||||
help_text="The periodic data sync.",
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="periodic_interval",
|
||||
to="database.datasync",
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
]
|
|
@ -11,6 +11,10 @@ from baserow_enterprise.audit_log.tasks import (
|
|||
clean_up_audit_log_entries,
|
||||
setup_periodic_audit_log_tasks,
|
||||
)
|
||||
from baserow_enterprise.data_sync.tasks import (
|
||||
call_periodic_data_sync_syncs_that_are_due,
|
||||
sync_periodic_data_sync,
|
||||
)
|
||||
|
||||
|
||||
@app.task(bind=True, queue="export")
|
||||
|
@ -64,4 +68,9 @@ def unsubscribe_subject_from_tables_currently_subscribed_to_task(
|
|||
)
|
||||
|
||||
|
||||
__all__ = ["clean_up_audit_log_entries", "setup_periodic_audit_log_tasks"]
|
||||
__all__ = [
|
||||
"clean_up_audit_log_entries",
|
||||
"setup_periodic_audit_log_tasks",
|
||||
"sync_periodic_data_sync",
|
||||
"call_periodic_data_sync_syncs_that_are_due",
|
||||
]
|
||||
|
|
|
@ -0,0 +1,198 @@
|
|||
from datetime import time
|
||||
|
||||
from django.shortcuts import reverse
|
||||
from django.test.utils import override_settings
|
||||
|
||||
import pytest
|
||||
from rest_framework.status import HTTP_200_OK, HTTP_402_PAYMENT_REQUIRED
|
||||
|
||||
from baserow_enterprise.audit_log.models import AuditLogEntry
|
||||
from baserow_enterprise.data_sync.handler import EnterpriseDataSyncHandler
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@override_settings(DEBUG=True)
|
||||
def test_get_existing_periodic_data_sync_interval(api_client, enterprise_data_fixture):
|
||||
enterprise_data_fixture.enable_enterprise()
|
||||
|
||||
user, token = enterprise_data_fixture.create_user_and_token()
|
||||
data_sync = enterprise_data_fixture.create_ical_data_sync(user=user)
|
||||
|
||||
EnterpriseDataSyncHandler.update_periodic_data_sync_interval(
|
||||
user=user,
|
||||
data_sync=data_sync,
|
||||
interval="DAILY",
|
||||
when=time(hour=12, minute=10, second=1, microsecond=1),
|
||||
)
|
||||
|
||||
response = api_client.get(
|
||||
reverse(
|
||||
f"api:enterprise:data_sync:periodic_interval",
|
||||
kwargs={"data_sync_id": data_sync.id},
|
||||
),
|
||||
format="json",
|
||||
HTTP_AUTHORIZATION=f"JWT {token}",
|
||||
)
|
||||
assert response.status_code == HTTP_200_OK
|
||||
assert response.json() == {
|
||||
"interval": "DAILY",
|
||||
"when": "12:10:01.000001",
|
||||
"automatically_deactivated": False,
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@override_settings(DEBUG=True)
|
||||
def test_get_not_existing_periodic_data_sync_interval(
|
||||
api_client, enterprise_data_fixture
|
||||
):
|
||||
enterprise_data_fixture.enable_enterprise()
|
||||
|
||||
user, token = enterprise_data_fixture.create_user_and_token()
|
||||
data_sync = enterprise_data_fixture.create_ical_data_sync(user=user)
|
||||
|
||||
response = api_client.get(
|
||||
reverse(
|
||||
f"api:enterprise:data_sync:periodic_interval",
|
||||
kwargs={"data_sync_id": data_sync.id},
|
||||
),
|
||||
format="json",
|
||||
HTTP_AUTHORIZATION=f"JWT {token}",
|
||||
)
|
||||
assert response.status_code == HTTP_200_OK
|
||||
assert response.json() == {
|
||||
"interval": "MANUAL",
|
||||
"when": None,
|
||||
"automatically_deactivated": False,
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@override_settings(DEBUG=True)
|
||||
def test_get_periodic_data_sync_interval_without_license(
|
||||
api_client, enterprise_data_fixture
|
||||
):
|
||||
user, token = enterprise_data_fixture.create_user_and_token()
|
||||
data_sync = enterprise_data_fixture.create_ical_data_sync(user=user)
|
||||
|
||||
response = api_client.get(
|
||||
reverse(
|
||||
f"api:enterprise:data_sync:periodic_interval",
|
||||
kwargs={"data_sync_id": data_sync.id},
|
||||
),
|
||||
format="json",
|
||||
HTTP_AUTHORIZATION=f"JWT {token}",
|
||||
)
|
||||
assert response.status_code == HTTP_402_PAYMENT_REQUIRED
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@override_settings(DEBUG=True)
|
||||
def test_update_periodic_data_sync(api_client, enterprise_data_fixture):
|
||||
enterprise_data_fixture.enable_enterprise()
|
||||
|
||||
user, token = enterprise_data_fixture.create_user_and_token()
|
||||
data_sync = enterprise_data_fixture.create_ical_data_sync(user=user)
|
||||
|
||||
response = api_client.patch(
|
||||
reverse(
|
||||
f"api:enterprise:data_sync:periodic_interval",
|
||||
kwargs={"data_sync_id": data_sync.id},
|
||||
),
|
||||
{"interval": "HOURLY", "when": "12:10:01.000001"},
|
||||
format="json",
|
||||
HTTP_AUTHORIZATION=f"JWT {token}",
|
||||
)
|
||||
assert response.status_code == HTTP_200_OK
|
||||
assert response.json() == {
|
||||
"interval": "HOURLY",
|
||||
"when": "12:10:01.000001",
|
||||
"automatically_deactivated": False,
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@override_settings(DEBUG=True)
|
||||
def test_update_periodic_data_sync_without_license(api_client, enterprise_data_fixture):
|
||||
user, token = enterprise_data_fixture.create_user_and_token()
|
||||
data_sync = enterprise_data_fixture.create_ical_data_sync(user=user)
|
||||
|
||||
response = api_client.patch(
|
||||
reverse(
|
||||
f"api:enterprise:data_sync:periodic_interval",
|
||||
kwargs={"data_sync_id": data_sync.id},
|
||||
),
|
||||
{"interval": "HOURLY", "when": "12:10:01.000001"},
|
||||
format="json",
|
||||
HTTP_AUTHORIZATION=f"JWT {token}",
|
||||
)
|
||||
assert response.status_code == HTTP_402_PAYMENT_REQUIRED
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@override_settings(DEBUG=True)
|
||||
def test_update_periodic_data_sync_automatically_deactivated_false(
|
||||
api_client, enterprise_data_fixture
|
||||
):
|
||||
enterprise_data_fixture.enable_enterprise()
|
||||
|
||||
user, token = enterprise_data_fixture.create_user_and_token()
|
||||
data_sync = enterprise_data_fixture.create_ical_data_sync(user=user)
|
||||
|
||||
periodic_data_sync = EnterpriseDataSyncHandler.update_periodic_data_sync_interval(
|
||||
user=user,
|
||||
data_sync=data_sync,
|
||||
interval="DAILY",
|
||||
when=time(hour=12, minute=10, second=1, microsecond=1),
|
||||
)
|
||||
periodic_data_sync.automatically_deactivated = True
|
||||
|
||||
response = api_client.patch(
|
||||
reverse(
|
||||
f"api:enterprise:data_sync:periodic_interval",
|
||||
kwargs={"data_sync_id": data_sync.id},
|
||||
),
|
||||
{
|
||||
"interval": "HOURLY",
|
||||
"when": "12:10:01.000001",
|
||||
},
|
||||
format="json",
|
||||
HTTP_AUTHORIZATION=f"JWT {token}",
|
||||
)
|
||||
assert response.status_code == HTTP_200_OK
|
||||
assert response.json()["automatically_deactivated"] is False
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@override_settings(DEBUG=True)
|
||||
def test_update_periodic_data_sync_audit_log_created(
|
||||
api_client, enterprise_data_fixture
|
||||
):
|
||||
enterprise_data_fixture.enable_enterprise()
|
||||
|
||||
user, token = enterprise_data_fixture.create_user_and_token()
|
||||
data_sync = enterprise_data_fixture.create_ical_data_sync(user=user)
|
||||
|
||||
response = api_client.patch(
|
||||
reverse(
|
||||
f"api:enterprise:data_sync:periodic_interval",
|
||||
kwargs={"data_sync_id": data_sync.id},
|
||||
),
|
||||
{"interval": "HOURLY", "when": "12:10:01.000001"},
|
||||
format="json",
|
||||
HTTP_AUTHORIZATION=f"JWT {token}",
|
||||
)
|
||||
assert response.status_code == HTTP_200_OK
|
||||
|
||||
audit_log_entry = AuditLogEntry.objects.get(
|
||||
action_type="update_periodic_data_sync_interval"
|
||||
)
|
||||
assert audit_log_entry.action_params == {
|
||||
"when": "12:10:01",
|
||||
"interval": "HOURLY",
|
||||
"table_id": data_sync.table_id,
|
||||
"table_name": data_sync.table.name,
|
||||
"database_id": data_sync.table.database_id,
|
||||
"data_sync_id": data_sync.id,
|
||||
"database_name": data_sync.table.database.name,
|
||||
}
|
|
@ -0,0 +1,669 @@
|
|||
from datetime import datetime, time, timezone
|
||||
from unittest.mock import patch
|
||||
|
||||
from django.core.cache import cache
|
||||
from django.db import transaction
|
||||
from django.test.utils import override_settings
|
||||
from django.utils import timezone as django_timezone
|
||||
|
||||
import pytest
|
||||
import responses
|
||||
from baserow_premium.license.exceptions import FeaturesNotAvailableError
|
||||
from baserow_premium.license.models import License
|
||||
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_enterprise.data_sync.handler import EnterpriseDataSyncHandler
|
||||
from baserow_enterprise.data_sync.models import PeriodicDataSyncInterval
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@override_settings(DEBUG=True)
|
||||
def test_update_periodic_data_sync_interval_licence_check(enterprise_data_fixture):
|
||||
user = enterprise_data_fixture.create_user()
|
||||
data_sync = enterprise_data_fixture.create_ical_data_sync(user=user)
|
||||
|
||||
with pytest.raises(FeaturesNotAvailableError):
|
||||
EnterpriseDataSyncHandler.update_periodic_data_sync_interval(
|
||||
user=user,
|
||||
data_sync=data_sync,
|
||||
interval="MANUAL",
|
||||
when=time(hour=12, minute=10),
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@override_settings(DEBUG=True)
|
||||
def test_update_periodic_data_sync_interval_check_permissions(enterprise_data_fixture):
|
||||
enterprise_data_fixture.enable_enterprise()
|
||||
|
||||
user = enterprise_data_fixture.create_user()
|
||||
data_sync = enterprise_data_fixture.create_ical_data_sync()
|
||||
|
||||
with pytest.raises(UserNotInWorkspace):
|
||||
EnterpriseDataSyncHandler.update_periodic_data_sync_interval(
|
||||
user=user,
|
||||
data_sync=data_sync,
|
||||
interval="MANUAL",
|
||||
when=time(hour=12, minute=10),
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@override_settings(DEBUG=True)
|
||||
def test_update_periodic_data_sync_interval_create(enterprise_data_fixture):
|
||||
enterprise_data_fixture.enable_enterprise()
|
||||
|
||||
user = enterprise_data_fixture.create_user()
|
||||
data_sync = enterprise_data_fixture.create_ical_data_sync(user=user)
|
||||
|
||||
periodic_data_sync_interval = (
|
||||
EnterpriseDataSyncHandler.update_periodic_data_sync_interval(
|
||||
user=user,
|
||||
data_sync=data_sync,
|
||||
interval="DAILY",
|
||||
when=time(hour=12, minute=10, second=1, microsecond=1),
|
||||
)
|
||||
)
|
||||
|
||||
fetched_periodic_data_sync_interval = PeriodicDataSyncInterval.objects.all().first()
|
||||
assert periodic_data_sync_interval.id == fetched_periodic_data_sync_interval.id
|
||||
assert (
|
||||
periodic_data_sync_interval.data_sync_id
|
||||
== periodic_data_sync_interval.data_sync_id
|
||||
== data_sync.id
|
||||
)
|
||||
assert (
|
||||
periodic_data_sync_interval.interval
|
||||
== periodic_data_sync_interval.interval
|
||||
== "DAILY"
|
||||
)
|
||||
assert (
|
||||
periodic_data_sync_interval.when
|
||||
== periodic_data_sync_interval.when
|
||||
== time(hour=12, minute=10, second=1, microsecond=1)
|
||||
)
|
||||
assert periodic_data_sync_interval.authorized_user_id == user.id
|
||||
assert periodic_data_sync_interval.automatically_deactivated is False
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@override_settings(DEBUG=True)
|
||||
def test_update_periodic_data_sync_interval_update(enterprise_data_fixture):
|
||||
enterprise_data_fixture.enable_enterprise()
|
||||
|
||||
user = enterprise_data_fixture.create_user()
|
||||
data_sync = enterprise_data_fixture.create_ical_data_sync(user=user)
|
||||
|
||||
EnterpriseDataSyncHandler.update_periodic_data_sync_interval(
|
||||
user=user,
|
||||
data_sync=data_sync,
|
||||
interval="DAILY",
|
||||
when=time(hour=12, minute=10, second=1, microsecond=1),
|
||||
)
|
||||
|
||||
periodic_data_sync_interval = (
|
||||
EnterpriseDataSyncHandler.update_periodic_data_sync_interval(
|
||||
user=user,
|
||||
data_sync=data_sync,
|
||||
interval="HOURLY",
|
||||
when=time(hour=14, minute=12, second=1, microsecond=1),
|
||||
)
|
||||
)
|
||||
|
||||
fetched_periodic_data_sync_interval = PeriodicDataSyncInterval.objects.all().first()
|
||||
assert periodic_data_sync_interval.id == fetched_periodic_data_sync_interval.id
|
||||
assert (
|
||||
periodic_data_sync_interval.data_sync_id
|
||||
== periodic_data_sync_interval.data_sync_id
|
||||
== data_sync.id
|
||||
)
|
||||
assert (
|
||||
periodic_data_sync_interval.interval
|
||||
== periodic_data_sync_interval.interval
|
||||
== "HOURLY"
|
||||
)
|
||||
assert (
|
||||
periodic_data_sync_interval.when
|
||||
== periodic_data_sync_interval.when
|
||||
== time(hour=14, minute=12, second=1, microsecond=1)
|
||||
)
|
||||
assert periodic_data_sync_interval.authorized_user_id == user.id
|
||||
assert periodic_data_sync_interval.automatically_deactivated is False
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@override_settings(DEBUG=True)
|
||||
def test_update_periodic_data_sync_interval_update_automatically_disabled(
|
||||
enterprise_data_fixture,
|
||||
):
|
||||
enterprise_data_fixture.enable_enterprise()
|
||||
|
||||
user = enterprise_data_fixture.create_user()
|
||||
data_sync = enterprise_data_fixture.create_ical_data_sync(user=user)
|
||||
|
||||
periodic_data_sync = EnterpriseDataSyncHandler.update_periodic_data_sync_interval(
|
||||
user=user,
|
||||
data_sync=data_sync,
|
||||
interval="DAILY",
|
||||
when=time(hour=12, minute=10, second=1, microsecond=1),
|
||||
)
|
||||
periodic_data_sync.automatically_deactivated = True
|
||||
periodic_data_sync.save()
|
||||
|
||||
periodic_data_sync = EnterpriseDataSyncHandler.update_periodic_data_sync_interval(
|
||||
user=user,
|
||||
data_sync=data_sync,
|
||||
interval="HOURLY",
|
||||
when=time(hour=14, minute=12, second=1, microsecond=1),
|
||||
)
|
||||
assert periodic_data_sync.automatically_deactivated is False
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@override_settings(DEBUG=True)
|
||||
def test_call_daily_periodic_data_sync_syncs(enterprise_data_fixture):
|
||||
enterprise_data_fixture.enable_enterprise()
|
||||
user = enterprise_data_fixture.create_user()
|
||||
|
||||
not_yet_executed_1 = 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),
|
||||
)
|
||||
not_yet_executed_1.refresh_from_db()
|
||||
|
||||
not_yet_executed_2 = 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=30, second=1, microsecond=1),
|
||||
)
|
||||
|
||||
already_executed_today_1 = (
|
||||
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),
|
||||
)
|
||||
)
|
||||
already_executed_today_1.last_periodic_sync = datetime(
|
||||
2024, 10, 10, 11, 0, 1, 1, tzinfo=timezone.utc
|
||||
)
|
||||
already_executed_today_1.save()
|
||||
|
||||
already_executed_yesterday_1 = (
|
||||
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),
|
||||
)
|
||||
)
|
||||
already_executed_yesterday_1.last_periodic_sync = datetime(
|
||||
2024, 10, 9, 11, 0, 1, 1, tzinfo=timezone.utc
|
||||
)
|
||||
already_executed_yesterday_1.save()
|
||||
|
||||
with freeze_time("2024-10-10T12:15:00.00Z") as frozen:
|
||||
EnterpriseDataSyncHandler.call_periodic_data_sync_syncs_that_are_due()
|
||||
frozen_datetime = django_timezone.now()
|
||||
|
||||
not_yet_executed_1.refresh_from_db()
|
||||
# executed because not yet executed before and due.
|
||||
assert not_yet_executed_1.last_periodic_sync == frozen_datetime
|
||||
|
||||
not_yet_executed_2.refresh_from_db()
|
||||
# skipped because not yet due
|
||||
assert not_yet_executed_2.last_periodic_sync != frozen_datetime
|
||||
|
||||
already_executed_today_1.refresh_from_db()
|
||||
# skipped because already executed
|
||||
assert already_executed_today_1.last_periodic_sync != frozen_datetime
|
||||
|
||||
already_executed_yesterday_1.refresh_from_db()
|
||||
# executed because was last executed yesterday.
|
||||
assert already_executed_yesterday_1.last_periodic_sync == frozen_datetime
|
||||
|
||||
with freeze_time("2024-10-10T12:31:00.00Z") as frozen:
|
||||
EnterpriseDataSyncHandler.call_periodic_data_sync_syncs_that_are_due()
|
||||
frozen_datetime = django_timezone.now()
|
||||
|
||||
not_yet_executed_1.refresh_from_db()
|
||||
# not executed because not yet due.
|
||||
assert not_yet_executed_1.last_periodic_sync != frozen_datetime
|
||||
|
||||
not_yet_executed_2.refresh_from_db()
|
||||
# executed because not yet executed before and due.
|
||||
assert not_yet_executed_2.last_periodic_sync == frozen_datetime
|
||||
|
||||
already_executed_today_1.refresh_from_db()
|
||||
# not executed because not yet due.
|
||||
assert already_executed_today_1.last_periodic_sync != frozen_datetime
|
||||
|
||||
already_executed_yesterday_1.refresh_from_db()
|
||||
# not executed because not yet due.
|
||||
assert already_executed_yesterday_1.last_periodic_sync != frozen_datetime
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@override_settings(DEBUG=True)
|
||||
def test_call_hourly_periodic_data_sync_syncs(enterprise_data_fixture):
|
||||
enterprise_data_fixture.enable_enterprise()
|
||||
user = enterprise_data_fixture.create_user()
|
||||
|
||||
not_yet_executed_1 = EnterpriseDataSyncHandler.update_periodic_data_sync_interval(
|
||||
user=user,
|
||||
data_sync=enterprise_data_fixture.create_ical_data_sync(user=user),
|
||||
interval="HOURLY",
|
||||
when=time(hour=12, minute=10, second=1, microsecond=1),
|
||||
)
|
||||
not_yet_executed_1.refresh_from_db()
|
||||
|
||||
not_yet_executed_2 = EnterpriseDataSyncHandler.update_periodic_data_sync_interval(
|
||||
user=user,
|
||||
data_sync=enterprise_data_fixture.create_ical_data_sync(user=user),
|
||||
interval="HOURLY",
|
||||
when=time(hour=12, minute=30, second=1, microsecond=1),
|
||||
)
|
||||
|
||||
already_executed_this_hour_1 = (
|
||||
EnterpriseDataSyncHandler.update_periodic_data_sync_interval(
|
||||
user=user,
|
||||
data_sync=enterprise_data_fixture.create_ical_data_sync(user=user),
|
||||
interval="HOURLY",
|
||||
when=time(hour=12, minute=10, second=1, microsecond=1),
|
||||
)
|
||||
)
|
||||
already_executed_this_hour_1.last_periodic_sync = datetime(
|
||||
2024, 10, 10, 12, 10, 1, 1, tzinfo=timezone.utc
|
||||
)
|
||||
already_executed_this_hour_1.save()
|
||||
|
||||
already_executed_last_hour_1 = (
|
||||
EnterpriseDataSyncHandler.update_periodic_data_sync_interval(
|
||||
user=user,
|
||||
data_sync=enterprise_data_fixture.create_ical_data_sync(user=user),
|
||||
interval="HOURLY",
|
||||
when=time(hour=12, minute=10, second=1, microsecond=1),
|
||||
)
|
||||
)
|
||||
already_executed_last_hour_1.last_periodic_sync = datetime(
|
||||
2024, 10, 10, 11, 20, 1, 1, tzinfo=timezone.utc
|
||||
)
|
||||
already_executed_last_hour_1.save()
|
||||
|
||||
with freeze_time("2024-10-10T12:15:00.00Z") as frozen:
|
||||
EnterpriseDataSyncHandler.call_periodic_data_sync_syncs_that_are_due()
|
||||
frozen_datetime = django_timezone.now()
|
||||
|
||||
not_yet_executed_1.refresh_from_db()
|
||||
# executed because not yet executed before and due.
|
||||
assert not_yet_executed_1.last_periodic_sync == frozen_datetime
|
||||
|
||||
not_yet_executed_2.refresh_from_db()
|
||||
# skipped because not yet due
|
||||
assert not_yet_executed_2.last_periodic_sync != frozen_datetime
|
||||
|
||||
already_executed_this_hour_1.refresh_from_db()
|
||||
# skipped because already executed
|
||||
assert already_executed_this_hour_1.last_periodic_sync != frozen_datetime
|
||||
|
||||
already_executed_last_hour_1.refresh_from_db()
|
||||
# executed because was last executed yesterday.
|
||||
assert already_executed_last_hour_1.last_periodic_sync == frozen_datetime
|
||||
|
||||
with freeze_time("2024-10-10T12:35:00.00Z") as frozen:
|
||||
EnterpriseDataSyncHandler.call_periodic_data_sync_syncs_that_are_due()
|
||||
frozen_datetime = django_timezone.now()
|
||||
|
||||
not_yet_executed_1.refresh_from_db()
|
||||
# not executed because not yet due.
|
||||
assert not_yet_executed_1.last_periodic_sync != frozen_datetime
|
||||
|
||||
not_yet_executed_2.refresh_from_db()
|
||||
# executed because not yet executed before and due.
|
||||
assert not_yet_executed_2.last_periodic_sync == frozen_datetime
|
||||
|
||||
already_executed_this_hour_1.refresh_from_db()
|
||||
# not executed because not yet due.
|
||||
assert already_executed_this_hour_1.last_periodic_sync != frozen_datetime
|
||||
|
||||
already_executed_last_hour_1.refresh_from_db()
|
||||
# not executed because not yet due.
|
||||
assert already_executed_last_hour_1.last_periodic_sync != frozen_datetime
|
||||
|
||||
|
||||
@pytest.mark.django_db(transaction=True)
|
||||
@override_settings(DEBUG=True)
|
||||
@patch("baserow_enterprise.data_sync.handler.sync_periodic_data_sync")
|
||||
def test_call_periodic_data_sync_syncs_starts_task(
|
||||
mock_sync_periodic_data_sync, enterprise_data_fixture
|
||||
):
|
||||
enterprise_data_fixture.enable_enterprise()
|
||||
user = enterprise_data_fixture.create_user()
|
||||
|
||||
not_yet_executed_1 = 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),
|
||||
)
|
||||
not_yet_executed_1.refresh_from_db()
|
||||
|
||||
with freeze_time("2024-10-10T12:15:00.00Z"):
|
||||
with transaction.atomic():
|
||||
EnterpriseDataSyncHandler.call_periodic_data_sync_syncs_that_are_due()
|
||||
|
||||
mock_sync_periodic_data_sync.delay.assert_called_once()
|
||||
args = mock_sync_periodic_data_sync.delay.call_args
|
||||
assert args[0][0] == not_yet_executed_1.id
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@override_settings(DEBUG=True)
|
||||
def test_skip_automatically_deactivated_periodic_data_syncs(enterprise_data_fixture):
|
||||
enterprise_data_fixture.enable_enterprise()
|
||||
user = enterprise_data_fixture.create_user()
|
||||
|
||||
not_yet_executed_1 = 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),
|
||||
)
|
||||
|
||||
License.objects.all().delete()
|
||||
|
||||
with freeze_time("2024-10-10T12:15:00.00Z"):
|
||||
with transaction.atomic():
|
||||
EnterpriseDataSyncHandler.call_periodic_data_sync_syncs_that_are_due()
|
||||
|
||||
not_yet_executed_1.refresh_from_db()
|
||||
# Should not be triggered because there was no license.
|
||||
assert not_yet_executed_1.last_periodic_sync is None
|
||||
|
||||
|
||||
@pytest.mark.django_db(transaction=True, databases=["default", "default-copy"])
|
||||
@override_settings(DEBUG=True)
|
||||
def test_skip_locked_data_syncs(enterprise_data_fixture):
|
||||
enterprise_data_fixture.enable_enterprise()
|
||||
user = enterprise_data_fixture.create_user()
|
||||
|
||||
not_yet_executed_1 = 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),
|
||||
)
|
||||
not_yet_executed_2 = 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),
|
||||
)
|
||||
|
||||
with transaction.atomic(using="default-copy"):
|
||||
PeriodicDataSyncInterval.objects.using("default-copy").filter(
|
||||
id=not_yet_executed_1.id
|
||||
).select_for_update().get()
|
||||
DataSync.objects.using("default-copy").filter(
|
||||
id=not_yet_executed_2.data_sync_id
|
||||
).select_for_update().get()
|
||||
|
||||
with freeze_time("2024-10-10T12:15:00.00Z"):
|
||||
with transaction.atomic():
|
||||
EnterpriseDataSyncHandler.call_periodic_data_sync_syncs_that_are_due()
|
||||
|
||||
not_yet_executed_1.refresh_from_db()
|
||||
# Should not be triggered because the periodic data sync object was locked.
|
||||
assert not_yet_executed_1.last_periodic_sync is None
|
||||
|
||||
not_yet_executed_2.refresh_from_db()
|
||||
# Should not be triggered because there the data sync was locked.
|
||||
assert not_yet_executed_2.last_periodic_sync is None
|
||||
|
||||
|
||||
@pytest.mark.django_db(transaction=True)
|
||||
@override_settings(DEBUG=True)
|
||||
@patch("baserow_enterprise.data_sync.handler.sync_periodic_data_sync")
|
||||
def test_skip_syncing_data_syncs(mock_sync_periodic_data_sync, enterprise_data_fixture):
|
||||
enterprise_data_fixture.enable_enterprise()
|
||||
user = enterprise_data_fixture.create_user()
|
||||
|
||||
not_yet_executed_1 = 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),
|
||||
)
|
||||
|
||||
lock_key = DataSyncHandler().get_table_sync_lock_key(
|
||||
not_yet_executed_1.data_sync_id
|
||||
)
|
||||
cache.add(lock_key, "locked", timeout=2)
|
||||
|
||||
with freeze_time("2024-10-10T12:15:00.00Z"):
|
||||
with transaction.atomic():
|
||||
EnterpriseDataSyncHandler.call_periodic_data_sync_syncs_that_are_due()
|
||||
|
||||
not_yet_executed_1.refresh_from_db()
|
||||
# Should be updated if the data sync is already running.
|
||||
assert not_yet_executed_1.last_periodic_sync is not None
|
||||
|
||||
# Should not be called if the data sync is already running.
|
||||
mock_sync_periodic_data_sync.delay.assert_not_called()
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@override_settings(DEBUG=True)
|
||||
def test_sync_periodic_data_sync_deactivated(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),
|
||||
)
|
||||
periodic_data_sync.automatically_deactivated = True
|
||||
periodic_data_sync.save()
|
||||
|
||||
assert (
|
||||
EnterpriseDataSyncHandler.sync_periodic_data_sync(periodic_data_sync.id)
|
||||
is False
|
||||
)
|
||||
|
||||
periodic_data_sync.data_sync.refresh_from_db()
|
||||
assert periodic_data_sync.data_sync.last_sync is None
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@override_settings(DEBUG=True)
|
||||
def test_sync_periodic_data_sync_already_syncing(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),
|
||||
)
|
||||
|
||||
lock_key = DataSyncHandler().get_table_sync_lock_key(
|
||||
periodic_data_sync.data_sync_id
|
||||
)
|
||||
cache.add(lock_key, "locked", timeout=2)
|
||||
|
||||
assert (
|
||||
EnterpriseDataSyncHandler.sync_periodic_data_sync(periodic_data_sync.id)
|
||||
is False
|
||||
)
|
||||
|
||||
periodic_data_sync.data_sync.refresh_from_db()
|
||||
assert periodic_data_sync.data_sync.last_sync is None
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@override_settings(DEBUG=True)
|
||||
@responses.activate
|
||||
def test_sync_periodic_data_sync_consecutive_failed_count_increases(
|
||||
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),
|
||||
)
|
||||
|
||||
assert (
|
||||
EnterpriseDataSyncHandler.sync_periodic_data_sync(periodic_data_sync.id) is True
|
||||
)
|
||||
|
||||
periodic_data_sync.refresh_from_db()
|
||||
assert periodic_data_sync.consecutive_failed_count == 1
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@override_settings(
|
||||
DEBUG=True, BASEROW_ENTERPRISE_MAX_PERIODIC_DATA_SYNC_CONSECUTIVE_ERRORS=2
|
||||
)
|
||||
@responses.activate
|
||||
def test_sync_periodic_data_sync_consecutive_failed_count_reset(
|
||||
enterprise_data_fixture,
|
||||
):
|
||||
responses.add(
|
||||
responses.GET,
|
||||
"https://baserow.io/ical.ics",
|
||||
status=200,
|
||||
body="""BEGIN:VCALENDAR
|
||||
VERSION:2.0
|
||||
END:VCALENDAR""",
|
||||
)
|
||||
|
||||
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, ical_url="https://baserow.io/ical.ics"
|
||||
),
|
||||
interval="DAILY",
|
||||
when=time(hour=12, minute=10, second=1, microsecond=1),
|
||||
)
|
||||
periodic_data_sync.consecutive_failed_count = 1
|
||||
periodic_data_sync.save()
|
||||
|
||||
assert (
|
||||
EnterpriseDataSyncHandler.sync_periodic_data_sync(periodic_data_sync.id) is True
|
||||
)
|
||||
|
||||
periodic_data_sync.refresh_from_db()
|
||||
assert periodic_data_sync.consecutive_failed_count == 0
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@override_settings(DEBUG=True)
|
||||
@responses.activate
|
||||
def test_sync_periodic_data_sync_deactivated_max_failure(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()
|
||||
|
||||
assert (
|
||||
EnterpriseDataSyncHandler.sync_periodic_data_sync(periodic_data_sync.id) is True
|
||||
)
|
||||
|
||||
periodic_data_sync.refresh_from_db()
|
||||
assert periodic_data_sync.consecutive_failed_count == 4
|
||||
assert periodic_data_sync.automatically_deactivated is True
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@override_settings(DEBUG=True)
|
||||
def test_sync_periodic_data_sync_authorized_user_is_none(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),
|
||||
)
|
||||
periodic_data_sync.authorized_user is None
|
||||
periodic_data_sync.save()
|
||||
|
||||
assert (
|
||||
EnterpriseDataSyncHandler.sync_periodic_data_sync(periodic_data_sync.id) is True
|
||||
)
|
||||
|
||||
periodic_data_sync.refresh_from_db()
|
||||
assert periodic_data_sync.consecutive_failed_count == 1
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@override_settings(DEBUG=True)
|
||||
@responses.activate
|
||||
def test_sync_periodic_data_sync(enterprise_data_fixture):
|
||||
responses.add(
|
||||
responses.GET,
|
||||
"https://baserow.io/ical.ics",
|
||||
status=200,
|
||||
body="""BEGIN:VCALENDAR
|
||||
VERSION:2.0
|
||||
END:VCALENDAR""",
|
||||
)
|
||||
|
||||
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, ical_url="https://baserow.io/ical.ics"
|
||||
),
|
||||
interval="DAILY",
|
||||
when=time(hour=12, minute=10, second=1, microsecond=1),
|
||||
)
|
||||
|
||||
assert (
|
||||
EnterpriseDataSyncHandler.sync_periodic_data_sync(periodic_data_sync.id) is True
|
||||
)
|
||||
|
||||
periodic_data_sync.data_sync.refresh_from_db()
|
||||
assert periodic_data_sync.data_sync.last_sync is not None
|
||||
assert periodic_data_sync.data_sync.last_error is None
|
|
@ -0,0 +1,175 @@
|
|||
<template>
|
||||
<div>
|
||||
<h2 class="box__title">
|
||||
{{ $t('configureDataSyncPeriodicInterval.title') }}
|
||||
</h2>
|
||||
<div v-if="hasPermissions">
|
||||
<div v-if="fetchLoading">
|
||||
<div class="loading"></div>
|
||||
</div>
|
||||
<div v-if="!fetchLoaded">
|
||||
<Error :error="error"></Error>
|
||||
</div>
|
||||
<div v-else-if="fetchLoaded">
|
||||
<Error :error="error"></Error>
|
||||
<Alert
|
||||
v-if="periodicInterval.automatically_deactivated"
|
||||
type="info-primary"
|
||||
>
|
||||
<template #title>{{
|
||||
$t('configureDataSyncPeriodicInterval.deactivatedTitle')
|
||||
}}</template>
|
||||
<p>{{ $t('configureDataSyncPeriodicInterval.deactivatedText') }}</p>
|
||||
<template #actions>
|
||||
<Button
|
||||
type="primary"
|
||||
size="small"
|
||||
:loading="saveLoading"
|
||||
@click="activate"
|
||||
>{{ $t('configureDataSyncPeriodicInterval.activate') }}</Button
|
||||
>
|
||||
</template>
|
||||
</Alert>
|
||||
<DataSyncPeriodicIntervalForm
|
||||
v-if="!periodicInterval.automatically_deactivated"
|
||||
:default-values="periodicInterval"
|
||||
:disabled="saveLoading"
|
||||
@submitted="submitted"
|
||||
@values-changed="saved = false"
|
||||
>
|
||||
<div class="flex align-items-center justify-content-end">
|
||||
<Button
|
||||
v-if="!saved"
|
||||
type="primary"
|
||||
size="large"
|
||||
:loading="saveLoading"
|
||||
:disabled="saveLoading"
|
||||
>
|
||||
{{ $t('action.save') }}
|
||||
</Button>
|
||||
<template v-if="saved">
|
||||
<strong class="color-success">{{
|
||||
$t('configureDataSyncPeriodicInterval.saved')
|
||||
}}</strong>
|
||||
<Button type="secondary" size="large" @click="$emit('hide')">
|
||||
{{ $t('action.hide') }}
|
||||
</Button>
|
||||
</template>
|
||||
</div>
|
||||
</DataSyncPeriodicIntervalForm>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else>
|
||||
<div class="placeholder">
|
||||
<div class="placeholder__icon">
|
||||
<i class="iconoir-timer"></i>
|
||||
</div>
|
||||
<p class="placeholder__content">
|
||||
{{ $t('configureDataSyncPeriodicInterval.enterprise') }}
|
||||
</p>
|
||||
<div class="placeholder__action">
|
||||
<Button
|
||||
type="primary"
|
||||
icon="iconoir-no-lock"
|
||||
@click="$refs.enterpriseModal.show()"
|
||||
>
|
||||
{{ $t('configureDataSyncPeriodicInterval.more') }}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<EnterpriseModal
|
||||
ref="enterpriseModal"
|
||||
:name="$t('configureDataSyncPeriodicInterval.title')"
|
||||
:workspace="database.workspace"
|
||||
></EnterpriseModal>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import EnterpriseDataSyncService from '@baserow_enterprise/services/dataSync'
|
||||
import error from '@baserow/modules/core/mixins/error'
|
||||
import DataSyncPeriodicIntervalForm from '@baserow_enterprise/components/dataSync/DataSyncPeriodicIntervalForm'
|
||||
import EnterpriseFeatures from '@baserow_enterprise/features'
|
||||
import EnterpriseModal from '@baserow_enterprise/components/EnterpriseModal'
|
||||
import { clone } from '@baserow/modules/core/utils/object'
|
||||
|
||||
export default {
|
||||
name: 'ConfigureDataSyncPeriodicInterval',
|
||||
components: { EnterpriseModal, DataSyncPeriodicIntervalForm },
|
||||
mixins: [error],
|
||||
props: {
|
||||
database: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
table: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
fetchLoading: false,
|
||||
fetchLoaded: false,
|
||||
periodicInterval: {},
|
||||
saveLoading: false,
|
||||
saved: false,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
hasPermissions() {
|
||||
return this.$hasFeature(
|
||||
EnterpriseFeatures.DATA_SYNC,
|
||||
this.database.workspace.id
|
||||
)
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.hideError()
|
||||
this.fetchPeriodicInterval(this.table)
|
||||
},
|
||||
methods: {
|
||||
async fetchPeriodicInterval(table) {
|
||||
this.fetchLoading = true
|
||||
|
||||
try {
|
||||
const { data } = await EnterpriseDataSyncService(
|
||||
this.$client
|
||||
).getPeriodicInterval(table.data_sync.id)
|
||||
this.periodicInterval = data
|
||||
this.fetchLoaded = true
|
||||
} catch (error) {
|
||||
this.handleError(error)
|
||||
} finally {
|
||||
this.fetchLoading = false
|
||||
}
|
||||
},
|
||||
async activate() {
|
||||
const values = clone(this.periodicInterval)
|
||||
values.automatically_deactivated = false
|
||||
// Updating the periodic interval sets automatically_disabled = false.
|
||||
await this.submitted(values)
|
||||
this.periodicInterval = values
|
||||
this.saved = false
|
||||
},
|
||||
async submitted(values) {
|
||||
this.hideError()
|
||||
this.saveLoading = true
|
||||
|
||||
try {
|
||||
await EnterpriseDataSyncService(this.$client).updatePeriodicInterval(
|
||||
this.table.data_sync.id,
|
||||
values.interval,
|
||||
values.when
|
||||
)
|
||||
this.saved = true
|
||||
} catch (error) {
|
||||
this.handleError(error)
|
||||
} finally {
|
||||
this.saveLoading = false
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
|
@ -0,0 +1,176 @@
|
|||
<template>
|
||||
<form @submit.prevent="submit">
|
||||
<FormGroup
|
||||
:error="fieldHasErrors('interval')"
|
||||
:label="$t('dataSyncPeriodicIntervalForm.intervalLabel')"
|
||||
:helper-text="$t('dataSyncPeriodicIntervalForm.intervalHelper')"
|
||||
required
|
||||
small-label
|
||||
class="margin-bottom-2"
|
||||
>
|
||||
<Dropdown v-model="values.interval" :disabled="disabled" size="large">
|
||||
<DropdownItem
|
||||
:name="$t('dataSyncPeriodicIntervalForm.manual')"
|
||||
value="MANUAL"
|
||||
></DropdownItem>
|
||||
<DropdownItem
|
||||
:name="$t('dataSyncPeriodicIntervalForm.daily')"
|
||||
value="DAILY"
|
||||
></DropdownItem>
|
||||
<DropdownItem
|
||||
:name="$t('dataSyncPeriodicIntervalForm.hourly')"
|
||||
value="HOURLY"
|
||||
></DropdownItem>
|
||||
</Dropdown>
|
||||
</FormGroup>
|
||||
<template v-if="values.interval !== 'MANUAL'">
|
||||
<div class="flex align-items-end">
|
||||
<FormGroup
|
||||
v-if="values.interval === 'DAILY'"
|
||||
small-label
|
||||
:label="$t('dataSyncPeriodicIntervalForm.hour')"
|
||||
:error="$v.hour.$dirty && $v.hour.$error"
|
||||
required
|
||||
>
|
||||
<FormInput
|
||||
v-model="hour"
|
||||
:disabled="disabled"
|
||||
size="large"
|
||||
type="number"
|
||||
:min="0"
|
||||
:max="23"
|
||||
@blur="$v.hour.$touch()"
|
||||
@input="updateWhen"
|
||||
/>
|
||||
</FormGroup>
|
||||
<FormGroup
|
||||
small-label
|
||||
:label="$t('dataSyncPeriodicIntervalForm.minute')"
|
||||
:error="$v.minute.$dirty && $v.minute.$error"
|
||||
required
|
||||
>
|
||||
<FormInput
|
||||
v-model="minute"
|
||||
:disabled="disabled"
|
||||
size="large"
|
||||
type="number"
|
||||
:min="0"
|
||||
:max="59"
|
||||
@blur="$v.minute.$touch()"
|
||||
@input="updateWhen"
|
||||
/>
|
||||
</FormGroup>
|
||||
<FormGroup
|
||||
small-label
|
||||
:label="$t('dataSyncPeriodicIntervalForm.second')"
|
||||
:error="$v.second.$dirty && $v.second.$error"
|
||||
required
|
||||
>
|
||||
<FormInput
|
||||
v-model="second"
|
||||
:disabled="disabled"
|
||||
size="large"
|
||||
type="number"
|
||||
:min="0"
|
||||
:max="59"
|
||||
@blur="$v.second.$touch()"
|
||||
@input="updateWhen"
|
||||
/>
|
||||
</FormGroup>
|
||||
<div class="color-neutral">
|
||||
{{ timezone }}
|
||||
</div>
|
||||
</div>
|
||||
<p class="control__helper-text">
|
||||
{{ $t('dataSyncPeriodicIntervalForm.whenHelper') }}
|
||||
</p>
|
||||
</template>
|
||||
<slot></slot>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import moment from '@baserow/modules/core/moment'
|
||||
import { required, numeric, minValue, maxValue } from 'vuelidate/lib/validators'
|
||||
import form from '@baserow/modules/core/mixins/form'
|
||||
|
||||
export default {
|
||||
name: 'DataSyncPeriodicIntervalForm',
|
||||
mixins: [form],
|
||||
props: {
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
||||
allowedValues: ['interval', 'when'],
|
||||
values: {
|
||||
interval: 'MANUAL',
|
||||
when: '',
|
||||
},
|
||||
hour: '',
|
||||
minute: '',
|
||||
second: '',
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
if (this.values.when) {
|
||||
const localTime = moment
|
||||
.utc(this.values.when, 'HH:mm:ss')
|
||||
.local()
|
||||
.format('HH:mm:ss')
|
||||
const splitted = localTime.split(':')
|
||||
this.hour = parseInt(splitted[0], 10) || 0
|
||||
this.minute = parseInt(splitted[1], 10) || 0
|
||||
this.second = parseInt(splitted[2], 10) || 0
|
||||
} else {
|
||||
this.setDefaultTime()
|
||||
}
|
||||
this.updateWhen()
|
||||
},
|
||||
methods: {
|
||||
setDefaultTime() {
|
||||
const localTime = moment().format('HH:mm:ss')
|
||||
const splitted = localTime.split(':')
|
||||
this.hour = splitted[0]
|
||||
this.minute = splitted[1]
|
||||
this.second = splitted[2]
|
||||
},
|
||||
updateWhen() {
|
||||
const timeInLocal = `${this.hour}:${this.minute}:${this.second}`
|
||||
const timeInUTC = moment(timeInLocal, 'HH:mm:ss').utc().format('HH:mm:ss')
|
||||
this.values.when = timeInUTC
|
||||
},
|
||||
},
|
||||
validations() {
|
||||
return {
|
||||
values: {
|
||||
interval: { required },
|
||||
when: { required },
|
||||
},
|
||||
hour: {
|
||||
required,
|
||||
numeric,
|
||||
minValue: minValue(0),
|
||||
maxValue: maxValue(24),
|
||||
},
|
||||
minute: {
|
||||
required,
|
||||
numeric,
|
||||
minValue: minValue(0),
|
||||
maxValue: maxValue(59),
|
||||
},
|
||||
second: {
|
||||
required,
|
||||
numeric,
|
||||
minValue: minValue(0),
|
||||
maxValue: maxValue(59),
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
|
@ -0,0 +1,20 @@
|
|||
import { ConfigureDataSyncType } from '@baserow/modules/database/configureDataSyncTypes'
|
||||
import ConfigureDataSyncPeriodicInterval from '@baserow_enterprise/components/dataSync/ConfigureDataSyncPeriodicInterval'
|
||||
|
||||
export class PeriodicIntervalFieldsConfigureDataSyncType extends ConfigureDataSyncType {
|
||||
static getType() {
|
||||
return 'periodic-interval'
|
||||
}
|
||||
|
||||
get name() {
|
||||
return this.app.i18n.t('configureDataSyncModal.periodicInterval')
|
||||
}
|
||||
|
||||
get iconClass() {
|
||||
return 'iconoir-timer'
|
||||
}
|
||||
|
||||
get component() {
|
||||
return ConfigureDataSyncPeriodicInterval
|
||||
}
|
||||
}
|
|
@ -409,5 +409,28 @@
|
|||
"placeholderWithSaml": "{login} with SAML",
|
||||
"provideEmail": "Provide your SAML account email",
|
||||
"emailPlaceholder": "Enter your email..."
|
||||
},
|
||||
"configureDataSyncModal": {
|
||||
"periodicInterval": "Periodic settings"
|
||||
},
|
||||
"configureDataSyncPeriodicInterval": {
|
||||
"title": "Periodic settings",
|
||||
"saved": "Saved",
|
||||
"deactivatedTitle": "Periodic data sync deactivated",
|
||||
"deactivatedText": "The periodic data sync was deactivated because it failed to many consecutive times. Please try to sync manually, and if it works as expected, then click on the activate button.",
|
||||
"activate": "Activate periodic sync",
|
||||
"enterprise": "Periodic data sync is an advanced/enterprise feature.",
|
||||
"more": "More information"
|
||||
},
|
||||
"dataSyncPeriodicIntervalForm": {
|
||||
"intervalLabel": "Interval",
|
||||
"intervalHelper": "How frequently should the the data sync automatically sync.",
|
||||
"manual": "Manual",
|
||||
"daily": "Daily",
|
||||
"hourly": "Hourly",
|
||||
"whenHelper": "Provide the exact hour, minute, and second when the data sync should sync.",
|
||||
"hour": "Hour",
|
||||
"minute": "Minute",
|
||||
"second": "Second"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -49,6 +49,7 @@ import {
|
|||
GitLabIssuesDataSyncType,
|
||||
HubspotContactsDataSyncType,
|
||||
} from '@baserow_enterprise/dataSyncTypes'
|
||||
import { PeriodicIntervalFieldsConfigureDataSyncType } from '@baserow_enterprise/configureDataSyncTypes'
|
||||
|
||||
import { FF_AB_SSO } from '@baserow/modules/core/plugins/featureFlags'
|
||||
|
||||
|
@ -144,4 +145,9 @@ export default (context) => {
|
|||
app.$registry.register('dataSync', new GitHubIssuesDataSyncType(context))
|
||||
app.$registry.register('dataSync', new GitLabIssuesDataSyncType(context))
|
||||
app.$registry.register('dataSync', new HubspotContactsDataSyncType(context))
|
||||
|
||||
app.$registry.register(
|
||||
'configureDataSync',
|
||||
new PeriodicIntervalFieldsConfigureDataSyncType(context)
|
||||
)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
export default (client) => {
|
||||
return {
|
||||
getPeriodicInterval(dataSyncId) {
|
||||
return client.get(`/data-sync/${dataSyncId}/periodic-interval/`)
|
||||
},
|
||||
updatePeriodicInterval(dataSyncId, interval, when) {
|
||||
return client.patch(`/data-sync/${dataSyncId}/periodic-interval/`, {
|
||||
interval,
|
||||
when,
|
||||
})
|
||||
},
|
||||
}
|
||||
}
|
|
@ -210,10 +210,18 @@
|
|||
align-items: center;
|
||||
}
|
||||
|
||||
.align-items-end {
|
||||
align-items: end;
|
||||
}
|
||||
|
||||
.justify-content-space-between {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.justify-content-end {
|
||||
justify-content: end;
|
||||
}
|
||||
|
||||
.position-relative {
|
||||
position: relative;
|
||||
}
|
||||
|
|
|
@ -7,11 +7,11 @@
|
|||
</div>
|
||||
</div>
|
||||
<ul class="modal-sidebar__nav">
|
||||
<li v-for="page in pages" :key="page.type">
|
||||
<li v-for="page in pages" :key="page.getType()">
|
||||
<a
|
||||
class="modal-sidebar__nav-link"
|
||||
:class="{ active: selectedPage === page.type }"
|
||||
@click="setPage(page.type)"
|
||||
:class="{ active: selectedPage === page.getType() }"
|
||||
@click="setPage(page.getType())"
|
||||
>
|
||||
<i class="modal-sidebar__nav-icon" :class="page.iconClass"></i>
|
||||
{{ page.name }}
|
||||
|
@ -52,26 +52,15 @@ export default {
|
|||
},
|
||||
data() {
|
||||
return {
|
||||
pages: [
|
||||
{
|
||||
type: 'visible-fields',
|
||||
name: this.$t('configureDataSyncModal.syncedFields'),
|
||||
iconClass: 'iconoir-switch-on',
|
||||
component: ConfigureDataSyncVisibleFields,
|
||||
},
|
||||
{
|
||||
type: 'settings',
|
||||
name: this.$t('configureDataSyncModal.syncSettings'),
|
||||
iconClass: 'iconoir-settings',
|
||||
component: ConfigureDataSyncSettings,
|
||||
},
|
||||
],
|
||||
selectedPage: 'visible-fields',
|
||||
selectedPage: 'synced-fields',
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
pages() {
|
||||
return Object.values(this.$registry.getAll('configureDataSync'))
|
||||
},
|
||||
selectedPageObject() {
|
||||
return this.pages.find((page) => page.type === this.selectedPage)
|
||||
return this.pages.find((page) => page.getType() === this.selectedPage)
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
|
|
|
@ -333,8 +333,12 @@ export default {
|
|||
}
|
||||
},
|
||||
openConfigureDataSyncModal() {
|
||||
if (this.dataSyncDeactivated) {
|
||||
this.$refs.deactivatedDataSyncClickModal.show()
|
||||
} else {
|
||||
this.$refs.context.hide()
|
||||
this.$refs.configureDataSyncModal.show()
|
||||
}
|
||||
},
|
||||
enableRename() {
|
||||
this.$refs.context.hide()
|
||||
|
|
59
web-frontend/modules/database/configureDataSyncTypes.js
Normal file
59
web-frontend/modules/database/configureDataSyncTypes.js
Normal file
|
@ -0,0 +1,59 @@
|
|||
import { Registerable } from '@baserow/modules/core/registry'
|
||||
import ConfigureDataSyncVisibleFields from '@baserow/modules/database/components/dataSync/ConfigureDataSyncVisibleFields'
|
||||
import ConfigureDataSyncSettings from '@baserow/modules/database/components/dataSync/ConfigureDataSyncSettings'
|
||||
|
||||
export class ConfigureDataSyncType extends Registerable {
|
||||
get name() {
|
||||
throw new Error(
|
||||
'name getter must be implemented in the ConfigureDataSyncType.'
|
||||
)
|
||||
}
|
||||
|
||||
get iconClass() {
|
||||
throw new Error(
|
||||
'iconClass getter must be implemented in the ConfigureDataSyncType.'
|
||||
)
|
||||
}
|
||||
|
||||
get component() {
|
||||
throw new Error(
|
||||
'component getter must be implemented in the ConfigureDataSyncType.'
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export class SyncedFieldsConfigureDataSyncType extends ConfigureDataSyncType {
|
||||
static getType() {
|
||||
return 'synced-fields'
|
||||
}
|
||||
|
||||
get name() {
|
||||
return this.app.i18n.t('configureDataSyncModal.syncedFields')
|
||||
}
|
||||
|
||||
get iconClass() {
|
||||
return 'iconoir-switch-on'
|
||||
}
|
||||
|
||||
get component() {
|
||||
return ConfigureDataSyncVisibleFields
|
||||
}
|
||||
}
|
||||
|
||||
export class SettingsConfigureDataSyncType extends ConfigureDataSyncType {
|
||||
static getType() {
|
||||
return 'settings'
|
||||
}
|
||||
|
||||
get name() {
|
||||
return this.app.i18n.t('configureDataSyncModal.syncSettings')
|
||||
}
|
||||
|
||||
get iconClass() {
|
||||
return 'iconoir-settings'
|
||||
}
|
||||
|
||||
get component() {
|
||||
return ConfigureDataSyncSettings
|
||||
}
|
||||
}
|
|
@ -331,6 +331,10 @@ import {
|
|||
DatabaseScratchTrackTaskFieldsOnboardingType,
|
||||
DatabaseScratchTrackTeamFieldsOnboardingType,
|
||||
} from '@baserow/modules/database/databaseScratchTrackFieldsStepType'
|
||||
import {
|
||||
SyncedFieldsConfigureDataSyncType,
|
||||
SettingsConfigureDataSyncType,
|
||||
} from '@baserow/modules/database/configureDataSyncTypes'
|
||||
|
||||
export default (context) => {
|
||||
const { store, app, isDev } = context
|
||||
|
@ -1000,5 +1004,14 @@ export default (context) => {
|
|||
new DatabaseScratchTrackCustomFieldsOnboardingType(context)
|
||||
)
|
||||
|
||||
app.$registry.register(
|
||||
'configureDataSync',
|
||||
new SyncedFieldsConfigureDataSyncType(context)
|
||||
)
|
||||
app.$registry.register(
|
||||
'configureDataSync',
|
||||
new SettingsConfigureDataSyncType(context)
|
||||
)
|
||||
|
||||
registerRealtimeEvents(app.$realtime)
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue