1
0
Fork 0
mirror of https://gitlab.com/bramw/baserow.git synced 2025-04-14 00:59:06 +00:00

Allow editors to subscribe to a form view

This commit is contained in:
Bram Wiepjes 2025-01-21 21:20:44 +00:00
parent 731c1c70d5
commit a9527603e9
11 changed files with 151 additions and 11 deletions
backend/src/baserow/contrib/database
changelog/entries/unreleased/bug
enterprise/backend
src/baserow_enterprise/role
tests/baserow_enterprise_tests/role
premium/backend/src/baserow_premium
web-frontend/modules/database/components/view/form

View file

@ -686,6 +686,7 @@ class DatabaseConfig(AppConfig):
object_scope_type_registry.register(TokenObjectScopeType()) object_scope_type_registry.register(TokenObjectScopeType())
from baserow.contrib.database.views.operations import ( from baserow.contrib.database.views.operations import (
CanReceiveNotificationOnSubmitFormViewOperationType,
UpdateViewFieldOptionsOperationType, UpdateViewFieldOptionsOperationType,
) )
@ -826,6 +827,9 @@ class DatabaseConfig(AppConfig):
operation_type_registry.register(CreateAndUsePersonalViewOperationType()) operation_type_registry.register(CreateAndUsePersonalViewOperationType())
operation_type_registry.register(ReadViewOperationType()) operation_type_registry.register(ReadViewOperationType())
operation_type_registry.register(UpdateViewOperationType()) operation_type_registry.register(UpdateViewOperationType())
operation_type_registry.register(
CanReceiveNotificationOnSubmitFormViewOperationType()
)
operation_type_registry.register(DeleteViewOperationType()) operation_type_registry.register(DeleteViewOperationType())
operation_type_registry.register(DuplicateViewOperationType()) operation_type_registry.register(DuplicateViewOperationType())
operation_type_registry.register(CreateViewFilterOperationType()) operation_type_registry.register(CreateViewFilterOperationType())

View file

