1
0
Fork 0
mirror of https://gitlab.com/bramw/baserow.git synced 2025-04-11 07:51:20 +00:00

Merge branch '1448-personal-views-2' into 'develop'

Resolve "Personal views"

Closes 

See merge request 
This commit is contained in:
Petr Stribny 2023-01-27 15:45:11 +00:00
commit 3f68954882
72 changed files with 2884 additions and 495 deletions
backend
changelog.mddocker-compose.local-build.ymldocker-compose.no-caddy.ymldocker-compose.yml
e2e-tests/playwright-report
enterprise/backend/src/baserow_enterprise/role
premium
web-frontend/modules

View file

@ -17,6 +17,7 @@ markers =
field_link_row: All tests related to link row field
field_formula: All tests related to formula field
field_multiple_collaborators: All tests related to multiple collaborator field
view_ownership: All tests related to view ownership type
api_rows: All tests to manipulate rows via HTTP API
disabled_in_ci: All tests that are disabled in CI
once_per_day_in_ci: All tests that are run once per day in CI

View file

@ -667,10 +667,16 @@ class Everything(object):
if "*" in FEATURE_FLAGS:
FEATURE_FLAGS = Everything()
PERMISSION_MANAGERS = os.getenv(
"BASEROW_PERMISSION_MANAGERS",
"core,setting_operation,staff,member,token,role,basic",
).split(",")
PERMISSION_MANAGERS = [
"view_ownership",
"core",
"setting_operation",
"staff",
"member",
"token",
"role",
"basic",
]
OLD_ACTION_CLEANUP_INTERVAL_MINUTES = os.getenv(
"OLD_ACTION_CLEANUP_INTERVAL_MINUTES", 5

View file

@ -122,7 +122,9 @@ class ExportTableView(APIView):
option_data = _validate_options(request.data)
view_id = option_data.pop("view_id", None)
view = ViewHandler().get_view(view_id) if view_id else None
view = (
ViewHandler().get_view_as_user(request.user, view_id) if view_id else None
)
job = ExportHandler.create_and_start_new_job(
request.user, table, view, option_data

View file

@ -335,7 +335,7 @@ class RowsView(APIView):
if view_id:
view_handler = ViewHandler()
view = view_handler.get_view(view_id)
view = view_handler.get_view_as_user(request.user, view_id)
if view.table_id != table.id:
raise ViewDoesNotExist()
@ -1386,7 +1386,7 @@ class RowAdjacentView(APIView):
view = None
if view_id:
view_handler = ViewHandler()
view = view_handler.get_view(view_id)
view = view_handler.get_view_as_user(request.user, view_id)
if view.table_id != table.id:
raise ViewDoesNotExist()

View file

@ -94,3 +94,8 @@ ERROR_NO_AUTHORIZATION_TO_PUBLICLY_SHARED_VIEW = (
HTTP_401_UNAUTHORIZED,
"The user does not have the permissions to see this password protected shared view.",
)
ERROR_VIEW_OWNERSHIP_TYPE_DOES_NOT_EXIST = (
"ERROR_VIEW_OWNERSHIP_TYPE_DOES_NOT_EXIST",
HTTP_404_NOT_FOUND,
"The view ownership type does not exist.",
)

View file

@ -149,7 +149,7 @@ class GalleryViewView(APIView):
"""Lists the rows for the gallery view."""
view_handler = ViewHandler()
view = view_handler.get_view(view_id, GalleryView)
view = view_handler.get_view_as_user(request.user, view_id, GalleryView)
view_type = view_type_registry.get_by_model(view)
group = view.table.database.group

View file

@ -50,14 +50,8 @@ from baserow.contrib.database.fields.field_filters import (
FILTER_TYPE_OR,
)
from baserow.contrib.database.fields.handler import FieldHandler
from baserow.contrib.database.fields.operations import (
ReadAggregationDatabaseTableOperationType,
)
from baserow.contrib.database.rows.registries import row_metadata_registry
from baserow.contrib.database.table.operations import (
ListAggregationDatabaseTableOperationType,
ListRowsDatabaseTableOperationType,
)
from baserow.contrib.database.table.operations import ListRowsDatabaseTableOperationType
from baserow.contrib.database.views.exceptions import (
AggregationTypeDoesNotExist,
NoAuthorizationToPubliclySharedView,
@ -247,7 +241,7 @@ class GridViewView(APIView):
exclude_fields = request.GET.get("exclude_fields")
view_handler = ViewHandler()
view = view_handler.get_view(view_id, GridView)
view = view_handler.get_view_as_user(request.user, view_id, GridView)
view_type = view_type_registry.get_by_model(view)
group = view.table.database.group
@ -348,7 +342,7 @@ class GridViewView(APIView):
requested fields.
"""
view = ViewHandler().get_view(view_id, GridView)
view = ViewHandler().get_view_as_user(request.user, view_id, GridView)
CoreHandler().check_permissions(
request.user,
ListRowsDatabaseTableOperationType.type,
@ -443,19 +437,11 @@ class GridViewFieldAggregationsView(APIView):
view_handler = ViewHandler()
view = view_handler.get_view(view_id, GridView)
CoreHandler().check_permissions(
request.user,
ListAggregationDatabaseTableOperationType.type,
group=view.table.database.group,
context=view.table,
allow_if_template=True,
)
# Compute aggregation
# Note: we can't optimize model by giving a model with just
# the aggregated field because we may need other fields for filtering
result = view_handler.get_view_field_aggregations(
view, with_total=total, search=search
request.user, view, with_total=total, search=search
)
# Decimal("NaN") can't be serialized, therefore we have to replace it
@ -558,13 +544,6 @@ class GridViewFieldAggregationView(APIView):
view = view_handler.get_view(view_id, GridView)
field_instance = FieldHandler().get_field(field_id)
CoreHandler().check_permissions(
request.user,
ReadAggregationDatabaseTableOperationType.type,
group=view.table.database.group,
context=field_instance,
allow_if_template=True,
)
aggregation_type = request.GET.get("type")
@ -572,7 +551,7 @@ class GridViewFieldAggregationView(APIView):
# Note: we can't optimize model by giving a model with just
# the aggregated field because we may need other fields for filtering
aggregations = view_handler.get_field_aggregations(
view, [(field_instance, aggregation_type)], with_total=total
request.user, view, [(field_instance, aggregation_type)], with_total=total
)
result = {

View file

@ -9,6 +9,7 @@ from baserow.contrib.database.api.fields.serializers import FieldSerializer
from baserow.contrib.database.api.serializers import TableSerializer
from baserow.contrib.database.fields.registries import field_type_registry
from baserow.contrib.database.views.models import (
OWNERSHIP_TYPE_COLLABORATIVE,
View,
ViewDecoration,
ViewFilter,
@ -18,6 +19,7 @@ from baserow.contrib.database.views.registries import (
decorator_type_registry,
decorator_value_provider_type_registry,
view_filter_type_registry,
view_ownership_type_registry,
view_type_registry,
)
@ -265,6 +267,7 @@ class ViewSerializer(serializers.ModelSerializer):
many=True, source="viewdecoration_set", required=False
)
show_logo = serializers.BooleanField(required=False)
ownership_type = serializers.CharField()
class Meta:
model = View
@ -282,11 +285,13 @@ class ViewSerializer(serializers.ModelSerializer):
"filters_disabled",
"public_view_has_password",
"show_logo",
"ownership_type",
)
extra_kwargs = {
"id": {"read_only": True},
"table_id": {"read_only": True},
"public_view_has_password": {"read_only": True},
"ownership_type": {"read_only": True},
}
def __init__(self, *args, **kwargs):
@ -322,10 +327,14 @@ class CreateViewSerializer(serializers.ModelSerializer):
type = serializers.ChoiceField(
choices=lazy(view_type_registry.get_types, list)(), required=True
)
ownership_type = serializers.ChoiceField(
choices=lazy(view_ownership_type_registry.get_types, list)(),
default=OWNERSHIP_TYPE_COLLABORATIVE,
)
class Meta:
model = View
fields = ("name", "type", "filter_type", "filters_disabled")
fields = ("name", "type", "ownership_type", "filter_type", "filters_disabled")
class UpdateViewSerializer(serializers.ModelSerializer):
@ -363,7 +372,9 @@ class UpdateViewSerializer(serializers.ModelSerializer):
class OrderViewsSerializer(serializers.Serializer):
view_ids = serializers.ListField(
child=serializers.IntegerField(), help_text="View ids in the desired order."
child=serializers.IntegerField(),
help_text="View ids in the desired order.",
min_length=1,
)

View file

@ -81,29 +81,17 @@ from baserow.contrib.database.views.exceptions import (
ViewFilterNotSupported,
ViewFilterTypeNotAllowedForField,
ViewNotInTable,
ViewOwnerhshipTypeDoesNotExist,
ViewSortDoesNotExist,
ViewSortFieldAlreadyExist,
ViewSortFieldNotSupported,
ViewSortNotSupported,
)
from baserow.contrib.database.views.handler import ViewHandler
from baserow.contrib.database.views.models import (
View,
ViewDecoration,
ViewFilter,
ViewSort,
)
from baserow.contrib.database.views.models import ViewDecoration, ViewFilter, ViewSort
from baserow.contrib.database.views.operations import (
CreateViewDecorationOperationType,
DeleteViewDecorationOperationType,
ListViewDecorationOperationType,
ListViewFilterOperationType,
ListViewsOperationType,
ListViewSortOperationType,
ReadViewDecorationOperationType,
ReadViewFieldOptionsOperationType,
ReadViewOperationType,
UpdateViewDecorationOperationType,
)
from baserow.contrib.database.views.registries import (
decorator_value_provider_type_registry,
@ -127,6 +115,7 @@ from .errors import (
ERROR_VIEW_FILTER_NOT_SUPPORTED,
ERROR_VIEW_FILTER_TYPE_UNSUPPORTED_FIELD,
ERROR_VIEW_NOT_IN_TABLE,
ERROR_VIEW_OWNERSHIP_TYPE_DOES_NOT_EXIST,
ERROR_VIEW_SORT_DOES_NOT_EXIST,
ERROR_VIEW_SORT_FIELD_ALREADY_EXISTS,
ERROR_VIEW_SORT_FIELD_NOT_SUPPORTED,
@ -263,26 +252,15 @@ class ViewsView(APIView):
allow_if_template=True,
)
views = View.objects.filter(table=table).select_related("content_type", "table")
if query_params["type"]:
view_type = view_type_registry.get(query_params["type"])
content_type = ContentType.objects.get_for_model(view_type.model_class)
views = views.filter(content_type=content_type)
if filters:
views = views.prefetch_related("viewfilter_set")
if sortings:
views = views.prefetch_related("viewsort_set")
if decorations:
views = views.prefetch_related("viewdecoration_set")
if query_params["limit"]:
views = views[: query_params["limit"]]
views = specific_iterator(views)
views = ViewHandler().list_views(
request.user,
table,
query_params["type"],
filters,
sortings,
decorations,
query_params["limit"],
)
data = [
view_type_registry.get_serializer(
@ -354,6 +332,7 @@ class ViewsView(APIView):
{
TableDoesNotExist: ERROR_TABLE_DOES_NOT_EXIST,
UserNotInGroup: ERROR_USER_NOT_IN_GROUP,
ViewOwnerhshipTypeDoesNotExist: ERROR_VIEW_OWNERSHIP_TYPE_DOES_NOT_EXIST,
}
)
@allowed_includes("filters", "sortings", "decorations")
@ -431,13 +410,7 @@ class ViewView(APIView):
def get(self, request, view_id, filters, sortings, decorations):
"""Selects a single view and responds with a serialized version."""
view = ViewHandler().get_view(view_id)
CoreHandler().check_permissions(
request.user,
ReadViewOperationType.type,
group=view.table.database.group,
context=view,
)
view = ViewHandler().get_view_as_user(request.user, view_id)
serializer = view_type_registry.get_serializer(
view,
@ -514,7 +487,7 @@ class ViewView(APIView):
) -> Response:
"""Updates the view if the user belongs to the group."""
view = ViewHandler().get_view_for_update(view_id).specific
view = ViewHandler().get_view_for_update(request.user, view_id).specific
view_type = view_type_registry.get_by_model(view)
data = validate_data_custom_fields(
view_type.type,
@ -731,12 +704,7 @@ class ViewFiltersView(APIView):
has access to that group.
"""
view = ViewHandler().get_view(view_id)
group = view.table.database.group
CoreHandler().check_permissions(
request.user, ListViewFilterOperationType.type, group=group, context=view
)
filters = ViewFilter.objects.filter(view=view)
filters = ViewHandler().list_filters(request.user, view_id)
serializer = ViewFilterSerializer(filters, many=True)
return Response(serializer.data)
@ -998,14 +966,7 @@ class ViewDecorationsView(APIView):
if the user has access to that group.
"""
view = ViewHandler().get_view(view_id)
CoreHandler().check_permissions(
request.user,
ListViewDecorationOperationType.type,
group=view.table.database.group,
context=view,
)
decorations = ViewDecoration.objects.filter(view=view)
decorations = ViewHandler().list_decorations(request.user, view_id)
serializer = ViewDecorationSerializer(decorations, many=True)
return Response(serializer.data)
@ -1061,14 +1022,6 @@ class ViewDecorationsView(APIView):
view_handler = ViewHandler()
view = view_handler.get_view(view_id)
group = view.table.database.group
CoreHandler().check_permissions(
request.user,
CreateViewDecorationOperationType.type,
group=group,
context=view,
)
# We can safely assume the field exists because the
# CreateViewDecorationSerializer has already checked that.
view_decoration = action_type_registry.get_by_type(
@ -1118,16 +1071,7 @@ class ViewDecorationView(APIView):
def get(self, request, view_decoration_id):
"""Selects a single decoration and responds with a serialized version."""
view_decoration = ViewHandler().get_decoration(view_decoration_id)
group = view_decoration.view.table.database.group
CoreHandler().check_permissions(
request.user,
ReadViewDecorationOperationType.type,
group=group,
context=view_decoration,
)
view_decoration = ViewHandler().get_decoration(request.user, view_decoration_id)
serializer = ViewDecorationSerializer(view_decoration)
return Response(serializer.data)
@ -1172,18 +1116,11 @@ class ViewDecorationView(APIView):
handler = ViewHandler()
view_decoration = handler.get_decoration(
request.user,
view_decoration_id,
base_queryset=ViewDecoration.objects.select_for_update(of=("self",)),
)
group = view_decoration.view.table.database.group
CoreHandler().check_permissions(
request.user,
UpdateViewDecorationOperationType.type,
group=group,
context=view_decoration,
)
type_name = request.data.get(
"value_provider_type", view_decoration.value_provider_type
)
@ -1255,7 +1192,7 @@ class ViewDecorationView(APIView):
def delete(self, request, view_decoration_id):
"""Deletes an existing decoration if the user belongs to the group."""
view_decoration = ViewHandler().get_decoration(view_decoration_id)
view_decoration = ViewHandler().get_decoration(request.user, view_decoration_id)
group = view_decoration.view.table.database.group
CoreHandler().check_permissions(
@ -1311,14 +1248,7 @@ class ViewSortingsView(APIView):
has access to that group.
"""
view = ViewHandler().get_view(view_id)
CoreHandler().check_permissions(
request.user,
ListViewSortOperationType.type,
group=view.table.database.group,
context=view,
)
sortings = ViewSort.objects.filter(view=view)
sortings = ViewHandler().list_sorts(request.user, view_id)
serializer = ViewSortSerializer(sortings, many=True)
return Response(serializer.data)
@ -1576,16 +1506,7 @@ class ViewFieldOptionsView(APIView):
"""Returns the field options of the view."""
view = ViewHandler().get_view(view_id).specific
group = view.table.database.group
CoreHandler().check_permissions(
request.user,
ReadViewFieldOptionsOperationType.type,
group=group,
context=view,
allow_if_template=True,
)
view_type = view_type_registry.get_by_model(view)
view_type = ViewHandler().get_field_options_as_user(request.user, view)
try:
serializer_class = view_type.get_field_options_serializer_class(
create_if_missing=True
@ -1594,7 +1515,6 @@ class ViewFieldOptionsView(APIView):
raise ViewDoesNotSupportFieldOptions(
"The view type does not have a `field_options_serializer_class`"
) from exc
return Response(serializer_class(view).data)
@extend_schema(
@ -1703,7 +1623,8 @@ class RotateViewSlugView(APIView):
"""Rotates the slug of a view."""
view = action_type_registry.get_by_type(RotateViewSlugActionType).do(
request.user, ViewHandler().get_view_for_update(view_id).specific
request.user,
ViewHandler().get_view_for_update(request.user, view_id).specific,
)
serializer = view_type_registry.get_serializer(view, ViewSerializer)

View file

@ -146,6 +146,7 @@ class DatabaseConfig(AppConfig):
form_view_mode_registry,
view_aggregation_type_registry,
view_filter_type_registry,
view_ownership_type_registry,
view_type_registry,
)
from .webhooks.registries import webhook_event_type_registry
@ -340,6 +341,10 @@ class DatabaseConfig(AppConfig):
form_view_mode_registry.register(FormViewModeTypeForm())
from .views.view_ownership_types import CollaborativeViewOwnershipType
view_ownership_type_registry.register(CollaborativeViewOwnershipType())
from .application_types import DatabaseApplicationType
application_type_registry.register(DatabaseApplicationType())
@ -476,7 +481,6 @@ class DatabaseConfig(AppConfig):
DeleteRelatedLinkRowFieldOperationType,
DuplicateFieldOperationType,
ListFieldsOperationType,
ReadAggregationDatabaseTableOperationType,
ReadFieldOperationType,
RestoreFieldOperationType,
UpdateFieldOperationType,
@ -500,7 +504,6 @@ class DatabaseConfig(AppConfig):
DeleteDatabaseTableOperationType,
DuplicateDatabaseTableOperationType,
ImportRowsDatabaseTableOperationType,
ListAggregationDatabaseTableOperationType,
ListenToAllDatabaseTableEventsOperationType,
ListRowNamesDatabaseTableOperationType,
ListRowsDatabaseTableOperationType,
@ -523,11 +526,13 @@ class DatabaseConfig(AppConfig):
DeleteViewOperationType,
DeleteViewSortOperationType,
DuplicateViewOperationType,
ListAggregationsViewOperationType,
ListViewDecorationOperationType,
ListViewFilterOperationType,
ListViewsOperationType,
ListViewSortOperationType,
OrderViewsOperationType,
ReadAggregationsViewOperationType,
ReadViewDecorationOperationType,
ReadViewFieldOptionsOperationType,
ReadViewFilterOperationType,
@ -601,8 +606,8 @@ class DatabaseConfig(AppConfig):
operation_type_registry.register(TypeFormulaOperationType())
operation_type_registry.register(ListRowNamesDatabaseTableOperationType())
operation_type_registry.register(ReadAdjacentRowDatabaseRowOperationType())
operation_type_registry.register(ReadAggregationDatabaseTableOperationType())
operation_type_registry.register(ListAggregationDatabaseTableOperationType())
operation_type_registry.register(ReadAggregationsViewOperationType())
operation_type_registry.register(ListAggregationsViewOperationType())
operation_type_registry.register(ExportTableOperationType())
operation_type_registry.register(ListFieldsOperationType())
operation_type_registry.register(ListViewsOperationType())

View file

@ -39,7 +39,3 @@ class RestoreFieldOperationType(FieldOperationType):
class DuplicateFieldOperationType(FieldOperationType):
type = "database.table.field.duplicate"
class ReadAggregationDatabaseTableOperationType(FieldOperationType):
type = "database.table.field.read_aggregation"

View file

@ -0,0 +1,30 @@
# Generated by Django 3.2.13 on 2022-12-16 12:28
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
("database", "0097_add_ip_address_to_jobs"),
]
operations = [
migrations.AddField(
model_name="view",
name="created_by",
field=models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.SET_NULL,
to=settings.AUTH_USER_MODEL,
),
),
migrations.AddField(
model_name="view",
name="ownership_type",
field=models.CharField(default="collaborative", max_length=255),
),
]

View file

@ -0,0 +1,24 @@
# Generated by Django 3.2.13 on 2023-01-23 13:02
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("database", "0098_view_ownership_type"),
]
operations = [
migrations.AlterField(
model_name="view",
name="ownership_type",
field=models.CharField(
default="collaborative",
help_text="Indicates how the access to the view is determined. "
"By default, views are collaborative and shared for all users "
"that have access to the table.",
max_length=255,
),
),
]

View file

@ -39,10 +39,6 @@ class ListRowNamesDatabaseTableOperationType(DatabaseTableOperationType):
type = "database.table.list_row_names"
class ListAggregationDatabaseTableOperationType(DatabaseTableOperationType):
type = "database.table.list_aggregations"
class CreateRowDatabaseTableOperationType(DatabaseTableOperationType):
type = "database.table.create_row"

View file

@ -16,6 +16,7 @@ from baserow.contrib.database.fields.handler import FieldHandler
from baserow.contrib.database.fields.models import Field
from baserow.contrib.database.table.handler import TableHandler
from baserow.contrib.database.table.models import Table
from baserow.contrib.database.views.exceptions import ViewDoesNotExist, ViewNotInTable
from baserow.contrib.database.views.handler import FieldOptionsDict, ViewHandler
from baserow.contrib.database.views.models import (
View,
@ -634,7 +635,11 @@ class OrderViewsActionType(UndoableActionType):
:param order: The new order of the views.
"""
original_order = ViewHandler().get_views_order(user, table)
try:
view = ViewHandler().get_view(order[0])
except ViewDoesNotExist:
raise ViewNotInTable
original_order = ViewHandler().get_views_order(user, table, view.ownership_type)
ViewHandler().order_views(user, table, order)
@ -818,13 +823,13 @@ class RotateViewSlugActionType(UndoableActionType):
@classmethod
def undo(cls, user: AbstractUser, params: Params, action_to_undo: Action):
view_handler = ViewHandler()
view = view_handler.get_view_for_update(params.view_id)
view = view_handler.get_view_for_update(user, params.view_id)
view_handler.update_view_slug(user, view, params.original_slug)
@classmethod
def redo(cls, user: AbstractUser, params: Params, action_to_redo: Action):
view_handler = ViewHandler()
view = view_handler.get_view_for_update(params.view_id)
view = view_handler.get_view_for_update(user, params.view_id)
view_handler.update_view_slug(user, view, params.slug)
@ -903,13 +908,13 @@ class UpdateViewActionType(UndoableActionType):
@classmethod
def undo(cls, user: AbstractUser, params: Params, action_to_undo: Action):
view_handler = ViewHandler()
view = view_handler.get_view_for_update(params.view_id).specific
view = view_handler.get_view_for_update(user, params.view_id).specific
view_handler.update_view(user, view, **params.original_data)
@classmethod
def redo(cls, user: AbstractUser, params: Params, action_to_redo: Action):
view_handler = ViewHandler()
view = view_handler.get_view_for_update(params.view_id).specific
view = view_handler.get_view_for_update(user, params.view_id).specific
view_handler.update_view(user, view, **params.data)
@ -1190,7 +1195,7 @@ class CreateDecorationActionType(UndoableActionType):
@classmethod
def undo(cls, user: AbstractUser, params: Params, action_to_undo: Action):
view_decoration = ViewHandler().get_decoration(params.decorator_id)
view_decoration = ViewHandler().get_decoration(user, params.decorator_id)
ViewHandler().delete_decoration(view_decoration, user=user)
@classmethod
@ -1308,7 +1313,7 @@ class UpdateDecorationActionType(UndoableActionType):
@classmethod
def undo(cls, user: AbstractUser, params: Params, action_being_undone: Action):
view_decoration = ViewHandler().get_decoration(params.decorator_id)
view_decoration = ViewHandler().get_decoration(user, params.decorator_id)
ViewHandler().update_decoration(
view_decoration,
user=user,
@ -1320,7 +1325,7 @@ class UpdateDecorationActionType(UndoableActionType):
@classmethod
def redo(cls, user: AbstractUser, params: Params, action_being_redone: Action):
view_decoration = ViewHandler().get_decoration(params.decorator_id)
view_decoration = ViewHandler().get_decoration(user, params.decorator_id)
ViewHandler().update_decoration(
view_decoration,
user=user,
@ -1411,5 +1416,7 @@ class DeleteDecorationActionType(UndoableActionType):
@classmethod
def redo(cls, user: AbstractUser, params: Any, action_being_redone: Action):
view_decoration = ViewHandler().get_decoration(params.original_decorator_id)
view_decoration = ViewHandler().get_decoration(
user, params.original_decorator_id
)
ViewHandler().delete_decoration(view_decoration, user=user)

View file

@ -173,3 +173,10 @@ class NoAuthorizationToPubliclySharedView(Exception):
Raised when someone tries to access a view without a valid authorization
token.
"""
class ViewOwnerhshipTypeDoesNotExist(InstanceTypeDoesNotExist):
"""
Raised when trying to get a view ownership type
that does not exist.
"""

View file

@ -6,6 +6,7 @@ from typing import Any, Dict, Iterable, List, Optional, Set, Tuple, Type, Union
from django.conf import settings
from django.contrib.auth.models import AbstractUser, AnonymousUser
from django.contrib.contenttypes.models import ContentType
from django.core.cache import cache
from django.core.exceptions import FieldDoesNotExist, ValidationError
from django.db import models as django_models
@ -26,6 +27,7 @@ from baserow.contrib.database.rows.handler import RowHandler
from baserow.contrib.database.rows.signals import rows_created
from baserow.contrib.database.table.models import GeneratedTableModel, Table
from baserow.contrib.database.views.operations import (
CreateViewDecorationOperationType,
CreateViewFilterOperationType,
CreateViewOperationType,
CreateViewSortOperationType,
@ -34,17 +36,28 @@ from baserow.contrib.database.views.operations import (
DeleteViewOperationType,
DeleteViewSortOperationType,
DuplicateViewOperationType,
ListAggregationsViewOperationType,
ListViewDecorationOperationType,
ListViewFilterOperationType,
ListViewsOperationType,
ListViewSortOperationType,
OrderViewsOperationType,
ReadAggregationsViewOperationType,
ReadViewDecorationOperationType,
ReadViewFieldOptionsOperationType,
ReadViewFilterOperationType,
ReadViewOperationType,
ReadViewsOrderOperationType,
ReadViewSortOperationType,
UpdateViewDecorationOperationType,
UpdateViewFieldOptionsOperationType,
UpdateViewFilterOperationType,
UpdateViewOperationType,
UpdateViewSlugOperationType,
UpdateViewSortOperationType,
)
from baserow.contrib.database.views.registries import view_ownership_type_registry
from baserow.core.db import specific_iterator
from baserow.core.handler import CoreHandler
from baserow.core.trash.handler import TrashHandler
from baserow.core.utils import (
@ -74,7 +87,13 @@ from .exceptions import (
ViewSortFieldNotSupported,
ViewSortNotSupported,
)
from .models import View, ViewDecoration, ViewFilter, ViewSort
from .models import (
OWNERSHIP_TYPE_COLLABORATIVE,
View,
ViewDecoration,
ViewFilter,
ViewSort,
)
from .registries import (
decorator_type_registry,
decorator_value_provider_type_registry,
@ -109,6 +128,90 @@ ending_number_regex = re.compile(r"(.+) (\d+)$")
class ViewHandler:
PUBLIC_VIEW_TOKEN_ALGORITHM = "HS256" # nosec
def list_views(
self,
user: AbstractUser,
table: Table,
_type: str,
filters: bool,
sortings: bool,
decorations: bool,
limit: int,
) -> Iterable[View]:
"""
Lists available views for a user/table combination.
:user: The user on whose behalf we want to return views.
:table: The table for which the views should be returned.
:_type: The view type to get.
:filters: If filters should be prefetched.
:sortings: If sorts should be prefetched.
:decorations: If view decorations should be prefetched.
:limit: To limit the number of returned views.
:return: Iterator over returned views.
"""
views = View.objects.filter(table=table)
views = CoreHandler().filter_queryset(
user, ListViewsOperationType.type, views, table.database.group
)
views = views.select_related("content_type", "table")
if _type:
view_type = view_type_registry.get(_type)
content_type = ContentType.objects.get_for_model(view_type.model_class)
views = views.filter(content_type=content_type)
if filters:
views = views.prefetch_related("viewfilter_set")
if sortings:
views = views.prefetch_related("viewsort_set")
if decorations:
views = views.prefetch_related("viewdecoration_set")
if limit:
views = views[:limit]
views = specific_iterator(views)
return views
def get_view_as_user(
self,
user: AbstractUser,
view_id: int,
view_model: Optional[Type[View]] = None,
base_queryset: Optional[QuerySet] = None,
) -> View:
"""
Selects a view and checks if the user has access to that view.
If everything is fine the view is returned.
:param user: User on whose behalf to get the view.
:param view_id: The identifier of the view that must be returned.
:param view_model: If provided that models objects are used to select the
view. This can for example be useful when you want to select a GridView or
other child of the View model.
:param base_queryset: The base queryset from where to select the view
object. This can for example be used to do a `select_related`. Note that
if this is used the `view_model` parameter doesn't work anymore.
:raises ViewDoesNotExist: When the view with the provided id does not exist.
:raises PermissionDenied: When not allowed.
:return: the view instance.
"""
view = self.get_view(view_id, view_model, base_queryset)
CoreHandler().check_permissions(
user,
ReadViewOperationType.type,
group=view.table.database.group,
context=view,
allow_if_template=True,
)
return view
def get_view(
self,
view_id: int,
@ -152,6 +255,7 @@ class ViewHandler:
def get_view_for_update(
self,
user: AbstractUser,
view_id: int,
view_model: Optional[Type[View]] = None,
base_queryset: Optional[QuerySet] = None,
@ -160,6 +264,7 @@ class ViewHandler:
Selects a view for update and checks if the user has access to that view.
If everything is fine the view is returned.
:param: User on whose behalf to get the view.
:param view_id: The identifier of the view that must be returned.
:param view_model: If provided that models objects are used to select the
view. This can for example be useful when you want to select a GridView or
@ -182,7 +287,7 @@ class ViewHandler:
tables_to_lock = ("self", "view_ptr_id")
base_queryset = view_model.objects.select_for_update(of=tables_to_lock)
return self.get_view(view_id, view_model, base_queryset)
return self.get_view_as_user(user, view_id, view_model, base_queryset)
def create_view(
self, user: AbstractUser, table: Table, type_name: str, **kwargs
@ -194,6 +299,9 @@ class ViewHandler:
:param table: The table that the view instance belongs to.
:param type_name: The type name of the view.
:param kwargs: The fields that need to be set upon creation.
:raises PermissionDenied: When not allowed.
:raises ViewOwnerhshipTypeDoesNotExist: When the provided
view ownership type in kwargs doesn't exist.
:return: The created view instance.
"""
@ -208,8 +316,13 @@ class ViewHandler:
model_class = view_type.model_class
view_values = view_type.prepare_values(kwargs, table, user)
view_ownership_type = kwargs.get("ownership_type", OWNERSHIP_TYPE_COLLABORATIVE)
view_ownership_type_registry.get(view_ownership_type)
allowed_fields = [
"name",
"ownership_type",
"filter_type",
"filters_disabled",
] + view_type.allowed_fields
@ -217,7 +330,7 @@ class ViewHandler:
last_order = model_class.get_last_order(table)
instance = model_class.objects.create(
table=table, order=last_order, **view_values
table=table, order=last_order, created_by=user, **view_values
)
view_type.view_created(view=instance)
@ -283,7 +396,10 @@ class ViewHandler:
original_view.table, serialized, id_mapping
)
queryset = View.objects.filter(table_id=original_view.table.id)
# We want to order views from the same table with the same ownership_type only
queryset = View.objects.filter(
table_id=original_view.table.id, ownership_type=original_view.ownership_type
)
view_ids = queryset.values_list("id", flat=True)
ordered_ids = []
@ -300,7 +416,10 @@ class ViewHandler:
self, view=duplicated_view, user=user, type_name=view_type.type
)
views_reordered.send(
self, table=original_view.table, order=full_order, user=None
self,
table=original_view.table,
order=full_order,
user=user,
)
return duplicated_view
@ -364,14 +483,17 @@ class ViewHandler:
user, OrderViewsOperationType.type, group=group, context=table
)
all_views = View.objects.filter(table_id=table.id)
try:
first_view = self.get_view(order[0])
except ViewDoesNotExist:
raise ViewNotInTable()
all_views = View.objects.filter(table_id=table.id).filter(
ownership_type=first_view.ownership_type
)
user_views = CoreHandler().filter_queryset(
user,
OrderViewsOperationType.type,
all_views,
group=group,
context=table,
user, ListViewsOperationType.type, all_views, group=table.database.group
)
view_ids = user_views.values_list("id", flat=True)
@ -380,25 +502,39 @@ class ViewHandler:
if view_id not in view_ids:
raise ViewNotInTable(view_id)
full_order = View.order_objects(all_views, order)
views_reordered.send(self, table=table, order=full_order, user=user)
full_order = View.order_objects(user_views, order)
views_reordered.send(
self,
table=table,
order=full_order,
user=user,
)
def get_views_order(self, user: AbstractUser, table: Table):
def get_views_order(self, user: AbstractUser, table: Table, ownership_type: str):
"""
Returns the order of the views in the given table.
:param user: The user on whose behalf the views are ordered.
:param table: The table of which the views must be updated.
:param ownership_type: The type of views for which to return the order.
:raises ViewNotInTable: If one of the view ids in the order does not belong
to the table.
"""
if ownership_type is None:
ownership_type = OWNERSHIP_TYPE_COLLABORATIVE
group = table.database.group
CoreHandler().check_permissions(
user, ReadViewsOrderOperationType.type, group=group, context=table
)
queryset = View.objects.filter(table_id=table.id)
queryset = View.objects.filter(table_id=table.id).filter(
ownership_type=ownership_type
)
queryset = CoreHandler().filter_queryset(
user, ListViewsOperationType.type, queryset, table.database.group
)
order = queryset.values_list("id", flat=True)
order = list(order)
@ -413,7 +549,7 @@ class ViewHandler:
:param view_id: The view instance id that needs to be deleted.
"""
view = self.get_view_for_update(view_id)
view = self.get_view_for_update(user, view_id)
self.delete_view(user, view)
def delete_view(self, user: AbstractUser, view: View):
@ -439,6 +575,26 @@ class ViewHandler:
view_deleted.send(self, view_id=view_id, view=view, user=user)
def get_field_options_as_user(self, user: AbstractUser, view: View):
"""
Returns a serializer class to get field options stored for the view.
:param user: The user on whose behalf the options are requested.
:param view: The view for which the options should be returned.
:returns: View type that has get_field_options_serializer_class().
"""
group = view.table.database.group
CoreHandler().check_permissions(
user,
ReadViewFieldOptionsOperationType.type,
group=group,
context=view,
allow_if_template=True,
)
view_type = view_type_registry.get_by_model(view)
return view_type
def update_field_options(
self,
view: View,
@ -684,6 +840,23 @@ class ViewHandler:
filter_builder = self._get_filter_builder(view, model)
return filter_builder.apply_to_queryset(queryset)
def list_filters(self, user: AbstractUser, view_id: int) -> QuerySet[ViewFilter]:
"""
Returns the ViewFilter queryset for the provided view_id.
:param user: The user on whose behalf the filters are requested.
:param view_id: The id of the view for which we want to return filters.
:returns: ViewFilter queryset for the view_id.
"""
view = self.get_view(view_id)
group = view.table.database.group
CoreHandler().check_permissions(
user, ListViewFilterOperationType.type, group=group, context=view
)
filters = ViewFilter.objects.filter(view=view)
return filters
def get_filter(
self,
user: AbstractUser,
@ -977,6 +1150,25 @@ class ViewHandler:
return queryset
def list_sorts(self, user: AbstractUser, view_id: int) -> QuerySet[ViewSort]:
"""
Returns the ViewSort queryset for provided view_id.
:param user: The user on whose behalf the sorts are requested.
:param view_id: The id of the view for which to return sorts.
:return: ViewSort queryset of the view's sorts.
"""
view = ViewHandler().get_view(view_id)
CoreHandler().check_permissions(
user,
ListViewSortOperationType.type,
group=view.table.database.group,
context=view,
)
sortings = ViewSort.objects.filter(view=view)
return sortings
def get_sort(self, user, view_sort_id, base_queryset=None):
"""
Returns an existing view sort with the given id.
@ -1195,11 +1387,20 @@ class ViewHandler:
:param value_provider_conf: The configuration used by the value provider to
compute the values for the decorator.
:param order: The order of the decoration.
:param user: Optional user who have created the decoration.
:param user: Optional user who is creating the decoration.
:param primary_key: An optional primary key to give to the new view sort.
:return: The created view decoration instance.
"""
if user:
group = view.table.database.group
CoreHandler().check_permissions(
user,
CreateViewDecorationOperationType.type,
group=group,
context=view,
)
# Check if view supports decoration
view_type = view_type_registry.get_by_model(view.specific_class)
if not view_type.can_decorate:
@ -1238,14 +1439,37 @@ class ViewHandler:
return view_decoration
def list_decorations(
self, user: AbstractUser, view_id: int
) -> QuerySet[ViewDecoration]:
"""
Lists view's decorations.
:param user: The user on whose behalf are the decorations requested.
:param view_id: The id of the view for which to list decorations.
:return: ViewDecoration queryset for the particular view.
"""
view = ViewHandler().get_view(view_id)
CoreHandler().check_permissions(
user,
ListViewDecorationOperationType.type,
group=view.table.database.group,
context=view,
)
decorations = ViewDecoration.objects.filter(view=view)
return decorations
def get_decoration(
self,
user: AbstractUser,
view_decoration_id: int,
base_queryset: QuerySet = None,
) -> ViewDecoration:
"""
Returns an existing view decoration with the given id.
:param user: The user on whose behalf is the decoration requested.
:param view_decoration_id: The id of the view decoration.
:param base_queryset: The base queryset from where to select the view decoration
object from. This can for example be used to do a `select_related`.
@ -1261,6 +1485,13 @@ class ViewHandler:
view_decoration = base_queryset.select_related(
"view__table__database__group"
).get(pk=view_decoration_id)
group = view_decoration.view.table.database.group
CoreHandler().check_permissions(
user,
ReadViewDecorationOperationType.type,
group=group,
context=view_decoration,
)
except ViewDecoration.DoesNotExist:
raise ViewDecorationDoesNotExist(
f"The view decoration with id {view_decoration_id} does not exist."
@ -1288,7 +1519,7 @@ class ViewHandler:
Updates the values of an existing view decoration.
:param view_decoration: The view decoration that needs to be updated.
:param user: Optional user who have created the decoration..
:param user: Optionally a user on whose behalf the decoration is updated.
:param decorator_type_name: The type of the decorator.
:param value_provider_type_name: The value provider that provides the value
to the decorator.
@ -1302,6 +1533,15 @@ class ViewHandler:
:return: The updated view decoration instance.
"""
if user:
group = view_decoration.view.table.database.group
CoreHandler().check_permissions(
user,
UpdateViewDecorationOperationType.type,
group=group,
context=view_decoration,
)
if decorator_type_name is None:
decorator_type_name = view_decoration.type
if value_provider_type_name is None:
@ -1519,6 +1759,7 @@ class ViewHandler:
def get_view_field_aggregations(
self,
user: AbstractUser,
view: View,
model: Union[GeneratedTableModel, None] = None,
with_total: bool = False,
@ -1532,6 +1773,7 @@ class ViewHandler:
The dict keys are field names and value are aggregation values. The total is
included in result if the with_total is specified.
:param user: The user on whose behalf we are requesting the aggregations.
:param view: The view to get the field aggregation for.
:param model: The model for this view table to generate the aggregation
query from, if not specified then the model will be generated
@ -1545,6 +1787,14 @@ class ViewHandler:
:return: A dict of aggregation value
"""
CoreHandler().check_permissions(
user,
ListAggregationsViewOperationType.type,
group=view.table.database.group,
context=view,
allow_if_template=True,
)
view_type = view_type_registry.get_by_model(view.specific_class)
# Check if view supports field aggregation
@ -1581,6 +1831,7 @@ class ViewHandler:
# Do we need to compute some aggregations?
if need_computation or with_total:
db_result = self.get_field_aggregations(
user,
view,
[
(n["instance"], n["aggregation_type"])
@ -1619,6 +1870,7 @@ class ViewHandler:
def get_field_aggregations(
self,
user: AbstractUser,
view: View,
aggregations: Iterable[Tuple[django_models.Field, str]],
model: Union[GeneratedTableModel, None] = None,
@ -1630,6 +1882,7 @@ class ViewHandler:
The dict keys are field names and value are aggregation values. The total is
included in result if the with_total is specified.
:param user: The user on whose behalf we are requesting the aggregations.
:param view: The view to get the field aggregation for.
:param aggregations: A list of (field_instance, aggregation_type).
:param model: The model for this view table to generate the aggregation
@ -1645,6 +1898,14 @@ class ViewHandler:
:return: A dict of aggregation values
"""
CoreHandler().check_permissions(
user,
ReadAggregationsViewOperationType.type,
group=view.table.database.group,
context=view,
allow_if_template=True,
)
if model is None:
model = view.table.get_model()

View file

@ -1,6 +1,7 @@
import secrets
from django.contrib.auth.hashers import check_password, make_password
from django.contrib.auth.models import User
from django.contrib.contenttypes.models import ContentType
from django.db import models, transaction
from django.db.models import Q
@ -40,6 +41,9 @@ FORM_VIEW_SUBMIT_ACTION_CHOICES = (
(FORM_VIEW_SUBMIT_ACTION_REDIRECT, "Redirect"),
)
OWNERSHIP_TYPE_COLLABORATIVE = "collaborative"
VIEW_OWNERSHIP_TYPES = [OWNERSHIP_TYPE_COLLABORATIVE]
def get_default_view_content_type():
return ContentType.objects.get_for_model(View)
@ -53,6 +57,7 @@ class View(
PolymorphicContentTypeMixin,
models.Model,
):
table = models.ForeignKey("database.Table", on_delete=models.CASCADE)
order = models.PositiveIntegerField()
name = models.CharField(max_length=255)
@ -94,6 +99,16 @@ class View(
default=True,
help_text="Indicates whether the logo should be shown in the public view.",
)
created_by = models.ForeignKey(User, null=True, on_delete=models.SET_NULL)
ownership_type = models.CharField(
max_length=255,
default=OWNERSHIP_TYPE_COLLABORATIVE,
help_text=(
"Indicates how the access to the view is determined."
" By default, views are collaborative and shared for all users"
" that have access to the table."
),
)
@property
def public_view_has_password(self) -> bool:

View file

@ -102,6 +102,14 @@ class ListViewFilterOperationType(ViewOperationType):
object_scope_name = DatabaseViewFilterObjectScopeType.type
class ListAggregationsViewOperationType(ViewOperationType):
type = "database.table.view.list_aggregations"
class ReadAggregationsViewOperationType(ViewOperationType):
type = "database.table.view.read_aggregation"
class ReadViewFilterOperationType(ViewFilterOperationType):
type = "database.table.view.filter.read"
@ -129,6 +137,7 @@ class ViewDecorationOperationType(ViewOperationType, abc.ABC):
class ReadViewDecorationOperationType(ViewDecorationOperationType):
type = "database.table.view.decoration.read"
object_scope_name = DatabaseViewDecorationObjectScopeType.type
class UpdateViewDecorationOperationType(ViewDecorationOperationType):

View file

@ -5,6 +5,7 @@ from typing import (
Dict,
Iterable,
List,
Literal,
Optional,
Set,
Tuple,
@ -21,6 +22,7 @@ from rest_framework.fields import CharField
from rest_framework.serializers import Serializer
from baserow.contrib.database.fields.field_filters import OptionallyAnnotatedQ
from baserow.core.models import Group, GroupUser
from baserow.core.registry import (
APIUrlsInstanceMixin,
APIUrlsRegistryMixin,
@ -43,6 +45,7 @@ from .exceptions import (
DecoratorValueProviderTypeDoesNotExist,
ViewFilterTypeAlreadyRegistered,
ViewFilterTypeDoesNotExist,
ViewOwnerhshipTypeDoesNotExist,
ViewTypeAlreadyRegistered,
ViewTypeDoesNotExist,
)
@ -203,6 +206,8 @@ class ViewType(
"type": self.type,
"name": view.name,
"order": view.order,
"ownership_type": view.ownership_type,
"created_by": view.created_by.email if view.created_by else None,
}
if self.can_filter:
@ -250,7 +255,7 @@ class ViewType(
id_mapping: Dict[str, Any],
files_zip: Optional[ZipFile] = None,
storage: Optional[Storage] = None,
) -> "View":
) -> Optional["View"]:
"""
Imported an exported serialized view dict that was exported via the
`export_serialized` method. Note that all the fields must be imported first
@ -275,6 +280,38 @@ class ViewType(
id_mapping["database_view_sortings"] = {}
id_mapping["database_view_decorations"] = {}
if "created_by" not in id_mapping:
id_mapping["created_by"] = {}
created_by_group = table.database.group
if (
id_mapping.get("import_group_id", None) is not None
and created_by_group is None
):
created_by_group = Group.objects.get(id=id_mapping["import_group_id"])
if created_by_group is not None:
groupusers_from_group = GroupUser.objects.filter(
group_id=created_by_group.id
).select_related("user")
for groupuser in groupusers_from_group:
id_mapping["created_by"][groupuser.user.email] = groupuser.user
try:
ownership_type = view_ownership_type_registry.get(
serialized_values.get("ownership_type", None)
)
except view_ownership_type_registry.does_not_exist_exception_class:
return None
if not ownership_type.can_import_view(serialized_values, id_mapping):
return None
email = serialized_values.get("created_by", None)
serialized_values["created_by"] = id_mapping["created_by"].get(email, None)
serialized_copy = serialized_values.copy()
view_id = serialized_copy.pop("id")
serialized_copy.pop("type")
@ -1054,6 +1091,47 @@ class FormViewModeRegistry(Registry):
return [(t, t) for t in form_view_mode_registry.get_types()]
class ViewOwnershipType(Instance):
"""
A `ViewOwnershipType` represents allowed style of view ownership.
"""
def can_import_view(self, serialized_data: Dict, id_mapping: Dict) -> bool:
"""
Returns True if the a view with this ownership can be imported.
"""
return True
def should_broadcast_signal_to(
self, view: "View"
) -> Tuple[Literal["table", "users", ""], Optional[List[int]]]:
"""
Returns a tuple that represent the kind of signaling that must be done for the
given view.
:param view: the view we want to send the signal for.
:return: The first element of the tuple must be "" if no signaling is needed,
"users" if signal has to be send to a list of users and "table" if the
signal can be send to all the users of the view table.
The second member of the tuple can be any object necessary for the signal
depending of the type.
If the signal type is "users", it must be a list of user ids.
For other type it's None.
"""
return "table", None
class ViewOwnershipTypeRegistry(Registry):
"""
Contains all registered view ownership types.
"""
name = "view_ownership_type"
does_not_exist_exception_class = ViewOwnerhshipTypeDoesNotExist
# A default view type registry is created here, this is the one that is used
# throughout the whole Baserow application to add a new view type.
view_type_registry = ViewTypeRegistry()
@ -1062,3 +1140,4 @@ view_aggregation_type_registry = ViewAggregationTypeRegistry()
decorator_type_registry = DecoratorTypeRegistry()
decorator_value_provider_type_registry = DecoratorValueProviderTypeRegistry()
form_view_mode_registry = FormViewModeRegistry()
view_ownership_type_registry: ViewOwnershipTypeRegistry = ViewOwnershipTypeRegistry()

View file

@ -0,0 +1,10 @@
from baserow.contrib.database.views.registries import ViewOwnershipType
class CollaborativeViewOwnershipType(ViewOwnershipType):
"""
Represents views that are shared between all users that can access
a specific table.
"""
type = "collaborative"

View file

@ -132,22 +132,22 @@ class GridViewType(ViewType):
grid_view = super().import_serialized(
table, serialized_copy, id_mapping, files_zip, storage
)
if grid_view:
if "database_grid_view_field_options" not in id_mapping:
id_mapping["database_grid_view_field_options"] = {}
if "database_grid_view_field_options" not in id_mapping:
id_mapping["database_grid_view_field_options"] = {}
for field_option in field_options:
field_option_copy = field_option.copy()
field_option_id = field_option_copy.pop("id")
field_option_copy["field_id"] = id_mapping["database_fields"][
field_option["field_id"]
]
field_option_object = GridViewFieldOptions.objects.create(
grid_view=grid_view, **field_option_copy
)
id_mapping["database_grid_view_field_options"][
field_option_id
] = field_option_object.id
for field_option in field_options:
field_option_copy = field_option.copy()
field_option_id = field_option_copy.pop("id")
field_option_copy["field_id"] = id_mapping["database_fields"][
field_option["field_id"]
]
field_option_object = GridViewFieldOptions.objects.create(
grid_view=grid_view, **field_option_copy
)
id_mapping["database_grid_view_field_options"][
field_option_id
] = field_option_object.id
return grid_view
@ -158,7 +158,6 @@ class GridViewType(ViewType):
"""
grid_view = ViewHandler().get_view(view.id, view_model=GridView)
ordered_visible_field_ids = self.get_visible_field_options_in_order(
grid_view
).values_list("field__id", flat=True)

View file

@ -8,239 +8,201 @@ from baserow.contrib.database.api.views.serializers import (
ViewSortSerializer,
)
from baserow.contrib.database.views import signals as view_signals
from baserow.contrib.database.views.registries import view_type_registry
from baserow.contrib.database.views.registries import (
view_ownership_type_registry,
view_type_registry,
)
from baserow.ws.registries import page_registry
from baserow.ws.tasks import broadcast_to_users
def broadcast_to(user, view, payload):
ownership_type = view_ownership_type_registry.get(view.ownership_type)
broadcast_type, params = ownership_type.should_broadcast_signal_to(view)
if broadcast_type == "users":
transaction.on_commit(
lambda: broadcast_to_users.delay(
params,
payload,
getattr(user, "web_socket_id", None),
)
)
if broadcast_type == "table":
table_page_type = page_registry.get("table")
transaction.on_commit(
lambda: table_page_type.broadcast(
payload,
getattr(user, "web_socket_id", None),
table_id=view.table_id,
)
)
@receiver(view_signals.view_created)
def view_created(sender, view, user, **kwargs):
table_page_type = page_registry.get("table")
transaction.on_commit(
lambda: table_page_type.broadcast(
{
"type": "view_created",
"view": view_type_registry.get_serializer(
view,
ViewSerializer,
filters=True,
sortings=True,
decorations=True,
).data,
},
getattr(user, "web_socket_id", None),
table_id=view.table_id,
)
)
payload = {
"type": "view_created",
"view": view_type_registry.get_serializer(
view,
ViewSerializer,
filters=True,
sortings=True,
decorations=True,
).data,
}
broadcast_to(user, view, payload)
@receiver(view_signals.view_updated)
def view_updated(sender, view, user, **kwargs):
table_page_type = page_registry.get("table")
transaction.on_commit(
lambda: table_page_type.broadcast(
{
"type": "view_updated",
"view_id": view.id,
"view": view_type_registry.get_serializer(
view,
ViewSerializer,
# We do not want to broad cast the filters, decorations and sortings
# every time the view changes.
# There are separate views and handlers for them
# each will broad cast their own message.
filters=False,
sortings=False,
decorations=False,
).data,
},
getattr(user, "web_socket_id", None),
table_id=view.table_id,
)
)
payload = {
"type": "view_updated",
"view_id": view.id,
"view": view_type_registry.get_serializer(
view,
ViewSerializer,
# We do not want to broad cast the filters, decorations and sortings
# every time the view changes.
# There are separate views and handlers for them
# each will broad cast their own message.
filters=False,
sortings=False,
decorations=False,
).data,
}
broadcast_to(user, view, payload)
@receiver(view_signals.view_deleted)
def view_deleted(sender, view_id, view, user, **kwargs):
table_page_type = page_registry.get("table")
transaction.on_commit(
lambda: table_page_type.broadcast(
{"type": "view_deleted", "table_id": view.table_id, "view_id": view_id},
getattr(user, "web_socket_id", None),
table_id=view.table_id,
)
)
payload = {"type": "view_deleted", "table_id": view.table_id, "view_id": view_id}
broadcast_to(user, view, payload)
@receiver(view_signals.views_reordered)
def views_reordered(sender, table, order, user, **kwargs):
table_page_type = page_registry.get("table")
transaction.on_commit(
lambda: table_page_type.broadcast(
{"type": "views_reordered", "table_id": table.id, "order": order},
getattr(user, "web_socket_id", None),
table_id=table.id,
)
)
payload = {"type": "views_reordered", "table_id": table.id, "order": order}
first_view = table.view_set.get(id=order[0])
# Here we are assuming that the views are all broadcasted the same way as they are
# and there should be from the same "owner"
broadcast_to(user, first_view, payload)
@receiver(view_signals.view_filter_created)
def view_filter_created(sender, view_filter, user, **kwargs):
table_page_type = page_registry.get("table")
transaction.on_commit(
lambda: table_page_type.broadcast(
{
"type": "view_filter_created",
"view_filter": ViewFilterSerializer(view_filter).data,
},
getattr(user, "web_socket_id", None),
table_id=view_filter.view.table_id,
)
)
payload = {
"type": "view_filter_created",
"view_filter": ViewFilterSerializer(view_filter).data,
}
broadcast_to(user, view_filter.view, payload)
@receiver(view_signals.view_filter_updated)
def view_filter_updated(sender, view_filter, user, **kwargs):
table_page_type = page_registry.get("table")
transaction.on_commit(
lambda: table_page_type.broadcast(
{
"type": "view_filter_updated",
"view_filter_id": view_filter.id,
"view_filter": ViewFilterSerializer(view_filter).data,
},
getattr(user, "web_socket_id", None),
table_id=view_filter.view.table_id,
)
)
payload = {
"type": "view_filter_updated",
"view_filter_id": view_filter.id,
"view_filter": ViewFilterSerializer(view_filter).data,
}
broadcast_to(user, view_filter.view, payload)
@receiver(view_signals.view_filter_deleted)
def view_filter_deleted(sender, view_filter_id, view_filter, user, **kwargs):
table_page_type = page_registry.get("table")
transaction.on_commit(
lambda: table_page_type.broadcast(
{
"type": "view_filter_deleted",
"view_id": view_filter.view_id,
"view_filter_id": view_filter_id,
},
getattr(user, "web_socket_id", None),
table_id=view_filter.view.table_id,
)
)
payload = {
"type": "view_filter_deleted",
"view_id": view_filter.view_id,
"view_filter_id": view_filter_id,
}
broadcast_to(user, view_filter.view, payload)
@receiver(view_signals.view_sort_created)
def view_sort_created(sender, view_sort, user, **kwargs):
table_page_type = page_registry.get("table")
transaction.on_commit(
lambda: table_page_type.broadcast(
{
"type": "view_sort_created",
"view_sort": ViewSortSerializer(view_sort).data,
},
getattr(user, "web_socket_id", None),
table_id=view_sort.view.table_id,
)
)
payload = {
"type": "view_sort_created",
"view_sort": ViewSortSerializer(view_sort).data,
}
broadcast_to(user, view_sort.view, payload)
@receiver(view_signals.view_sort_updated)
def view_sort_updated(sender, view_sort, user, **kwargs):
table_page_type = page_registry.get("table")
transaction.on_commit(
lambda: table_page_type.broadcast(
{
"type": "view_sort_updated",
"view_sort_id": view_sort.id,
"view_sort": ViewSortSerializer(view_sort).data,
},
getattr(user, "web_socket_id", None),
table_id=view_sort.view.table_id,
)
)
payload = {
"type": "view_sort_updated",
"view_sort_id": view_sort.id,
"view_sort": ViewSortSerializer(view_sort).data,
}
broadcast_to(user, view_sort.view, payload)
@receiver(view_signals.view_sort_deleted)
def view_sort_deleted(sender, view_sort_id, view_sort, user, **kwargs):
table_page_type = page_registry.get("table")
transaction.on_commit(
lambda: table_page_type.broadcast(
{
"type": "view_sort_deleted",
"view_id": view_sort.view_id,
"view_sort_id": view_sort_id,
},
getattr(user, "web_socket_id", None),
table_id=view_sort.view.table_id,
)
)
payload = {
"type": "view_sort_deleted",
"view_id": view_sort.view_id,
"view_sort_id": view_sort_id,
}
broadcast_to(user, view_sort.view, payload)
@receiver(view_signals.view_decoration_created)
def view_decoration_created(sender, view_decoration, user, **kwargs):
table_page_type = page_registry.get("table")
transaction.on_commit(
lambda: table_page_type.broadcast(
{
"type": "view_decoration_created",
"view_decoration": ViewDecorationSerializer(view_decoration).data,
},
getattr(user, "web_socket_id", None),
table_id=view_decoration.view.table_id,
)
)
payload = {
"type": "view_decoration_created",
"view_decoration": ViewDecorationSerializer(view_decoration).data,
}
broadcast_to(user, view_decoration.view, payload)
@receiver(view_signals.view_decoration_updated)
def view_decoration_updated(sender, view_decoration, user, **kwargs):
table_page_type = page_registry.get("table")
transaction.on_commit(
lambda: table_page_type.broadcast(
{
"type": "view_decoration_updated",
"view_decoration_id": view_decoration.id,
"view_decoration": ViewDecorationSerializer(view_decoration).data,
},
getattr(user, "web_socket_id", None),
table_id=view_decoration.view.table_id,
)
)
payload = {
"type": "view_decoration_updated",
"view_decoration_id": view_decoration.id,
"view_decoration": ViewDecorationSerializer(view_decoration).data,
}
broadcast_to(user, view_decoration.view, payload)
@receiver(view_signals.view_decoration_deleted)
def view_decoration_deleted(
sender, view_decoration_id, view_decoration, user, **kwargs
):
table_page_type = page_registry.get("table")
transaction.on_commit(
lambda: table_page_type.broadcast(
{
"type": "view_decoration_deleted",
"view_id": view_decoration.view_id,
"view_decoration_id": view_decoration_id,
},
getattr(user, "web_socket_id", None),
table_id=view_decoration.view.table_id,
)
)
payload = {
"type": "view_decoration_deleted",
"view_id": view_decoration.view_id,
"view_decoration_id": view_decoration_id,
}
broadcast_to(user, view_decoration.view, payload)
@receiver(view_signals.view_field_options_updated)
def view_field_options_updated(sender, view, user, **kwargs):
table_page_type = page_registry.get("table")
view_type = view_type_registry.get_by_model(view.specific_class)
serializer_class = view_type.get_field_options_serializer_class(
create_if_missing=False
)
transaction.on_commit(
lambda: table_page_type.broadcast(
{
"type": "view_field_options_updated",
"view_id": view.id,
"field_options": serializer_class(view).data["field_options"],
},
getattr(user, "web_socket_id", None),
table_id=view.table_id,
)
)
payload = {
"type": "view_field_options_updated",
"view_id": view.id,
"field_options": serializer_class(view).data["field_options"],
}
broadcast_to(user, view, payload)

View file

@ -273,8 +273,10 @@ def test_to_baserow_database_export():
"filters": [],
"sortings": [],
"decorations": [],
"ownership_type": "collaborative",
"public": False,
"field_options": [],
"created_by": None,
}
]

View file

@ -91,9 +91,12 @@ def test_exporting_missing_view_returns_error(data_fixture, api_client, tmpdir):
def test_exporting_view_which_isnt_for_table_returns_error(
data_fixture, api_client, tmpdir
):
user, token = data_fixture.create_user_and_token()
table = data_fixture.create_database_table(user=user)
grid_view_for_other_table = data_fixture.create_grid_view()
group = data_fixture.create_group()
user, token = data_fixture.create_user_and_token(group=group)
database = data_fixture.create_database_application(group=group)
table = data_fixture.create_database_table(user=user, database=database)
table2 = data_fixture.create_database_table(user=user, database=database)
grid_view_for_other_table = data_fixture.create_grid_view(table=table2)
response = api_client.post(
reverse(
"api:database:export:export_table",

View file

@ -391,8 +391,9 @@ def test_list_rows(api_client, data_fixture):
HTTP_AUTHORIZATION=f"JWT {jwt_token}",
data={"view_id": unrelated_view.id},
)
assert response.status_code == HTTP_404_NOT_FOUND
assert response_json["error"] == "ERROR_VIEW_DOES_NOT_EXIST"
response_json = response.json()
assert response.status_code == HTTP_400_BAD_REQUEST
assert response_json["error"] == "ERROR_USER_NOT_IN_GROUP"
@pytest.mark.django_db

View file

@ -306,8 +306,8 @@ def test_get_view_decoration(api_client, data_fixture):
),
HTTP_AUTHORIZATION=f"JWT {token}",
)
assert response.status_code == HTTP_404_NOT_FOUND
assert response.json()["error"] == "ERROR_VIEW_DECORATION_DOES_NOT_EXIST"
assert response.status_code == HTTP_400_BAD_REQUEST
assert response.json()["error"] == "ERROR_USER_NOT_IN_GROUP"
@pytest.mark.django_db

View file

@ -3,6 +3,7 @@ from unittest.mock import patch
from django.contrib.contenttypes.models import ContentType
from django.db import connection
from django.shortcuts import reverse
from django.test import override_settings
from django.test.utils import CaptureQueriesContext
import pytest
@ -94,6 +95,30 @@ def test_list_views(api_client, data_fixture):
assert response.json()["error"] == "ERROR_TABLE_DOES_NOT_EXIST"
@override_settings(PERMISSION_MANAGERS=["basic"])
@pytest.mark.django_db
def test_list_views_ownership_type(api_client, data_fixture):
user, token = data_fixture.create_user_and_token(
email="test@test.nl", password="password", first_name="Test1"
)
table_1 = data_fixture.create_database_table(user=user)
view_1 = data_fixture.create_grid_view(
table=table_1, order=1, ownership_type="collaborative"
)
view_2 = data_fixture.create_grid_view(
table=table_1, order=3, ownership_type="personal"
)
response = api_client.get(
reverse("api:database:views:list", kwargs={"table_id": table_1.id}),
**{"HTTP_AUTHORIZATION": f"JWT {token}"},
)
assert response.status_code == HTTP_200_OK
response_json = response.json()
assert len(response_json) == 2
@pytest.mark.django_db
def test_list_views_with_limit(api_client, data_fixture):
user, token = data_fixture.create_user_and_token(
@ -356,7 +381,7 @@ def test_order_views(api_client, data_fixture):
HTTP_AUTHORIZATION=f"JWT {token}",
)
assert response.status_code == HTTP_400_BAD_REQUEST
assert response.json()["error"] == "ERROR_USER_NOT_IN_GROUP"
assert response.json()["error"] == "ERROR_REQUEST_BODY_VALIDATION"
response = api_client.post(
reverse("api:database:views:order", kwargs={"table_id": 999999}),
@ -364,8 +389,8 @@ def test_order_views(api_client, data_fixture):
format="json",
HTTP_AUTHORIZATION=f"JWT {token}",
)
assert response.status_code == HTTP_404_NOT_FOUND
assert response.json()["error"] == "ERROR_TABLE_DOES_NOT_EXIST"
assert response.status_code == HTTP_400_BAD_REQUEST
assert response.json()["error"] == "ERROR_REQUEST_BODY_VALIDATION"
response = api_client.post(
reverse("api:database:views:order", kwargs={"table_id": table_1.id}),
@ -387,7 +412,9 @@ def test_order_views(api_client, data_fixture):
response = api_client.post(
reverse("api:database:views:order", kwargs={"table_id": table_1.id}),
{"view_ids": [view_3.id, view_2.id, view_1.id]},
{
"view_ids": [view_3.id, view_2.id, view_1.id],
},
format="json",
HTTP_AUTHORIZATION=f"JWT {token}",
)

View file

@ -22,6 +22,7 @@ from baserow.contrib.database.table.cache import invalidate_table_in_model_cache
from baserow.contrib.database.table.models import Table
from baserow.contrib.database.trash.models import TrashedRows
from baserow.contrib.database.views.handler import ViewHandler
from baserow.core.exceptions import PermissionDenied
from baserow.core.models import TrashEntry
from baserow.core.trash.exceptions import (
CannotRestoreChildBeforeParent,
@ -1549,9 +1550,12 @@ def test_trashing_two_linked_tables_after_one_perm_deleted_can_restore(
@pytest.mark.django_db
@pytest.mark.view_ownership
def test_trash_restore_view(data_fixture):
user = data_fixture.create_user()
database = data_fixture.create_database_application(user=user, name="Placeholder")
group = data_fixture.create_group(name="Group 1")
user = data_fixture.create_user(group=group)
user2 = data_fixture.create_user(group=group)
database = data_fixture.create_database_application(group=group)
table = data_fixture.create_database_table(name="Table 1", database=database)
view = data_fixture.create_grid_view(name="View 1", table=table)
@ -1567,6 +1571,20 @@ def test_trash_restore_view(data_fixture):
assert view.trashed is False
# test view ownership
view2 = data_fixture.create_grid_view(name="View 1", table=table)
view2.ownership_type = "personal"
view2.save()
TrashHandler.trash(user, database.group, database, view2)
with pytest.raises(PermissionDenied):
TrashHandler.restore_item(user, "view", view2.id)
with pytest.raises(PermissionDenied):
TrashHandler.restore_item(user2, "view", view2.id)
@pytest.mark.django_db
def test_can_perm_delete_application_with_linked_tables(data_fixture):

View file

@ -21,15 +21,15 @@ def test_can_undo_order_views(data_fixture):
ViewHandler().order_views(user, table, original_order)
assert ViewHandler().get_views_order(user, table) == original_order
assert ViewHandler().get_views_order(user, table, "collaborative") == original_order
action_type_registry.get_by_type(OrderViewsActionType).do(user, table, new_order)
assert ViewHandler().get_views_order(user, table) == new_order
assert ViewHandler().get_views_order(user, table, "collaborative") == new_order
ActionHandler.undo(user, [TableActionScopeType.value(table.id)], session_id)
assert ViewHandler().get_views_order(user, table) == original_order
assert ViewHandler().get_views_order(user, table, "collaborative") == original_order
@pytest.mark.django_db
@ -46,16 +46,16 @@ def test_can_undo_redo_order_views(data_fixture):
ViewHandler().order_views(user, table, original_order)
assert ViewHandler().get_views_order(user, table) == original_order
assert ViewHandler().get_views_order(user, table, "collaborative") == original_order
action_type_registry.get_by_type(OrderViewsActionType).do(user, table, new_order)
assert ViewHandler().get_views_order(user, table) == new_order
assert ViewHandler().get_views_order(user, table, "collaborative") == new_order
ActionHandler.undo(user, [TableActionScopeType.value(table.id)], session_id)
assert ViewHandler().get_views_order(user, table) == original_order
assert ViewHandler().get_views_order(user, table, "collaborative") == original_order
ActionHandler.redo(user, [TableActionScopeType.value(table.id)], session_id)
assert ViewHandler().get_views_order(user, table) == new_order
assert ViewHandler().get_views_order(user, table, "collaborative") == new_order

View file

@ -54,6 +54,7 @@ def test_view_empty_count_aggregation(data_fixture):
)
result = view_handler.get_field_aggregations(
user,
grid_view,
[
(
@ -76,6 +77,7 @@ def test_view_empty_count_aggregation(data_fixture):
assert result[f"field_{number_field.id}"] == 1
result = view_handler.get_field_aggregations(
user,
grid_view,
[
(
@ -97,6 +99,7 @@ def test_view_empty_count_aggregation(data_fixture):
assert result[f"field_{number_field.id}"] == 3
result = view_handler.get_field_aggregations(
user,
grid_view,
[
(
@ -112,7 +115,8 @@ def test_view_empty_count_aggregation(data_fixture):
@pytest.mark.django_db
def test_view_empty_count_aggregation_for_interesting_table(data_fixture):
table, _, _, _, context = setup_interesting_test_table(data_fixture)
user = data_fixture.create_user()
table, _, _, _, context = setup_interesting_test_table(data_fixture, user=user)
grid_view = data_fixture.create_grid_view(table=table)
model = table.get_model()
@ -130,7 +134,7 @@ def test_view_empty_count_aggregation_for_interesting_table(data_fixture):
)
result_empty = view_handler.get_field_aggregations(
grid_view, aggregation_query, model=model, with_total=True
user, grid_view, aggregation_query, model=model, with_total=True
)
aggregation_query = []
@ -144,7 +148,7 @@ def test_view_empty_count_aggregation_for_interesting_table(data_fixture):
)
result_not_empty = view_handler.get_field_aggregations(
grid_view, aggregation_query, model=model
user, grid_view, aggregation_query, model=model
)
for field in model._field_objects.values():
@ -157,7 +161,8 @@ def test_view_empty_count_aggregation_for_interesting_table(data_fixture):
@pytest.mark.django_db
def test_view_unique_count_aggregation_for_interesting_table(data_fixture):
table, _, _, _, context = setup_interesting_test_table(data_fixture)
user = data_fixture.create_user()
table, _, _, _, context = setup_interesting_test_table(data_fixture, user=user)
grid_view = data_fixture.create_grid_view(table=table)
model = table.get_model()
@ -178,7 +183,7 @@ def test_view_unique_count_aggregation_for_interesting_table(data_fixture):
)
result = view_handler.get_field_aggregations(
grid_view, aggregation_query, model=model, with_total=True
user, grid_view, aggregation_query, model=model, with_total=True
)
assert len(result.keys()) == 29
@ -233,6 +238,7 @@ def test_view_number_aggregation(data_fixture):
)
result = view_handler.get_field_aggregations(
user,
grid_view,
[
(
@ -244,6 +250,7 @@ def test_view_number_aggregation(data_fixture):
assert result[number_field.db_column] == 1
result = view_handler.get_field_aggregations(
user,
grid_view,
[
(
@ -255,6 +262,7 @@ def test_view_number_aggregation(data_fixture):
assert result[number_field.db_column] == 94
result = view_handler.get_field_aggregations(
user,
grid_view,
[
(
@ -267,6 +275,7 @@ def test_view_number_aggregation(data_fixture):
assert result[number_field.db_column] == 1546
result = view_handler.get_field_aggregations(
user,
grid_view,
[
(
@ -278,6 +287,7 @@ def test_view_number_aggregation(data_fixture):
assert round(result[number_field.db_column], 2) == Decimal("51.53")
result = view_handler.get_field_aggregations(
user,
grid_view,
[
(
@ -289,6 +299,7 @@ def test_view_number_aggregation(data_fixture):
assert round(result[number_field.db_column], 2) == Decimal("52.5")
result = view_handler.get_field_aggregations(
user,
grid_view,
[
(
@ -300,6 +311,7 @@ def test_view_number_aggregation(data_fixture):
assert round(result[number_field.db_column], 2) == Decimal("26.73")
result = view_handler.get_field_aggregations(
user,
grid_view,
[
(
@ -311,6 +323,7 @@ def test_view_number_aggregation(data_fixture):
assert round(result[number_field.db_column], 2) == Decimal("714.72")
result = view_handler.get_field_aggregations(
user,
grid_view,
[
(
@ -347,6 +360,7 @@ def test_view_aggregation_errors(data_fixture):
with pytest.raises(FieldAggregationNotSupported):
view_handler.get_field_aggregations(
user,
form_view,
[
(
@ -358,6 +372,7 @@ def test_view_aggregation_errors(data_fixture):
with pytest.raises(FieldNotInTable):
view_handler.get_field_aggregations(
user,
grid_view,
[
(
@ -419,27 +434,33 @@ def test_aggregation_is_updated_when_view_is_trashed(data_fixture):
)
# Verify both views have an aggregation
aggregations_view_one = view_handler.get_view_field_aggregations(grid_view_one)
aggregations_view_two = view_handler.get_view_field_aggregations(grid_view_two)
aggregations_view_one = view_handler.get_view_field_aggregations(
user, grid_view_one
)
aggregations_view_two = view_handler.get_view_field_aggregations(
user, grid_view_two
)
assert field.db_column in aggregations_view_one
assert field.db_column in aggregations_view_two
# Trash the view and verify that the aggregation is not retrievable anymore
TrashHandler().trash(user, application.group, application, trash_item=grid_view_one)
aggregations = view_handler.get_view_field_aggregations(grid_view_one)
aggregations = view_handler.get_view_field_aggregations(user, grid_view_one)
assert field.db_column not in aggregations
# Update the field and verify that the aggregation is removed from the
# not trashed view
FieldHandler().update_field(user, field, new_type_name="text")
aggregations_not_trashed_view = view_handler.get_view_field_aggregations(
grid_view_two
user, grid_view_two
)
assert field.db_column not in aggregations_not_trashed_view
# Restore the view and verify that the aggregation
# is also removed from the restored view
TrashHandler().restore_item(user, "view", grid_view_one.id)
aggregations_restored_view = view_handler.get_view_field_aggregations(grid_view_one)
aggregations_restored_view = view_handler.get_view_field_aggregations(
user, grid_view_one
)
assert field.db_column not in aggregations_restored_view

View file

@ -21,6 +21,7 @@ from baserow.contrib.database.views.exceptions import (
ViewFilterTypeDoesNotExist,
ViewFilterTypeNotAllowedForField,
ViewNotInTable,
ViewOwnerhshipTypeDoesNotExist,
ViewSortDoesNotExist,
ViewSortFieldAlreadyExist,
ViewSortFieldNotSupported,
@ -29,6 +30,7 @@ from baserow.contrib.database.views.exceptions import (
)
from baserow.contrib.database.views.handler import PublicViewRows, ViewHandler
from baserow.contrib.database.views.models import (
OWNERSHIP_TYPE_COLLABORATIVE,
FormView,
GridView,
View,
@ -40,8 +42,11 @@ from baserow.contrib.database.views.registries import (
view_filter_type_registry,
view_type_registry,
)
from baserow.contrib.database.views.view_ownership_types import (
CollaborativeViewOwnershipType,
)
from baserow.contrib.database.views.view_types import GridViewType
from baserow.core.exceptions import UserNotInGroup
from baserow.core.exceptions import PermissionDenied, UserNotInGroup
from baserow.core.trash.handler import TrashHandler
@ -54,9 +59,9 @@ def test_get_view(data_fixture):
handler = ViewHandler()
with pytest.raises(ViewDoesNotExist):
handler.get_view(view_id=99999)
handler.get_view_as_user(user, view_id=99999)
view = handler.get_view(view_id=grid.id)
view = handler.get_view_as_user(user, view_id=grid.id)
assert view.id == grid.id
assert view.name == grid.name
@ -64,7 +69,7 @@ def test_get_view(data_fixture):
assert not view.filters_disabled
assert isinstance(view, View)
view = handler.get_view(view_id=grid.id, view_model=GridView)
view = handler.get_view_as_user(user, view_id=grid.id, view_model=GridView)
assert view.id == grid.id
assert view.name == grid.name
@ -74,18 +79,20 @@ def test_get_view(data_fixture):
# If the error is raised we know for sure that the query has resolved.
with pytest.raises(AttributeError):
handler.get_view(
view_id=grid.id, base_queryset=View.objects.prefetch_related("UNKNOWN")
handler.get_view_as_user(
user,
view_id=grid.id,
base_queryset=View.objects.prefetch_related("UNKNOWN"),
)
# If the table is trashed the view should not be available.
TrashHandler.trash(user, grid.table.database.group, grid.table.database, grid.table)
with pytest.raises(ViewDoesNotExist):
handler.get_view(view_id=grid.id, view_model=GridView)
handler.get_view_as_user(user, view_id=grid.id, view_model=GridView)
# Restoring the table should restore the view
TrashHandler.restore_item(user, "table", grid.table.id)
view = handler.get_view(view_id=grid.id, view_model=GridView)
view = handler.get_view_as_user(user, view_id=grid.id, view_model=GridView)
assert view.id == grid.id
@ -113,6 +120,7 @@ def test_create_grid_view(send_mock, data_fixture):
assert grid.name == "Test grid"
assert grid.order == 1
assert grid.table == table
assert grid.created_by == user
assert grid.filter_type == "AND"
assert not grid.filters_disabled
@ -472,6 +480,12 @@ def test_order_views(send_mock, data_fixture):
grid_1 = data_fixture.create_grid_view(table=table, order=1)
grid_2 = data_fixture.create_grid_view(table=table, order=2)
grid_3 = data_fixture.create_grid_view(table=table, order=3)
grid_diff_ownership = data_fixture.create_grid_view(table=table, order=2)
grid_diff_ownership.ownership_type = "personal"
grid_diff_ownership.save()
grid_diff_ownership2 = data_fixture.create_grid_view(table=table, order=3)
grid_diff_ownership2.ownership_type = "personal"
grid_diff_ownership2.save()
handler = ViewHandler()
@ -481,7 +495,25 @@ def test_order_views(send_mock, data_fixture):
with pytest.raises(ViewNotInTable):
handler.order_views(user=user, table=table, order=[0])
handler.order_views(user=user, table=table, order=[grid_3.id, grid_2.id, grid_1.id])
with pytest.raises(ViewNotInTable):
handler.order_views(
user=user,
table=table,
order=[grid_diff_ownership.id, grid_3.id, grid_2.id, grid_1.id],
)
with pytest.raises(ViewNotInTable):
handler.order_views(
user=user,
table=table,
order=[grid_diff_ownership.id, grid_diff_ownership2.id],
)
handler.order_views(
user=user,
table=table,
order=[grid_3.id, grid_2.id, grid_1.id],
)
grid_1.refresh_from_db()
grid_2.refresh_from_db()
grid_3.refresh_from_db()
@ -494,7 +526,11 @@ def test_order_views(send_mock, data_fixture):
assert send_mock.call_args[1]["user"].id == user.id
assert send_mock.call_args[1]["order"] == [grid_3.id, grid_2.id, grid_1.id]
handler.order_views(user=user, table=table, order=[grid_1.id, grid_3.id, grid_2.id])
handler.order_views(
user=user,
table=table,
order=[grid_1.id, grid_3.id, grid_2.id],
)
grid_1.refresh_from_db()
grid_2.refresh_from_db()
grid_3.refresh_from_db()
@ -1690,13 +1726,15 @@ def test_get_public_views_which_include_rows(data_fixture):
assert checker.get_public_views_where_rows_are_visible([row, row2]) == [
PublicViewRows(
view=ViewHandler().get_view(public_view1.id), allowed_row_ids={1}
view=ViewHandler().get_view_as_user(user, public_view1.id),
allowed_row_ids={1},
),
PublicViewRows(
view=ViewHandler().get_view(public_view2.id), allowed_row_ids={2}
view=ViewHandler().get_view_as_user(user, public_view2.id),
allowed_row_ids={2},
),
PublicViewRows(
view=ViewHandler().get_view(public_view3.id),
view=ViewHandler().get_view_as_user(user, public_view3.id),
allowed_row_ids=PublicViewRows.ALL_ROWS_ALLOWED,
),
]
@ -2199,3 +2237,510 @@ def test_can_submit_form_view_handler_with_zero_number_required(data_fixture):
handler.submit_form_view(form=form, values={f"field_{number_field.id}": 0})
with pytest.raises(ValidationError):
handler.submit_form_view(form=form, values={f"field_{number_field.id}": False})
@pytest.mark.django_db
@pytest.mark.view_ownership
def test_list_views_ownership_type(data_fixture):
user = data_fixture.create_user()
table = data_fixture.create_database_table(user=user)
handler = ViewHandler()
view = handler.create_view(
user=user,
table=table,
type_name="grid",
name="Test grid",
ownership_type=OWNERSHIP_TYPE_COLLABORATIVE,
)
view2 = handler.create_view(
user=user,
table=table,
type_name="grid",
name="Test grid",
ownership_type=OWNERSHIP_TYPE_COLLABORATIVE,
)
view2.ownership_type = "personal"
view2.save()
result = handler.list_views(user, table, "grid", None, None, None, 10)
assert len(result) == 1
@pytest.mark.django_db
@pytest.mark.view_ownership
def test_get_view_ownership_type(data_fixture):
user = data_fixture.create_user()
table = data_fixture.create_database_table(user=user)
handler = ViewHandler()
view = handler.create_view(
user=user,
table=table,
type_name="grid",
name="Test grid",
ownership_type=OWNERSHIP_TYPE_COLLABORATIVE,
)
grid = handler.get_view_as_user(user, view.id)
assert grid.ownership_type == OWNERSHIP_TYPE_COLLABORATIVE
view.ownership_type = "personal"
view.save()
with pytest.raises(PermissionDenied):
handler.get_view_as_user(user, view.id)
@pytest.mark.django_db
@pytest.mark.view_ownership
def test_create_view_ownership_type(data_fixture):
ownership_types = {"collaborative": CollaborativeViewOwnershipType()}
with patch(
"baserow.contrib.database.views.registries.view_ownership_type_registry.registry",
ownership_types,
):
user = data_fixture.create_user()
table = data_fixture.create_database_table(user=user)
handler = ViewHandler()
view = handler.create_view(
user=user,
table=table,
type_name="grid",
name="Test grid",
ownership_type=OWNERSHIP_TYPE_COLLABORATIVE,
)
grid = GridView.objects.first()
assert grid.ownership_type == OWNERSHIP_TYPE_COLLABORATIVE
with pytest.raises(ViewOwnerhshipTypeDoesNotExist):
handler.create_view(
user=user,
table=table,
type_name="grid",
name="grid",
ownership_type="personal",
)
@pytest.mark.django_db
@pytest.mark.view_ownership
def test_update_view_ownership_type(data_fixture):
"""
Updating view.ownership_type is currently not allowed.
"""
user = data_fixture.create_user()
table = data_fixture.create_database_table(user=user)
form = data_fixture.create_form_view(table=table)
handler = ViewHandler()
handler.update_view(user=user, view=form, ownership_type="new_ownership_type")
form.refresh_from_db()
assert form.ownership_type == OWNERSHIP_TYPE_COLLABORATIVE
@pytest.mark.django_db
@pytest.mark.view_ownership
def test_duplicate_view_ownership_type(data_fixture):
group = data_fixture.create_group(name="Group 1")
user = data_fixture.create_user(group=group)
user2 = data_fixture.create_user(group=group)
database = data_fixture.create_database_application(group=group)
table = data_fixture.create_database_table(user=user, database=database)
handler = ViewHandler()
view = handler.create_view(
user=user,
table=table,
type_name="grid",
name="Test grid",
ownership_type=OWNERSHIP_TYPE_COLLABORATIVE,
)
duplicated = handler.duplicate_view(user2, view)
assert duplicated.ownership_type == OWNERSHIP_TYPE_COLLABORATIVE
view.ownership_type = "personal"
view.save()
with pytest.raises(PermissionDenied):
handler.duplicate_view(user, view)
with pytest.raises(PermissionDenied):
handler.duplicate_view(user2, view)
@pytest.mark.django_db
@pytest.mark.view_ownership
def test_delete_view_ownership_type(data_fixture):
group = data_fixture.create_group(name="Group 1")
user = data_fixture.create_user(group=group)
user2 = data_fixture.create_user(group=group)
database = data_fixture.create_database_application(group=group)
table = data_fixture.create_database_table(user=user, database=database)
handler = ViewHandler()
view = handler.create_view(
user=user,
table=table,
type_name="grid",
name="Test grid",
ownership_type=OWNERSHIP_TYPE_COLLABORATIVE,
)
view.ownership_type = "personal"
view.save()
with pytest.raises(PermissionDenied):
handler.delete_view(user, view)
with pytest.raises(PermissionDenied):
handler.delete_view(user2, view)
@pytest.mark.django_db
@pytest.mark.view_ownership
def test_field_options_view_ownership_type(data_fixture):
group = data_fixture.create_group(name="Group 1")
user = data_fixture.create_user(group=group)
user2 = data_fixture.create_user(group=group)
database = data_fixture.create_database_application(group=group)
table = data_fixture.create_database_table(user=user, database=database)
handler = ViewHandler()
view = handler.create_view(
user=user,
table=table,
type_name="grid",
name="Test grid",
ownership_type=OWNERSHIP_TYPE_COLLABORATIVE,
)
view.ownership_type = "personal"
view.save()
with pytest.raises(PermissionDenied):
handler.get_field_options_as_user(user, view)
with pytest.raises(PermissionDenied):
handler.get_field_options_as_user(user2, view)
with pytest.raises(PermissionDenied):
handler.update_field_options(view, {}, user)
with pytest.raises(PermissionDenied):
handler.update_field_options(view, {}, user2)
@pytest.mark.django_db
@pytest.mark.view_ownership
def test_filters_view_ownership_type(data_fixture):
group = data_fixture.create_group(name="Group 1")
user = data_fixture.create_user(group=group)
user2 = data_fixture.create_user(group=group)
database = data_fixture.create_database_application(group=group)
table = data_fixture.create_database_table(user=user, database=database)
handler = ViewHandler()
view = handler.create_view(
user=user,
table=table,
type_name="grid",
name="Test grid",
ownership_type=OWNERSHIP_TYPE_COLLABORATIVE,
)
field = data_fixture.create_text_field(table=view.table)
filter = handler.create_filter(user, view, field, "equal", "value")
view.ownership_type = "personal"
view.save()
with pytest.raises(PermissionDenied):
handler.create_filter(user, view, field, "equal", "value")
with pytest.raises(PermissionDenied):
handler.create_filter(user2, view, field, "equal", "value")
with pytest.raises(PermissionDenied):
handler.get_filter(user, filter.id)
with pytest.raises(PermissionDenied):
handler.get_filter(user2, filter.id)
with pytest.raises(PermissionDenied):
handler.list_filters(user, view.id)
with pytest.raises(PermissionDenied):
handler.list_filters(user2, view.id)
with pytest.raises(PermissionDenied):
handler.update_filter(user, filter, field, "equal", "another value")
with pytest.raises(PermissionDenied):
handler.update_filter(user2, filter, field, "equal", "another value")
with pytest.raises(PermissionDenied):
handler.delete_filter(user, filter)
with pytest.raises(PermissionDenied):
handler.delete_filter(user2, filter)
@pytest.mark.django_db
@pytest.mark.view_ownership
def test_sorts_view_ownership_type(data_fixture):
group = data_fixture.create_group(name="Group 1")
user = data_fixture.create_user(group=group)
user2 = data_fixture.create_user(group=group)
database = data_fixture.create_database_application(group=group)
table = data_fixture.create_database_table(user=user, database=database)
handler = ViewHandler()
view = handler.create_view(
user=user,
table=table,
type_name="grid",
name="Test grid",
ownership_type=OWNERSHIP_TYPE_COLLABORATIVE,
)
field = data_fixture.create_text_field(table=view.table)
equal_sort = data_fixture.create_view_sort(user=user, view=view, field=field)
view.ownership_type = "personal"
view.save()
with pytest.raises(PermissionDenied):
handler.create_sort(user=user, view=view, field=field, order="ASC")
with pytest.raises(PermissionDenied):
handler.create_sort(user=user2, view=view, field=field, order="ASC")
with pytest.raises(PermissionDenied):
handler.get_sort(user, equal_sort.id)
with pytest.raises(PermissionDenied):
handler.get_sort(user2, equal_sort.id)
with pytest.raises(PermissionDenied):
handler.list_sorts(user, view.id)
with pytest.raises(PermissionDenied):
handler.list_sorts(user2, view.id)
with pytest.raises(PermissionDenied):
handler.update_sort(user, equal_sort, field)
with pytest.raises(PermissionDenied):
handler.update_sort(user2, equal_sort, field)
with pytest.raises(PermissionDenied):
handler.delete_sort(user, equal_sort)
with pytest.raises(PermissionDenied):
handler.delete_sort(user2, equal_sort)
@pytest.mark.django_db
@pytest.mark.view_ownership
def test_decorations_view_ownership_type(data_fixture):
group = data_fixture.create_group(name="Group 1")
user = data_fixture.create_user(group=group)
user2 = data_fixture.create_user(group=group)
database = data_fixture.create_database_application(group=group)
table = data_fixture.create_database_table(user=user, database=database)
handler = ViewHandler()
view = handler.create_view(
user=user,
table=table,
type_name="grid",
name="Test grid",
ownership_type=OWNERSHIP_TYPE_COLLABORATIVE,
)
decorator_type_name = "left_border_color"
value_provider_type_name = ""
value_provider_conf = {}
decoration = data_fixture.create_view_decoration(user=user, view=view)
view.ownership_type = "personal"
view.save()
with pytest.raises(PermissionDenied):
handler.create_decoration(
view,
decorator_type_name,
value_provider_type_name,
value_provider_conf,
user=user,
)
with pytest.raises(PermissionDenied):
handler.create_decoration(
view,
decorator_type_name,
value_provider_type_name,
value_provider_conf,
user=user2,
)
with pytest.raises(PermissionDenied):
handler.get_decoration(user, decoration.id)
with pytest.raises(PermissionDenied):
handler.get_decoration(user2, decoration.id)
with pytest.raises(PermissionDenied):
handler.list_decorations(user, view.id)
with pytest.raises(PermissionDenied):
handler.list_decorations(user2, view.id)
with pytest.raises(PermissionDenied):
handler.update_decoration(decoration, user)
with pytest.raises(PermissionDenied):
handler.update_decoration(decoration, user2)
with pytest.raises(PermissionDenied):
handler.delete_decoration(decoration, user)
with pytest.raises(PermissionDenied):
handler.delete_decoration(decoration, user2)
@pytest.mark.django_db
@pytest.mark.view_ownership
def test_aggregations_view_ownership_type(data_fixture):
group = data_fixture.create_group(name="Group 1")
user = data_fixture.create_user(group=group)
user2 = data_fixture.create_user(group=group)
database = data_fixture.create_database_application(group=group)
table = data_fixture.create_database_table(user=user, database=database)
handler = ViewHandler()
field = data_fixture.create_number_field(user=user, table=table)
view = handler.create_view(
user=user,
table=table,
type_name="grid",
name="Test grid",
ownership_type=OWNERSHIP_TYPE_COLLABORATIVE,
)
handler.update_field_options(
view=view,
field_options={
field.id: {
"aggregation_type": "sum",
"aggregation_raw_type": "sum",
}
},
)
aggr = [
(
field,
"max",
),
]
handler.get_view_field_aggregations(user, view)
handler.get_field_aggregations(user, view, aggr)
view.ownership_type = "personal"
view.save()
with pytest.raises(PermissionDenied):
handler.get_view_field_aggregations(user, view)
with pytest.raises(PermissionDenied):
handler.get_view_field_aggregations(user2, view)
with pytest.raises(PermissionDenied):
handler.get_field_aggregations(user, view, aggr)
with pytest.raises(PermissionDenied):
handler.get_field_aggregations(user2, view, aggr)
@pytest.mark.django_db
@pytest.mark.view_ownership
def test_update_view_slug_ownership_type(data_fixture):
group = data_fixture.create_group(name="Group 1")
user = data_fixture.create_user(group=group)
user2 = data_fixture.create_user(group=group)
database = data_fixture.create_database_application(group=group)
table = data_fixture.create_database_table(user=user, database=database)
handler = ViewHandler()
field = data_fixture.create_number_field(user=user, table=table)
view = handler.create_view(
user=user,
table=table,
type_name="form",
name="Form",
ownership_type=OWNERSHIP_TYPE_COLLABORATIVE,
)
view.ownership_type = "personal"
view.save()
with pytest.raises(PermissionDenied):
handler.update_view_slug(user, view, "new-slug")
with pytest.raises(PermissionDenied):
handler.update_view_slug(user2, view, "new-slug")
@pytest.mark.django_db
@pytest.mark.view_ownership
def test_get_public_view_ownership_type(data_fixture):
group = data_fixture.create_group(name="Group 1")
user = data_fixture.create_user(group=group)
user2 = data_fixture.create_user(group=group)
database = data_fixture.create_database_application(group=group)
table = data_fixture.create_database_table(user=user, database=database)
handler = ViewHandler()
view = handler.create_view(
user=user,
table=table,
type_name="grid",
name="Grid",
ownership_type=OWNERSHIP_TYPE_COLLABORATIVE,
)
view.ownership_type = "personal"
view.public = False
view.slug = "slug"
view.save()
with pytest.raises(ViewDoesNotExist):
handler.get_public_view_by_slug(user, "slug")
with pytest.raises(ViewDoesNotExist):
handler.get_public_view_by_slug(user2, "slug")
view.public = True
view.save()
handler.get_public_view_by_slug(user, "slug")
@pytest.mark.django_db
@pytest.mark.view_ownership
def test_order_views_ownership_type(data_fixture):
user = data_fixture.create_user()
table = data_fixture.create_database_table(user=user)
handler = ViewHandler()
view = handler.create_view(
user=user,
table=table,
type_name="grid",
name="Test grid",
ownership_type=OWNERSHIP_TYPE_COLLABORATIVE,
)
view2 = handler.create_view(
user=user,
table=table,
type_name="grid",
name="Test grid",
ownership_type=OWNERSHIP_TYPE_COLLABORATIVE,
)
view2.ownership_type = "personal"
view2.save()
handler.order_views(user, table, [view.id])
with pytest.raises(ViewNotInTable):
handler.order_views(user, table, [view2.id])

View file

@ -1,5 +1,6 @@
import secrets
from io import BytesIO
from unittest.mock import patch
from zipfile import ZIP_DEFLATED, ZipFile
from django.core.files.base import ContentFile
@ -14,6 +15,10 @@ from baserow.contrib.database.views.registries import (
view_aggregation_type_registry,
view_type_registry,
)
from baserow.contrib.database.views.view_ownership_types import (
CollaborativeViewOwnershipType,
)
from baserow.core.models import GroupUser
from baserow.core.user_files.handler import UserFileHandler
@ -429,3 +434,90 @@ def test_import_export_form_view(data_fixture, tmpdir):
assert imported_field_option_condition_2.field_option_id == imported_field_option.id
assert imported_field_option_condition_2.type == condition_2.type
assert imported_field_option_condition_2.value == "2"
@pytest.mark.django_db
def test_import_export_view_ownership_type(data_fixture):
group = data_fixture.create_group()
user = data_fixture.create_user(group=group)
user2 = data_fixture.create_user(group=group)
database = data_fixture.create_database_application(group=group)
table = data_fixture.create_database_table(user=user, database=database)
grid_view = data_fixture.create_grid_view(
table=table,
name="Test",
order=1,
filter_type="AND",
filters_disabled=False,
row_identifier_type="count",
)
grid_view.ownership_type = "personal"
grid_view.created_by = user2
grid_view.save()
grid_view_type = view_type_registry.get("grid")
serialized = grid_view_type.export_serialized(grid_view, None, None)
imported_grid_view = grid_view_type.import_serialized(
grid_view.table, serialized, {}, None, None
)
assert grid_view.id != imported_grid_view.id
assert grid_view.ownership_type == imported_grid_view.ownership_type
assert grid_view.created_by == imported_grid_view.created_by
# view should not be imported if the user is gone
GroupUser.objects.filter(user=user2).delete()
imported_grid_view = grid_view_type.import_serialized(
grid_view.table, serialized, {}, None, None
)
assert imported_grid_view is None
# created by is not set
grid_view.created_by = None
grid_view.ownership_type = "collaborative"
grid_view.save()
serialized = grid_view_type.export_serialized(grid_view, None, None)
imported_grid_view = grid_view_type.import_serialized(
grid_view.table, serialized, {}, None, None
)
assert grid_view.id != imported_grid_view.id
assert imported_grid_view.ownership_type == "collaborative"
assert imported_grid_view.created_by is None
@pytest.mark.django_db
def test_import_export_view_ownership_type_not_in_registry(data_fixture):
ownership_types = {"collaborative": CollaborativeViewOwnershipType()}
group = data_fixture.create_group()
user = data_fixture.create_user(group=group)
user2 = data_fixture.create_user(group=group)
database = data_fixture.create_database_application(group=group)
table = data_fixture.create_database_table(user=user, database=database)
grid_view = data_fixture.create_grid_view(
table=table,
name="Test",
order=1,
filter_type="AND",
filters_disabled=False,
row_identifier_type="count",
)
grid_view.ownership_type = "personal"
grid_view.created_by = user2
grid_view.save()
grid_view_type = view_type_registry.get("grid")
serialized = grid_view_type.export_serialized(grid_view, None, None)
with patch(
"baserow.contrib.database.views.registries.view_ownership_type_registry.registry",
ownership_types,
):
imported_grid_view = grid_view_type.import_serialized(
grid_view.table, serialized, {}, None, None
)
assert imported_grid_view is None

View file

@ -1067,8 +1067,8 @@ def test_batch_update_rows_visible_in_public_view_to_some_not_be_visible_event_s
[initially_visible_row, initially_visible_row2]
) == [
PublicViewRows(
ViewHandler().get_view(
public_view_with_filters_initially_hiding_all_rows.id
ViewHandler().get_view_as_user(
user, public_view_with_filters_initially_hiding_all_rows.id
),
allowed_row_ids={1, 2},
)
@ -1288,7 +1288,7 @@ def test_batch_update_rows_visible_in_public_view_to_be_not_visible_event_sent(
[initially_visible_row, initially_visible_row2]
) == [
PublicViewRows(
ViewHandler().get_view(public_view_with_row_showing.id),
ViewHandler().get_view_as_user(user, public_view_with_row_showing.id),
allowed_row_ids={1, 2},
)
]
@ -1500,7 +1500,7 @@ def test_batch_update_rows_visible_in_public_view_still_be_visible_event_sent(
[initially_visible_row, initially_visible_row2]
) == [
PublicViewRows(
ViewHandler().get_view(public_view_with_row_showing.id),
ViewHandler().get_view_as_user(user, public_view_with_row_showing.id),
allowed_row_ids={1, 2},
)
]
@ -1602,7 +1602,7 @@ def test_batch_update_subset_rows_visible_in_public_view_no_filters(
[initially_visible_row, initially_visible_row2]
) == [
PublicViewRows(
ViewHandler().get_view(public_view_with_row_showing.id),
ViewHandler().get_view_as_user(user, public_view_with_row_showing.id),
allowed_row_ids=PublicViewRows.ALL_ROWS_ALLOWED,
)
]

View file

@ -11,6 +11,7 @@ For example:
### New Features
* Introduced a new command, `permanently_empty_database`, which will empty a database of all its tables. [#1090](https://gitlab.com/bramw/baserow/-/issues/1090)
* Users can now create their own personal views. [#1448](https://gitlab.com/bramw/baserow/-/issues/1448)
* Link row field can now be imported. [#1312](https://gitlab.com/bramw/baserow/-/issues/1312)
* Can add a row with textual values for single select, multiple select and link row field. [#1312](https://gitlab.com/bramw/baserow/-/issues/1312)
@ -53,6 +54,7 @@ For example:
* Frequent Flyer Rewards
* Grocery Planner
### Bug Fixes
* Fixed encoding issue where you couldn't import xml files with non-ascii characters [#1360](https://gitlab.com/bramw/baserow/-/issues/1360)
* Fixed bug preventing groups from being restored when RBAC was enabled [#1485](https://gitlab.com/bramw/baserow/-/issues/1485)

View file

@ -106,7 +106,6 @@ x-backend-variables: &backend-variables
BASEROW_WEBHOOKS_MAX_PER_TABLE:
BASEROW_WEBHOOKS_MAX_CALL_LOG_ENTRIES:
BASEROW_WEBHOOKS_REQUEST_TIMEOUT_SECONDS:
BASEROW_PERMISSION_MANAGERS:
BASEROW_ENTERPRISE_AUDIT_LOG_CLEANUP_INTERVAL_MINUTES:
BASEROW_ENTERPRISE_AUDIT_LOG_RETENTION_DAYS:

View file

@ -125,7 +125,6 @@ x-backend-variables: &backend-variables
BASEROW_WEBHOOKS_MAX_PER_TABLE:
BASEROW_WEBHOOKS_MAX_CALL_LOG_ENTRIES:
BASEROW_WEBHOOKS_REQUEST_TIMEOUT_SECONDS:
BASEROW_PERMISSION_MANAGERS:
BASEROW_ENTERPRISE_AUDIT_LOG_CLEANUP_INTERVAL_MINUTES:
BASEROW_ENTERPRISE_AUDIT_LOG_RETENTION_DAYS:

View file

@ -124,7 +124,6 @@ x-backend-variables: &backend-variables
BASEROW_WEBHOOKS_MAX_PER_TABLE:
BASEROW_WEBHOOKS_MAX_CALL_LOG_ENTRIES:
BASEROW_WEBHOOKS_REQUEST_TIMEOUT_SECONDS:
BASEROW_PERMISSION_MANAGERS:
BASEROW_ENTERPRISE_AUDIT_LOG_CLEANUP_INTERVAL_MINUTES:
BASEROW_ENTERPRISE_AUDIT_LOG_RETENTION_DAYS:

File diff suppressed because one or more lines are too long

View file

@ -13,7 +13,6 @@ from baserow.contrib.database.fields.operations import (
DeleteRelatedLinkRowFieldOperationType,
DuplicateFieldOperationType,
ListFieldsOperationType,
ReadAggregationDatabaseTableOperationType,
ReadFieldOperationType,
RestoreFieldOperationType,
UpdateFieldOperationType,
@ -37,7 +36,6 @@ from baserow.contrib.database.table.operations import (
DeleteDatabaseTableOperationType,
DuplicateDatabaseTableOperationType,
ImportRowsDatabaseTableOperationType,
ListAggregationDatabaseTableOperationType,
ListenToAllDatabaseTableEventsOperationType,
ListRowNamesDatabaseTableOperationType,
ListRowsDatabaseTableOperationType,
@ -61,11 +59,13 @@ from baserow.contrib.database.views.operations import (
DeleteViewOperationType,
DeleteViewSortOperationType,
DuplicateViewOperationType,
ListAggregationsViewOperationType,
ListViewDecorationOperationType,
ListViewFilterOperationType,
ListViewsOperationType,
ListViewSortOperationType,
OrderViewsOperationType,
ReadAggregationsViewOperationType,
ReadViewDecorationOperationType,
ReadViewFieldOptionsOperationType,
ReadViewFilterOperationType,
@ -172,8 +172,8 @@ VIEWER_OPS = NO_ACCESS_OPS + [
ListViewFilterOperationType,
ListViewsOperationType,
ListFieldsOperationType,
ListAggregationDatabaseTableOperationType,
ReadAggregationDatabaseTableOperationType,
ListAggregationsViewOperationType,
ReadAggregationsViewOperationType,
ReadAdjacentRowDatabaseRowOperationType,
ListRowNamesDatabaseTableOperationType,
ReadViewFilterOperationType,

View file

@ -225,8 +225,9 @@ class RolePermissionManagerType(PermissionManagerType):
roles_by_scope, operation_type
)
policy_per_operation[operation_type.type]["default"] = default
policy_per_operation[operation_type.type]["exceptions"] = exceptions
if default or exceptions:
policy_per_operation[operation_type.type]["default"] = default
policy_per_operation[operation_type.type]["exceptions"] = exceptions
if exceptions:
# We store the exceptions by scope to get all objects at once later

View file

@ -130,7 +130,7 @@ class KanbanViewView(APIView):
"""Responds with the rows grouped by the view's select option field value."""
view_handler = ViewHandler()
view = view_handler.get_view(view_id, KanbanView)
view = view_handler.get_view_as_user(request.user, view_id, KanbanView)
group = view.table.database.group
# We don't want to check if there is an active premium license if the group

View file

@ -21,6 +21,7 @@ class BaserowPremiumConfig(AppConfig):
decorator_type_registry,
decorator_value_provider_type_registry,
form_view_mode_registry,
view_ownership_type_registry,
view_type_registry,
)
from baserow.core.registries import plugin_registry
@ -59,6 +60,10 @@ class BaserowPremiumConfig(AppConfig):
ConditionalColorValueProviderType()
)
from .views.view_ownership_types import PersonalViewOwnershipType
view_ownership_type_registry.register(PersonalViewOwnershipType())
from baserow_premium.license.license_types import PremiumLicenseType
from baserow_premium.license.registries import license_type_registry
@ -74,5 +79,13 @@ class BaserowPremiumConfig(AppConfig):
# The signals must always be imported last because they use the registries
# which need to be filled first.
import baserow_premium.views.signals # noqa: F403, F401
import baserow_premium.views.signals as view_signals # noqa: F403, F401
import baserow_premium.ws.signals # noqa: F403, F401
view_signals.connect_to_user_pre_delete_signal()
from baserow.core.registries import permission_manager_type_registry
from .permission_manager import ViewOwnershipPermissionManagerType
permission_manager_type_registry.register(ViewOwnershipPermissionManagerType())

View file

@ -0,0 +1,144 @@
from typing import TYPE_CHECKING, Any, Optional
from django.contrib.auth import get_user_model
from django.db.models import Q, QuerySet
from baserow_premium.license.features import PREMIUM
from baserow_premium.license.handler import LicenseHandler
from baserow_premium.views.models import OWNERSHIP_TYPE_PERSONAL
from baserow.contrib.database.views.operations import ListViewsOperationType
from baserow.core.exceptions import PermissionDenied
from baserow.core.registries import (
PermissionManagerType,
object_scope_type_registry,
operation_type_registry,
)
User = get_user_model()
if TYPE_CHECKING:
from django.contrib.auth.models import AbstractUser
from .models import Group
class ViewOwnershipPermissionManagerType(PermissionManagerType):
type = "view_ownership"
def __init__(self):
view_scope_type = object_scope_type_registry.get("database_view")
self.operations = [
op.type
for op in operation_type_registry.get_all()
if object_scope_type_registry.scope_type_includes_scope_type(
view_scope_type, op.context_scope
)
]
super().__init__()
def check_permissions(
self,
actor: "AbstractUser",
operation_name: str,
group: Optional["Group"] = None,
context: Optional[Any] = None,
include_trash: bool = False,
) -> Optional[bool]:
"""
check_permissions() impl for view ownership checks.
There are other instances that this method cannot check:
- CreateViewDecorationOperationType is currently implemented via view_created
signal since the permission system doesn't allow to pass richer context.
- OrderViewsOperationType is currently implemented via views_reordered signal
since the permission system doesn't allow to pass richer context.
- ListViewsOperationType and ReadViewsOrderOperationType operations invoke
filter_queryset() method and hence don't need to be checked.
:param actor: The actor who wants to execute the operation. Generally a `User`,
but can be a `Token`.
:param operation_name: The operation name the actor wants to execute.
:param group: The optional group in which the operation takes place.
:param context: The optional object affected by the operation. For instance
if you are updating a `Table` object, the context is this `Table` object.
:param include_trash: If true then also checks if the given group has been
trashed instead of raising a DoesNotExist exception.
:raise PermissionDenied: If the operation is disallowed a PermissionDenied is
raised.
:return: `True` if the operation is permitted, None if the permission manager
can't decide.
"""
if not isinstance(actor, User):
return
if operation_name not in self.operations:
return
if not group:
return
if not context:
return
premium = LicenseHandler.user_has_feature(PREMIUM, actor, group)
view_scope_type = object_scope_type_registry.get("database_view")
view = object_scope_type_registry.get_parent(
context, at_scope_type=view_scope_type
)
if premium:
if (
view.ownership_type == OWNERSHIP_TYPE_PERSONAL
and view.created_by != actor
):
raise PermissionDenied()
else:
if view.ownership_type == OWNERSHIP_TYPE_PERSONAL:
raise PermissionDenied()
return
def filter_queryset(
self,
actor: "AbstractUser",
operation_name: str,
queryset: QuerySet,
group: Optional["Group"] = None,
context: Optional[Any] = None,
) -> QuerySet:
"""
filter_queryset() impl for view ownership filtering.
:param actor: The actor whom we want to filter the queryset for.
Generally a `User` but can be a Token.
:param operation: The operation name for which we want to filter the queryset
for.
:param group: An optional group into which the operation takes place.
:param context: An optional context object related to the current operation.
:return: The queryset potentially filtered.
"""
if not isinstance(actor, User):
return queryset
if operation_name != ListViewsOperationType.type:
return queryset
if not group:
return queryset
premium = LicenseHandler.user_has_feature(PREMIUM, actor, group)
if premium:
return queryset.filter(
~Q(ownership_type=OWNERSHIP_TYPE_PERSONAL)
| (Q(ownership_type=OWNERSHIP_TYPE_PERSONAL) & Q(created_by=actor))
)
else:
return queryset.exclude(ownership_type=OWNERSHIP_TYPE_PERSONAL)

View file

@ -3,6 +3,8 @@ from typing import Dict, Optional, Union
from django.db.models import Count, Q, QuerySet
from baserow_premium.views.models import OWNERSHIP_TYPE_PERSONAL
from baserow.contrib.database.fields.models import SingleSelectField
from baserow.contrib.database.table.models import GeneratedTableModel
from baserow.contrib.database.views.handler import ViewHandler
@ -126,3 +128,15 @@ def get_rows_grouped_by_single_select_field(
rows[key]["count"] = value
return rows
def delete_personal_views(user_id: int):
"""
Deletes all personal views associated with the provided user.
:param user_id: The id of the user for whom to delete personal views.
"""
View.objects.filter(ownership_type=OWNERSHIP_TYPE_PERSONAL).filter(
created_by__id=user_id
).delete()

View file

@ -5,6 +5,8 @@ from baserow.contrib.database.fields.models import Field, FileField, SingleSelec
from baserow.contrib.database.views.models import View
from baserow.core.mixins import HierarchicalModelMixin
OWNERSHIP_TYPE_PERSONAL = "personal"
class KanbanView(View):
field_options = models.ManyToManyField(Field, through="KanbanViewFieldOptions")

View file

@ -1,10 +1,24 @@
from django.contrib.auth import get_user_model
from django.contrib.auth.models import AbstractUser
from django.db.models.signals import pre_delete
from django.dispatch import receiver
from baserow_premium.license.features import PREMIUM
from baserow_premium.license.handler import LicenseHandler
from baserow_premium.views.models import OWNERSHIP_TYPE_PERSONAL
from baserow.contrib.database.fields import signals as field_signals
from baserow.contrib.database.fields.models import FileField
from baserow.contrib.database.views import signals as view_signals
from baserow.contrib.database.views.models import OWNERSHIP_TYPE_COLLABORATIVE
from baserow.core.exceptions import PermissionDenied
from baserow.core.models import Group
from .handler import delete_personal_views
from .models import KanbanView
User = get_user_model()
@receiver(field_signals.field_deleted)
def field_deleted(sender, field, **kwargs):
@ -14,6 +28,50 @@ def field_deleted(sender, field, **kwargs):
)
def premium_check_ownership_type(
user: AbstractUser, group: Group, ownership_type: str
) -> None:
"""
Checks whether the provided ownership type is supported for the user.
Should be replaced with a support for creating views
in the ViewOwnershipPermissionManagerType once it is possible.
:param user: The user on whose behalf the operation is performed.
:param group: The group for which the check is performed.
:param ownership_type: View's ownership type.
:raises PermissionDenied: When not allowed.
"""
premium = LicenseHandler.user_has_feature(PREMIUM, user, group)
if premium:
if ownership_type not in [
OWNERSHIP_TYPE_COLLABORATIVE,
OWNERSHIP_TYPE_PERSONAL,
]:
raise PermissionDenied()
else:
if ownership_type != OWNERSHIP_TYPE_COLLABORATIVE:
raise PermissionDenied()
@receiver(view_signals.view_created)
def view_created(sender, view, user, **kwargs):
group = view.table.database.group
premium_check_ownership_type(user, group, view.ownership_type)
def before_user_permanently_deleted(sender, instance, **kwargs):
delete_personal_views(instance.id)
def connect_to_user_pre_delete_signal():
pre_delete.connect(before_user_permanently_deleted, User)
__all__ = [
"field_deleted",
"view_created",
"connect_to_user_pre_delete_signal",
]

View file

@ -0,0 +1,19 @@
from baserow.contrib.database.views.registries import ViewOwnershipType
class PersonalViewOwnershipType(ViewOwnershipType):
"""
Represents views that are intended only for a specific user.
"""
type = "personal"
def can_import_view(self, serialized_values, id_mapping):
email = serialized_values.get("created_by", None)
return id_mapping["created_by"].get(email, None) is not None
def should_broadcast_signal_to(self, view):
if view.created_by is None:
return "", None
return "users", [view.created_by_id]

View file

@ -0,0 +1,47 @@
from django.shortcuts import reverse
import pytest
from rest_framework.status import HTTP_200_OK
@pytest.mark.django_db
def test_list_views_ownership_type(
api_client, data_fixture, alternative_per_group_license_service
):
"""
In premium, both collaborative and personal views are returned.
"""
group = data_fixture.create_group(name="Group 1")
database = data_fixture.create_database_application(group=group)
user, token = data_fixture.create_user_and_token(
group=group, email="test@test.nl", password="password", first_name="Test1"
)
user2, token2 = data_fixture.create_user_and_token(
group=group, email="test2@test.nl", password="password", first_name="Test2"
)
table_1 = data_fixture.create_database_table(user=user, database=database)
view_1 = data_fixture.create_grid_view(
table=table_1, order=1, ownership_type="collaborative", created_by=user
)
view_2 = data_fixture.create_grid_view(
table=table_1, order=3, ownership_type="personal", created_by=user
)
# view belongs to another user
view_3 = data_fixture.create_grid_view(
table=table_1, order=3, ownership_type="personal", created_by=user2
)
alternative_per_group_license_service.restrict_user_premium_to(user, group.id)
response = api_client.get(
reverse("api:database:views:list", kwargs={"table_id": table_1.id}),
**{"HTTP_AUTHORIZATION": f"JWT {token}"},
)
assert response.status_code == HTTP_200_OK
response_json = response.json()
assert len(response_json) == 2
assert response_json[0]["id"] == view_1.id
assert response_json[0]["ownership_type"] == "collaborative"
assert response_json[1]["id"] == view_2.id
assert response_json[1]["ownership_type"] == "personal"

View file

@ -0,0 +1,40 @@
import pytest
from baserow.contrib.database.views.handler import ViewHandler
from baserow.core.exceptions import PermissionDenied
from baserow.core.trash.handler import TrashHandler
@pytest.mark.django_db
@pytest.mark.view_ownership
def test_trash_restore_view(
data_fixture, premium_data_fixture, alternative_per_group_license_service
):
group = data_fixture.create_group(name="Group 1")
user = premium_data_fixture.create_user(group=group)
user2 = premium_data_fixture.create_user(group=group)
database = data_fixture.create_database_application(group=group)
table = data_fixture.create_database_table(user=user, database=database)
handler = ViewHandler()
alternative_per_group_license_service.restrict_user_premium_to(user, group.id)
alternative_per_group_license_service.restrict_user_premium_to(user2, group.id)
view = handler.create_view(
user=user,
table=table,
type_name="grid",
name="Test grid",
ownership_type="personal",
)
TrashHandler.trash(user, database.group, database, view)
view.refresh_from_db()
assert view.trashed is True
with pytest.raises(PermissionDenied):
TrashHandler.restore_item(user2, "view", view.id)
TrashHandler.restore_item(user, "view", view.id)
view.refresh_from_db()
assert view.trashed is False

View file

@ -184,7 +184,7 @@ def test_can_undo_delete_decoration(premium_data_fixture):
assert ViewDecoration.objects.count() == 1
view_decoration = ViewHandler().get_decoration(view_decoration_id)
view_decoration = ViewHandler().get_decoration(user, view_decoration_id)
assert view_decoration.order == order

View file

@ -42,14 +42,6 @@ def test_create_left_border_color_without_premium_license(premium_data_fixture):
user=user,
)
decoration = handler.create_decoration(
view=grid_view,
decorator_type_name="left_border_color",
value_provider_type_name="",
value_provider_conf={},
)
assert isinstance(decoration, ViewDecoration)
@pytest.mark.django_db
@override_settings(DEBUG=True)
@ -118,14 +110,6 @@ def test_create_background_color_without_premium_license(premium_data_fixture):
user=user,
)
decoration = handler.create_decoration(
view=grid_view,
decorator_type_name="background_color",
value_provider_type_name="",
value_provider_conf={},
)
assert isinstance(decoration, ViewDecoration)
@pytest.mark.django_db
@override_settings(DEBUG=True)

View file

@ -72,15 +72,19 @@ def test_import_export_grid_view_w_decorator(data_fixture):
@pytest.mark.django_db
def test_field_type_changed_w_decoration(data_fixture):
user = data_fixture.create_user()
table = data_fixture.create_database_table(user=user)
group = data_fixture.create_group()
user = data_fixture.create_user(group=group)
database = data_fixture.create_database_application(group=group)
table = data_fixture.create_database_table(user=user, database=database)
text_field = data_fixture.create_text_field(table=table)
option_field = data_fixture.create_single_select_field(
table=table, name="option_field", order=1
)
data_fixture.create_select_option(field=option_field, value="A", color="blue")
grid_view = data_fixture.create_grid_view(table=table)
grid_view = data_fixture.create_grid_view(
table=table, ownership_type="collaborative"
)
select_view_decoration = data_fixture.create_view_decoration(
view=grid_view,
@ -237,8 +241,13 @@ def test_create_single_select_color_with_premium_license(premium_data_fixture):
@pytest.mark.django_db
@override_settings(DEBUG=True)
def test_create_single_select_color_without_premium_license(premium_data_fixture):
user = premium_data_fixture.create_user(has_active_premium_license=False)
grid_view = premium_data_fixture.create_grid_view(user=user)
group = premium_data_fixture.create_group()
database = premium_data_fixture.create_database_application(group=group)
table = premium_data_fixture.create_database_table(database=database)
user = premium_data_fixture.create_user(
has_active_premium_license=False, group=group
)
grid_view = premium_data_fixture.create_grid_view(user=user, table=table)
handler = ViewHandler()

View file

@ -1,7 +1,14 @@
import pytest
from baserow_premium.views.handler import get_rows_grouped_by_single_select_field
from baserow.contrib.database.views.models import View
from baserow.contrib.database.views.exceptions import ViewDoesNotExist, ViewNotInTable
from baserow.contrib.database.views.handler import ViewHandler
from baserow.contrib.database.views.models import (
OWNERSHIP_TYPE_COLLABORATIVE,
GridView,
View,
)
from baserow.core.exceptions import PermissionDenied
@pytest.mark.django_db
@ -232,3 +239,544 @@ def test_get_rows_grouped_by_single_select_field_with_empty_table(
assert len(rows) == 1
assert rows["null"]["count"] == 0
assert len(rows["null"]["results"]) == 0
@pytest.mark.django_db
@pytest.mark.view_ownership
def test_list_views_personal_ownership_type(
data_fixture, premium_data_fixture, alternative_per_group_license_service
):
group = data_fixture.create_group(name="Group 1")
user = premium_data_fixture.create_user(group=group)
user2 = premium_data_fixture.create_user(group=group)
database = data_fixture.create_database_application(group=group)
table = data_fixture.create_database_table(user=user, database=database)
handler = ViewHandler()
alternative_per_group_license_service.restrict_user_premium_to(user, group.id)
alternative_per_group_license_service.restrict_user_premium_to(user2, group.id)
view = handler.create_view(
user=user,
table=table,
type_name="grid",
name="Test grid",
ownership_type=OWNERSHIP_TYPE_COLLABORATIVE,
)
view2 = handler.create_view(
user=user,
table=table,
type_name="grid",
name="Test grid",
ownership_type="personal",
)
user_views = handler.list_views(user, table, "grid", None, None, None, 10)
assert len(user_views) == 2
user2_views = handler.list_views(user2, table, "grid", None, None, None, 10)
assert len(user2_views) == 1
assert user2_views[0].id == view.id
@pytest.mark.django_db
@pytest.mark.view_ownership
def test_get_view_personal_ownership_type(
data_fixture, premium_data_fixture, alternative_per_group_license_service
):
group = data_fixture.create_group(name="Group 1")
user = premium_data_fixture.create_user(group=group)
user2 = premium_data_fixture.create_user(group=group)
database = data_fixture.create_database_application(group=group)
table = data_fixture.create_database_table(user=user, database=database)
handler = ViewHandler()
alternative_per_group_license_service.restrict_user_premium_to(user, group.id)
view = handler.create_view(
user=user,
table=table,
type_name="grid",
name="Test grid",
ownership_type="personal",
)
handler.get_view_as_user(user, view.id)
with pytest.raises(PermissionDenied):
handler.get_view_as_user(user2, view.id)
@pytest.mark.django_db
@pytest.mark.view_ownership
def test_create_view_personal_ownership_type(
data_fixture, premium_data_fixture, alternative_per_group_license_service
):
group = data_fixture.create_group(name="Group 1")
user = premium_data_fixture.create_user(group=group)
database = data_fixture.create_database_application(group=group)
table = data_fixture.create_database_table(user=user, database=database)
handler = ViewHandler()
alternative_per_group_license_service.restrict_user_premium_to(user, group.id)
view = handler.create_view(
user=user,
table=table,
type_name="grid",
name="Test grid",
ownership_type="personal",
)
grid = GridView.objects.all().first()
assert grid.created_by == user
assert grid.ownership_type == "personal"
@pytest.mark.django_db
@pytest.mark.view_ownership
def test_update_view_personal_ownership_type(
data_fixture, premium_data_fixture, alternative_per_group_license_service
):
group = data_fixture.create_group(name="Group 1")
user = premium_data_fixture.create_user(group=group)
user2 = premium_data_fixture.create_user(group=group)
database = data_fixture.create_database_application(group=group)
table = data_fixture.create_database_table(user=user, database=database)
handler = ViewHandler()
alternative_per_group_license_service.restrict_user_premium_to(user, group.id)
alternative_per_group_license_service.restrict_user_premium_to(user2, group.id)
view = handler.create_view(
user=user,
table=table,
type_name="grid",
name="Test grid",
ownership_type="personal",
)
handler.update_view(user=user, view=view, name="Renamed")
view.refresh_from_db()
assert view.name == "Renamed"
with pytest.raises(PermissionDenied):
handler.update_view(user=user2, view=view, name="Not my view")
@pytest.mark.django_db
@pytest.mark.view_ownership
def test_duplicate_view_personal_ownership_type(
data_fixture, premium_data_fixture, alternative_per_group_license_service
):
group = data_fixture.create_group(name="Group 1")
user = premium_data_fixture.create_user(group=group)
user2 = premium_data_fixture.create_user(group=group)
database = data_fixture.create_database_application(group=group)
table = data_fixture.create_database_table(user=user, database=database)
handler = ViewHandler()
alternative_per_group_license_service.restrict_user_premium_to(user, group.id)
alternative_per_group_license_service.restrict_user_premium_to(user2, group.id)
view = handler.create_view(
user=user,
table=table,
type_name="grid",
name="Test grid",
ownership_type="personal",
)
duplicated_view = handler.duplicate_view(user, view)
assert duplicated_view.ownership_type == "personal"
assert duplicated_view.created_by == user
with pytest.raises(PermissionDenied):
handler.get_view_as_user(user2, duplicated_view.id)
with pytest.raises(PermissionDenied):
duplicated_view = handler.duplicate_view(user2, view)
@pytest.mark.django_db
@pytest.mark.view_ownership
def test_delete_view_personal_ownership_type(
data_fixture, premium_data_fixture, alternative_per_group_license_service
):
group = data_fixture.create_group(name="Group 1")
user = premium_data_fixture.create_user(group=group)
user2 = premium_data_fixture.create_user(group=group)
database = data_fixture.create_database_application(group=group)
table = data_fixture.create_database_table(user=user, database=database)
handler = ViewHandler()
alternative_per_group_license_service.restrict_user_premium_to(user, group.id)
alternative_per_group_license_service.restrict_user_premium_to(user2, group.id)
view = handler.create_view(
user=user,
table=table,
type_name="grid",
name="Test grid",
ownership_type="personal",
)
handler.delete_view(user, view)
view = handler.create_view(
user=user,
table=table,
type_name="grid",
name="Test grid",
ownership_type="personal",
)
with pytest.raises(PermissionDenied):
handler.delete_view(user2, view)
@pytest.mark.django_db
@pytest.mark.view_ownership
def test_field_options_personal_ownership_type(
data_fixture, premium_data_fixture, alternative_per_group_license_service
):
group = data_fixture.create_group(name="Group 1")
user = premium_data_fixture.create_user(group=group)
user2 = premium_data_fixture.create_user(group=group)
database = data_fixture.create_database_application(group=group)
table = data_fixture.create_database_table(user=user, database=database)
handler = ViewHandler()
alternative_per_group_license_service.restrict_user_premium_to(user, group.id)
alternative_per_group_license_service.restrict_user_premium_to(user2, group.id)
view = handler.create_view(
user=user,
table=table,
type_name="grid",
name="Test grid",
ownership_type="personal",
)
handler.get_field_options_as_user(user, view)
with pytest.raises(PermissionDenied):
handler.get_field_options_as_user(user2, view)
handler.update_field_options(view, {}, user)
with pytest.raises(PermissionDenied):
handler.update_field_options(view, {}, user2)
@pytest.mark.django_db
@pytest.mark.view_ownership
def test_filters_personal_ownership_type(
data_fixture, premium_data_fixture, alternative_per_group_license_service
):
group = data_fixture.create_group(name="Group 1")
user = premium_data_fixture.create_user(group=group)
user2 = premium_data_fixture.create_user(group=group)
database = data_fixture.create_database_application(group=group)
table = data_fixture.create_database_table(user=user, database=database)
handler = ViewHandler()
alternative_per_group_license_service.restrict_user_premium_to(user, group.id)
alternative_per_group_license_service.restrict_user_premium_to(user2, group.id)
view = handler.create_view(
user=user,
table=table,
type_name="grid",
name="Test grid",
ownership_type="personal",
)
field = data_fixture.create_text_field(table=view.table)
filter = handler.create_filter(user, view, field, "equal", "value")
with pytest.raises(PermissionDenied):
handler.create_filter(user2, view, field, "equal", "value")
handler.get_filter(user, filter.id)
with pytest.raises(PermissionDenied):
handler.get_filter(user2, filter.id)
list = handler.list_filters(user, view.id)
assert len(list) == 1
with pytest.raises(PermissionDenied):
handler.list_filters(user2, view.id)
handler.update_filter(user, filter, field, "equal", "another value")
with pytest.raises(PermissionDenied):
handler.update_filter(user2, filter, field, "equal", "another value")
with pytest.raises(PermissionDenied):
handler.delete_filter(user2, filter)
handler.delete_filter(user, filter)
@pytest.mark.django_db
@pytest.mark.view_ownership
def test_sorts_personal_ownership_type(
data_fixture, premium_data_fixture, alternative_per_group_license_service
):
group = data_fixture.create_group(name="Group 1")
user = premium_data_fixture.create_user(group=group)
user2 = premium_data_fixture.create_user(group=group)
database = data_fixture.create_database_application(group=group)
table = data_fixture.create_database_table(user=user, database=database)
handler = ViewHandler()
alternative_per_group_license_service.restrict_user_premium_to(user, group.id)
alternative_per_group_license_service.restrict_user_premium_to(user2, group.id)
view = handler.create_view(
user=user,
table=table,
type_name="grid",
name="Test grid",
ownership_type="personal",
)
field = data_fixture.create_text_field(table=view.table)
sort = handler.create_sort(user=user, view=view, field=field, order="ASC")
with pytest.raises(PermissionDenied):
handler.create_sort(user=user2, view=view, field=field, order="ASC")
handler.get_sort(user, sort.id)
with pytest.raises(PermissionDenied):
handler.get_sort(user2, sort.id)
list = handler.list_sorts(user, view.id)
assert len(list) == 1
with pytest.raises(PermissionDenied):
handler.list_sorts(user2, view.id)
handler.update_sort(user, sort, field)
with pytest.raises(PermissionDenied):
handler.update_sort(user2, sort, field)
with pytest.raises(PermissionDenied):
handler.delete_sort(user2, sort)
handler.delete_sort(user, sort)
@pytest.mark.django_db
@pytest.mark.view_ownership
def test_decorations_personal_ownership_type(
data_fixture, premium_data_fixture, alternative_per_group_license_service
):
group = data_fixture.create_group(name="Group 1")
user = premium_data_fixture.create_user(group=group)
user2 = premium_data_fixture.create_user(group=group)
database = data_fixture.create_database_application(group=group)
table = data_fixture.create_database_table(user=user, database=database)
handler = ViewHandler()
alternative_per_group_license_service.restrict_user_premium_to(user, group.id)
alternative_per_group_license_service.restrict_user_premium_to(user2, group.id)
view = handler.create_view(
user=user,
table=table,
type_name="grid",
name="Test grid",
ownership_type="personal",
)
decorator_type_name = "left_border_color"
value_provider_type_name = ""
value_provider_conf = {}
decoration = handler.create_decoration(
view,
decorator_type_name,
value_provider_type_name,
value_provider_conf,
user=user,
)
with pytest.raises(PermissionDenied):
handler.create_decoration(
view,
decorator_type_name,
value_provider_type_name,
value_provider_conf,
user=user2,
)
result = handler.get_decoration(user, decoration.id)
assert result == decoration
with pytest.raises(PermissionDenied):
handler.get_decoration(user2, decoration.id)
list = handler.list_decorations(user, view.id)
assert len(list) == 1
with pytest.raises(PermissionDenied):
handler.list_decorations(user2, view.id)
handler.update_decoration(decoration, user)
with pytest.raises(PermissionDenied):
handler.update_decoration(decoration, user2)
with pytest.raises(PermissionDenied):
handler.delete_decoration(decoration, user2)
handler.delete_decoration(decoration, user)
@pytest.mark.django_db
@pytest.mark.view_ownership
def test_aggregations_personal_ownership_type(
data_fixture, premium_data_fixture, alternative_per_group_license_service
):
group = data_fixture.create_group(name="Group 1")
user = premium_data_fixture.create_user(group=group)
user2 = premium_data_fixture.create_user(group=group)
database = data_fixture.create_database_application(group=group)
table = data_fixture.create_database_table(user=user, database=database)
handler = ViewHandler()
alternative_per_group_license_service.restrict_user_premium_to(user, group.id)
alternative_per_group_license_service.restrict_user_premium_to(user2, group.id)
field = data_fixture.create_number_field(user=user, table=table)
view = handler.create_view(
user=user,
table=table,
type_name="grid",
name="Test grid",
ownership_type="personal",
)
handler.update_field_options(
view=view,
field_options={
field.id: {
"aggregation_type": "sum",
"aggregation_raw_type": "sum",
}
},
)
aggr = [
(
field,
"max",
),
]
aggregations = handler.get_view_field_aggregations(user, view)
assert field.db_column in aggregations
handler.get_field_aggregations(user, view, aggr)
with pytest.raises(PermissionDenied):
handler.get_view_field_aggregations(user2, view)
with pytest.raises(PermissionDenied):
handler.get_field_aggregations(user2, view, aggr)
@pytest.mark.django_db
@pytest.mark.view_ownership
def test_update_view_slug_personal_ownership_type(
data_fixture, premium_data_fixture, alternative_per_group_license_service
):
group = data_fixture.create_group(name="Group 1")
user = premium_data_fixture.create_user(group=group)
user2 = premium_data_fixture.create_user(group=group)
database = data_fixture.create_database_application(group=group)
table = data_fixture.create_database_table(user=user, database=database)
handler = ViewHandler()
alternative_per_group_license_service.restrict_user_premium_to(user, group.id)
alternative_per_group_license_service.restrict_user_premium_to(user2, group.id)
view = handler.create_view(
user=user,
table=table,
type_name="form",
name="Form",
ownership_type="personal",
)
handler.update_view_slug(user, view, "new-slug")
view.refresh_from_db()
assert view.slug == "new-slug"
with pytest.raises(PermissionDenied):
handler.update_view_slug(user2, view, "new-slug")
@pytest.mark.django_db
@pytest.mark.view_ownership
def test_get_public_view_personal_ownership_type(
data_fixture, premium_data_fixture, alternative_per_group_license_service
):
group = data_fixture.create_group(name="Group 1")
user = premium_data_fixture.create_user(group=group)
user2 = premium_data_fixture.create_user(group=group)
database = data_fixture.create_database_application(group=group)
table = data_fixture.create_database_table(user=user, database=database)
handler = ViewHandler()
alternative_per_group_license_service.restrict_user_premium_to(user, group.id)
alternative_per_group_license_service.restrict_user_premium_to(user2, group.id)
view = handler.create_view(
user=user,
table=table,
type_name="form",
name="Form",
ownership_type="personal",
)
view.public = False
view.slug = "slug"
view.save()
handler.get_public_view_by_slug(user, "slug")
with pytest.raises(ViewDoesNotExist):
handler.get_public_view_by_slug(user2, "slug")
@pytest.mark.django_db
@pytest.mark.view_ownership
def test_order_views_personal_ownership_type(
data_fixture, premium_data_fixture, alternative_per_group_license_service
):
group = data_fixture.create_group(name="Group 1")
user = premium_data_fixture.create_user(group=group)
user2 = premium_data_fixture.create_user(group=group)
database = data_fixture.create_database_application(group=group)
table = data_fixture.create_database_table(user=user, database=database)
handler = ViewHandler()
alternative_per_group_license_service.restrict_user_premium_to(user, group.id)
alternative_per_group_license_service.restrict_user_premium_to(user2, group.id)
grid_1 = data_fixture.create_grid_view(
table=table, user=user, created_by=user, order=1, ownership_type="collaborative"
)
grid_2 = data_fixture.create_grid_view(
table=table, user=user, created_by=user, order=2, ownership_type="collaborative"
)
grid_3 = data_fixture.create_grid_view(
table=table, user=user, created_by=user, order=3, ownership_type="collaborative"
)
personal_grid = data_fixture.create_grid_view(
table=table, user=user, created_by=user, order=2, ownership_type="personal"
)
personal_grid_2 = data_fixture.create_grid_view(
table=table, user=user, created_by=user, order=3, ownership_type="personal"
)
handler.order_views(
user=user,
table=table,
order=[personal_grid_2.id, personal_grid.id],
)
grid_1.refresh_from_db()
grid_2.refresh_from_db()
grid_3.refresh_from_db()
personal_grid.refresh_from_db()
personal_grid_2.refresh_from_db()
assert personal_grid_2.order == 1
assert personal_grid.order == 2
assert grid_1.order == 1
assert grid_2.order == 2
assert grid_3.order == 3
with pytest.raises(ViewNotInTable):
handler.order_views(
user=user2,
table=table,
order=[personal_grid_2.id, personal_grid_2.id],
)

View file

@ -0,0 +1,53 @@
from datetime import timedelta
from django.utils import timezone
import pytest
from baserow.contrib.database.views.handler import ViewHandler
from baserow.contrib.database.views.models import View
from baserow.core.user.handler import UserHandler
@pytest.mark.django_db(transaction=True)
@pytest.mark.view_ownership
def test_remove_unused_personal_views(
data_fixture, alternative_per_group_license_service
):
group = data_fixture.create_group(name="Group 1")
user = data_fixture.create_user(group=group)
user2 = data_fixture.create_user(group=group)
database = data_fixture.create_database_application(group=group)
table = data_fixture.create_database_table(user=user, database=database)
handler = ViewHandler()
alternative_per_group_license_service.restrict_user_premium_to(user, group.id)
view = handler.create_view(
user=user,
table=table,
type_name="form",
name="Form personal",
ownership_type="personal",
)
view2 = handler.create_view(
user=user,
table=table,
type_name="form",
name="Form collaborative",
ownership_type="collaborative",
)
views = View.objects.filter(table=table)
assert len(views) == 2
user.profile.to_be_deleted = True
user.profile.save()
user.last_login = timezone.now() - timedelta(weeks=100)
user.save()
UserHandler().delete_expired_users_and_related_groups_if_last_admin(
grace_delay=timedelta(days=1)
)
views = View.objects.filter(table=table)
assert len(views) == 1
assert views[0].name == "Form collaborative"

View file

@ -0,0 +1,154 @@
from unittest.mock import patch
import pytest
from baserow.contrib.database.views.handler import ViewHandler
@pytest.mark.django_db(transaction=True)
def test_view_signals_not_collaborative(
data_fixture, alternative_per_group_license_service
):
group = data_fixture.create_group(name="Group 1")
user = data_fixture.create_user(group=group)
database = data_fixture.create_database_application(group=group)
table = data_fixture.create_database_table(user=user, database=database)
alternative_per_group_license_service.restrict_user_premium_to(user, group.id)
field = data_fixture.create_text_field(table=table)
field2 = data_fixture.create_text_field(table=table)
view = data_fixture.create_grid_view(user=user, table=table)
view.ownership_type = "personal"
view.created_by = user
view.save()
# view_created
with patch("baserow.ws.registries.broadcast_to_channel_group") as broadcast:
ViewHandler().create_view(
user=user,
table=table,
type_name="grid",
name="Grid",
ownership_type="personal",
)
broadcast.delay.assert_not_called()
with patch(
"baserow.contrib.database.ws.views.signals.broadcast_to_users"
) as broadcast:
ViewHandler().create_view(
user=user,
table=table,
type_name="grid",
name="Grid",
ownership_type="personal",
)
args = broadcast.delay.call_args
assert args[0][0] == [user.id]
# view_updated
with patch("baserow.ws.registries.broadcast_to_channel_group") as broadcast:
ViewHandler().update_view(user=user, view=view, name="View")
broadcast.delay.assert_not_called()
# view_deleted
with patch("baserow.ws.registries.broadcast_to_channel_group") as broadcast:
ViewHandler().delete_view(user=user, view=view)
broadcast.delay.assert_not_called()
view = data_fixture.create_grid_view(user=user, table=table)
view.ownership_type = "personal"
view.created_by = user
view.save()
with patch(
"baserow.contrib.database.ws.views.signals.broadcast_to_users"
) as broadcast:
ViewHandler().delete_view(user=user, view=view)
args = broadcast.delay.call_args
assert args[0][0] == [user.id]
view = data_fixture.create_grid_view(user=user, table=table)
view.ownership_type = "personal"
view.created_by = user
view.save()
filter = ViewHandler().create_filter(user, view, field, "equal", "value")
equal_sort = data_fixture.create_view_sort(user=user, view=view, field=field)
decorator_type_name = "left_border_color"
value_provider_type_name = ""
value_provider_conf = {}
decoration = data_fixture.create_view_decoration(user=user, view=view)
# views_reordered
with patch("baserow.ws.registries.broadcast_to_channel_group") as broadcast:
ViewHandler().order_views(user=user, table=table, order=[view.id])
broadcast.delay.assert_not_called()
# view_filter_created
with patch("baserow.ws.registries.broadcast_to_channel_group") as broadcast:
ViewHandler().create_filter(user, view, field, "equal", "value")
broadcast.delay.assert_not_called()
# view_filter_updated
with patch("baserow.ws.registries.broadcast_to_channel_group") as broadcast:
ViewHandler().update_filter(user, filter, field, "equal", "another value")
broadcast.delay.assert_not_called()
# view_filter_deleted
with patch("baserow.ws.registries.broadcast_to_channel_group") as broadcast:
ViewHandler().delete_filter(user, filter)
broadcast.delay.assert_not_called()
# view_sort_created
with patch("baserow.ws.registries.broadcast_to_channel_group") as broadcast:
ViewHandler().create_sort(user=user, view=view, field=field2, order="ASC")
broadcast.delay.assert_not_called()
# view_sort_updated
with patch("baserow.ws.registries.broadcast_to_channel_group") as broadcast:
ViewHandler().update_sort(user, equal_sort, field)
broadcast.delay.assert_not_called()
# view_sort_deleted
with patch("baserow.ws.registries.broadcast_to_channel_group") as broadcast:
ViewHandler().delete_sort(user, equal_sort)
broadcast.delay.assert_not_called()
# view_decoration_created
with patch("baserow.ws.registries.broadcast_to_channel_group") as broadcast:
ViewHandler().create_decoration(
view,
decorator_type_name,
value_provider_type_name,
value_provider_conf,
user=user,
)
broadcast.delay.assert_not_called()
# view_decoration_updated
with patch("baserow.ws.registries.broadcast_to_channel_group") as broadcast:
ViewHandler().update_decoration(decoration, user)
broadcast.delay.assert_not_called()
# view_decoration_deleted
with patch("baserow.ws.registries.broadcast_to_channel_group") as broadcast:
ViewHandler().delete_decoration(decoration, user)
broadcast.delay.assert_not_called()
# view_field_options_updated
with patch("baserow.ws.registries.broadcast_to_channel_group") as broadcast:
ViewHandler().update_field_options(view, {}, user)
broadcast.delay.assert_not_called()

View file

@ -282,5 +282,8 @@
"label": "Hide Baserow logo on shared view",
"premiumModalName": "public logo removal"
}
},
"viewsContext": {
"personal": "Personal"
}
}

View file

@ -35,6 +35,7 @@ import es from '@baserow_premium/locales/es.json'
import it from '@baserow_premium/locales/it.json'
import pl from '@baserow_premium/locales/pl.json'
import { PremiumLicenseType } from '@baserow_premium/licenseTypes'
import { PersonalViewOwnershipType } from '@baserow_premium/viewOwnershipTypes'
export default (context) => {
const { store, app, isDev } = context
@ -89,6 +90,11 @@ export default (context) => {
new ConditionalColorValueProviderType(context)
)
app.$registry.register(
'viewOwnershipType',
new PersonalViewOwnershipType(context)
)
app.$registry.register('formViewMode', new FormViewSurveyModeType(context))
app.$registry.register('license', new PremiumLicenseType(context))

View file

@ -0,0 +1,25 @@
import { ViewOwnershipType } from '@baserow/modules/database/viewOwnershipTypes'
import PremiumFeatures from '@baserow_premium/features'
export class PersonalViewOwnershipType extends ViewOwnershipType {
static getType() {
return 'personal'
}
getName() {
const { i18n } = this.app
return i18n.t('viewOwnershipType.personal')
}
getIconClass() {
return 'fas fa-lock'
}
isDeactivated(groupId) {
return !this.app.$hasFeature(PremiumFeatures.PREMIUM, groupId)
}
getListViewTypeSort() {
return 40
}
}

View file

@ -48,6 +48,8 @@
@import 'views/gallery';
@import 'views/sharing';
@import 'views/public';
@import 'views/view_ownership_select';
@import 'views/views_context';
@import 'decorator/list';
@import 'decorator/item';
@import 'decorator/context';

View file

@ -0,0 +1,7 @@
.view-ownership-select {
display: flex;
}
.view-ownership-select .radio {
padding-right: 15px;
}

View file

@ -0,0 +1,10 @@
.views-context .section-header {
color: $color-neutral-600;
margin-left: 10px;
margin-top: 10px;
font-size: 14px;
}
.views-context .section-header ~ .section-header {
margin-top: 0;
}

View file

@ -16,6 +16,7 @@
<CreateViewModal
ref="createModal"
:table="table"
:database="database"
:view-type="viewType"
@created="$emit('created', $event)"
></CreateViewModal>

View file

@ -12,6 +12,7 @@
:is="viewType.getViewFormComponent()"
ref="viewForm"
:default-name="getDefaultName()"
:database="database"
@submitted="submitted"
>
<div class="actions">
@ -46,6 +47,10 @@ export default {
type: Object,
required: true,
},
database: {
type: Object,
required: true,
},
viewType: {
type: Object,
required: true,

View file

@ -2,7 +2,6 @@
<form @submit.prevent="submit">
<FormElement :error="fieldHasErrors('name')" class="control">
<label class="control__label">
<i class="fas fa-font"></i>
{{ $t('viewForm.name') }}
</label>
<div class="control__elements">
@ -20,17 +19,38 @@
</div>
</div>
</FormElement>
<FormElement class="control">
<label class="control__label">
{{ $t('viewForm.whoCanEdit') }}
</label>
<div class="control__elements view-ownership-select">
<Radio
v-for="type in viewOwnershipTypes"
:key="type.getType()"
v-model="values.ownershipType"
:value="type.getType()"
:disabled="type.isDeactivated()"
>
<i :class="type.getIconClass()"></i>
{{ type.getName() }}
<div v-if="type.isDeactivated()" class="deactivated-label">
<i class="fas fa-lock"></i>
</div>
</Radio>
</div>
</FormElement>
<slot></slot>
</form>
</template>
<script>
import { required } from 'vuelidate/lib/validators'
import form from '@baserow/modules/core/mixins/form'
import Radio from '@baserow/modules/core/components/Radio'
export default {
name: 'ViewForm',
components: { Radio },
mixins: [form],
props: {
defaultName: {
@ -38,14 +58,24 @@ export default {
required: false,
default: '',
},
database: {
type: Object,
required: true,
},
},
data() {
return {
values: {
name: this.defaultName,
ownershipType: 'collaborative',
},
}
},
computed: {
viewOwnershipTypes() {
return this.$registry.getAll('viewOwnershipType')
},
},
mounted() {
this.$refs.name.focus()
},

View file

@ -1,5 +1,5 @@
<template>
<Context ref="viewsContext" class="select" @shown="shown">
<Context ref="viewsContext" class="select views-context" @shown="shown">
<div class="select__search">
<i class="select__search-icon fas fa-search"></i>
<input
@ -12,35 +12,43 @@
<div v-if="isLoading" class="context--loading">
<div class="loading"></div>
</div>
<ul
v-if="!isLoading && views.length > 0"
ref="dropdown"
v-auto-overflow-scroll
class="select__items"
>
<ViewsContextItem
v-for="view in searchAndOrder(views)"
:ref="'view-' + view.id"
:key="view.id"
v-sortable="{
enabled:
!readOnly &&
$hasPermission(
'database.table.order_views',
table,
database.group.id
),
id: view.id,
update: order,
marginTop: -1.5,
}"
:database="database"
:view="view"
:table="table"
:read-only="readOnly"
@selected="selectedView"
></ViewsContextItem>
</ul>
<div v-for="type in activeViewOwnershipTypes" :key="type.getType()">
<div
v-if="viewsByOwnership(views, type.getType()).length > 0"
class="section-header"
>
{{ type.getName() }}
</div>
<ul
v-if="!isLoading && viewsByOwnership(views, type.getType()).length > 0"
ref="dropdown"
v-auto-overflow-scroll
class="select__items"
>
<ViewsContextItem
v-for="view in viewsByOwnership(views, type.getType())"
:ref="'view-' + view.id"
:key="view.id"
v-sortable="{
enabled:
!readOnly &&
$hasPermission(
'database.table.order_views',
table,
database.group.id
),
id: view.id,
update: createOrderCall(view.ownership_type),
marginTop: -1.5,
}"
:database="database"
:view="view"
:table="table"
:read-only="readOnly"
@selected="selectedView"
></ViewsContextItem>
</ul>
</div>
<div v-if="!isLoading && views.length == 0" class="context__description">
{{ $t('viewsContext.noViews') }}
</div>
@ -118,6 +126,20 @@ export default {
isLoading: (state) => state.view.loading,
isLoaded: (state) => state.view.loaded,
}),
viewOwnershipTypes() {
return this.$registry.getAll('viewOwnershipType')
},
activeViewOwnershipTypes() {
return Object.fromEntries(
Object.entries(this.viewOwnershipTypes)
.filter(
([key]) => this.viewOwnershipTypes[key].isDeactivated() === false
)
.sort(
(a, b) => a[1].getListViewTypeSort() - b[1].getListViewTypeSort()
)
)
},
},
methods: {
shown() {
@ -201,10 +223,16 @@ export default {
})
.sort((a, b) => a.order - b.order)
},
async order(order, oldOrder) {
viewsByOwnership(views, ownershipType) {
return this.searchAndOrder(views).filter(
(view) => view.ownership_type === ownershipType
)
},
async order(ownershipType, order, oldOrder) {
try {
await this.$store.dispatch('view/order', {
table: this.table,
ownershipType,
order,
oldOrder,
})
@ -212,6 +240,11 @@ export default {
notifyIf(error, 'view')
}
},
createOrderCall(ownershipType) {
return (...lastArgs) => {
return this.order(ownershipType, ...lastArgs)
}
},
},
}
</script>

View file

@ -412,7 +412,8 @@
},
"viewsContext": {
"searchView": "Search views",
"noViews": "No views found"
"noViews": "No views found",
"collaborative": "Collaborative"
},
"viewFilterTypeLinkRow": {
"unnamed": "unnamed row {value}",
@ -518,7 +519,12 @@
"delete": "Delete view"
},
"viewForm": {
"name": "Name"
"name": "Name",
"whoCanEdit": "Who can edit"
},
"viewOwnershipType": {
"collaborative": "Collaborative",
"personal": "Personal"
},
"galleryViewHeader": {
"customizeCards": "Customize cards"
@ -741,4 +747,4 @@
"errorEmptyFileNameTitle": "Invalid file name",
"errorEmptyFileNameMessage": "You can't set an empty name for a file."
}
}
}

View file

@ -206,6 +206,7 @@ import {
MedianViewAggregationType,
} from '@baserow/modules/database/viewAggregationTypes'
import { FormViewFormModeType } from '@baserow/modules/database/formViewModeTypes'
import { CollaborativeViewOwnershipType } from '@baserow/modules/database/viewOwnershipTypes'
import { DatabasePlugin } from '@baserow/modules/database/plugins'
import en from '@baserow/modules/database/locales/en.json'
@ -343,6 +344,12 @@ export default (context) => {
)
app.$registry.register('viewFilter', new EmptyViewFilterType(context))
app.$registry.register('viewFilter', new NotEmptyViewFilterType(context))
app.$registry.register(
'viewOwnershipType',
new CollaborativeViewOwnershipType(context)
)
app.$registry.register('field', new TextFieldType(context))
app.$registry.register('field', new LongTextFieldType(context))
app.$registry.register('field', new LinkRowFieldType(context))

View file

@ -254,7 +254,7 @@ export const registerRealtimeEvents = (realtime) => {
realtime.registerEvent('views_reordered', ({ store, app }, data) => {
const table = store.getters['table/getSelected']
if (table !== undefined && table.id === data.table_id) {
store.commit('view/ORDER_ITEMS', data.order)
store.commit('view/ORDER_ITEMS', { order: data.order })
}
})

View file

@ -43,7 +43,11 @@ export default (client) => {
return client.get(`/database/views/table/${tableId}/`, config)
},
create(tableId, values) {
return client.post(`/database/views/table/${tableId}/`, values)
return client.post(`/database/views/table/${tableId}/`, {
name: values.name,
ownership_type: values.ownershipType,
type: values.type,
})
},
get(
viewId,
@ -78,9 +82,10 @@ export default (client) => {
duplicate(viewId) {
return client.post(`/database/views/${viewId}/duplicate/`)
},
order(tableId, order) {
order(tableId, ownershipType, order) {
return client.post(`/database/views/table/${tableId}/order/`, {
view_ids: order,
ownership_type: ownershipType,
})
},
delete(viewId) {

View file

@ -94,8 +94,15 @@ export const mutations = {
populateView(state.items[index], this.$registry)
}
},
ORDER_ITEMS(state, order) {
state.items.forEach((view) => {
ORDER_ITEMS(state, { ownershipType, order }) {
if (ownershipType === undefined) {
const firstView = state.items.find((item) => item.id === order[0])
ownershipType = firstView.ownership_type
}
const items = state.items.filter(
(view) => view.ownership_type === ownershipType
)
items.forEach((view) => {
const index = order.findIndex((value) => value === view.id)
view.order = index === -1 ? 0 : index + 1
})
@ -315,13 +322,13 @@ export const actions = {
/**
* Updates the order of all the views in a table.
*/
async order({ commit, getters }, { table, order, oldOrder }) {
commit('ORDER_ITEMS', order)
async order({ commit, getters }, { table, ownershipType, order, oldOrder }) {
commit('ORDER_ITEMS', { ownershipType, order })
try {
await ViewService(this.$client).order(table.id, order)
await ViewService(this.$client).order(table.id, ownershipType, order)
} catch (error) {
commit('ORDER_ITEMS', oldOrder)
commit('ORDER_ITEMS', { ownershipType, oldOrder })
throw error
}
},

View file

@ -0,0 +1,61 @@
import { Registerable } from '@baserow/modules/core/registry'
export class ViewOwnershipType extends Registerable {
/**
* A human readable name of the view ownership type.
*/
getName() {
return null
}
/**
* The icon for the type in the form of CSS class.
*/
getIconClass() {
return null
}
/**
* Indicates if the view ownership type is disabled.
*/
isDeactivated(groupId) {
return false
}
/**
* The order in which groups of diff. view ownership
* types appear in the list views.
*/
getListViewTypeSort() {
return 50
}
/**
* @return object
*/
serialize() {
return {
type: this.type,
name: this.getName(),
}
}
}
export class CollaborativeViewOwnershipType extends ViewOwnershipType {
static getType() {
return 'collaborative'
}
getName() {
const { i18n } = this.app
return i18n.t('viewOwnershipType.collaborative')
}
getIconClass() {
return 'fas fa-users'
}
isDeactivated(groupId) {
return false
}
}