1
0
Fork 0
mirror of https://gitlab.com/bramw/baserow.git synced 2025-04-04 21:25:24 +00:00

Batch webhooks

This commit is contained in:
Petr Stribny 2022-07-19 21:00:07 +00:00
parent 036dfb8683
commit f75ed23f4d
36 changed files with 897 additions and 987 deletions

View file

@ -387,7 +387,7 @@ SPECTACULAR_SETTINGS = {
"multiple_select_has",
"multiple_select_has_not",
],
"EventTypesEnum": ["row.created", "row.updated", "row.deleted"],
"EventTypesEnum": ["rows.created", "rows.updated", "rows.deleted"],
},
}

View file

@ -1,6 +1,6 @@
import logging
from copy import deepcopy
from typing import Dict
from typing import Dict, List
from django.conf import settings
from rest_framework import serializers
@ -291,7 +291,24 @@ def get_example_row_metadata_field_serializer():
)
def remap_serialized_row_to_user_field_names(serialized_row: Dict, model: ModelBase):
def remap_serialized_rows_to_user_field_names(
serialized_rows: List[Dict], model: ModelBase
) -> List[Dict]:
"""
Remap the values of rows from field ids to the user defined field names.
:param serialized_rows: The rows whose fields to remap.
:param model: The model for which to generate a serializer.
"""
return [
remap_serialized_row_to_user_field_names(row, model) for row in serialized_rows
]
def remap_serialized_row_to_user_field_names(
serialized_row: Dict, model: ModelBase
) -> Dict:
"""
Remap the values of a row from field ids to the user defined field names.

View file

@ -53,7 +53,13 @@ class TableWebhookCreateRequestSerializer(serializers.ModelSerializer):
class TableWebhookUpdateRequestSerializer(serializers.ModelSerializer):
events = serializers.ListField(
required=False,
child=serializers.ChoiceField(choices=webhook_event_type_registry.get_types()),
child=serializers.ChoiceField(
choices=[
t
for t in webhook_event_type_registry.get_types()
if t not in ["row.created", "row.updated", "row.deleted"]
]
),
help_text="A list containing the events that will trigger this webhook.",
)
headers = serializers.DictField(

View file

@ -348,13 +348,19 @@ class DatabaseConfig(AppConfig):
register_formula_functions(formula_function_registry)
from .rows.webhook_event_types import (
RowsCreatedEventType,
RowCreatedEventType,
RowsUpdatedEventType,
RowUpdatedEventType,
RowsDeletedEventType,
RowDeletedEventType,
)
webhook_event_type_registry.register(RowsCreatedEventType())
webhook_event_type_registry.register(RowCreatedEventType())
webhook_event_type_registry.register(RowsUpdatedEventType())
webhook_event_type_registry.register(RowUpdatedEventType())
webhook_event_type_registry.register(RowsDeletedEventType())
webhook_event_type_registry.register(RowDeletedEventType())
from .airtable.airtable_column_types import (

View file

@ -0,0 +1,46 @@
# Generated by Django 3.2.13 on 2022-07-04 15:16
from django.db import migrations, transaction
from baserow.contrib.database.models import TableWebhook, TableWebhookEvent
def forward(apps, schema_editor):
"""
This migration will create individual TableWebhookEvent entries
for all deprecated single row webhooks that has include_all_events
set to True.
"""
with transaction.atomic():
webhooks = TableWebhook.objects.filter(include_all_events=True)
create_webhooks = []
for webhook in webhooks:
create_webhooks.append(
TableWebhookEvent(webhook=webhook, event_type="row.created")
)
create_webhooks.append(
TableWebhookEvent(webhook=webhook, event_type="row.updated")
)
create_webhooks.append(
TableWebhookEvent(webhook=webhook, event_type="row.deleted")
)
TableWebhookEvent.objects.bulk_create(create_webhooks, batch_size=100)
TableWebhook.objects.filter(include_all_events=True).update(
include_all_events=False
)
def reverse(apps, schema_editor):
...
class Migration(migrations.Migration):
dependencies = [
("database", "0080_auto_20220702_1612"),
]
operations = [
migrations.RunPython(forward, reverse),
]

View file

@ -41,7 +41,7 @@ class CreateRowActionType(ActionType):
) -> GeneratedTableModel:
"""
Creates a new row for a given table with the provided values if the user
belongs to the related group. It also calls the row_created signal.
belongs to the related group. It also calls the rows_created signal.
See the baserow.contrib.database.rows.handler.RowHandler.create_row
for more information.
Undoing this action trashes the row and redoing restores it.

View file

@ -22,15 +22,10 @@ from baserow.core.trash.handler import TrashHandler
from baserow.core.utils import get_non_unique_values
from .exceptions import RowDoesNotExist, RowIdsNotUnique
from .signals import (
before_row_update,
before_row_delete,
before_rows_update,
before_rows_delete,
row_created,
rows_created,
row_updated,
rows_updated,
row_deleted,
rows_deleted,
)
@ -404,7 +399,7 @@ class RowHandler:
) -> GeneratedTableModel:
"""
Creates a new row for a given table with the provided values if the user
belongs to the related group. It also calls the row_created signal.
belongs to the related group. It also calls the rows_created signal.
:param user: The user of whose behalf the row is created.
:param table: The table for which to create a row for.
@ -429,8 +424,13 @@ class RowHandler:
table, values, model, before_row, user_field_names
)
row_created.send(
self, row=instance, before=before_row, user=user, table=table, model=model
rows_created.send(
self,
rows=[instance],
before=before_row,
user=user,
table=table,
model=model,
)
return instance
@ -617,14 +617,15 @@ class RowHandler:
updated_fields_by_name[field["name"]] = field["field"]
updated_fields.append(field["field"])
before_return = before_row_update.send(
before_return = before_rows_update.send(
self,
row=row,
rows=[row],
user=user,
table=table,
model=model,
updated_field_ids=updated_field_ids,
)
values = self.prepare_values(model._field_objects, values)
values, manytomany_values = self.extract_manytomany_values(values, model)
@ -683,9 +684,9 @@ class RowHandler:
ViewHandler().field_value_updated(updated_fields)
row_updated.send(
rows_updated.send(
self,
row=row,
rows=[row],
user=user,
table=table,
model=model,
@ -1165,8 +1166,8 @@ class RowHandler:
if model is None:
model = table.get_model()
before_return = before_row_update.send(
self, row=row, user=user, table=table, model=model, updated_field_ids=[]
before_return = before_rows_update.send(
self, rows=[row], user=user, table=table, model=model, updated_field_ids=[]
)
row.order = self.get_order_before_row(before_row, model)[0]
@ -1204,9 +1205,9 @@ class RowHandler:
ViewHandler().field_value_updated(updated_fields)
row_updated.send(
rows_updated.send(
self,
row=row,
rows=[row],
user=user,
table=table,
model=model,
@ -1264,12 +1265,10 @@ class RowHandler:
if model is None:
model = table.get_model()
before_return = before_row_delete.send(
self, row=row, user=user, table=table, model=model
before_return = before_rows_delete.send(
self, rows=[row], user=user, table=table, model=model
)
row_id = row.id
TrashHandler.trash(user, group, table.database, row, parent_id=table.id)
update_collector = FieldUpdateCollector(table, starting_row_ids=[row.id])
@ -1305,10 +1304,9 @@ class RowHandler:
ViewHandler().field_value_updated(updated_fields)
row_deleted.send(
rows_deleted.send(
self,
row_id=row_id,
row=row,
rows=[row],
user=user,
table=table,
model=model,

View file

@ -3,14 +3,9 @@ from django.dispatch import Signal
# Note that it could happen that this signal is triggered, but the actual update still
# fails because of a validation error.
before_row_update = Signal()
before_row_delete = Signal()
before_rows_update = Signal()
before_rows_delete = Signal()
row_created = Signal()
rows_created = Signal()
row_updated = Signal()
rows_updated = Signal()
row_deleted = Signal()
rows_deleted = Signal()

View file

@ -1,14 +1,14 @@
from baserow.contrib.database.api.rows.serializers import (
get_row_serializer_class,
remap_serialized_row_to_user_field_names,
remap_serialized_rows_to_user_field_names,
RowSerializer,
)
from baserow.contrib.database.webhooks.registries import WebhookEventType
from baserow.contrib.database.ws.rows.signals import before_row_update
from .signals import row_created, row_updated, row_deleted
from baserow.contrib.database.ws.rows.signals import before_rows_update
from .signals import rows_created, rows_updated, rows_deleted
class RowEventType(WebhookEventType):
class RowsEventType(WebhookEventType):
def get_row_serializer(self, webhook, model):
return get_row_serializer_class(
model,
@ -17,26 +17,67 @@ class RowEventType(WebhookEventType):
user_field_names=webhook.use_user_field_names,
)
def get_payload(self, event_id, webhook, model, table, row, **kwargs):
def get_payload(self, event_id, webhook, model, table, rows, **kwargs):
payload = super().get_payload(event_id, webhook, **kwargs)
payload["row_id"] = row.id
payload["values"] = self.get_row_serializer(webhook, model)(row).data
payload["items"] = self.get_row_serializer(webhook, model)(rows, many=True).data
return payload
class RowCreatedEventType(RowEventType):
class RowsCreatedEventType(RowsEventType):
type = "rows.created"
signal = rows_created
def get_test_call_payload(self, table, model, event_id, webhook):
rows = [model(id=0, order=0)]
payload = self.get_payload(
event_id=event_id,
webhook=webhook,
model=model,
table=table,
rows=rows,
)
return payload
class RowCreatedEventType(RowsCreatedEventType):
"""
Handling of deprecated single row.created webhook
"""
type = "row.created"
signal = row_created
signal = rows_created
def get_payload(self, *args, **kwargs):
payload = super().get_payload(*args, **kwargs)
payload["row_id"] = payload["items"][0]["id"]
payload["values"] = payload["items"][0]
del payload["items"]
return payload
class RowUpdatedEventType(RowEventType):
type = "row.updated"
signal = row_updated
class RowsUpdatedEventType(RowsEventType):
type = "rows.updated"
signal = rows_updated
def get_test_call_before_return(self, table, row, model):
return {
before_row_update: before_row_update(
row=row,
def get_payload(
self, event_id, webhook, model, table, rows, before_return, **kwargs
):
payload = super().get_payload(event_id, webhook, model, table, rows, **kwargs)
old_items = dict(before_return)[before_rows_update]
if webhook.use_user_field_names:
old_items = remap_serialized_rows_to_user_field_names(old_items, model)
payload["old_items"] = old_items
return payload
def get_test_call_payload(self, table, model, event_id, webhook):
rows = [model(id=0, order=0)]
before_return = {
before_rows_update: before_rows_update(
rows=rows,
model=model,
sender=None,
user=None,
@ -44,26 +85,71 @@ class RowUpdatedEventType(RowEventType):
updated_field_ids=None,
)
}
payload = self.get_payload(
event_id=event_id,
webhook=webhook,
model=model,
table=table,
rows=rows,
before_return=before_return,
)
return payload
class RowUpdatedEventType(RowsUpdatedEventType):
"""
Handling of deprecated single row.updated webhook
"""
type = "row.updated"
signal = rows_updated
def get_payload(
self, event_id, webhook, model, table, row, before_return, **kwargs
self, event_id, webhook, model, table, rows, before_return, **kwargs
):
payload = super().get_payload(event_id, webhook, model, table, row, **kwargs)
old_values = dict(before_return)[before_row_update]
if webhook.use_user_field_names:
old_values = remap_serialized_row_to_user_field_names(old_values, model)
payload["old_values"] = old_values
payload = super().get_payload(
event_id, webhook, model, table, rows, before_return, **kwargs
)
payload["row_id"] = payload["items"][0]["id"]
payload["values"] = payload["items"][0]
payload["old_values"] = payload["old_items"][0]
del payload["items"]
del payload["old_items"]
return payload
class RowDeletedEventType(WebhookEventType):
type = "row.deleted"
signal = row_deleted
class RowsDeletedEventType(WebhookEventType):
type = "rows.deleted"
signal = rows_deleted
def get_payload(self, event_id, webhook, row, **kwargs):
def get_payload(self, event_id, webhook, rows, **kwargs):
payload = super().get_payload(event_id, webhook, **kwargs)
payload["row_id"] = row.id
payload["row_ids"] = [row.id for row in rows]
return payload
def get_test_call_payload(self, table, model, event_id, webhook):
rows = [model(id=0, order=0)]
payload = self.get_payload(
event_id=event_id,
webhook=webhook,
model=model,
table=table,
rows=rows,
)
return payload
class RowDeletedEventType(RowsDeletedEventType):
"""
Handling of deprecated single row.deleted webhook
"""
type = "row.deleted"
signal = rows_deleted
def get_payload(self, event_id, webhook, rows, **kwargs):
payload = super().get_payload(event_id, webhook, rows, **kwargs)
payload["row_id"] = rows[0].id
del payload["row_ids"]
return payload

View file

@ -10,7 +10,7 @@ from baserow.contrib.database.fields.dependencies.update_collector import (
from baserow.contrib.database.fields.handler import FieldHandler
from baserow.contrib.database.fields.models import Field
from baserow.contrib.database.fields.registries import field_type_registry
from baserow.contrib.database.rows.signals import row_created, rows_created
from baserow.contrib.database.rows.signals import rows_created
from baserow.contrib.database.table.models import Table, GeneratedTableModel
from baserow.contrib.database.table.signals import table_created
from baserow.contrib.database.views.handler import ViewHandler
@ -258,9 +258,9 @@ class RowTrashableItemType(TrashableItemType):
ViewHandler().field_value_updated(updated_fields)
row_created.send(
rows_created.send(
self,
row=trashed_item,
rows=[trashed_item],
table=table,
model=model,
before=None,

View file

@ -35,7 +35,7 @@ from baserow.contrib.database.fields.field_sortings import AnnotatedOrder
from baserow.contrib.database.fields.models import Field
from baserow.contrib.database.fields.registries import field_type_registry
from baserow.contrib.database.rows.handler import RowHandler
from baserow.contrib.database.rows.signals import row_created
from baserow.contrib.database.rows.signals import rows_created
from baserow.contrib.database.table.models import Table, GeneratedTableModel
from baserow.core.trash.handler import TrashHandler
from baserow.core.utils import (
@ -1712,8 +1712,8 @@ class ViewHandler:
allowed_values = extract_allowed(values, allowed_field_names)
instance = RowHandler().force_create_row(table, allowed_values, model)
row_created.send(
self, row=instance, before=None, user=None, table=table, model=model
rows_created.send(
self, rows=[instance], before=None, user=None, table=table, model=model
)
return instance

View file

@ -365,19 +365,10 @@ class WebhookHandler:
event_id = str(uuid.uuid4())
model = table.get_model()
row = model(id=0, order=0)
event = webhook_event_type_registry.get(event_type)
before_return = event.get_test_call_before_return(
table=table, row=row, model=model
)
payload = event.get_payload(
event_id=event_id,
webhook=webhook,
model=model,
table=table,
row=row,
before_return=before_return,
)
payload = event.get_test_call_payload(table, model, event_id, webhook)
headers.update(self.get_headers(event_type, event_id))
return self.make_request(webhook.request_method, webhook.url, headers, payload)

View file

@ -39,8 +39,27 @@ class WebhookEventType(Instance):
super().__init__()
self.signal.connect(self.listener)
def get_test_call_before_return(self, **kwargs):
"""Prepare a `before_return` value for a webhook event."""
def get_test_call_payload(self, table, model, event_id, webhook):
"""
Constructs a test payload for a webhook call.
:param table: The table with changes.
:param model: The table's model.
:param event_id: The id of the event.
:param webhook: The webhook object related to the call.
:return: A JSON serializable dict with the test payload.
"""
row = model(id=0, order=0)
payload = self.get_payload(
event_id=event_id,
webhook=webhook,
model=model,
table=table,
row=row,
before_return=None,
)
return payload
def get_payload(self, event_id, webhook, **kwargs):
"""