@ -75,7 +75,6 @@ from baserow.contrib.database.views.operations import (
UpdateViewFilterGroupOperationType, UpdateViewFilterGroupOperationType,
UpdateViewFilterOperationType, UpdateViewFilterOperationType,
UpdateViewGroupByOperationType, UpdateViewGroupByOperationType,
UpdateViewOperationType,
UpdateViewPublicOperationType, UpdateViewPublicOperationType,
UpdateViewSlugOperationType, UpdateViewSlugOperationType,
UpdateViewSortOperationType, UpdateViewSortOperationType,
@ -947,7 +946,7 @@ class ViewHandler(metaclass=baserow_trace_methods(tracer)):
:param user: The user on whose behalf the view is updated. :param user: The user on whose behalf the view is updated.
:param view: The view instance that needs to be updated. :param view: The view instance that needs to be updated.
:param data: The fields that need to be updated. :param data: The properties that need to be updated.
:raises ValueError: When the provided view not an instance of View. :raises ValueError: When the provided view not an instance of View.
:return: The updated view instance. :return: The updated view instance.
""" """
@ -955,16 +954,12 @@ class ViewHandler(metaclass=baserow_trace_methods(tracer)):
if not isinstance(view, View): if not isinstance(view, View):
raise ValueError("The view is not an instance of View.") raise ValueError("The view is not an instance of View.")
workspace = view.table.database.workspace view_type = view_type_registry.get_by_model(view)
CoreHandler().check_permissions( view_type.check_view_update_permissions(user, view, data)
user, UpdateViewOperationType.type, workspace=workspace, context=view view_type.before_view_update(data, view, user)
)
old_view = deepcopy(view) old_view = deepcopy(view)
view_type = view_type_registry.get_by_model(view)
view_type.before_view_update(data, view, user)
view_values = view_type.prepare_values(data, view.table, user) view_values = view_type.prepare_values(data, view.table, user)
allowed_fields = [ allowed_fields = [
"name", "name",
@ -1003,6 +998,7 @@ class ViewHandler(metaclass=baserow_trace_methods(tracer)):
) )
view = set_allowed_attrs(view_values, allowed_attrs, view) view = set_allowed_attrs(view_values, allowed_attrs, view)
if previous_public_value != view.public: if previous_public_value != view.public:
workspace = view.table.database.workspace
CoreHandler().check_permissions( CoreHandler().check_permissions(
user, user,
UpdateViewPublicOperationType.type, UpdateViewPublicOperationType.type,

View file

@ -6,7 +6,9 @@ from django.dispatch import receiver
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
from django.utils.translation import ngettext from django.utils.translation import ngettext
from baserow.contrib.database.views.operations import UpdateViewOperationType from baserow.contrib.database.views.operations import (
CanReceiveNotificationOnSubmitFormViewOperationType,
)
from baserow.core.handler import CoreHandler from baserow.core.handler import CoreHandler
from baserow.core.notifications.handler import NotificationHandler from baserow.core.notifications.handler import NotificationHandler
from baserow.core.notifications.registries import ( from baserow.core.notifications.registries import (
@ -116,7 +118,7 @@ def create_form_submitted_notification(sender, form, row, values, user, **kwargs
# Ensure all users still have permissions on the table to see the notification # Ensure all users still have permissions on the table to see the notification
allowed_users = CoreHandler().check_permission_for_multiple_actors( allowed_users = CoreHandler().check_permission_for_multiple_actors(
users_to_notify, users_to_notify,
UpdateViewOperationType.type, CanReceiveNotificationOnSubmitFormViewOperationType.type,
workspace=form.table.database.workspace, workspace=form.table.database.workspace,
context=form, context=form,
) )

View file

@ -124,6 +124,10 @@ class UpdateViewOperationType(ViewOperationType):
type = "database.table.view.update" type = "database.table.view.update"
class CanReceiveNotificationOnSubmitFormViewOperationType(ViewOperationType):
type = "database.table.view.can_receive_notification_on_submit_form_view"
class DeleteViewOperationType(ViewOperationType): class DeleteViewOperationType(ViewOperationType):
type = "database.table.view.delete" type = "database.table.view.delete"

View file

@ -23,6 +23,7 @@ from rest_framework.serializers import Serializer
from baserow.contrib.database.fields.field_filters import OptionallyAnnotatedQ from baserow.contrib.database.fields.field_filters import OptionallyAnnotatedQ
from baserow.core.exceptions import PermissionDenied from baserow.core.exceptions import PermissionDenied
from baserow.core.handler import CoreHandler
from baserow.core.models import Workspace, WorkspaceUser from baserow.core.models import Workspace, WorkspaceUser
from baserow.core.registries import OperationType from baserow.core.registries import OperationType
from baserow.core.registry import ( from baserow.core.registry import (
@ -837,6 +838,29 @@ class ViewType(
}, },
) )
def check_view_update_permissions(
self, user: AbstractUser, view: "View", data: Dict[str, Any]
):
"""
Hook that's called just before a view is updated. By default, it checks the
`UpdateViewOperationType`, but when overwritten, it can optionally check for
different permissions depending on the data. It returns nothing if the user has
permissions, or raises a PermissionDenied error otherwise.
:param user: The user on whose behalf the view is updated.
:param view: The view instance that needs to be updated.
:param data: The properties that need to be updated.
:raises PermissionDenied: if the user doesn't have permissions to update the
view.
"""
from .operations import UpdateViewOperationType
workspace = view.table.database.workspace
CoreHandler().check_permissions(
user, UpdateViewOperationType.type, workspace=workspace, context=view
)
class ViewTypeRegistry( class ViewTypeRegistry(
APIUrlsRegistryMixin, CustomFieldsRegistryMixin, ModelRegistryMixin, Registry APIUrlsRegistryMixin, CustomFieldsRegistryMixin, ModelRegistryMixin, Registry

View file

@ -46,6 +46,7 @@ from baserow.contrib.database.fields.models import Field, FileField, SelectOptio
from baserow.contrib.database.fields.registries import field_type_registry from baserow.contrib.database.fields.registries import field_type_registry
from baserow.contrib.database.table.models import Table from baserow.contrib.database.table.models import Table
from baserow.contrib.database.views.registries import view_aggregation_type_registry from baserow.contrib.database.views.registries import view_aggregation_type_registry
from baserow.core.handler import CoreHandler
from baserow.core.import_export.utils import file_chunk_generator from baserow.core.import_export.utils import file_chunk_generator
from baserow.core.storage import ExportZipFile from baserow.core.storage import ExportZipFile
from baserow.core.user_files.handler import UserFileHandler from baserow.core.user_files.handler import UserFileHandler
@ -1403,3 +1404,26 @@ class FormViewType(ViewType):
return FormViewFieldOptions( return FormViewFieldOptions(
field_id=field_id, form_view_id=view.id, enabled=False field_id=field_id, form_view_id=view.id, enabled=False
) )
def check_view_update_permissions(self, user, view, data):
from .operations import CanReceiveNotificationOnSubmitFormViewOperationType
workspace = view.table.database.workspace
if "receive_notification_on_submit" in data:
# If `receive_notification_on_submit` is in the data, then we must check if
# the user has permissions to receive a notification on submit.
CoreHandler().check_permissions(
user,
CanReceiveNotificationOnSubmitFormViewOperationType.type,
workspace=workspace,
context=view,
)
# If only the `receive_notification_on_submit` is provided, then there is
# no need to check if the user has permissions to update the view because
# nothing else is changed.
if len(data) == 1:
return
return super().check_view_update_permissions(user, view, data)

View file

@ -0,0 +1,7 @@
{
"type": "bug",
"message": "Allow editors to subscribe to a form view.",
"issue_number": 3271,
"bullet_points": [],
"created_at": "2025-01-18"
}

View file

@ -126,6 +126,7 @@ from baserow.contrib.database.tokens.operations import (
UseTokenOperationType, UseTokenOperationType,
) )
from baserow.contrib.database.views.operations import ( from baserow.contrib.database.views.operations import (
CanReceiveNotificationOnSubmitFormViewOperationType,
CreateAndUsePersonalViewOperationType, CreateAndUsePersonalViewOperationType,
CreatePublicViewOperationType, CreatePublicViewOperationType,
CreateViewDecorationOperationType, CreateViewDecorationOperationType,
@ -347,6 +348,7 @@ default_roles[EDITOR_ROLE_UID].extend(
RestoreDatabaseRowOperationType, RestoreDatabaseRowOperationType,
ListTeamSubjectsOperationType, ListTeamSubjectsOperationType,
ReadTeamSubjectOperationType, ReadTeamSubjectOperationType,
CanReceiveNotificationOnSubmitFormViewOperationType,
] ]
) )
default_roles[BUILDER_ROLE_UID].extend( default_roles[BUILDER_ROLE_UID].extend(

View file

@ -0,0 +1,68 @@
from django.test import override_settings
import pytest
from baserow.contrib.database.views.handler import ViewHandler
from baserow.core.exceptions import PermissionDenied
from baserow_enterprise.role.handler import RoleAssignmentHandler
@pytest.mark.django_db
@override_settings(DEBUG=True)
def test_update_form_view_as_editor_fails(enterprise_data_fixture):
enterprise_data_fixture.enable_enterprise()
user, token = enterprise_data_fixture.create_user_and_token(
email="test@test.nl", password="password", first_name="Test1"
)
table = enterprise_data_fixture.create_database_table(user)
form = enterprise_data_fixture.create_form_view(table=table)
editor_role = RoleAssignmentHandler().get_role_by_uid("EDITOR")
RoleAssignmentHandler().assign_role(
user, table.database.workspace, role=editor_role, scope=table
)
handler = ViewHandler()
with pytest.raises(PermissionDenied):
handler.update_view(user=user, view=form, name="Test 1")
@pytest.mark.django_db
@override_settings(DEBUG=True)
def test_update_form_view_notification_as_editor_succeeds(enterprise_data_fixture):
enterprise_data_fixture.enable_enterprise()
user, token = enterprise_data_fixture.create_user_and_token(
email="test@test.nl", password="password", first_name="Test1"
)
table = enterprise_data_fixture.create_database_table(user)
form = enterprise_data_fixture.create_form_view(table=table)
editor_role = RoleAssignmentHandler().get_role_by_uid("EDITOR")
RoleAssignmentHandler().assign_role(
user, table.database.workspace, role=editor_role, scope=table
)
handler = ViewHandler()
handler.update_view(user=user, view=form, receive_notification_on_submit=True)
assert form.users_to_notify_on_submit.count() == 1
@pytest.mark.django_db
@override_settings(DEBUG=True)
def test_update_form_view_and_notification_as_editor_fails(enterprise_data_fixture):
enterprise_data_fixture.enable_enterprise()
user, token = enterprise_data_fixture.create_user_and_token(
email="test@test.nl", password="password", first_name="Test1"
)
table = enterprise_data_fixture.create_database_table(user)
form = enterprise_data_fixture.create_form_view(table=table)
editor_role = RoleAssignmentHandler().get_role_by_uid("EDITOR")
RoleAssignmentHandler().assign_role(
user, table.database.workspace, role=editor_role, scope=table
)
handler = ViewHandler()
with pytest.raises(PermissionDenied):
handler.update_view(
user=user, view=form, receive_notification_on_submit=True, name="Test"
)

View file

@ -9,6 +9,7 @@ from baserow_premium.views.models import OWNERSHIP_TYPE_PERSONAL
from baserow.contrib.database.table.models import Table from baserow.contrib.database.table.models import Table
from baserow.contrib.database.views.operations import ( from baserow.contrib.database.views.operations import (
CanReceiveNotificationOnSubmitFormViewOperationType,
CreateAndUsePersonalViewOperationType, CreateAndUsePersonalViewOperationType,
CreateViewDecorationOperationType, CreateViewDecorationOperationType,
CreateViewFilterGroupOperationType, CreateViewFilterGroupOperationType,
@ -91,6 +92,7 @@ class ViewOwnershipPermissionManagerType(PermissionManagerType):
DeleteViewSortOperationType.type, DeleteViewSortOperationType.type,
ReadViewOperationType.type, ReadViewOperationType.type,
UpdateViewOperationType.type, UpdateViewOperationType.type,
CanReceiveNotificationOnSubmitFormViewOperationType.type,
DeleteViewOperationType.type, DeleteViewOperationType.type,
DuplicateViewOperationType.type, DuplicateViewOperationType.type,
CreateViewFilterOperationType.type, CreateViewFilterOperationType.type,

View file

@ -1,6 +1,13 @@
<template> <template>
<div class="form-view__meta-controls"> <div class="form-view__meta-controls">
<SwitchInput <SwitchInput
v-if="
$hasPermission(
'database.table.view.can_receive_notification_on_submit_form_view',
view,
database.workspace.id
)
"
small small
:value="view.receive_notification_on_submit" :value="view.receive_notification_on_submit"
class="margin-bottom-3" class="margin-bottom-3"