From a9527603e96dae266784b18675b90d9e1b1dc3b8 Mon Sep 17 00:00:00 2001 From: Bram Wiepjes <bramw@protonmail.com> Date: Tue, 21 Jan 2025 21:20:44 +0000 Subject: [PATCH] Allow editors to subscribe to a form view --- backend/src/baserow/contrib/database/apps.py | 4 ++ .../baserow/contrib/database/views/handler.py | 14 ++-- .../database/views/notification_types.py | 6 +- .../contrib/database/views/operations.py | 4 ++ .../contrib/database/views/registries.py | 24 +++++++ .../contrib/database/views/view_types.py | 24 +++++++ ...w_editors_to_subscribe_to_a_form_view.json | 7 ++ .../baserow_enterprise/role/default_roles.py | 2 + .../role/test_view_permissions.py | 68 +++++++++++++++++++ .../src/baserow_premium/permission_manager.py | 2 + .../view/form/FormViewMetaControls.vue | 7 ++ 11 files changed, 151 insertions(+), 11 deletions(-) create mode 100644 changelog/entries/unreleased/bug/3271_allow_editors_to_subscribe_to_a_form_view.json create mode 100644 enterprise/backend/tests/baserow_enterprise_tests/role/test_view_permissions.py diff --git a/backend/src/baserow/contrib/database/apps.py b/backend/src/baserow/contrib/database/apps.py index c1a8767f2..3ac89d311 100755 --- a/backend/src/baserow/contrib/database/apps.py +++ b/backend/src/baserow/contrib/database/apps.py @@ -686,6 +686,7 @@ class DatabaseConfig(AppConfig): object_scope_type_registry.register(TokenObjectScopeType()) from baserow.contrib.database.views.operations import ( + CanReceiveNotificationOnSubmitFormViewOperationType, UpdateViewFieldOptionsOperationType, ) @@ -826,6 +827,9 @@ class DatabaseConfig(AppConfig): operation_type_registry.register(CreateAndUsePersonalViewOperationType()) operation_type_registry.register(ReadViewOperationType()) operation_type_registry.register(UpdateViewOperationType()) + operation_type_registry.register( + CanReceiveNotificationOnSubmitFormViewOperationType() + ) operation_type_registry.register(DeleteViewOperationType()) operation_type_registry.register(DuplicateViewOperationType()) operation_type_registry.register(CreateViewFilterOperationType()) diff --git a/backend/src/baserow/contrib/database/views/handler.py b/backend/src/baserow/contrib/database/views/handler.py index 6e4bac539..70c831a4d 100644 --- a/backend/src/baserow/contrib/database/views/handler.py +++ b/backend/src/baserow/contrib/database/views/handler.py @@ -75,7 +75,6 @@ from baserow.contrib.database.views.operations import ( UpdateViewFilterGroupOperationType, UpdateViewFilterOperationType, UpdateViewGroupByOperationType, - UpdateViewOperationType, UpdateViewPublicOperationType, UpdateViewSlugOperationType, UpdateViewSortOperationType, @@ -947,7 +946,7 @@ class ViewHandler(metaclass=baserow_trace_methods(tracer)): :param user: The user on whose behalf the view is 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. :return: The updated view instance. """ @@ -955,16 +954,12 @@ class ViewHandler(metaclass=baserow_trace_methods(tracer)): if not isinstance(view, View): raise ValueError("The view is not an instance of View.") - workspace = view.table.database.workspace - CoreHandler().check_permissions( - user, UpdateViewOperationType.type, workspace=workspace, context=view - ) + view_type = view_type_registry.get_by_model(view) + view_type.check_view_update_permissions(user, view, data) + view_type.before_view_update(data, view, user) 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) allowed_fields = [ "name", @@ -1003,6 +998,7 @@ class ViewHandler(metaclass=baserow_trace_methods(tracer)): ) view = set_allowed_attrs(view_values, allowed_attrs, view) if previous_public_value != view.public: + workspace = view.table.database.workspace CoreHandler().check_permissions( user, UpdateViewPublicOperationType.type, diff --git a/backend/src/baserow/contrib/database/views/notification_types.py b/backend/src/baserow/contrib/database/views/notification_types.py index e8de2c338..783f2a044 100644 --- a/backend/src/baserow/contrib/database/views/notification_types.py +++ b/backend/src/baserow/contrib/database/views/notification_types.py @@ -6,7 +6,9 @@ from django.dispatch import receiver from django.utils.translation import gettext as _ 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.notifications.handler import NotificationHandler 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 allowed_users = CoreHandler().check_permission_for_multiple_actors( users_to_notify, - UpdateViewOperationType.type, + CanReceiveNotificationOnSubmitFormViewOperationType.type, workspace=form.table.database.workspace, context=form, ) diff --git a/backend/src/baserow/contrib/database/views/operations.py b/backend/src/baserow/contrib/database/views/operations.py index 395795685..65e371ad5 100644 --- a/backend/src/baserow/contrib/database/views/operations.py +++ b/backend/src/baserow/contrib/database/views/operations.py @@ -124,6 +124,10 @@ class UpdateViewOperationType(ViewOperationType): type = "database.table.view.update" +class CanReceiveNotificationOnSubmitFormViewOperationType(ViewOperationType): + type = "database.table.view.can_receive_notification_on_submit_form_view" + + class DeleteViewOperationType(ViewOperationType): type = "database.table.view.delete" diff --git a/backend/src/baserow/contrib/database/views/registries.py b/backend/src/baserow/contrib/database/views/registries.py index 7e33896bc..6fd7eaa0d 100644 --- a/backend/src/baserow/contrib/database/views/registries.py +++ b/backend/src/baserow/contrib/database/views/registries.py @@ -23,6 +23,7 @@ from rest_framework.serializers import Serializer from baserow.contrib.database.fields.field_filters import OptionallyAnnotatedQ from baserow.core.exceptions import PermissionDenied +from baserow.core.handler import CoreHandler from baserow.core.models import Workspace, WorkspaceUser from baserow.core.registries import OperationType 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( APIUrlsRegistryMixin, CustomFieldsRegistryMixin, ModelRegistryMixin, Registry diff --git a/backend/src/baserow/contrib/database/views/view_types.py b/backend/src/baserow/contrib/database/views/view_types.py index 573f3045a..a8b01374e 100644 --- a/backend/src/baserow/contrib/database/views/view_types.py +++ b/backend/src/baserow/contrib/database/views/view_types.py @@ -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.table.models import Table 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.storage import ExportZipFile from baserow.core.user_files.handler import UserFileHandler @@ -1403,3 +1404,26 @@ class FormViewType(ViewType): return FormViewFieldOptions( 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) diff --git a/changelog/entries/unreleased/bug/3271_allow_editors_to_subscribe_to_a_form_view.json b/changelog/entries/unreleased/bug/3271_allow_editors_to_subscribe_to_a_form_view.json new file mode 100644 index 000000000..a20bfe7f1 --- /dev/null +++ b/changelog/entries/unreleased/bug/3271_allow_editors_to_subscribe_to_a_form_view.json @@ -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" +} diff --git a/enterprise/backend/src/baserow_enterprise/role/default_roles.py b/enterprise/backend/src/baserow_enterprise/role/default_roles.py index 3397192c9..60d85fb7d 100755 --- a/enterprise/backend/src/baserow_enterprise/role/default_roles.py +++ b/enterprise/backend/src/baserow_enterprise/role/default_roles.py @@ -126,6 +126,7 @@ from baserow.contrib.database.tokens.operations import ( UseTokenOperationType, ) from baserow.contrib.database.views.operations import ( + CanReceiveNotificationOnSubmitFormViewOperationType, CreateAndUsePersonalViewOperationType, CreatePublicViewOperationType, CreateViewDecorationOperationType, @@ -347,6 +348,7 @@ default_roles[EDITOR_ROLE_UID].extend( RestoreDatabaseRowOperationType, ListTeamSubjectsOperationType, ReadTeamSubjectOperationType, + CanReceiveNotificationOnSubmitFormViewOperationType, ] ) default_roles[BUILDER_ROLE_UID].extend( diff --git a/enterprise/backend/tests/baserow_enterprise_tests/role/test_view_permissions.py b/enterprise/backend/tests/baserow_enterprise_tests/role/test_view_permissions.py new file mode 100644 index 000000000..80f0dce80 --- /dev/null +++ b/enterprise/backend/tests/baserow_enterprise_tests/role/test_view_permissions.py @@ -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" + ) diff --git a/premium/backend/src/baserow_premium/permission_manager.py b/premium/backend/src/baserow_premium/permission_manager.py index 6b2f820b0..a63cf6e72 100644 --- a/premium/backend/src/baserow_premium/permission_manager.py +++ b/premium/backend/src/baserow_premium/permission_manager.py @@ -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.views.operations import ( + CanReceiveNotificationOnSubmitFormViewOperationType, CreateAndUsePersonalViewOperationType, CreateViewDecorationOperationType, CreateViewFilterGroupOperationType, @@ -91,6 +92,7 @@ class ViewOwnershipPermissionManagerType(PermissionManagerType): DeleteViewSortOperationType.type, ReadViewOperationType.type, UpdateViewOperationType.type, + CanReceiveNotificationOnSubmitFormViewOperationType.type, DeleteViewOperationType.type, DuplicateViewOperationType.type, CreateViewFilterOperationType.type, diff --git a/web-frontend/modules/database/components/view/form/FormViewMetaControls.vue b/web-frontend/modules/database/components/view/form/FormViewMetaControls.vue index 027338edf..5e8ce7104 100644 --- a/web-frontend/modules/database/components/view/form/FormViewMetaControls.vue +++ b/web-frontend/modules/database/components/view/form/FormViewMetaControls.vue @@ -1,6 +1,13 @@ <template> <div class="form-view__meta-controls"> <SwitchInput + v-if=" + $hasPermission( + 'database.table.view.can_receive_notification_on_submit_form_view', + view, + database.workspace.id + ) + " small :value="view.receive_notification_on_submit" class="margin-bottom-3"