View file

@ -1,4 +1,4 @@
from typing import Optional, Any, Dict, Iterable, List
from typing import Optional, Any, Dict, List
from django.db import transaction
from django.dispatch import receiver
@ -11,10 +11,8 @@ from baserow.contrib.database.api.rows.serializers import (
from baserow.contrib.database.rows import signals as row_signals
from baserow.contrib.database.table.models import GeneratedTableModel
from baserow.contrib.database.views.handler import PublicViewRows, ViewHandler
from baserow.contrib.database.views.models import View
from baserow.contrib.database.views.registries import view_type_registry
from baserow.contrib.database.ws.rows.signals import (
before_row_update,
before_rows_update,
RealtimeRowMessages,
)
@ -27,32 +25,6 @@ def _serialize_row(model, row, many=False):
).data
def _send_row_created_event_to_views(
serialized_row: Dict[Any, Any],
before: Optional[GeneratedTableModel],
public_views: Iterable[View],
):
view_page_type = page_registry.get("view")
handler = ViewHandler()
for public_view in public_views:
view_type = view_type_registry.get_by_model(public_view.specific_class)
if not view_type.when_shared_publicly_requires_realtime_events:
continue
restricted_serialized_row = handler.restrict_row_for_view(
public_view, serialized_row
)
view_page_type.broadcast(
RealtimeRowMessages.row_created(
table_id=PUBLIC_PLACEHOLDER_ENTITY_ID,
serialized_row=restricted_serialized_row,
metadata={},
before=before,
),
slug=public_view.slug,
)
def _send_rows_created_event_to_views(
serialized_rows: List[Dict[Any, Any]],
before: Optional[GeneratedTableModel],
@ -80,28 +52,6 @@ def _send_rows_created_event_to_views(
)
def _send_row_deleted_event_to_views(
serialized_deleted_row: Dict[Any, Any], public_views: Iterable[View]
):
view_page_type = page_registry.get("view")
handler = ViewHandler()
for public_view in public_views:
view_type = view_type_registry.get_by_model(public_view.specific_class)
if not view_type.when_shared_publicly_requires_realtime_events:
continue
restricted_serialized_deleted_row = handler.restrict_row_for_view(
public_view, serialized_deleted_row
)
view_page_type.broadcast(
RealtimeRowMessages.row_deleted(
table_id=PUBLIC_PLACEHOLDER_ENTITY_ID,
serialized_row=restricted_serialized_deleted_row,
),
slug=public_view.slug,
)
def _send_rows_deleted_event_to_views(
serialized_deleted_rows: List[Dict[Any, Any]],
public_views: List[PublicViewRows],
@ -125,20 +75,6 @@ def _send_rows_deleted_event_to_views(
)
@receiver(row_signals.row_created)
def public_row_created(sender, row, before, user, table, model, **kwargs):
row_checker = ViewHandler().get_public_views_row_checker(
table, model, only_include_views_which_want_realtime_events=True
)
transaction.on_commit(
lambda: _send_row_created_event_to_views(
_serialize_row(model, row),
before,
row_checker.get_public_views_where_row_is_visible(row),
),
)
@receiver(row_signals.rows_created)
def public_rows_created(sender, rows, before, user, table, model, **kwargs):
row_checker = ViewHandler().get_public_views_row_checker(
@ -153,19 +89,6 @@ def public_rows_created(sender, rows, before, user, table, model, **kwargs):
)
@receiver(row_signals.before_row_delete)
def public_before_row_delete(sender, row, user, table, model, **kwargs):
row_checker = ViewHandler().get_public_views_row_checker(
table, model, only_include_views_which_want_realtime_events=True
)
return {
"deleted_row_public_views": (
row_checker.get_public_views_where_row_is_visible(row)
),
"deleted_row": _serialize_row(model, row),
}
@receiver(row_signals.before_rows_delete)
def public_before_rows_delete(sender, rows, user, table, model, **kwargs):
row_checker = ViewHandler().get_public_views_row_checker(
@ -179,21 +102,6 @@ def public_before_rows_delete(sender, rows, user, table, model, **kwargs):
}
@receiver(row_signals.row_deleted)
def public_row_deleted(
sender, row_id, row, user, table, model, before_return, **kwargs
):
public_views = dict(before_return)[public_before_row_delete][
"deleted_row_public_views"
]
serialized_deleted_row = dict(before_return)[public_before_row_delete][
"deleted_row"
]
transaction.on_commit(
lambda: _send_row_deleted_event_to_views(serialized_deleted_row, public_views)
)
@receiver(row_signals.rows_deleted)
def public_rows_deleted(sender, rows, user, table, model, before_return, **kwargs):
public_views = dict(before_return)[public_before_rows_delete][
@ -207,25 +115,6 @@ def public_rows_deleted(sender, rows, user, table, model, before_return, **kwarg
)
@receiver(row_signals.before_row_update)
def public_before_row_update(
sender, row, user, table, model, updated_field_ids, **kwargs
):
# Generate a serialized version of the row before it is updated. The
# `row_updated` receiver needs this serialized version because it can't serialize
# the old row after it has been updated.
row_checker = ViewHandler().get_public_views_row_checker(
table,
model,
only_include_views_which_want_realtime_events=True,
updated_field_ids=updated_field_ids,
)
return {
"old_row_public_views": row_checker.get_public_views_where_row_is_visible(row),
"caching_row_checker": row_checker,
}
@receiver(row_signals.before_rows_update)
def public_before_rows_update(
sender, rows, user, table, model, updated_field_ids, **kwargs
@ -244,73 +133,6 @@ def public_before_rows_update(
}
@receiver(row_signals.row_updated)
def public_row_updated(
sender, row, user, table, model, before_return, updated_field_ids, **kwargs
):
before_return_dict = dict(before_return)[public_before_row_update]
serialized_old_row = dict(before_return)[before_row_update]
serialized_updated_row = _serialize_row(model, row)
old_row_public_views = before_return_dict["old_row_public_views"]
existing_checker = before_return_dict["caching_row_checker"]
views = existing_checker.get_public_views_where_row_is_visible(row)
updated_row_public_views = {view.slug: view for view in views}
# When a row is updated from the point of view of a public view it might not always
# result in a `row_updated` event. For example if the row was previously not visible
# in the public view due to its filters, but the row update makes it now match
# the filters we want to send a `row_created` event to that views page as the
# clients won't know anything about the row and hence a `row_updated` event makes
# no sense for them.
public_views_where_row_was_deleted = []
public_views_where_row_was_updated = []
for old_row_view in old_row_public_views:
updated_row_view = updated_row_public_views.pop(old_row_view.slug, None)
if updated_row_view is None:
# The updated row is no longer visible in `old_row_view` hence we should
# send that view a deleted event.
public_views_where_row_was_deleted.append(old_row_view)
else:
# The updated row is still visible so here we want a normal updated event.
public_views_where_row_was_updated.append(old_row_view)
# Any remaining views in the updated_row_public_views dict are views which
# previously didn't show the old row, but now show the new row, so we want created.
public_views_where_row_was_created = updated_row_public_views.values()
def _send_created_updated_deleted_row_signals_to_views():
_send_row_deleted_event_to_views(
serialized_old_row, public_views_where_row_was_deleted
)
_send_row_created_event_to_views(
serialized_updated_row,
before=None,
public_views=public_views_where_row_was_created,
)
view_page_type = page_registry.get("view")
handler = ViewHandler()
for public_view in public_views_where_row_was_updated:
(
visible_fields_only_updated_row,
visible_fields_only_old_row,
) = handler.restrict_rows_for_view(
public_view, [serialized_updated_row, serialized_old_row]
)
view_page_type.broadcast(
RealtimeRowMessages.row_updated(
table_id=PUBLIC_PLACEHOLDER_ENTITY_ID,
serialized_row_before_update=visible_fields_only_old_row,
serialized_row=visible_fields_only_updated_row,
metadata={},
),
slug=public_view.slug,
)
transaction.on_commit(_send_created_updated_deleted_row_signals_to_views)
@receiver(row_signals.rows_updated)
def public_rows_updated(
sender, rows, user, table, model, before_return, updated_field_ids, **kwargs
@ -332,10 +154,10 @@ def public_rows_updated(
}
# When a row is updated from the point of view of a public view it might not always
# result in a `row_updated` event. For example if the row was previously not visible
# result in a `rows_updated` event. For example if a row was previously not visible
# in the public view due to its filters, but the row update makes it now match
# the filters we want to send a `row_created` event to that views page as the
# clients won't know anything about the row and hence a `row_updated` event makes
# the filters we want to send a `rows_created` event to that views page as the
# clients won't know anything about the row and hence a `rows_updated` event makes
# no sense for them.
public_views_where_rows_were_created: List[PublicViewRows] = []
public_views_where_rows_were_updated: List[PublicViewRows] = []

View file

@ -13,27 +13,6 @@ from baserow.contrib.database.table.models import GeneratedTableModel
from baserow.ws.registries import page_registry
@receiver(row_signals.row_created)
def row_created(sender, row, before, user, table, model, **kwargs):
table_page_type = page_registry.get("table")
transaction.on_commit(
lambda: table_page_type.broadcast(
RealtimeRowMessages.row_created(
table_id=table.id,
serialized_row=get_row_serializer_class(
model, RowSerializer, is_response=True
)(row).data,
metadata=row_metadata_registry.generate_and_merge_metadata_for_row(
table, row.id
),
before=before,
),
getattr(user, "web_socket_id", None),
table_id=table.id,
)
)
@receiver(row_signals.rows_created)
def rows_created(sender, rows, before, user, table, model, **kwargs):
table_page_type = page_registry.get("table")
@ -55,14 +34,6 @@ def rows_created(sender, rows, before, user, table, model, **kwargs):
)
@receiver(row_signals.before_row_update)
def before_row_update(sender, row, user, table, model, updated_field_ids, **kwargs):
# Generate a serialized version of the row before it is updated. The
# `row_updated` receiver needs this serialized version because it can't serialize
# the old row after it has been updated.
return get_row_serializer_class(model, RowSerializer, is_response=True)(row).data
@receiver(row_signals.before_rows_update)
def before_rows_update(sender, rows, user, table, model, updated_field_ids, **kwargs):
return get_row_serializer_class(model, RowSerializer, is_response=True)(
@ -70,29 +41,6 @@ def before_rows_update(sender, rows, user, table, model, updated_field_ids, **kw
).data
@receiver(row_signals.row_updated)
def row_updated(
sender, row, user, table, model, before_return, updated_field_ids, **kwargs
):
table_page_type = page_registry.get("table")
transaction.on_commit(
lambda: table_page_type.broadcast(
RealtimeRowMessages.row_updated(
table_id=table.id,
serialized_row_before_update=dict(before_return)[before_row_update],
serialized_row=get_row_serializer_class(
model, RowSerializer, is_response=True
)(row).data,
metadata=row_metadata_registry.generate_and_merge_metadata_for_row(
table, row.id
),
),
getattr(user, "web_socket_id", None),
table_id=table.id,
)
)
@receiver(row_signals.rows_updated)
def rows_updated(
sender, rows, user, table, model, before_return, updated_field_ids, **kwargs
@ -116,14 +64,6 @@ def rows_updated(
)
@receiver(row_signals.before_row_delete)
def before_row_delete(sender, row, user, table, model, **kwargs):
# Generate a serialized version of the row before it is deleted. The
# `row_deleted` receiver needs this serialized version because it can't serialize
# the row after is has been deleted.
return get_row_serializer_class(model, RowSerializer, is_response=True)(row).data
@receiver(row_signals.before_rows_delete)
def before_rows_delete(sender, rows, user, table, model, **kwargs):
return get_row_serializer_class(model, RowSerializer, is_response=True)(
@ -131,20 +71,6 @@ def before_rows_delete(sender, rows, user, table, model, **kwargs):
).data
@receiver(row_signals.row_deleted)
def row_deleted(sender, row_id, row, user, table, model, before_return, **kwargs):
table_page_type = page_registry.get("table")
transaction.on_commit(
lambda: table_page_type.broadcast(
RealtimeRowMessages.row_deleted(
table_id=table.id, serialized_row=dict(before_return)[before_row_delete]
),
getattr(user, "web_socket_id", None),
table_id=table.id,
)
)
@receiver(row_signals.rows_deleted)
def rows_deleted(sender, rows, user, table, model, before_return, **kwargs):
table_page_type = page_registry.get("table")
@ -166,18 +92,6 @@ class RealtimeRowMessages:
websocket messages related to rows.
"""
@staticmethod
def row_deleted(table_id: int, serialized_row: Dict[str, Any]) -> Dict[str, Any]:
return {
"type": "row_deleted",
"table_id": table_id,
"row_id": serialized_row["id"],
# The web-frontend expects a serialized version of the row that is
# deleted in order the estimate what position the row had in the view,
# or find which kanban column the row was in etc.
"row": serialized_row,
}
@staticmethod
def rows_deleted(
table_id: int, serialized_rows: List[Dict[str, Any]]
@ -189,21 +103,6 @@ class RealtimeRowMessages:
"rows": serialized_rows,
}
@staticmethod
def row_created(
table_id: int,
serialized_row: Dict[str, Any],
metadata: Dict[str, Any],
before: Optional[GeneratedTableModel],
) -> Dict[str, Any]:
return {
"type": "row_created",
"table_id": table_id,
"row": serialized_row,
"metadata": metadata,
"before_row_id": before.id if before else None,
}
@staticmethod
def rows_created(
table_id: int,
@ -219,24 +118,6 @@ class RealtimeRowMessages:
"before_row_id": before.id if before else None,
}
@staticmethod
def row_updated(
table_id: int,
serialized_row_before_update: Dict[str, Any],
serialized_row: Dict[str, Any],
metadata: Dict[str, Any],
) -> Dict[str, Any]:
return {
"type": "row_updated",
"table_id": table_id,
# The web-frontend expects a serialized version of the row before it
# was updated in order the estimate what position the row had in the
# view.
"row_before_update": serialized_row_before_update,
"row": serialized_row,
"metadata": metadata,
}
@staticmethod
def rows_updated(
table_id: int,

View file

@ -3,11 +3,9 @@ from django.conf import settings
from .table.signals import table_created, table_updated, table_deleted
from .views.signals import view_created, views_reordered, view_updated, view_deleted
from .rows.signals import (
row_created,
rows_created,
row_updated,
rows_updated,
row_deleted,
rows_deleted,
)
from .fields.signals import field_created, field_updated, field_deleted
@ -16,9 +14,9 @@ if settings.DISABLE_ANONYMOUS_PUBLIC_VIEW_WS_CONNECTIONS:
else:
# noinspection PyUnresolvedReferences
from .public.rows.signals import ( # noqa: F401
public_row_created,
public_row_deleted,
public_row_updated,
public_rows_created,
public_rows_deleted,
public_rows_updated,
)
# noinspection PyUnresolvedReferences
@ -39,9 +37,9 @@ else:
)
PUBLIC_SIGNALS = [
"public_row_created",
"public_row_deleted",
"public_row_updated",
"public_rows_created",
"public_rows_deleted",
"public_rows_updated",
"public_view_filter_updated",
"public_view_filter_deleted",
"public_view_filter_created",
@ -61,11 +59,9 @@ __all__ = [
"view_created",
"view_updated",
"view_deleted",
"row_created",
"rows_created",
"row_updated",
"rows_updated",
"row_deleted",
"rows_deleted",
"field_created",
"field_updated",
"field_deleted",

View file

@ -42,7 +42,7 @@ class TableWebhookFixture:
kwargs["webhook"] = self.create_table_webhook(user=None)
if "event_type" not in kwargs:
kwargs["event_type"] = "row.created"
kwargs["event_type"] = "rows.created"
if "called_url" not in kwargs:
kwargs["called_url"] = self.fake.url()

View file

@ -24,7 +24,7 @@ def test_list_webhooks(api_client, data_fixture):
)
call_1 = data_fixture.create_table_webhook_call(webhook=webhook_1)
webhook_2 = data_fixture.create_table_webhook(
table=table, include_all_events=False, events=["row.created"]
table=table, include_all_events=False, events=["rows.created"]
)
data_fixture.create_table_webhook()
@ -74,7 +74,7 @@ def test_list_webhooks(api_client, data_fixture):
}
assert response_json[1]["id"] == webhook_2.id
assert response_json[1]["events"] == ["row.created"]
assert response_json[1]["events"] == ["rows.created"]
@pytest.mark.django_db
@ -138,7 +138,7 @@ def test_create_webhooks(api_client, data_fixture):
"url": "https://mydomain.com/endpoint",
"name": "My Webhook 2",
"include_all_events": False,
"events": ["row.created"],
"events": ["rows.created"],
"headers": {"Baserow-add-1": "Value 1"},
"request_method": "PATCH",
"use_user_field_names": False,
@ -154,7 +154,7 @@ def test_create_webhooks(api_client, data_fixture):
assert response_json["name"] == "My Webhook 2"
assert response_json["include_all_events"] is False
assert response_json["failed_triggers"] == 0
assert response_json["events"] == ["row.created"]
assert response_json["events"] == ["rows.created"]
assert response_json["headers"] == {"Baserow-add-1": "Value 1"}
assert response_json["calls"] == []
assert TableWebhook.objects.all().count() == 2
@ -164,7 +164,7 @@ def test_create_webhooks(api_client, data_fixture):
{
"url": "https://mydomain.com/endpoint",
"name": "My Webhook 2",
"events": ["row.created"],
"events": ["rows.created"],
"headers": {"Test:": "Value 1"},
"request_method": "PATCH",
},
@ -197,7 +197,7 @@ def test_create_webhooks(api_client, data_fixture):
{
"url": "https://mydomain.com:8a/endpoint",
"name": "My Webhook 2",
"events": ["row.created"],
"events": ["rows.created"],
"request_method": "PATCH",
},
format="json",
@ -214,7 +214,7 @@ def test_create_webhooks(api_client, data_fixture):
{
"url": "https://md.com/" + (2001 - len("https://md.com/")) * "a",
"name": "My Webhook 2",
"events": ["row.created"],
"events": ["rows.created"],
"request_method": "PATCH",
},
format="json",
@ -318,7 +318,7 @@ def test_update_webhook(api_client, data_fixture):
"url": "https://mydomain.com/endpoint",
"name": "My Webhook 2",
"include_all_events": False,
"events": ["row.created"],
"events": ["rows.created"],
"headers": {"Baserow-add-1": "Value 1"},
"request_method": "PATCH",
"use_user_field_names": False,
@ -335,7 +335,7 @@ def test_update_webhook(api_client, data_fixture):
assert response_json["name"] == "My Webhook 2"
assert response_json["include_all_events"] is False
assert response_json["failed_triggers"] == 0
assert response_json["events"] == ["row.created"]
assert response_json["events"] == ["rows.created"]
assert response_json["headers"] == {"Baserow-add-1": "Value 1"}
assert response_json["calls"] == []
@ -401,7 +401,7 @@ def test_trigger_test_call(api_client, data_fixture):
reverse("api:database:webhooks:test", kwargs={"table_id": 0}),
{
"url": "http://baserow.io",
"event_type": "row.created",
"event_type": "rows.created",
},
format="json",
HTTP_AUTHORIZATION=f"JWT {jwt_token}",
@ -413,7 +413,7 @@ def test_trigger_test_call(api_client, data_fixture):
reverse("api:database:webhooks:test", kwargs={"table_id": table.id}),
{
"url": "http://baserow.io",
"event_type": "row.created",
"event_type": "rows.created",
},
format="json",
HTTP_AUTHORIZATION=f"JWT {jwt_token_2}",
@ -425,7 +425,7 @@ def test_trigger_test_call(api_client, data_fixture):
reverse("api:database:webhooks:test", kwargs={"table_id": table.id}),
{
"url": "http://baserow.io",
"event_type": "row.created",
"event_type": "rows.created",
},
format="json",
HTTP_AUTHORIZATION=f"JWT {jwt_token}",
@ -441,7 +441,7 @@ def test_trigger_test_call(api_client, data_fixture):
reverse("api:database:webhooks:test", kwargs={"table_id": table.id}),
{
"url": "http://baserow.io/invalid",
"event_type": "row.created",
"event_type": "rows.created",
},
format="json",
HTTP_AUTHORIZATION=f"JWT {jwt_token}",

View file

@ -3,10 +3,11 @@ import pytest
from baserow.contrib.database.webhooks.registries import webhook_event_type_registry
from baserow.contrib.database.fields.handler import FieldHandler
from baserow.contrib.database.rows.handler import RowHandler
from baserow.contrib.database.ws.rows.signals import before_rows_update
@pytest.mark.django_db()
def test_row_created_event_type(data_fixture):
def test_rows_created_event_type(data_fixture):
user = data_fixture.create_user()
table = data_fixture.create_database_table(user=user)
field = data_fixture.create_text_field(table=table, primary=True, name="Test 1")
@ -19,41 +20,43 @@ def test_row_created_event_type(data_fixture):
url="http://localhost",
use_user_field_names=False,
)
payload = webhook_event_type_registry.get("row.created").get_payload(
event_id="1", webhook=webhook, model=model, table=table, row=row
payload = webhook_event_type_registry.get("rows.created").get_payload(
event_id="1", webhook=webhook, model=model, table=table, rows=[row]
)
assert payload == {
"table_id": table.id,
"event_id": "1",
"event_type": "row.created",
"row_id": row.id,
"values": {
"id": 1,
"order": "1.00000000000000000000",
f"field_{field.id}": None,
},
"event_type": "rows.created",
"items": [
{
"id": 1,
"order": "1.00000000000000000000",
f"field_{field.id}": None,
}
],
}
webhook.use_user_field_names = True
webhook.save()
payload = webhook_event_type_registry.get("row.created").get_payload(
event_id="1", webhook=webhook, model=model, table=table, row=row
payload = webhook_event_type_registry.get("rows.created").get_payload(
event_id="1", webhook=webhook, model=model, table=table, rows=[row]
)
assert payload == {
"table_id": table.id,
"event_id": "1",
"event_type": "row.created",
"row_id": row.id,
"values": {
"id": 1,
"order": "1.00000000000000000000",
"Test 1": None,
},
"event_type": "rows.created",
"items": [
{
"id": 1,
"order": "1.00000000000000000000",
"Test 1": None,
}
],
}
@pytest.mark.django_db()
def test_row_updated_event_type(data_fixture):
def test_rows_updated_event_type(data_fixture):
user = data_fixture.create_user()
table = data_fixture.create_database_table(user=user)
table_2 = data_fixture.create_database_table(database=table.database)
@ -85,9 +88,16 @@ def test_row_updated_event_type(data_fixture):
row = model.objects.create(**{f"field_{text_field.id}": "Old Test value"})
getattr(row, f"field_{link_row_field.id}").add(i1.id)
before_return = webhook_event_type_registry.get(
"row.updated"
).get_test_call_before_return(table, row, model)
before_return = {
before_rows_update: before_rows_update(
rows=[row],
model=model,
table=table,
sender=None,
user=None,
updated_field_ids=None,
)
}
row = RowHandler().update_row_by_id(
user=user,
@ -103,65 +113,71 @@ def test_row_updated_event_type(data_fixture):
url="http://localhost",
use_user_field_names=False,
)
payload = webhook_event_type_registry.get("row.updated").get_payload(
payload = webhook_event_type_registry.get("rows.updated").get_payload(
event_id="1",
webhook=webhook,
model=model,
table=table,
row=row,
rows=[row],
before_return=before_return,
)
assert payload == {
"table_id": table.id,
"event_id": "1",
"event_type": "row.updated",
"row_id": row.id,
"values": {
"id": 1,
"order": "1.00000000000000000000",
f"field_{text_field.id}": "New Test value",
f"field_{link_row_field.id}": [{"id": 1, "value": "Lookup 1"}],
},
"old_values": {
"id": 1,
"order": "1.00000000000000000000",
f"field_{text_field.id}": "Old Test value",
f"field_{link_row_field.id}": [{"id": 1, "value": "Lookup 1"}],
},
"event_type": "rows.updated",
"items": [
{
"id": 1,
"order": "1.00000000000000000000",
f"field_{text_field.id}": "New Test value",
f"field_{link_row_field.id}": [{"id": 1, "value": "Lookup 1"}],
}
],
"old_items": [
{
"id": 1,
"order": "1.00000000000000000000",
f"field_{text_field.id}": "Old Test value",
f"field_{link_row_field.id}": [{"id": 1, "value": "Lookup 1"}],
}
],
}
webhook.use_user_field_names = True
webhook.save()
payload = webhook_event_type_registry.get("row.updated").get_payload(
payload = webhook_event_type_registry.get("rows.updated").get_payload(
event_id="1",
webhook=webhook,
model=model,
table=table,
row=row,
rows=[row],
before_return=before_return,
)
assert payload == {
"table_id": table.id,
"event_id": "1",
"event_type": "row.updated",
"row_id": row.id,
"values": {
"id": 1,
"order": "1.00000000000000000000",
f"{text_field.name}": "New Test value",
f"{link_row_field.name}": [{"id": 1, "value": "Lookup 1"}],
},
"old_values": {
"id": 1,
"order": "1.00000000000000000000",
f"{text_field.name}": "Old Test value",
f"{link_row_field.name}": [{"id": 1, "value": "Lookup 1"}],
},
"event_type": "rows.updated",
"items": [
{
"id": 1,
"order": "1.00000000000000000000",
f"{text_field.name}": "New Test value",
f"{link_row_field.name}": [{"id": 1, "value": "Lookup 1"}],
}
],
"old_items": [
{
"id": 1,
"order": "1.00000000000000000000",
f"{text_field.name}": "Old Test value",
f"{link_row_field.name}": [{"id": 1, "value": "Lookup 1"}],
}
],
}
@pytest.mark.django_db()
def test_row_deleted_event_type(data_fixture):
def test_rows_deleted_event_type(data_fixture):
user = data_fixture.create_user()
table = data_fixture.create_database_table(user=user)
data_fixture.create_text_field(table=table, primary=True, name="Test 1")
@ -174,17 +190,17 @@ def test_row_deleted_event_type(data_fixture):
url="http://localhost",
use_user_field_names=False,
)
payload = webhook_event_type_registry.get("row.deleted").get_payload(
payload = webhook_event_type_registry.get("rows.deleted").get_payload(
event_id="1",
webhook=webhook,
model=model,
table=table,
row=row,
rows=[row],
)
assert payload == {
"table_id": table.id,
"event_id": "1",
"event_type": "row.deleted",
"row_id": row.id,
"event_type": "rows.deleted",
"row_ids": [row.id],
}

View file

@ -106,7 +106,7 @@ def test_extract_manytomany_values(data_fixture):
@pytest.mark.django_db
@patch("baserow.contrib.database.rows.signals.row_created.send")
@patch("baserow.contrib.database.rows.signals.rows_created.send")
def test_create_row(send_mock, data_fixture):
user = data_fixture.create_user()
user_2 = data_fixture.create_user()
@ -152,7 +152,7 @@ def test_create_row(send_mock, data_fixture):
assert row_1.order == Decimal("1.00000000000000000000")
send_mock.assert_called_once()
assert send_mock.call_args[1]["row"].id == row_1.id
assert send_mock.call_args[1]["rows"][0].id == row_1.id
assert send_mock.call_args[1]["user"].id == user.id
assert send_mock.call_args[1]["table"].id == table.id
assert send_mock.call_args[1]["before"] is None
@ -287,7 +287,7 @@ def test_get_row(data_fixture):
@pytest.mark.django_db
@patch("baserow.contrib.database.rows.signals.row_updated.send")
@patch("baserow.contrib.database.rows.signals.rows_updated.send")
def test_update_row(send_mock, data_fixture):
user = data_fixture.create_user()
user_2 = data_fixture.create_user()
@ -320,7 +320,7 @@ def test_update_row(send_mock, data_fixture):
)
with patch(
"baserow.contrib.database.rows.signals.before_row_update.send"
"baserow.contrib.database.rows.signals.before_rows_update.send"
) as before_send_mock:
handler.update_row_by_id(
user=user,
@ -339,13 +339,13 @@ def test_update_row(send_mock, data_fixture):
assert getattr(row, f"field_{price_field.id}") == Decimal("59999.99")
before_send_mock.assert_called_once()
assert before_send_mock.call_args[1]["row"].id == row.id
assert before_send_mock.call_args[1]["rows"][0].id == row.id
assert before_send_mock.call_args[1]["user"].id == user.id
assert before_send_mock.call_args[1]["table"].id == table.id
assert before_send_mock.call_args[1]["model"]._generated_table_model
send_mock.assert_called_once()
assert send_mock.call_args[1]["row"].id == row.id
assert send_mock.call_args[1]["rows"][0].id == row.id
assert send_mock.call_args[1]["user"].id == user.id
assert send_mock.call_args[1]["table"].id == table.id
assert send_mock.call_args[1]["model"]._generated_table_model
@ -387,8 +387,8 @@ def test_update_rows_created_on_and_last_modified(data_fixture):
@pytest.mark.django_db
@patch("baserow.contrib.database.rows.signals.row_updated.send")
@patch("baserow.contrib.database.rows.signals.before_row_update.send")
@patch("baserow.contrib.database.rows.signals.rows_updated.send")
@patch("baserow.contrib.database.rows.signals.before_rows_update.send")
def test_move_row(before_send_mock, send_mock, data_fixture):
user = data_fixture.create_user()
user_2 = data_fixture.create_user()
@ -414,13 +414,13 @@ def test_move_row(before_send_mock, send_mock, data_fixture):
assert row_3.order == Decimal("3.00000000000000000000")
before_send_mock.assert_called_once()
assert before_send_mock.call_args[1]["row"].id == row_1.id
assert before_send_mock.call_args[1]["rows"][0].id == row_1.id
assert before_send_mock.call_args[1]["user"].id == user.id
assert before_send_mock.call_args[1]["table"].id == table.id
assert before_send_mock.call_args[1]["model"]._generated_table_model
send_mock.assert_called_once()
assert send_mock.call_args[1]["row"].id == row_1.id
assert send_mock.call_args[1]["rows"][0].id == row_1.id
assert send_mock.call_args[1]["user"].id == user.id
assert send_mock.call_args[1]["table"].id == table.id
assert send_mock.call_args[1]["model"]._generated_table_model
@ -441,8 +441,8 @@ def test_move_row(before_send_mock, send_mock, data_fixture):
@pytest.mark.django_db
@patch("baserow.contrib.database.rows.signals.row_deleted.send")
@patch("baserow.contrib.database.rows.signals.before_row_delete.send")
@patch("baserow.contrib.database.rows.signals.rows_deleted.send")
@patch("baserow.contrib.database.rows.signals.before_rows_delete.send")
def test_delete_row(before_send_mock, send_mock, data_fixture):
user = data_fixture.create_user()
user_2 = data_fixture.create_user()
@ -468,14 +468,13 @@ def test_delete_row(before_send_mock, send_mock, data_fixture):
assert row.trashed
before_send_mock.assert_called_once()
assert before_send_mock.call_args[1]["row"]
assert before_send_mock.call_args[1]["rows"]
assert before_send_mock.call_args[1]["user"].id == user.id
assert before_send_mock.call_args[1]["table"].id == table.id
assert before_send_mock.call_args[1]["model"]._generated_table_model
send_mock.assert_called_once()
assert send_mock.call_args[1]["row_id"] == row_id
assert send_mock.call_args[1]["row"]
assert send_mock.call_args[1]["rows"][0].id == row_id
assert send_mock.call_args[1]["user"].id == user.id
assert send_mock.call_args[1]["table"].id == table.id
assert send_mock.call_args[1]["model"]._generated_table_model
@ -483,7 +482,7 @@ def test_delete_row(before_send_mock, send_mock, data_fixture):
@pytest.mark.django_db
@patch("baserow.contrib.database.rows.signals.row_created.send")
@patch("baserow.contrib.database.rows.signals.rows_created.send")
def test_restore_row(send_mock, data_fixture):
user = data_fixture.create_user()
table = data_fixture.create_database_table(name="Car", user=user)
@ -516,7 +515,7 @@ def test_restore_row(send_mock, data_fixture):
TrashHandler.restore_item(user, "row", row_1.id, parent_trash_item_id=table.id)
assert len(send_mock.call_args) == 2
assert send_mock.call_args[1]["row"].id == row_1.id
assert send_mock.call_args[1]["rows"][0].id == row_1.id
assert send_mock.call_args[1]["user"] is None
assert send_mock.call_args[1]["table"].id == table.id
assert send_mock.call_args[1]["before"] is None

View file

@ -475,16 +475,18 @@ def test_trash_and_restore_rows_in_batch(send_mock, data_fixture):
customers_primary_field = field_handler.create_field(
user=user, table=customers_table, type_name="text", name="Name", primary=True
)
row1 = row_handler.create_row(
user=user,
table=customers_table,
values={f"field_{customers_primary_field.id}": "Row A"},
)
row2 = row_handler.create_row(
user=user,
table=customers_table,
values={f"field_{customers_primary_field.id}": ""},
)
with patch("baserow.contrib.database.rows.signals.rows_created.send"):
row1 = row_handler.create_row(
user=user,
table=customers_table,
values={f"field_{customers_primary_field.id}": "Row A"},
)
row2 = row_handler.create_row(
user=user,
table=customers_table,
values={f"field_{customers_primary_field.id}": ""},
)
trashed_rows = TrashedRows.objects.create(
table=customers_table, row_ids=[row1.id, row2.id]

View file

@ -1419,7 +1419,7 @@ def test_get_public_view_by_slug(data_fixture):
@pytest.mark.django_db
@patch("baserow.contrib.database.rows.signals.row_created.send")
@patch("baserow.contrib.database.rows.signals.rows_created.send")
def test_submit_form_view(send_mock, data_fixture):
table = data_fixture.create_database_table()
form = data_fixture.create_form_view(table=table)
@ -1451,7 +1451,7 @@ def test_submit_form_view(send_mock, data_fixture):
)
send_mock.assert_called_once()
assert send_mock.call_args[1]["row"].id == instance.id
assert send_mock.call_args[1]["rows"][0].id == instance.id
assert send_mock.call_args[1]["user"] is None
assert send_mock.call_args[1]["table"].id == table.id
assert send_mock.call_args[1]["before"] is None

View file

@ -23,15 +23,15 @@ def test_find_webhooks_to_call(data_fixture):
table=table_1, include_all_events=True, active=True
)
webhook_2 = data_fixture.create_table_webhook(
table=table_1, include_all_events=False, events=["row.created"], active=True
table=table_1, include_all_events=False, events=["rows.created"], active=True
)
data_fixture.create_table_webhook(
table=table_1, include_all_events=False, events=["row.updated"], active=False
table=table_1, include_all_events=False, events=["rows.updated"], active=False
)
webhook_4 = data_fixture.create_table_webhook(
table=table_1,
include_all_events=False,
events=["row.updated", "row.deleted"],
events=["rows.updated", "rows.deleted"],
active=True,
)
webhook_5 = data_fixture.create_table_webhook(
@ -42,42 +42,42 @@ def test_find_webhooks_to_call(data_fixture):
webhook_6 = data_fixture.create_table_webhook(
table=table_2,
include_all_events=False,
events=["row.updated"],
events=["rows.updated"],
active=True,
)
handler = WebhookHandler()
webhooks = handler.find_webhooks_to_call(table_1.id, "row.created")
webhooks = handler.find_webhooks_to_call(table_1.id, "rows.created")
webhook_ids = [webhook.id for webhook in webhooks]
assert len(webhook_ids) == 2
assert webhook_1.id in webhook_ids
assert webhook_2.id in webhook_ids
webhooks = handler.find_webhooks_to_call(table_1.id, "row.updated")
webhooks = handler.find_webhooks_to_call(table_1.id, "rows.updated")
webhook_ids = [webhook.id for webhook in webhooks]
assert len(webhook_ids) == 2
assert webhook_1.id in webhook_ids
assert webhook_4.id in webhook_ids
webhooks = handler.find_webhooks_to_call(table_1.id, "row.deleted")
webhooks = handler.find_webhooks_to_call(table_1.id, "rows.deleted")
webhook_ids = [webhook.id for webhook in webhooks]
assert len(webhook_ids) == 2
assert webhook_1.id in webhook_ids
assert webhook_4.id in webhook_ids
webhooks = handler.find_webhooks_to_call(table_2.id, "row.created")
webhooks = handler.find_webhooks_to_call(table_2.id, "rows.created")
webhook_ids = [webhook.id for webhook in webhooks]
assert len(webhook_ids) == 1
assert webhook_5.id in webhook_ids
webhooks = handler.find_webhooks_to_call(table_2.id, "row.updated")
webhooks = handler.find_webhooks_to_call(table_2.id, "rows.updated")
webhook_ids = [webhook.id for webhook in webhooks]
assert len(webhook_ids) == 2
assert webhook_5.id in webhook_ids
assert webhook_6.id in webhook_ids
webhooks = handler.find_webhooks_to_call(table_2.id, "row.deleted")
webhooks = handler.find_webhooks_to_call(table_2.id, "rows.deleted")
webhook_ids = [webhook.id for webhook in webhooks]
assert len(webhook_ids) == 1
assert webhook_5.id in webhook_ids
@ -118,7 +118,7 @@ def test_get_all_table_webhooks(data_fixture, django_assert_num_queries):
webhook_1 = data_fixture.create_table_webhook(
table=table,
events=["row.created", "row.updated"],
events=["rows.created", "rows.updated"],
headers={"Baserow-test-2": "Value 2"},
include_all_events=False,
)
@ -184,7 +184,7 @@ def test_create_webhook(data_fixture):
# if "include_all_events" is True and we pass in events that are not empty
# the handler will not create the entry in the events table.
events = ["row.created"]
events = ["rows.created"]
webhook = webhook_handler.create_table_webhook(
user=user, table=table, events=events, headers={}, **dict(webhook_data)
)
@ -202,7 +202,7 @@ def test_create_webhook(data_fixture):
assert webhook.include_all_events is False
webhook_events = webhook.events.all()
assert len(webhook_events) == 1
assert webhook_events[0].event_type == "row.created"
assert webhook_events[0].event_type == "rows.created"
webhook_headers = webhook.headers.all()
assert len(webhook_headers) == 2
assert webhook_headers[0].name == "Baserow-test-1"
@ -232,7 +232,7 @@ def test_update_webhook(data_fixture):
table = data_fixture.create_database_table(user=user)
webhook = data_fixture.create_table_webhook(
table=table,
events=["row.created"],
events=["rows.created"],
headers={"Baserow-test-1": "Value 1", "Baserow-test-2": "Value 2"},
)
@ -262,24 +262,24 @@ def test_update_webhook(data_fixture):
events_before = list(webhook.events.all())
webhook = handler.update_table_webhook(
user=user, webhook=webhook, events=["row.created", "row.updated"]
user=user, webhook=webhook, events=["rows.created", "rows.updated"]
)
events = webhook.events.all().order_by("id")
assert len(events) == 2
assert events[0].id == events_before[0].id
assert events[0].event_type == events_before[0].event_type == "row.created"
assert events[1].event_type == "row.updated"
assert events[0].event_type == events_before[0].event_type == "rows.created"
assert events[1].event_type == "rows.updated"
webhook = handler.update_table_webhook(
user=user, webhook=webhook, events=["row.updated"]
user=user, webhook=webhook, events=["rows.updated"]
)
events_2 = webhook.events.all().order_by("id")
assert len(events_2) == 1
assert events_2[0].id == events[1].id
assert events_2[0].event_type == events[1].event_type == "row.updated"
assert events_2[0].event_type == events[1].event_type == "rows.updated"
webhook = handler.update_table_webhook(
user=user, webhook=webhook, include_all_events=True, events=["row.created"]
user=user, webhook=webhook, include_all_events=True, events=["rows.created"]
)
assert webhook.events.all().count() == 0
@ -305,7 +305,7 @@ def test_delete_webhook(data_fixture):
webhook = data_fixture.create_table_webhook(
user=user,
headers={"A": "B"},
events=["row.created"],
events=["rows.created"],
include_all_events=False,
)
data_fixture.create_table_webhook()
@ -330,23 +330,24 @@ def test_trigger_test_call(data_fixture):
handler = WebhookHandler()
with pytest.raises(UserNotInGroup):
handler.trigger_test_call(user=user_2, table=table, event_type="row.created")
handler.trigger_test_call(user=user_2, table=table, event_type="rows.created")
responses.add(responses.POST, "http://localhost", json={}, status=200)
request, response = handler.trigger_test_call(
user=user,
table=table,
event_type="row.created",
event_type="rows.created",
headers={"Baserow-add-1": "Value 1"},
request_method="POST",
url="http://localhost",
use_user_field_names=False,
)
assert response.ok
assert request.headers["Baserow-add-1"] == "Value 1"
assert request.headers["Content-type"] == "application/json"
assert request.headers["X-Baserow-Event"] == "row.created"
assert response.request.headers["Baserow-add-1"] == "Value 1"
assert response.request.headers["Content-type"] == "application/json"
assert response.request.headers["X-Baserow-Event"] == "rows.created"
assert "X-Baserow-Delivery" in request.headers
assert request.method == "POST"
@ -354,13 +355,14 @@ def test_trigger_test_call(data_fixture):
request_body = json.loads(request.body)
assert request_body["table_id"] == table.id
assert request_body["event_type"] == "row.created"
assert request_body["row_id"] == 0
assert request_body["values"] == {
"id": 0,
f"field_{field.id}": None,
"order": "0.00000000000000000000",
}
assert request_body["event_type"] == "rows.created"
assert request_body["items"] == [
{
"id": 0,
f"field_{field.id}": None,
"order": "0.00000000000000000000",
}
]
@pytest.mark.django_db

View file

@ -16,26 +16,26 @@ def test_signal_listener(mock_call_webhook, data_fixture):
table=table,
url="http://localhost/",
include_all_events=False,
events=["row.created"],
events=["rows.created"],
headers={"Baserow-header-1": "Value 1"},
)
RowHandler().create_row(user=user, table=table, values={})
mock_call_webhook.delay.assert_called_once()
args, kwargs = mock_call_webhook.delay.call_args
assert kwargs["webhook_id"] == webhook.id
assert isinstance(kwargs["event_id"], uuid.UUID)
assert kwargs["event_type"] == "row.created"
assert kwargs["event_type"] == "rows.created"
assert kwargs["headers"]["Baserow-header-1"] == "Value 1"
assert kwargs["headers"]["Content-type"] == "application/json"
assert kwargs["headers"]["X-Baserow-Event"] == "row.created"
assert kwargs["headers"]["X-Baserow-Event"] == "rows.created"
assert len(kwargs["headers"]["X-Baserow-Delivery"]) > 1
assert kwargs["method"] == "POST"
assert kwargs["url"] == "http://localhost/"
assert kwargs["payload"] == {
"table_id": table.id,
"event_id": kwargs["payload"]["event_id"],
"event_type": "row.created",
"row_id": 1,
"values": {"id": 1, "order": "1.00000000000000000000"},
"event_type": "rows.created",
"items": [{"id": 1, "order": "1.00000000000000000000"}],
}

View file

@ -20,11 +20,11 @@ def test_call_webhook(data_fixture):
call_webhook.run(
webhook_id=0,
event_id="00000000-0000-0000-0000-000000000000",
event_type="row.created",
event_type="rows.created",
method="POST",
url="http://localhost/",
headers={"Baserow-header-1": "Value 1"},
payload={"type": "row.created"},
payload={"type": "rows.created"},
)
assert TableWebhookCall.objects.all().count() == 0
@ -32,11 +32,11 @@ def test_call_webhook(data_fixture):
call_webhook.run(
webhook_id=webhook.id,
event_id="00000000-0000-0000-0000-000000000000",
event_type="row.created",
event_type="rows.created",
method="POST",
url="http://localhost/",
headers={"Baserow-header-1": "Value 1"},
payload={"type": "row.created"},
payload={"type": "rows.created"},
)
transaction.commit()
@ -44,7 +44,7 @@ def test_call_webhook(data_fixture):
created_call = TableWebhookCall.objects.all().first()
called_time_1 = created_call.called_time
assert created_call.webhook_id == webhook.id
assert created_call.event_type == "row.created"
assert created_call.event_type == "rows.created"
assert created_call.called_time
assert created_call.called_url == "http://localhost/"
assert "POST http://localhost/" in created_call.request
@ -56,11 +56,11 @@ def test_call_webhook(data_fixture):
call_webhook.run(
webhook_id=webhook.id,
event_id="00000000-0000-0000-0000-000000000000",
event_type="row.created",
event_type="rows.created",
method="POST",
url="http://localhost/",
headers={"Baserow-header-1": "Value 1"},
payload={"type": "row.created"},
payload={"type": "rows.created"},
)
webhook.refresh_from_db()
@ -74,11 +74,11 @@ def test_call_webhook(data_fixture):
call_webhook.run(
webhook_id=webhook.id,
event_id="00000000-0000-0000-0000-000000000001",
event_type="row.created",
event_type="rows.created",
method="POST",
url="http://localhost/",
headers={"Baserow-header-1": "Value 1"},
payload={"type": "row.created"},
payload={"type": "rows.created"},
)
webhook.refresh_from_db()
@ -92,17 +92,17 @@ def test_call_webhook(data_fixture):
call_webhook(
webhook_id=webhook.id,
event_id="00000000-0000-0000-0000-000000000002",
event_type="row.created",
event_type="rows.created",
method="POST",
url="http://localhost/",
headers={"Baserow-header-1": "Value 1"},
payload={"type": "row.created"},
payload={"type": "rows.created"},
)
assert TableWebhookCall.objects.all().count() == 3
created_call = TableWebhookCall.objects.all().first()
assert created_call.webhook_id == webhook.id
assert created_call.event_type == "row.created"
assert created_call.event_type == "rows.created"
assert created_call.called_time
assert created_call.called_url == "http://localhost/"
assert "POST http://localhost/" in created_call.request
@ -120,17 +120,17 @@ def test_call_webhook(data_fixture):
call_webhook(
webhook_id=webhook.id,
event_id="00000000-0000-0000-0000-000000000003",
event_type="row.created",
event_type="rows.created",
method="POST",
url="http://localhost2/",
headers={"Baserow-header-1": "Value 1"},
payload={"type": "row.created"},
payload={"type": "rows.created"},
)
assert TableWebhookCall.objects.all().count() == 4
created_call = TableWebhookCall.objects.all().first()
assert created_call.webhook_id == webhook.id
assert created_call.event_type == "row.created"
assert created_call.event_type == "rows.created"
assert created_call.called_time
assert created_call.called_url == "http://localhost2/"
assert "POST http://localhost2/" in created_call.request

View file

@ -45,14 +45,16 @@ def test_when_row_created_public_views_receive_restricted_row_created_ws_event(
call(
f"view-{public_view_only_showing_one_field.slug}",
{
"type": "row_created",
"type": "rows_created",
"table_id": PUBLIC_PLACEHOLDER_ENTITY_ID,
"row": {
"id": row.id,
"order": "1.00000000000000000000",
# Only the visible field should be sent
f"field_{visible_field.id}": "Visible",
},
"rows": [
{
"id": row.id,
"order": "1.00000000000000000000",
# Only the visible field should be sent
f"field_{visible_field.id}": "Visible",
}
],
"metadata": {},
"before_row_id": None,
},
@ -61,16 +63,18 @@ def test_when_row_created_public_views_receive_restricted_row_created_ws_event(
call(
f"view-{public_view_showing_all_fields.slug}",
{
"type": "row_created",
"type": "rows_created",
"table_id": PUBLIC_PLACEHOLDER_ENTITY_ID,
"row": {
"id": row.id,
"order": "1.00000000000000000000",
f"field_{visible_field.id}": "Visible",
# This field is not hidden for this public view and so should be
# included
f"field_{hidden_field.id}": "Hidden",
},
"rows": [
{
"id": row.id,
"order": "1.00000000000000000000",
f"field_{visible_field.id}": "Visible",
# This field is not hidden for this public view and so
# should be included
f"field_{hidden_field.id}": "Hidden",
}
],
"metadata": {},
"before_row_id": None,
},
@ -137,14 +141,16 @@ def test_when_row_created_public_views_receive_row_created_only_when_filters_mat
call(
f"view-{public_view_showing_row.slug}",
{
"type": "row_created",
"type": "rows_created",
"table_id": PUBLIC_PLACEHOLDER_ENTITY_ID,
"row": {
"id": row.id,
"order": "1.00000000000000000000",
# Only the visible field should be sent
f"field_{visible_field.id}": "Visible",
},
"rows": [
{
"id": row.id,
"order": "1.00000000000000000000",
# Only the visible field should be sent
f"field_{visible_field.id}": "Visible",
}
],
"metadata": {},
"before_row_id": None,
},
@ -360,32 +366,36 @@ def test_when_row_deleted_public_views_receive_restricted_row_deleted_ws_event(
call(
f"view-{public_view_only_showing_one_field.slug}",
{
"type": "row_deleted",
"row_id": row.id,
"type": "rows_deleted",
"row_ids": [row.id],
"table_id": PUBLIC_PLACEHOLDER_ENTITY_ID,
"row": {
"id": row.id,
"order": "1.00000000000000000000",
# Only the visible field should be sent
f"field_{visible_field.id}": "Visible",
},
"rows": [
{
"id": row.id,
"order": "1.00000000000000000000",
# Only the visible field should be sent
f"field_{visible_field.id}": "Visible",
}
],
},
None,
),
call(
f"view-{public_view_showing_all_fields.slug}",
{
"type": "row_deleted",
"row_id": row.id,
"type": "rows_deleted",
"row_ids": [row.id],
"table_id": PUBLIC_PLACEHOLDER_ENTITY_ID,
"row": {
"id": row.id,
"order": "1.00000000000000000000",
f"field_{visible_field.id}": "Visible",
# This field is not hidden for this public view
# and so should be included
f"field_{hidden_field.id}": "Hidden",
},
"rows": [
{
"id": row.id,
"order": "1.00000000000000000000",
f"field_{visible_field.id}": "Visible",
# This field is not hidden for this public view
# and so should be included
f"field_{hidden_field.id}": "Hidden",
}
],
},
None,
),
@ -450,15 +460,17 @@ def test_when_row_deleted_public_views_receive_row_deleted_only_when_filters_mat
call(
f"view-{public_view_showing_row.slug}",
{
"type": "row_deleted",
"type": "rows_deleted",
"table_id": PUBLIC_PLACEHOLDER_ENTITY_ID,
"row_id": row.id,
"row": {
"id": row.id,
"order": "1.00000000000000000000",
# Only the visible field should be sent
f"field_{visible_field.id}": "Visible",
},
"row_ids": [row.id],
"rows": [
{
"id": row.id,
"order": "1.00000000000000000000",
# Only the visible field should be sent
f"field_{visible_field.id}": "Visible",
}
],
},
None,
),
@ -706,14 +718,16 @@ def test_given_row_not_visible_in_public_view_when_updated_to_be_visible_event_s
{
# The row should appear as a created event as for the public view
# it effectively has been created as it didn't exist before.
"type": "row_created",
"type": "rows_created",
"table_id": PUBLIC_PLACEHOLDER_ENTITY_ID,
"row": {
"id": initially_hidden_row.id,
"order": "1.00000000000000000000",
# Only the visible field should be sent
f"field_{visible_field.id}": "Visible",
},
"rows": [
{
"id": initially_hidden_row.id,
"order": "1.00000000000000000000",
# Only the visible field should be sent
f"field_{visible_field.id}": "Visible",
}
],
"metadata": {},
"before_row_id": None,
},
@ -1154,16 +1168,18 @@ def test_given_row_visible_in_public_view_when_updated_to_be_not_visible_event_s
{
# The row should appear as a deleted event as for the public view
# it effectively has been.
"type": "row_deleted",
"type": "rows_deleted",
"table_id": PUBLIC_PLACEHOLDER_ENTITY_ID,
"row_id": initially_visible_row.id,
"row": {
"id": initially_visible_row.id,
"order": "1.00000000000000000000",
# Only the visible field should be sent in its state before it
# was updated
f"field_{visible_field.id}": "Visible",
},
"row_ids": [initially_visible_row.id],
"rows": [
{
"id": initially_visible_row.id,
"order": "1.00000000000000000000",
# Only the visible field should be sent in its state before
# it was updated
f"field_{visible_field.id}": "Visible",
}
],
},
None,
),
@ -1353,20 +1369,24 @@ def test_given_row_visible_in_public_view_when_updated_to_still_be_visible_event
call(
f"view-{public_view_with_row_showing.slug}",
{
"type": "row_updated",
"type": "rows_updated",
"table_id": PUBLIC_PLACEHOLDER_ENTITY_ID,
"row_before_update": {
"id": initially_visible_row.id,
"order": "1.00000000000000000000",
# Only the visible field should be sent
f"field_{visible_field.id}": "Visible",
},
"row": {
"id": initially_visible_row.id,
"order": "1.00000000000000000000",
# Only the visible field should be sent
f"field_{visible_field.id}": "StillVisibleButUpdated",
},
"rows_before_update": [
{
"id": initially_visible_row.id,
"order": "1.00000000000000000000",
# Only the visible field should be sent
f"field_{visible_field.id}": "Visible",
}
],
"rows": [
{
"id": initially_visible_row.id,
"order": "1.00000000000000000000",
# Only the visible field should be sent
f"field_{visible_field.id}": "StillVisibleButUpdated",
}
],
"metadata": {},
},
None,
@ -1620,14 +1640,16 @@ def test_when_row_restored_public_views_receive_restricted_row_created_ws_event(
call(
f"view-{public_view_only_showing_one_field.slug}",
{
"type": "row_created",
"type": "rows_created",
"table_id": PUBLIC_PLACEHOLDER_ENTITY_ID,
"row": {
"id": row.id,
"order": "1.00000000000000000000",
# Only the visible field should be sent
f"field_{visible_field.id}": "Visible",
},
"rows": [
{
"id": row.id,
"order": "1.00000000000000000000",
# Only the visible field should be sent
f"field_{visible_field.id}": "Visible",
}
],
"metadata": {},
"before_row_id": None,
},
@ -1636,16 +1658,18 @@ def test_when_row_restored_public_views_receive_restricted_row_created_ws_event(
call(
f"view-{public_view_showing_all_fields.slug}",
{
"type": "row_created",
"type": "rows_created",
"table_id": PUBLIC_PLACEHOLDER_ENTITY_ID,
"row": {
"id": row.id,
"order": "1.00000000000000000000",
f"field_{visible_field.id}": "Visible",
# This field is not hidden for this public view and so should be
# included
f"field_{hidden_field.id}": "Hidden",
},
"rows": [
{
"id": row.id,
"order": "1.00000000000000000000",
f"field_{visible_field.id}": "Visible",
# This field is not hidden for this public view and so
# should be included
f"field_{hidden_field.id}": "Hidden",
}
],
"metadata": {},
"before_row_id": None,
},
@ -1715,14 +1739,16 @@ def test_when_row_restored_public_views_receive_row_created_only_when_filters_ma
call(
f"view-{public_view_showing_row.slug}",
{
"type": "row_created",
"type": "rows_created",
"table_id": PUBLIC_PLACEHOLDER_ENTITY_ID,
"row": {
"id": row.id,
"order": "1.00000000000000000000",
# Only the visible field should be sent
f"field_{visible_field.id}": "Visible",
},
"rows": [
{
"id": row.id,
"order": "1.00000000000000000000",
# Only the visible field should be sent
f"field_{visible_field.id}": "Visible",
}
],
"metadata": {},
"before_row_id": None,
},
@ -1900,20 +1926,24 @@ def test_given_row_visible_in_public_view_when_moved_row_updated_sent(
{
# The row should appear as a deleted event as for the public view
# it effectively has been.
"type": "row_updated",
"type": "rows_updated",
"table_id": PUBLIC_PLACEHOLDER_ENTITY_ID,
"row_before_update": {
"id": visible_moving_row.id,
"order": "1.00000000000000000000",
# Only the visible field should be sent
f"field_{visible_field.id}": "Visible",
},
"row": {
"id": visible_moving_row.id,
"order": "0.99999999999999999999",
# Only the visible field should be sent
f"field_{visible_field.id}": "Visible",
},
"rows_before_update": [
{
"id": visible_moving_row.id,
"order": "1.00000000000000000000",
# Only the visible field should be sent
f"field_{visible_field.id}": "Visible",
}
],
"rows": [
{
"id": visible_moving_row.id,
"order": "0.99999999999999999999",
# Only the visible field should be sent
f"field_{visible_field.id}": "Visible",
}
],
"metadata": {},
},
None,

View file

@ -28,11 +28,11 @@ def test_row_created(mock_broadcast_to_channel_group, data_fixture):
mock_broadcast_to_channel_group.delay.assert_called_once()
args = mock_broadcast_to_channel_group.delay.call_args
assert args[0][0] == f"table-{table.id}"
assert args[0][1]["type"] == "row_created"
assert args[0][1]["type"] == "rows_created"
assert args[0][1]["table_id"] == table.id
assert args[0][1]["row"]["id"] == row.id
assert args[0][1]["rows"][0]["id"] == row.id
assert args[0][1]["before_row_id"] is None
assert args[0][1]["row"][f"field_{field.id}"] == "Test"
assert args[0][1]["rows"][0][f"field_{field.id}"] == "Test"
assert args[0][1]["metadata"] == {}
row_2 = RowHandler().create_row(
@ -40,11 +40,11 @@ def test_row_created(mock_broadcast_to_channel_group, data_fixture):
)
args = mock_broadcast_to_channel_group.delay.call_args
assert args[0][0] == f"table-{table.id}"
assert args[0][1]["type"] == "row_created"
assert args[0][1]["type"] == "rows_created"
assert args[0][1]["table_id"] == table.id
assert args[0][1]["row"]["id"] == row_2.id
assert args[0][1]["rows"][0]["id"] == row_2.id
assert args[0][1]["before_row_id"] == row.id
assert args[0][1]["row"][f"field_{field.id}"] == "Test2"
assert args[0][1]["rows"][0][f"field_{field.id}"] == "Test2"
assert args[0][1]["metadata"] == {}
@ -64,12 +64,12 @@ def test_row_created_with_metadata(mock_broadcast_to_channel_group, data_fixture
mock_broadcast_to_channel_group.delay.assert_called_once()
args = mock_broadcast_to_channel_group.delay.call_args
assert args[0][0] == f"table-{table.id}"
assert args[0][1]["type"] == "row_created"
assert args[0][1]["type"] == "rows_created"
assert args[0][1]["table_id"] == table.id
assert args[0][1]["row"]["id"] == row.id
assert args[0][1]["rows"][0]["id"] == row.id
assert args[0][1]["before_row_id"] is None
assert args[0][1]["row"][f"field_{field.id}"] == "Test"
assert args[0][1]["metadata"] == {"row_id": row.id}
assert args[0][1]["rows"][0][f"field_{field.id}"] == "Test"
assert args[0][1]["metadata"] == {1: {"row_id": row.id}}
def test_populates_with_row_id_metadata():
@ -102,14 +102,14 @@ def test_row_updated(mock_broadcast_to_channel_group, data_fixture):
mock_broadcast_to_channel_group.delay.assert_called_once()
args = mock_broadcast_to_channel_group.delay.call_args
assert args[0][0] == f"table-{table.id}"
assert args[0][1]["type"] == "row_updated"
assert args[0][1]["type"] == "rows_updated"
assert args[0][1]["table_id"] == table.id
assert args[0][1]["row_before_update"]["id"] == row.id
assert args[0][1]["row_before_update"][f"field_{field.id}"] is None
assert args[0][1]["row_before_update"][f"field_{field_2.id}"] is None
assert args[0][1]["row"]["id"] == row.id
assert args[0][1]["row"][f"field_{field.id}"] == "Test"
assert args[0][1]["row"][f"field_{field_2.id}"] is None
assert args[0][1]["rows_before_update"][0]["id"] == row.id
assert args[0][1]["rows_before_update"][0][f"field_{field.id}"] is None
assert args[0][1]["rows_before_update"][0][f"field_{field_2.id}"] is None
assert args[0][1]["rows"][0]["id"] == row.id
assert args[0][1]["rows"][0][f"field_{field.id}"] == "Test"
assert args[0][1]["rows"][0][f"field_{field_2.id}"] is None
assert args[0][1]["metadata"] == {}
row.refresh_from_db()
@ -121,11 +121,11 @@ def test_row_updated(mock_broadcast_to_channel_group, data_fixture):
args = mock_broadcast_to_channel_group.delay.call_args
assert args[0][0] == f"table-{table.id}"
assert args[0][1]["type"] == "row_updated"
assert args[0][1]["type"] == "rows_updated"
assert args[0][1]["table_id"] == table.id
assert args[0][1]["row"]["id"] == row.id
assert args[0][1]["row"][f"field_{field.id}"] == "First"
assert args[0][1]["row"][f"field_{field_2.id}"] == "Second"
assert args[0][1]["rows"][0]["id"] == row.id
assert args[0][1]["rows"][0][f"field_{field.id}"] == "First"
assert args[0][1]["rows"][0][f"field_{field_2.id}"] == "Second"
assert args[0][1]["metadata"] == {}
@ -148,15 +148,15 @@ def test_row_updated_with_metadata(mock_broadcast_to_channel_group, data_fixture
mock_broadcast_to_channel_group.delay.assert_called_once()
args = mock_broadcast_to_channel_group.delay.call_args
assert args[0][0] == f"table-{table.id}"
assert args[0][1]["type"] == "row_updated"
assert args[0][1]["type"] == "rows_updated"
assert args[0][1]["table_id"] == table.id
assert args[0][1]["row_before_update"]["id"] == row.id
assert args[0][1]["row_before_update"][f"field_{field.id}"] is None
assert args[0][1]["row_before_update"][f"field_{field_2.id}"] is None
assert args[0][1]["row"]["id"] == row.id
assert args[0][1]["row"][f"field_{field.id}"] == "Test"
assert args[0][1]["row"][f"field_{field_2.id}"] is None
assert args[0][1]["metadata"] == {"row_id": row.id}
assert args[0][1]["rows_before_update"][0]["id"] == row.id
assert args[0][1]["rows_before_update"][0][f"field_{field.id}"] is None
assert args[0][1]["rows_before_update"][0][f"field_{field_2.id}"] is None
assert args[0][1]["rows"][0]["id"] == row.id
assert args[0][1]["rows"][0][f"field_{field.id}"] == "Test"
assert args[0][1]["rows"][0][f"field_{field_2.id}"] is None
assert args[0][1]["metadata"] == {1: {"row_id": row.id}}
@pytest.mark.django_db(transaction=True)
@ -174,8 +174,8 @@ def test_row_deleted(mock_broadcast_to_channel_group, data_fixture):
mock_broadcast_to_channel_group.delay.assert_called_once()
args = mock_broadcast_to_channel_group.delay.call_args
assert args[0][0] == f"table-{table.id}"
assert args[0][1]["type"] == "row_deleted"
assert args[0][1]["row_id"] == row_id
assert args[0][1]["type"] == "rows_deleted"
assert args[0][1]["row_ids"] == [row_id]
assert args[0][1]["table_id"] == table.id
assert args[0][1]["row"]["id"] == row_id
assert args[0][1]["row"][f"field_{field.id}"] == "Value"
assert args[0][1]["rows"][0]["id"] == row_id
assert args[0][1]["rows"][0][f"field_{field.id}"] == "Value"

View file

@ -24,6 +24,12 @@ For example:
### Breaking Changes
* **breaking change** Webhooks `row.created`, `row.updated` and `row.deleted` are
replaced with `rows.created`, `rows.updated` and `rows.deleted`, containing multiple
changed rows at once. Already created webhooks will still be called, but the received
body will contain only the first changed row instead of all rows. It is highly
recommended to convert all webhooks to the new types.
## Released (2022-07-05 1.10.2)
@ -75,7 +81,6 @@ For example:
* Fix get_human_readable_value crashing for some formula types. [#1042](https://gitlab.com/bramw/baserow/-/issues/1042)
* Fix import form that gets stuck in a spinning state when it hits an error.
### Breaking Changes

View file

@ -145,13 +145,9 @@ are subscribed to the page.
* `field_updated`
* `field_deleted`
* `field_restored`
* `row_created`
* `rows_created`
* `row_updated`
* `rows_updated`
* `row_deleted`
* `before_row_update`
* `before_row_delete`
* `rows_deleted`
* `before_rows_update`
* `before_rows_delete`
* `view_created`

View file

@ -175,8 +175,11 @@
},
"eventType": {
"rowCreated": "When a row is created",
"rowsCreated": "Rows are created",
"rowUpdated": "When a row is updated",
"rowDeleted": "When a row is deleted"
"rowsUpdated": "Rows are updated",
"rowDeleted": "When a row is deleted",
"rowsDeleted": "Rows are deleted"
}
},
"clientHandler": {

View file

@ -306,3 +306,8 @@
color: $color-error-500;
}
}
.webhook__convert {
display: block;
margin-top: 20px;
}

View file

@ -1,221 +1,248 @@
<template>
<form @submit.prevent="submit" @input="$emit('formchange')">
<div
v-if="!values.active"
class="alert alert--simple alert--primary alert--has-icon"
>
<div class="alert__icon">
<i class="fas fa-exclamation"></i>
</div>
<div class="alert__title">{{ $t('webhookForm.deactivated.title') }}</div>
<p class="alert__content">
{{ $t('webhookForm.deactivated.content') }}
</p>
<a
class="button button--ghost margin-top-1"
@click="values.active = true"
>{{ $t('webhookForm.deactivated.activate') }}</a
<div v-if="!isDeprecated">
<div
v-if="!values.active"
class="alert alert--simple alert--primary alert--has-icon"
>
</div>
<div class="row">
<div class="col col-12">
<FormElement :error="fieldHasErrors('name')" class="control">
<label class="control__label">
{{ $t('webhookForm.inputLabels.name') }}
</label>
<div class="control__elements">
<input
v-model="values.name"
class="input"
:class="{ 'input--error': fieldHasErrors('name') }"
@blur="$v.values.name.$touch()"
/>
<div v-if="fieldHasErrors('name')" class="error">
{{ $t('error.requiredField') }}
</div>
</div>
</FormElement>
</div>
<div class="col col-12">
<div class="control">
<label class="control__label">
{{ $t('webhookForm.inputLabels.userFieldNames') }}
</label>
<div class="control__elements">
<Checkbox v-model="values.use_user_field_names">{{
$t('webhookForm.checkbox.sendUserFieldNames')
}}</Checkbox>
</div>
<div class="alert__icon">
<i class="fas fa-exclamation"></i>
</div>
</div>
<div class="col col-4">
<div class="control">
<div class="control__label">
{{ $t('webhookForm.inputLabels.requestMethod') }}
</div>
<div class="control__elements">
<Dropdown v-model="values.request_method">
<DropdownItem name="GET" value="GET"></DropdownItem>
<DropdownItem name="POST" value="POST"></DropdownItem>
<DropdownItem name="PATCH" value="PATCH"></DropdownItem>
<DropdownItem name="PUT" value="PUT"></DropdownItem>
<DropdownItem name="DELETE" value="DELETE"></DropdownItem>
</Dropdown>
</div>
<div class="alert__title">
{{ $t('webhookForm.deactivated.title') }}
</div>
</div>
<div class="col col-8">
<FormElement :error="fieldHasErrors('url')" class="control">
<label class="control__label">
{{ $t('webhookForm.inputLabels.url') }}
</label>
<div class="control__elements">
<input
v-model="values.url"
:placeholder="$t('webhookForm.inputLabels.url')"
class="input"
:class="{ 'input--error': fieldHasErrors('url') }"
@blur="$v.values.url.$touch()"
/>
<div
v-if="
fieldHasErrors('url') &&
(!$v.values.url.required || !$v.values.url.url)
"
class="error"
>
{{ $t('webhookForm.errors.urlField') }}
</div>
<div
v-else-if="$v.values.url.$error && !$v.values.url.maxLength"
class="error"
>
{{
$t('error.maxLength', {
max: $v.values.url.$params.maxLength.max,
})
}}
</div>
</div>
</FormElement>
</div>
</div>
<div class="control">
<label class="control__label">
{{ $t('webhookForm.inputLabels.events') }}
</label>
<div class="control__elements">
<Radio v-model="values.include_all_events" :value="true">{{
$t('webhookForm.radio.allEvents')
}}</Radio>
<Radio v-model="values.include_all_events" :value="false">
{{ $t('webhookForm.radio.customEvents') }}
</Radio>
<div v-if="!values.include_all_events" class="webhook__types">
<Checkbox
v-for="webhookEvent in webhookEventTypes"
:key="webhookEvent.type"
:value="values.events.includes(webhookEvent.type)"
class="webhook__type"
@input="
$event
? values.events.push(webhookEvent.type)
: values.events.splice(
values.events.indexOf(webhookEvent.type),
1
)
"
>{{ webhookEvent.getName() }}</Checkbox
>
</div>
</div>
</div>
<div class="control">
<div class="control__label">
{{ $t('webhookForm.inputLabels.headers') }}
</div>
<div class="control__elements">
<div
v-for="(header, index) in headers.concat({
name: '',
value: '',
})"
:key="`header-input-${index}`"
class="webhook__header"
<p class="alert__content">
{{ $t('webhookForm.deactivated.content') }}
</p>
<a
class="button button--ghost margin-top-1"
@click="values.active = true"
>{{ $t('webhookForm.deactivated.activate') }}</a
>
<div class="webhook__header-row">
<input
v-model="header.name"
class="input webhook__header-key"
:class="{
'input--error':
!lastHeader(index) && $v.headers.$each[index].name.$error,
}"
:placeholder="$t('webhookForm.inputLabels.name')"
@input="lastHeader(index) && addHeader(header.name, header.value)"
@blur="
!lastHeader(index) && $v.headers.$each[index].name.$touch()
"
/>
<input
v-model="header.value"
class="input webhook__header-value"
:class="{
'input--error':
!lastHeader(index) && $v.headers.$each[index].value.$error,
}"
:placeholder="$t('webhookForm.inputLabels.value')"
@input="lastHeader(index) && addHeader(header.name, header.value)"
@blur="
!lastHeader(index) && $v.headers.$each[index].value.$touch()
"
/>
<a
v-if="!lastHeader(index)"
class="button button--error webhook__header-delete"
@click="removeHeader(index)"
>
<i class="fas fa-trash button__icon"></i>
</a>
</div>
<div class="row">
<div class="col col-12">
<FormElement :error="fieldHasErrors('name')" class="control">
<label class="control__label">
{{ $t('webhookForm.inputLabels.name') }}
</label>
<div class="control__elements">
<input
v-model="values.name"
class="input"
:class="{ 'input--error': fieldHasErrors('name') }"
@blur="$v.values.name.$touch()"
/>
<div v-if="fieldHasErrors('name')" class="error">
{{ $t('error.requiredField') }}
</div>
</div>
</FormElement>
</div>
<div class="col col-12">
<div class="control">
<label class="control__label">
{{ $t('webhookForm.inputLabels.userFieldNames') }}
</label>
<div class="control__elements">
<Checkbox v-model="values.use_user_field_names">{{
$t('webhookForm.checkbox.sendUserFieldNames')
}}</Checkbox>
</div>
</div>
</div>
<div v-if="$v.headers.$anyError" class="error">
{{ $t('webhookForm.errors.invalidHeaders') }}
<div class="col col-4">
<div class="control">
<div class="control__label">
{{ $t('webhookForm.inputLabels.requestMethod') }}
</div>
<div class="control__elements">
<Dropdown v-model="values.request_method">
<DropdownItem name="GET" value="GET"></DropdownItem>
<DropdownItem name="POST" value="POST"></DropdownItem>
<DropdownItem name="PATCH" value="PATCH"></DropdownItem>
<DropdownItem name="PUT" value="PUT"></DropdownItem>
<DropdownItem name="DELETE" value="DELETE"></DropdownItem>
</Dropdown>
</div>
</div>
</div>
<div class="col col-8">
<FormElement :error="fieldHasErrors('url')" class="control">
<label class="control__label">
{{ $t('webhookForm.inputLabels.url') }}
</label>
<div class="control__elements">
<input
v-model="values.url"
:placeholder="$t('webhookForm.inputLabels.url')"
class="input"
:class="{ 'input--error': fieldHasErrors('url') }"
@blur="$v.values.url.$touch()"
/>
<div
v-if="
fieldHasErrors('url') &&
(!$v.values.url.required || !$v.values.url.url)
"
class="error"
>
{{ $t('webhookForm.errors.urlField') }}
</div>
<div
v-else-if="$v.values.url.$error && !$v.values.url.maxLength"
class="error"
>
{{
$t('error.maxLength', {
max: $v.values.url.$params.maxLength.max,
})
}}
</div>
</div>
</FormElement>
</div>
</div>
<div class="control">
<label class="control__label">
{{ $t('webhookForm.inputLabels.events') }}
</label>
<div class="control__elements">
<Radio v-model="values.include_all_events" :value="true">{{
$t('webhookForm.radio.allEvents')
}}</Radio>
<Radio v-model="values.include_all_events" :value="false">
{{ $t('webhookForm.radio.customEvents') }}
</Radio>
<div v-if="!values.include_all_events" class="webhook__types">
<Checkbox
v-for="webhookEvent in webhookEventTypes"
:key="webhookEvent.type"
:value="values.events.includes(webhookEvent.type)"
class="webhook__type"
@input="
$event
? values.events.push(webhookEvent.type)
: values.events.splice(
values.events.indexOf(webhookEvent.type),
1
)
"
>{{ webhookEvent.getName() }}</Checkbox
>
</div>
</div>
</div>
<div class="control">
<div class="control__label">
{{ $t('webhookForm.inputLabels.headers') }}
</div>
<div class="control__elements">
<div
v-for="(header, index) in headers.concat({
name: '',
value: '',
})"
:key="`header-input-${index}`"
class="webhook__header"
>
<div class="webhook__header-row">
<input
v-model="header.name"
class="input webhook__header-key"
:class="{
'input--error':
!lastHeader(index) && $v.headers.$each[index].name.$error,
}"
:placeholder="$t('webhookForm.inputLabels.name')"
@input="
lastHeader(index) && addHeader(header.name, header.value)
"
@blur="
!lastHeader(index) && $v.headers.$each[index].name.$touch()
"
/>
<input
v-model="header.value"
class="input webhook__header-value"
:class="{
'input--error':
!lastHeader(index) && $v.headers.$each[index].value.$error,
}"
:placeholder="$t('webhookForm.inputLabels.value')"
@input="
lastHeader(index) && addHeader(header.name, header.value)
"
@blur="
!lastHeader(index) && $v.headers.$each[index].value.$touch()
"
/>
<a
v-if="!lastHeader(index)"
class="button button--error webhook__header-delete"
@click="removeHeader(index)"
>
<i class="fas fa-trash button__icon"></i>
</a>
</div>
</div>
<div v-if="$v.headers.$anyError" class="error">
{{ $t('webhookForm.errors.invalidHeaders') }}
</div>
</div>
</div>
<div class="control">
<div class="control__label">
{{ $t('webhookForm.inputLabels.example') }}
</div>
<div class="control__elements">
<div class="webhook__code-with-dropdown">
<div class="webhook__code-dropdown">
<Dropdown
v-model="exampleWebhookEventType"
class="dropdown--floating-left"
>
<DropdownItem
v-for="webhookEvent in webhookEventTypes"
:key="webhookEvent.type"
:name="webhookEvent.getName()"
:value="webhookEvent.type"
></DropdownItem>
</Dropdown>
</div>
<div class="webhook__code-container">
<pre
class="webhook__code"
><code>{{ JSON.stringify(testExample, null, 4)}}</code></pre>
</div>
</div>
</div>
</div>
<a class="button button--ghost" @click="openTestModal()">{{
$t('webhookForm.triggerButton')
}}</a>
<slot></slot>
<TestWebhookModal ref="testModal" />
</div>
<div v-else>
<div class="alert alert--error alert--has-icon">
<div class="alert__icon">
<i class="fas fa-exclamation"></i>
</div>
<div class="alert__title">
{{ $t('webhookForm.deprecatedEventType.title') }}
</div>
<p class="alert__content">
{{ $t('webhookForm.deprecatedEventType.description') }}
<button
class="button webhook__convert"
@click="convertFromDeprecated"
>
{{ $t('webhookForm.deprecatedEventType.convert') }}
</button>
</p>
</div>
</div>
<div class="control">
<div class="control__label">
{{ $t('webhookForm.inputLabels.example') }}
</div>
<div class="control__elements">
<div class="webhook__code-with-dropdown">
<div class="webhook__code-dropdown">
<Dropdown
v-model="exampleWebhookEventType"
class="dropdown--floating-left"
>
<DropdownItem
v-for="webhookEvent in webhookEventTypes"
:key="webhookEvent.type"
:name="webhookEvent.getName()"
:value="webhookEvent.type"
></DropdownItem>
</Dropdown>
</div>
<div class="webhook__code-container">
<pre
class="webhook__code"
><code>{{ JSON.stringify(testExample, null, 4)}}</code></pre>
</div>
</div>
</div>
</div>
<a class="button button--ghost" @click="openTestModal()">{{
$t('webhookForm.triggerButton')
}}</a>
<slot></slot>
<TestWebhookModal ref="testModal" />
</form>
</template>
@ -272,6 +299,11 @@ export default {
webhookEventTypes() {
return this.$registry.getAll('webhookEvent')
},
isDeprecated() {
return this.values.events.some((eventName) =>
['row.created', 'row.updated', 'row.deleted'].includes(eventName)
)
},
/**
* Generates an example payload of the webhook event based on the chosen webhook
* event type.
@ -376,6 +408,12 @@ export default {
lastHeader(index) {
return index === this.headers.length
},
convertFromDeprecated() {
this.values.events = this.values.events.map((eventName) =>
eventName.replace('row.', 'rows.')
)
this.submit()
},
},
}
</script>

View file

@ -43,6 +43,11 @@
"title": "Webhook is deactivated",
"content": "This webhook has been deactivated because there have been too many consecutive failures. Please check the call log for more details. Click on the button below to activate it again. Don't forgot to save the webhook after activating.",
"activate": "Activate"
},
"deprecatedEventType": {
"title": "Deprecated event type",
"description": "This webhook doesn't receive information about all changed rows at once. Please convert it to batch-style event type. This changes the JSON body payload to a format that contains multiple rows.",
"convert": "Convert"
}
},
"webhook": {

View file

@ -64,9 +64,9 @@ import {
JSONImporterType,
} from '@baserow/modules/database/importerTypes'
import {
RowCreatedWebhookEventType,
RowUpdatedWebhookEventType,
RowDeletedWebhookEventType,
RowsCreatedWebhookEventType,
RowsUpdatedWebhookEventType,
RowsDeletedWebhookEventType,
} from '@baserow/modules/database/webhookEventTypes'
import {
ImageFilePreview,
@ -321,15 +321,15 @@ export default (context) => {
app.$registry.register('exporter', new CSVTableExporterType(context))
app.$registry.register(
'webhookEvent',
new RowCreatedWebhookEventType(context)
new RowsCreatedWebhookEventType(context)
)
app.$registry.register(
'webhookEvent',
new RowUpdatedWebhookEventType(context)
new RowsUpdatedWebhookEventType(context)
)
app.$registry.register(
'webhookEvent',
new RowDeletedWebhookEventType(context)
new RowsDeletedWebhookEventType(context)
)
// Text functions

View file

@ -155,21 +155,6 @@ export const registerRealtimeEvents = (realtime) => {
}
})
realtime.registerEvent('row_created', (context, data) => {
const { app, store } = context
for (const viewType of Object.values(app.$registry.getAll('view'))) {
viewType.rowCreated(
context,
data.table_id,
store.getters['field/getAll'],
store.getters['field/getPrimary'],
data.row,
data.metadata,
'page/'
)
}
})
realtime.registerEvent('rows_created', (context, data) => {
const { app, store } = context
for (const viewType of Object.values(app.$registry.getAll('view'))) {
@ -187,30 +172,7 @@ export const registerRealtimeEvents = (realtime) => {
}
})
realtime.registerEvent('row_updated', async (context, data) => {
const { app, store } = context
for (const viewType of Object.values(app.$registry.getAll('view'))) {
await viewType.rowUpdated(
context,
data.table_id,
store.getters['field/getAll'],
store.getters['field/getPrimary'],
data.row_before_update,
data.row,
data.metadata,
'page/'
)
}
store.dispatch('rowModal/updated', {
tableId: data.table_id,
values: data.row,
})
})
realtime.registerEvent('rows_updated', async (context, data) => {
// TODO: Rewrite
// This is currently a naive implementation of batch rows updates.
const { app, store } = context
for (const viewType of Object.values(app.$registry.getAll('view'))) {
for (let i = 0; i < data.rows.length; i++) {
@ -234,20 +196,6 @@ export const registerRealtimeEvents = (realtime) => {
}
})
realtime.registerEvent('row_deleted', (context, data) => {
const { app, store } = context
for (const viewType of Object.values(app.$registry.getAll('view'))) {
viewType.rowDeleted(
context,
data.table_id,
store.getters['field/getAll'],
store.getters['field/getPrimary'],
data.row,
'page/'
)
}
})
realtime.registerEvent('rows_deleted', (context, data) => {
const { app, store } = context
for (const viewType of Object.values(app.$registry.getAll('view'))) {

View file

@ -32,56 +32,54 @@ export class WebhookEventType extends Registerable {
}
}
export class RowCreatedWebhookEventType extends WebhookEventType {
export class RowsCreatedWebhookEventType extends WebhookEventType {
getType() {
return 'row.created'
return 'rows.created'
}
getName() {
const { i18n } = this.app
return i18n.t('webhook.eventType.rowCreated')
return i18n.t('webhook.eventType.rowsCreated')
}
getExamplePayload(table, rowExample) {
const payload = super.getExamplePayload(table, rowExample)
payload.row_id = rowExample.id
payload.values = rowExample
payload.items = [rowExample]
return payload
}
}
export class RowUpdatedWebhookEventType extends WebhookEventType {
export class RowsUpdatedWebhookEventType extends WebhookEventType {
getType() {
return 'row.updated'
return 'rows.updated'
}
getName() {
const { i18n } = this.app
return i18n.t('webhook.eventType.rowUpdated')
return i18n.t('webhook.eventType.rowsUpdated')
}
getExamplePayload(table, rowExample) {
const payload = super.getExamplePayload(table, rowExample)
payload.row_id = rowExample.id
payload.values = rowExample
payload.old_values = rowExample
payload.items = [rowExample]
payload.old_items = [rowExample]
return payload
}
}
export class RowDeletedWebhookEventType extends WebhookEventType {
export class RowsDeletedWebhookEventType extends WebhookEventType {
getType() {
return 'row.deleted'
return 'rows.deleted'
}
getName() {
const { i18n } = this.app
return i18n.t('webhook.eventType.rowDeleted')
return i18n.t('webhook.eventType.rowsDeleted')
}
getExamplePayload(table, rowExample) {
const payload = super.getExamplePayload(table, rowExample)
payload.row_id = rowExample.id
payload.row_ids = [rowExample.id]
return payload
}
}