1
0
Fork 0
mirror of https://gitlab.com/bramw/baserow.git synced 2025-04-07 22:35:36 +00:00

Prevent triggering webhook if query param is provided on row create, update, or delete endpoints

This commit is contained in:
Przemyslaw Kukulski 2024-11-19 19:33:32 +00:00 committed by Bram Wiepjes
parent 27e24b7a8b
commit 851826141e
13 changed files with 507 additions and 10 deletions
backend
src/baserow/contrib/database
tests/baserow/contrib/database/api
changelog/entries/unreleased/feature
web-frontend

View file

@ -54,6 +54,7 @@ from baserow.contrib.database.api.tokens.authentications import TokenAuthenticat
from baserow.contrib.database.api.tokens.errors import ERROR_NO_PERMISSION_TO_TABLE
from baserow.contrib.database.api.utils import (
extract_link_row_joins_from_request,
extract_send_webhook_events_from_params,
extract_user_field_names_from_params,
get_include_exclude_fields,
)
@ -445,6 +446,16 @@ class RowsView(APIView):
"field names (e.g., field_123)."
),
),
OpenApiParameter(
name="send_webhook_events",
location=OpenApiParameter.QUERY,
type=OpenApiTypes.BOOL,
description=(
"A flag query parameter that triggers webhooks after the operation,"
" if set to `y`, `yes`, `true`, `t`, `on`, `1`, `or` left empty. "
"Defaults to `true`"
),
),
CLIENT_SESSION_ID_SCHEMA_PARAMETER,
CLIENT_UNDO_REDO_ACTION_GROUP_ID_SCHEMA_PARAMETER,
],
@ -514,6 +525,7 @@ class RowsView(APIView):
)
user_field_names = extract_user_field_names_from_params(request.GET)
send_webhook_events = extract_send_webhook_events_from_params(request.GET)
model = table.get_model()
@ -537,6 +549,7 @@ class RowsView(APIView):
model=model,
before_row=before_row,
user_field_names=user_field_names,
send_webhook_events=send_webhook_events,
)
except ValidationError as e:
raise RequestBodyValidationException(detail=e.message)
@ -771,6 +784,16 @@ class RowView(APIView):
"field names (e.g., field_123)."
),
),
OpenApiParameter(
name="send_webhook_events",
location=OpenApiParameter.QUERY,
type=OpenApiTypes.BOOL,
description=(
"A flag query parameter that triggers webhooks after the operation,"
" if set to `y`, `yes`, `true`, `t`, `on`, `1`, `or` left empty. "
"Defaults to `true`"
),
),
CLIENT_SESSION_ID_SCHEMA_PARAMETER,
CLIENT_UNDO_REDO_ACTION_GROUP_ID_SCHEMA_PARAMETER,
],
@ -833,6 +856,7 @@ class RowView(APIView):
TokenHandler().check_table_permissions(request, "update", table, False)
user_field_names = extract_user_field_names_from_params(request.GET)
send_webhook_events = extract_send_webhook_events_from_params(request.GET)
field_ids, field_names = None, None
if user_field_names:
@ -852,7 +876,11 @@ class RowView(APIView):
try:
data["id"] = int(row_id)
row = action_type_registry.get_by_type(UpdateRowsActionType).do(
request.user, table, [data], model
request.user,
table,
[data],
model=model,
send_webhook_events=send_webhook_events,
)[0]
except ValidationError as exc:
raise RequestBodyValidationException(detail=exc.message) from exc
@ -877,6 +905,16 @@ class RowView(APIView):
type=OpenApiTypes.INT,
description="Deletes the row related to the value.",
),
OpenApiParameter(
name="send_webhook_events",
location=OpenApiParameter.QUERY,
type=OpenApiTypes.BOOL,
description=(
"A flag query parameter that triggers webhooks after the operation,"
" if set to `y`, `yes`, `true`, `t`, `on`, `1`, `or` left empty. "
"Defaults to `true`"
),
),
CLIENT_SESSION_ID_SCHEMA_PARAMETER,
CLIENT_UNDO_REDO_ACTION_GROUP_ID_SCHEMA_PARAMETER,
],
@ -913,11 +951,13 @@ class RowView(APIView):
table_id.
"""
send_webhook_events = extract_send_webhook_events_from_params(request.GET)
table = TableHandler().get_table(table_id)
TokenHandler().check_table_permissions(request, "delete", table, False)
action_type_registry.get_by_type(DeleteRowActionType).do(
request.user, table, row_id
request.user, table, row_id, send_webhook_events=send_webhook_events
)
return Response(status=204)
@ -961,6 +1001,16 @@ class RowMoveView(APIView):
"field names (e.g., field_123)."
),
),
OpenApiParameter(
name="send_webhook_events",
location=OpenApiParameter.QUERY,
type=OpenApiTypes.BOOL,
description=(
"A flag query parameter that triggers webhooks after the operation,"
" if set to `y`, `yes`, `true`, `t`, `on`, `1`, `or` left empty. "
"Defaults to `true`"
),
),
CLIENT_SESSION_ID_SCHEMA_PARAMETER,
CLIENT_UNDO_REDO_ACTION_GROUP_ID_SCHEMA_PARAMETER,
],
@ -1001,6 +1051,7 @@ class RowMoveView(APIView):
TokenHandler().check_table_permissions(request, "update", table, False)
user_field_names = extract_user_field_names_from_params(request.GET)
send_webhook_events = extract_send_webhook_events_from_params(request.GET)
model = table.get_model()
@ -1014,7 +1065,12 @@ class RowMoveView(APIView):
)
row = action_type_registry.get_by_type(MoveRowActionType).do(
request.user, table, row_id, before_row=before_row, model=model
request.user,
table,
row_id,
before_row=before_row,
model=model,
send_webhook_events=send_webhook_events,
)
serializer_class = get_row_serializer_class(
@ -1055,6 +1111,16 @@ class BatchRowsView(APIView):
"field names (e.g., field_123)."
),
),
OpenApiParameter(
name="send_webhook_events",
location=OpenApiParameter.QUERY,
type=OpenApiTypes.BOOL,
description=(
"A flag query parameter that triggers webhooks after the operation,"
" if set to `y`, `yes`, `true`, `t`, `on`, `1`, `or` left empty. "
"Defaults to `true`"
),
),
CLIENT_SESSION_ID_SCHEMA_PARAMETER,
CLIENT_UNDO_REDO_ACTION_GROUP_ID_SCHEMA_PARAMETER,
],
@ -1120,6 +1186,7 @@ class BatchRowsView(APIView):
model = table.get_model()
user_field_names = extract_user_field_names_from_params(request.GET)
send_webhook_events = extract_send_webhook_events_from_params(request.GET)
before_id = query_params.get("before")
before_row = (
RowHandler().get_row(request.user, table, before_id, model)
@ -1139,7 +1206,12 @@ class BatchRowsView(APIView):
try:
rows = action_type_registry.get_by_type(CreateRowsActionType).do(
request.user, table, data["items"], before_row, model
request.user,
table,
data["items"],
before_row,
model=model,
send_webhook_events=send_webhook_events,
)
except ValidationError as exc:
raise RequestBodyValidationException(detail=exc.message)
@ -1173,6 +1245,16 @@ class BatchRowsView(APIView):
"field names (e.g., field_123)."
),
),
OpenApiParameter(
name="send_webhook_events",
location=OpenApiParameter.QUERY,
type=OpenApiTypes.BOOL,
description=(
"A flag query parameter that triggers webhooks after the operation,"
" if set to `y`, `yes`, `true`, `t`, `on`, `1`, `or` left empty. "
"Defaults to `true`"
),
),
CLIENT_SESSION_ID_SCHEMA_PARAMETER,
CLIENT_UNDO_REDO_ACTION_GROUP_ID_SCHEMA_PARAMETER,
],
@ -1237,6 +1319,7 @@ class BatchRowsView(APIView):
model = table.get_model()
user_field_names = extract_user_field_names_from_params(request.GET)
send_webhook_events = extract_send_webhook_events_from_params(request.GET)
row_validation_serializer = get_row_serializer_class(
model,
@ -1253,7 +1336,11 @@ class BatchRowsView(APIView):
try:
rows = action_type_registry.get_by_type(UpdateRowsActionType).do(
request.user, table, data["items"], model
request.user,
table,
data["items"],
model=model,
send_webhook_events=send_webhook_events,
)
except ValidationError as e:
raise RequestBodyValidationException(detail=e.message)
@ -1280,6 +1367,16 @@ class BatchDeleteRowsView(APIView):
type=OpenApiTypes.INT,
description="Deletes the rows in the table related to the value.",
),
OpenApiParameter(
name="send_webhook_events",
location=OpenApiParameter.QUERY,
type=OpenApiTypes.BOOL,
description=(
"A flag query parameter that triggers webhooks after the operation,"
" if set to `y`, `yes`, `true`, `t`, `on`, `1`, `or` left empty. "
"Defaults to `true`"
),
),
CLIENT_SESSION_ID_SCHEMA_PARAMETER,
CLIENT_UNDO_REDO_ACTION_GROUP_ID_SCHEMA_PARAMETER,
],
@ -1327,10 +1424,13 @@ class BatchDeleteRowsView(APIView):
table = TableHandler().get_table(table_id)
TokenHandler().check_table_permissions(request, "delete", table, False)
send_webhook_events = extract_send_webhook_events_from_params(request.GET)
action_type_registry.get_by_type(DeleteRowsActionType).do(
request.user,
table,
row_ids=data["items"],
send_webhook_events=send_webhook_events,
)
return Response(status=204)

View file

@ -174,6 +174,20 @@ def extract_user_field_names_from_params(query_params):
return str_to_bool(value)
def extract_send_webhook_events_from_params(query_params) -> bool:
"""
Extracts the send_webhook_events parameter from the query_params and returns
boolean value. Defaults to true if not provided or empty.
"""
value = query_params.get("send_webhook_events")
if value is None or value == "":
return True
return str_to_bool(value)
@dataclass
class LinkedTargetField:
field_id: int

View file

@ -58,6 +58,7 @@ class CreateRowActionType(UndoableActionType):
model: Optional[Type[GeneratedTableModel]] = None,
before_row: Optional[GeneratedTableModel] = None,
user_field_names: bool = False,
send_webhook_events: bool = True,
) -> GeneratedTableModel:
"""
Creates a new row for a given table with the provided values if the user
@ -76,6 +77,8 @@ class CreateRowActionType(UndoableActionType):
instance.
:param user_field_names: Whether or not the values are keyed by the internal
Baserow field name (field_1,field_2 etc) or by the user field names.
:param send_webhook_events: If set the false then the webhooks will not be
triggered. Defaults to true.
:return: The created row instance.
"""
@ -91,6 +94,7 @@ class CreateRowActionType(UndoableActionType):
model=model,
before_row=before_row,
user_field_names=user_field_names,
send_webhook_events=send_webhook_events,
)
workspace = table.database.workspace
@ -148,6 +152,7 @@ class CreateRowsActionType(UndoableActionType):
rows_values: List[Dict[str, Any]],
before_row: Optional[GeneratedTableModel] = None,
model: Optional[Type[GeneratedTableModel]] = None,
send_webhook_events: bool = True,
) -> List[GeneratedTableModel]:
"""
Creates rows for a given table with the provided values if the user
@ -163,6 +168,8 @@ class CreateRowsActionType(UndoableActionType):
the row with this id.
:param model: If the correct model has already been generated it can be
provided so that it does not have to be generated for a second time.
:param send_webhook_events: If set the false then the webhooks will not be
triggered. Defaults to true.
:return: The created list of rows instances.
"""
@ -177,6 +184,7 @@ class CreateRowsActionType(UndoableActionType):
rows_values,
before_row=before_row,
model=model,
send_webhook_events=send_webhook_events,
)
workspace = table.database.workspace
@ -327,6 +335,7 @@ class DeleteRowActionType(UndoableActionType):
table: Table,
row_id: int,
model: Optional[Type[GeneratedTableModel]] = None,
send_webhook_events: bool = True,
):
"""
Deletes an existing row of the given table and with row_id.
@ -339,6 +348,8 @@ class DeleteRowActionType(UndoableActionType):
:param row_id: The id of the row that must be deleted.
:param model: If the correct model has already been generated, it can be
provided so that it does not have to be generated for a second time.
:param send_webhook_events: If set the false then the webhooks will not be
triggered. Defaults to true.
:raises RowDoesNotExist: When the row with the provided id does not exist.
"""
@ -347,7 +358,9 @@ class DeleteRowActionType(UndoableActionType):
"Can't delete rows because it has a data sync."
)
RowHandler().delete_row_by_id(user, table, row_id, model=model)
RowHandler().delete_row_by_id(
user, table, row_id, model=model, send_webhook_events=send_webhook_events
)
database = table.database
params = cls.Params(table.id, table.name, database.id, database.name, row_id)
@ -399,6 +412,7 @@ class DeleteRowsActionType(UndoableActionType):
table: Table,
row_ids: List[int],
model: Optional[Type[GeneratedTableModel]] = None,
send_webhook_events: bool = True,
):
"""
Deletes rows of the given table with the given row_ids.
@ -411,6 +425,8 @@ class DeleteRowsActionType(UndoableActionType):
:param row_ids: The id of the row that must be deleted.
:param model: If the correct model has already been generated, it can be
provided so that it does not have to be generated for a second time.
:param send_webhook_events: If set the false then the webhooks will not be
triggered. Defaults to true.
:raises RowDoesNotExist: When the row with the provided id does not exist.
"""
@ -419,7 +435,9 @@ class DeleteRowsActionType(UndoableActionType):
"Can't delete rows because it has a data sync."
)
trashed_rows_entry = RowHandler().delete_rows(user, table, row_ids, model=model)
trashed_rows_entry = RowHandler().delete_rows(
user, table, row_ids, model=model, send_webhook_events=send_webhook_events
)
workspace = table.database.workspace
params = cls.Params(
@ -548,6 +566,7 @@ class MoveRowActionType(UndoableActionType):
row_id: int,
before_row: Optional[GeneratedTableModel] = None,
model: Optional[Type[GeneratedTableModel]] = None,
send_webhook_events: bool = True,
) -> GeneratedTableModelForUpdate:
"""
Moves the row before another row or to the end if no before row is provided.
@ -566,6 +585,8 @@ class MoveRowActionType(UndoableActionType):
instance. Otherwise the row will be moved to the end.
:param model: If the correct model has already been generated, it can be
provided so that it does not have to be generated for a second time.
:param send_webhook_events: If set the false then the webhooks will not be
triggered. Defaults to true.
"""
if model is None:
@ -577,7 +598,12 @@ class MoveRowActionType(UndoableActionType):
original_row_order = row.order
updated_row = row_handler.move_row(
user, table, row, before_row=before_row, model=model
user,
table,
row,
before_row=before_row,
model=model,
send_webhook_events=send_webhook_events,
)
rows_displacement = get_rows_displacement(
@ -762,6 +788,7 @@ class UpdateRowsActionType(UndoableActionType):
table: Table,
rows_values: List[Dict[str, Any]],
model: Optional[Type[GeneratedTableModel]] = None,
send_webhook_events: bool = True,
) -> List[GeneratedTableModelForUpdate]:
"""
Updates field values in batch based on provided rows with the new values.
@ -776,6 +803,8 @@ class UpdateRowsActionType(UndoableActionType):
field ids plus the id of the row.
:param model: If the correct model has already been generated it can be
provided so that it does not have to be generated for a second time.
:param send_webhook_events: If set the false then the webhooks will not be
triggered. Defaults to true.
:return: The updated rows.
"""
@ -786,6 +815,7 @@ class UpdateRowsActionType(UndoableActionType):
table,
rows_values,
model=model,
send_webhook_events=send_webhook_events,
)
updated_rows = result.updated_rows

View file

@ -667,6 +667,7 @@ class RowHandler(metaclass=baserow_trace_methods(tracer)):
before_row: Optional[GeneratedTableModel] = None,
user_field_names: bool = False,
values_already_prepared: bool = False,
send_webhook_events: bool = True,
) -> GeneratedTableModel:
"""
Creates a new row for a given table with the provided values if the user
@ -685,6 +686,8 @@ class RowHandler(metaclass=baserow_trace_methods(tracer)):
:param values_already_prepared: Whether or not the values are already sanitized
and validated for every field and can be used directly by the handler
without any further check.
:param send_webhook_events: If set the false then the webhooks will not be
triggered. Defaults to true.
:return: The created row instance.
"""
@ -706,6 +709,7 @@ class RowHandler(metaclass=baserow_trace_methods(tracer)):
before_row,
user_field_names,
values_already_prepared=values_already_prepared,
send_webhook_events=send_webhook_events,
)
def force_create_row(
@ -717,6 +721,7 @@ class RowHandler(metaclass=baserow_trace_methods(tracer)):
before: Optional[GeneratedTableModel] = None,
user_field_names: bool = False,
values_already_prepared: bool = False,
send_webhook_events: bool = True,
):
"""
Creates a new row for a given table with the provided values.
@ -735,6 +740,8 @@ class RowHandler(metaclass=baserow_trace_methods(tracer)):
:param values_already_prepared: Whether or not the values are already sanitized
and validated for every field and can be used directly by the handler
without any further check.
:param send_webhook_events: If set the false then the webhooks will not be
triggered. Defaults to true.
:return: The created row instance.
:rtype: Model
"""
@ -806,7 +813,7 @@ class RowHandler(metaclass=baserow_trace_methods(tracer)):
table=table,
model=model,
send_realtime_update=True,
send_webhook_events=True,
send_webhook_events=send_webhook_events,
rows_values_refreshed_from_db=False,
m2m_change_tracker=m2m_change_tracker,
)
@ -2028,6 +2035,7 @@ class RowHandler(metaclass=baserow_trace_methods(tracer)):
row: GeneratedTableModelForUpdate,
before_row: Optional[GeneratedTableModel] = None,
model: Optional[Type[GeneratedTableModel]] = None,
send_webhook_events: bool = True,
) -> GeneratedTableModelForUpdate:
"""
Updates the row order value.
@ -2039,6 +2047,8 @@ class RowHandler(metaclass=baserow_trace_methods(tracer)):
instance. Otherwise the row will be moved to the end.
:param model: If the correct model has already been generated, it can be
provided so that it does not have to be generated for a second time.
:param send_webhook_events: If set the false then the webhooks will not be
triggered. Defaults to true.
"""
workspace = table.database.workspace
@ -2090,6 +2100,7 @@ class RowHandler(metaclass=baserow_trace_methods(tracer)):
before_return=before_return,
updated_field_ids=[],
prepared_rows_values=None,
send_webhook_events=send_webhook_events,
)
return row

View file

@ -1,4 +1,5 @@
from decimal import Decimal
from unittest.mock import patch
from django.conf import settings
from django.db import connection
@ -289,6 +290,57 @@ def test_batch_create_rows(api_client, data_fixture):
assert row_2.needs_background_update
@pytest.mark.django_db(transaction=True)
@pytest.mark.api_rows
def test_batch_create_rows_with_disabled_webhook_events(api_client, data_fixture):
user, jwt_token = data_fixture.create_user_and_token()
table = data_fixture.create_database_table(user=user)
text_field = data_fixture.create_text_field(
table=table, order=0, name="Color", text_default="white"
)
number_field = data_fixture.create_number_field(
table=table, order=1, name="Horsepower"
)
boolean_field = data_fixture.create_boolean_field(
table=table, order=2, name="For sale"
)
data_fixture.create_table_webhook(
table=table,
user=user,
request_method="POST",
url="http://localhost",
events=[],
)
url = reverse("api:database:rows:batch", kwargs={"table_id": table.id})
request_body = {
"items": [
{
f"field_{text_field.id}": "green",
f"field_{number_field.id}": 120,
f"field_{boolean_field.id}": True,
},
{
f"field_{text_field.id}": "yellow",
f"field_{number_field.id}": 240,
f"field_{boolean_field.id}": False,
},
]
}
with patch("baserow.contrib.database.webhooks.registries.call_webhook.delay") as m:
response = api_client.post(
f"{url}?send_webhook_events=false",
request_body,
format="json",
HTTP_AUTHORIZATION=f"JWT {jwt_token}",
)
assert response.status_code == HTTP_200_OK
m.assert_not_called()
@pytest.mark.django_db
@pytest.mark.api_rows
def test_batch_create_rows_id_field_ignored(api_client, data_fixture):
@ -1184,6 +1236,62 @@ def test_batch_update_rows(api_client, data_fixture):
assert row_2.needs_background_update
@pytest.mark.django_db(transaction=True)
@pytest.mark.api_rows
def test_batch_update_rows_with_disabled_webhook_events(api_client, data_fixture):
user, jwt_token = data_fixture.create_user_and_token()
table = data_fixture.create_database_table(user=user)
text_field = data_fixture.create_text_field(
table=table, order=0, name="Color", text_default="white"
)
number_field = data_fixture.create_number_field(
table=table, order=1, name="Horsepower"
)
boolean_field = data_fixture.create_boolean_field(
table=table, order=2, name="For sale"
)
model = table.get_model()
row_1 = model.objects.create()
row_2 = model.objects.create()
model.objects.update(needs_background_update=False)
data_fixture.create_table_webhook(
table=table,
user=user,
request_method="POST",
url="http://localhost",
events=[],
)
url = reverse("api:database:rows:batch", kwargs={"table_id": table.id})
request_body = {
"items": [
{
f"id": row_1.id,
f"field_{text_field.id}": "green",
f"field_{number_field.id}": 120,
f"field_{boolean_field.id}": True,
},
{
f"id": row_2.id,
f"field_{text_field.id}": "yellow",
f"field_{number_field.id}": 240,
f"field_{boolean_field.id}": False,
},
]
}
with patch("baserow.contrib.database.webhooks.registries.call_webhook.delay") as m:
response = api_client.patch(
f"{url}?send_webhook_events=false",
request_body,
format="json",
HTTP_AUTHORIZATION=f"JWT {jwt_token}",
)
assert response.status_code == HTTP_200_OK
m.assert_not_called()
@pytest.mark.django_db
@pytest.mark.api_rows
def test_batch_update_rows_last_modified_field(api_client, data_fixture):
@ -2275,3 +2383,34 @@ def test_batch_delete_rows_num_of_queries(api_client, data_fixture):
assert len(delete_one_row_ctx.captured_queries) == len(
delete_multiple_rows_ctx.captured_queries
)
@pytest.mark.django_db(transaction=True)
@pytest.mark.api_rows
def test_batch_delete_rows_disabled_webhook_events(api_client, data_fixture):
user, jwt_token = data_fixture.create_user_and_token()
table = data_fixture.create_database_table(user=user)
model = table.get_model()
row_1 = model.objects.create()
row_2 = model.objects.create()
model.objects.create()
url = reverse("api:database:rows:batch-delete", kwargs={"table_id": table.id})
request_body = {"items": [row_1.id, row_2.id]}
data_fixture.create_table_webhook(
table=table,
user=user,
request_method="POST",
url="http://localhost",
events=[],
)
with patch("baserow.contrib.database.webhooks.registries.call_webhook.delay") as m:
response = api_client.post(
f"{url}?send_webhook_events=false",
request_body,
format="json",
HTTP_AUTHORIZATION=f"JWT {jwt_token}",
)
assert response.status_code == HTTP_204_NO_CONTENT
m.assert_not_called()

View file

@ -1,6 +1,7 @@
import json
from datetime import datetime, timedelta, timezone
from decimal import Decimal
from unittest.mock import patch
from urllib.parse import quote
from django.db import connection
@ -1840,6 +1841,35 @@ def test_create_row(api_client, data_fixture):
}
@pytest.mark.django_db(transaction=True)
def test_create_row_with_disabled_webhook_events(api_client, data_fixture):
user, token = data_fixture.create_user_and_token()
table = data_fixture.create_database_table(user=user)
text_field = data_fixture.create_text_field(
table=table, order=0, name="Color", text_default="white"
)
data_fixture.create_table_webhook(
table=table,
user=user,
request_method="POST",
url="http://localhost",
events=[],
)
url = reverse("api:database:rows:list", kwargs={"table_id": table.id})
with patch("baserow.contrib.database.webhooks.registries.call_webhook.delay") as m:
response = api_client.post(
f"{url}?send_webhook_events=false",
{f"field_{text_field.id}": "Test 1"},
format="json",
HTTP_AUTHORIZATION=f"JWT {token}",
)
assert response.status_code == HTTP_200_OK
m.assert_not_called()
@pytest.mark.django_db
def test_create_row_with_read_only_field(api_client, data_fixture):
user, jwt_token = data_fixture.create_user_and_token()
@ -2290,6 +2320,40 @@ def test_update_row(api_client, data_fixture):
assert getattr(row_2, f"field_{boolean_field.id}") is False
@pytest.mark.django_db(transaction=True)
def test_update_row_with_disabled_webhook_events(api_client, data_fixture):
user, token = data_fixture.create_user_and_token()
table = data_fixture.create_database_table(user=user)
text_field = data_fixture.create_text_field(
table=table, order=0, name="Color", text_default="white"
)
model = table.get_model()
row_1 = model.objects.create()
data_fixture.create_table_webhook(
table=table,
user=user,
request_method="POST",
url="http://localhost",
events=[],
)
url = reverse(
"api:database:rows:item", kwargs={"table_id": table.id, "row_id": row_1.id}
)
with patch("baserow.contrib.database.webhooks.registries.call_webhook.delay") as m:
response = api_client.patch(
f"{url}?send_webhook_events=false",
{f"field_{text_field.id}": "Test 1"},
format="json",
HTTP_AUTHORIZATION=f"JWT {token}",
)
assert response.status_code == HTTP_200_OK
m.assert_not_called()
@pytest.mark.django_db
def test_update_row_with_read_only_field(api_client, data_fixture):
user, jwt_token = data_fixture.create_user_and_token()
@ -2472,6 +2536,40 @@ def test_move_row(api_client, data_fixture):
)
@pytest.mark.django_db(transaction=True)
def test_move_row_with_disabled_webhook_events(api_client, data_fixture):
user, token = data_fixture.create_user_and_token()
table = data_fixture.create_database_table(user=user)
data_fixture.create_text_field(
table=table, order=0, name="Color", text_default="white"
)
model = table.get_model()
row_1 = model.objects.create()
row_2 = model.objects.create()
data_fixture.create_table_webhook(
table=table,
user=user,
request_method="POST",
url="http://localhost",
events=[],
)
url = reverse(
"api:database:rows:move", kwargs={"table_id": table.id, "row_id": row_2.id}
)
with patch("baserow.contrib.database.webhooks.registries.call_webhook.delay") as m:
response = api_client.patch(
f"{url}?before_id={row_1.id}&send_webhook_events=false",
format="json",
HTTP_AUTHORIZATION=f"JWT {token}",
)
assert response.status_code == HTTP_200_OK
m.assert_not_called()
@pytest.mark.django_db
def test_cannot_delete_row_by_id_with_data_sync(api_client, data_fixture):
user, jwt_token = data_fixture.create_user_and_token()
@ -2566,6 +2664,39 @@ def test_delete_row_by_id(api_client, data_fixture):
assert model.objects.count() == 0
@pytest.mark.django_db(transaction=True)
def test_delete_row_by_id_with_disabled_webhook_events(api_client, data_fixture):
user, token = data_fixture.create_user_and_token()
table = data_fixture.create_database_table(user=user)
data_fixture.create_text_field(
table=table, order=0, name="Color", text_default="white"
)
model = table.get_model()
row_1 = model.objects.create()
data_fixture.create_table_webhook(
table=table,
user=user,
request_method="POST",
url="http://localhost",
events=[],
)
url = reverse(
"api:database:rows:item", kwargs={"table_id": table.id, "row_id": row_1.id}
)
with patch("baserow.contrib.database.webhooks.registries.call_webhook.delay") as m:
response = api_client.delete(
f"{url}?send_webhook_events=false",
format="json",
HTTP_AUTHORIZATION=f"JWT {token}",
)
assert response.status_code == HTTP_204_NO_CONTENT
m.assert_not_called()
@pytest.mark.django_db
def test_list_rows_with_attribute_names(api_client, data_fixture):
user, jwt_token = data_fixture.create_user_and_token(

View file

@ -229,6 +229,7 @@ def test_create_form_view_with_webhooks(api_client, data_fixture):
HTTP_AUTHORIZATION=f"JWT {token}",
)
assert m.called
print("CALL ARGS", m.call_args)
response_json = response.json()
assert response.status_code == HTTP_200_OK

View file

@ -0,0 +1,7 @@
{
"type": "feature",
"message": "Prevent triggering webhook if query param is provided on row create update or delete endpoints",
"issue_number": 3085,
"bullet_points": [],
"created_at": "2024-11-19"
}

View file

@ -432,6 +432,7 @@
"pathParameters": "Path parameters",
"requestBodySchema": "Request body schema",
"userFieldNamesDescription": "When the `user_field_names` GET parameter is provided and its value is one of the following: `y`, `yes`, `true`, `t`, `on`, `1`, or empty string, the field names returned by this endpoint will be the actual names of the fields.\n\nIf the `user_field_names` GET parameter is not provided, or if it does not match any of the above values, then all returned field names will be `field_` followed by the id of the field. For example `field_1` refers to the field with an id of `1`.",
"sendWebhookEventsDescription": "A flag query parameter that triggers webhooks after the operation, if set to `y`, `yes`, `true`, `t`, `on`, `1`, `or` left empty. Defaults to `true`",
"singleRow": "Single",
"batchRows": "Batch",
"fileUploads": "File uploads"

View file

@ -39,6 +39,16 @@
<APIDocsParameter :optional="true" name="before" type="integer">
{{ $t('apiDocsTableCreateRow.before') }}
</APIDocsParameter>
<APIDocsParameter
name="send_webhook_events"
:optional="true"
type="any"
>
<MarkdownIt
class="api-docs__content"
:content="$t('apiDocs.sendWebhookEventsDescription')"
/>
</APIDocsParameter>
</ul>
<h4 class="api-docs__heading-4">
{{ $t('apiDocs.requestBodySchema') }}
@ -72,6 +82,16 @@
<APIDocsParameter :optional="true" name="before" type="integer">
{{ $t('apiDocsTableCreateRows.before') }}
</APIDocsParameter>
<APIDocsParameter
name="send_webhook_events"
:optional="true"
type="any"
>
<MarkdownIt
class="api-docs__content"
:content="$t('apiDocs.sendWebhookEventsDescription')"
/>
</APIDocsParameter>
</ul>
<h4 class="api-docs__heading-4">
{{ $t('apiDocs.requestBodySchema') }}

View file

@ -33,7 +33,20 @@
</APIDocsParameter>
</ul>
</div>
<div v-else>
<h4 class="api-docs__heading-4">{{ $t('apiDocs.queryParameters') }}</h4>
<ul class="api-docs__parameters">
<APIDocsParameter
name="send_webhook_events"
:optional="true"
type="any"
>
<MarkdownIt
class="api-docs__content"
:content="$t('apiDocs.sendWebhookEventsDescription')"
/>
</APIDocsParameter>
</ul>
<div v-if="batchMode === true">
<h4 class="api-docs__heading-4">
{{ $t('apiDocs.requestBodySchema') }}
</h4>

View file

@ -28,6 +28,16 @@
<APIDocsParameter name="before_id" type="integer" :optional="true">
{{ $t('apiDocsTableMoveRow.before') }}
</APIDocsParameter>
<APIDocsParameter
name="send_webhook_events"
:optional="true"
type="any"
>
<MarkdownIt
class="api-docs__content"
:content="$t('apiDocs.sendWebhookEventsDescription')"
/>
</APIDocsParameter>
</ul>
</div>
<div class="api-docs__right">

View file

@ -40,6 +40,16 @@
:content="$t('apiDocs.userFieldNamesDescription')"
/>
</APIDocsParameter>
<APIDocsParameter
name="send_webhook_events"
:optional="true"
type="any"
>
<MarkdownIt
class="api-docs__content"
:content="$t('apiDocs.sendWebhookEventsDescription')"
/>
</APIDocsParameter>
</ul>
<h4 class="api-docs__heading-4">
{{ $t('apiDocs.requestBodySchema') }}
@ -68,6 +78,16 @@
:content="$t('apiDocs.userFieldNamesDescription')"
/>
</APIDocsParameter>
<APIDocsParameter
name="send_webhook_events"
:optional="true"
type="any"
>
<MarkdownIt
class="api-docs__content"
:content="$t('apiDocs.sendWebhookEventsDescription')"
/>
</APIDocsParameter>
</ul>
<h4 class="api-docs__heading-4">
{{ $t('apiDocs.requestBodySchema') }}