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 #1448 See merge request bramw/baserow!1201
This commit is contained in:
commit
3f68954882
72 changed files with 2884 additions and 495 deletions
backend
pytest.ini
changelog.mddocker-compose.local-build.ymldocker-compose.no-caddy.ymldocker-compose.ymlsrc/baserow
config/settings
contrib/database
api
apps.pyfields
migrations
table
views
actions.pyexceptions.pyhandler.pymodels.pyoperations.pyregistries.pyview_ownership_types.pyview_types.py
ws/views
tests/baserow/contrib/database
airtable
api
trash
view
ws/public
e2e-tests/playwright-report
enterprise/backend/src/baserow_enterprise/role
premium
backend
web-frontend/modules/baserow_premium
web-frontend/modules
core/assets/scss/components
database
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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.",
|
||||
)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -39,7 +39,3 @@ class RestoreFieldOperationType(FieldOperationType):
|
|||
|
||||
class DuplicateFieldOperationType(FieldOperationType):
|
||||
type = "database.table.field.duplicate"
|
||||
|
||||
|
||||
class ReadAggregationDatabaseTableOperationType(FieldOperationType):
|
||||
type = "database.table.field.read_aggregation"
|
||||
|
|
|
@ -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),
|
||||
),
|
||||
]
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
]
|
|
@ -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"
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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.
|
||||
"""
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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"
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -273,8 +273,10 @@ def test_to_baserow_database_export():
|
|||
"filters": [],
|
||||
"sortings": [],
|
||||
"decorations": [],
|
||||
"ownership_type": "collaborative",
|
||||
"public": False,
|
||||
"field_options": [],
|
||||
"created_by": None,
|
||||
}
|
||||
]
|
||||
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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}",
|
||||
)
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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])
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
]
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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:
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
||||
|
|
62
e2e-tests/playwright-report/index.html
Normal file
62
e2e-tests/playwright-report/index.html
Normal file
File diff suppressed because one or more lines are too long
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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())
|
||||
|
|
144
premium/backend/src/baserow_premium/permission_manager.py
Normal file
144
premium/backend/src/baserow_premium/permission_manager.py
Normal 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)
|
|
@ -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()
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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",
|
||||
]
|
||||
|
|
|
@ -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]
|
|
@ -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"
|
|
@ -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
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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],
|
||||
)
|
||||
|
|
|
@ -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"
|
|
@ -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()
|
|
@ -282,5 +282,8 @@
|
|||
"label": "Hide Baserow logo on shared view",
|
||||
"premiumModalName": "public logo removal"
|
||||
}
|
||||
},
|
||||
"viewsContext": {
|
||||
"personal": "Personal"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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';
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
.view-ownership-select {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.view-ownership-select .radio {
|
||||
padding-right: 15px;
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -16,6 +16,7 @@
|
|||
<CreateViewModal
|
||||
ref="createModal"
|
||||
:table="table"
|
||||
:database="database"
|
||||
:view-type="viewType"
|
||||
@created="$emit('created', $event)"
|
||||
></CreateViewModal>
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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()
|
||||
},
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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."
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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))
|
||||
|
|
|
@ -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 })
|
||||
}
|
||||
})
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
},
|
||||
|
|
61
web-frontend/modules/database/viewOwnershipTypes.js
Normal file
61
web-frontend/modules/database/viewOwnershipTypes.js
Normal 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
|
||||
}
|
||||
}
|
Loading…
Add table
Reference in a new issue