diff --git a/backend/src/baserow/api/applications/views.py b/backend/src/baserow/api/applications/views.py index 1fa73f0c0..f24339451 100755 --- a/backend/src/baserow/api/applications/views.py +++ b/backend/src/baserow/api/applications/views.py @@ -43,7 +43,7 @@ from baserow.core.job_types import DuplicateApplicationJobType from baserow.core.jobs.exceptions import MaxJobCountExceeded from baserow.core.jobs.handler import JobHandler from baserow.core.jobs.registries import job_type_registry -from baserow.core.models import Application, Workspace +from baserow.core.models import Application from baserow.core.operations import CreateApplicationsWorkspaceOperationType from baserow.core.service import CoreService from baserow.core.trash.exceptions import CannotDeleteAlreadyDeletedItem @@ -81,13 +81,15 @@ class AllApplicationsView(APIView): returned. """ - workspaces = Workspace.objects.filter(users=request.user).prefetch_related( - "workspaceuser_set", "template_set" - ) + workspaces = CoreService().list_workspaces(request.user) + applications_qs = Application.objects.none() for workspace in workspaces: + workspace_applications_qs = CoreService().list_applications_in_workspace( + request.user, workspace + ) applications_qs = applications_qs.union( - CoreService().list_applications_in_workspace(request.user, workspace) + workspace_applications_qs.order_by(), all=True ) data = [ @@ -148,7 +150,6 @@ class ApplicationsView(APIView): """ workspace = CoreService().get_workspace(request.user, workspace_id) - applications = CoreService().list_applications_in_workspace( request.user, workspace ) diff --git a/backend/src/baserow/contrib/database/api/fields/serializers.py b/backend/src/baserow/contrib/database/api/fields/serializers.py index 1036a2fd8..311306305 100644 --- a/backend/src/baserow/contrib/database/api/fields/serializers.py +++ b/backend/src/baserow/contrib/database/api/fields/serializers.py @@ -339,6 +339,37 @@ class CollaboratorSerializer(serializers.Serializer): name = serializers.CharField(source="first_name", read_only=True) +class AvailableCollaboratorsSerializer(serializers.ListField): + def __init__(self, **kwargs): + kwargs["child"] = CollaboratorSerializer() + kwargs["read_only"] = True + kwargs["source"] = "*" + kwargs["help_text"] = "A list of all the available collaborators." + + super().__init__(**kwargs) + + def get_attribute(self, instance): + return super().get_attribute(instance) + + def to_representation(self, instance): + field_type = instance.get_type() + if not field_type.can_represent_collaborators(instance): + return [] + + workspace = instance.table.database.workspace + if not hasattr(workspace, "available_collaborators"): + setattr( + workspace, + "available_collaborators", + workspace.users.order_by("first_name"), + ) + + return [ + CollaboratorSerializer(user).data + for user in workspace.available_collaborators + ] + + class DuplicateFieldParamsSerializer(serializers.Serializer): duplicate_data = serializers.BooleanField( default=False, help_text="Indicates whether the data should be duplicated." diff --git a/backend/src/baserow/contrib/database/api/formula/serializers.py b/backend/src/baserow/contrib/database/api/formula/serializers.py index f777091f3..d9d424ec6 100644 --- a/backend/src/baserow/contrib/database/api/formula/serializers.py +++ b/backend/src/baserow/contrib/database/api/formula/serializers.py @@ -39,21 +39,3 @@ class BaserowFormulaSelectOptionsSerializer(serializers.ListField): return [self.child.to_representation(item) for item in select_options] else: return [] - - -class BaserowFormulaCollaboratorsSerializer(serializers.ListField): - def get_attribute(self, instance): - return instance - - def to_representation(self, field): - field_type = field_type_registry.get_by_model(field) - - # Available collaborators are needed for view filters in the frontend, - # but let's avoid the potentially slow query if not required. - if field_type.can_represent_collaborators(field): - available_collaborators = field.table.database.workspace.users.all() - return [ - self.child.to_representation(item) for item in available_collaborators - ] - else: - return [] diff --git a/backend/src/baserow/contrib/database/fields/field_types.py b/backend/src/baserow/contrib/database/fields/field_types.py index 33936afe9..b57285a55 100755 --- a/backend/src/baserow/contrib/database/fields/field_types.py +++ b/backend/src/baserow/contrib/database/fields/field_types.py @@ -70,6 +70,7 @@ from baserow.contrib.database.api.fields.errors import ( ERROR_WITH_FORMULA, ) from baserow.contrib.database.api.fields.serializers import ( + AvailableCollaboratorsSerializer, BaserowBooleanField, CollaboratorSerializer, DurationFieldSerializer, @@ -1506,15 +1507,16 @@ class LastModifiedByFieldType(ReadOnlyFieldType): source_field_name = "last_modified_by" model_field_kwargs = {"sync_with": "last_modified_by"} + request_serializer_field_names = [] + request_serializer_field_overrides = {} serializer_field_names = ["available_collaborators"] serializer_field_overrides = { - "available_collaborators": serializers.ListField( - child=CollaboratorSerializer(), - read_only=True, - source="table.database.workspace.users.all", - ), + "available_collaborators": AvailableCollaboratorsSerializer(), } + def can_represent_collaborators(self, field): + return True + def get_model_field(self, instance, **kwargs): kwargs["null"] = True kwargs["blank"] = True @@ -1731,15 +1733,16 @@ class CreatedByFieldType(ReadOnlyFieldType): source_field_name = "created_by" model_field_kwargs = {"sync_with_add": "created_by"} + request_serializer_field_names = [] + request_serializer_field_overrides = {} serializer_field_names = ["available_collaborators"] serializer_field_overrides = { - "available_collaborators": serializers.ListField( - child=CollaboratorSerializer(), - read_only=True, - source="table.database.workspace.users.all", - ), + "available_collaborators": AvailableCollaboratorsSerializer(), } + def can_represent_collaborators(self, field): + return True + def get_model_field(self, instance, **kwargs): kwargs["null"] = True kwargs["blank"] = True @@ -6108,18 +6111,24 @@ class MultipleCollaboratorsFieldType( model_class = MultipleCollaboratorsField can_get_unique_values = False allowed_fields = ["notify_user_when_added"] - serializer_field_names = ["available_collaborators", "notify_user_when_added"] - serializer_field_overrides = { - "available_collaborators": serializers.ListField( - child=CollaboratorSerializer(), - read_only=True, - source="table.database.workspace.users.all", - ), + request_serializer_field_names = ["notify_user_when_added"] + request_serializer_field_overrides = { "notify_user_when_added": serializers.BooleanField(required=False), } + serializer_field_names = [ + "available_collaborators", + *request_serializer_field_names, + ] + serializer_field_overrides = { + "available_collaborators": AvailableCollaboratorsSerializer(), + **request_serializer_field_overrides, + } is_many_to_many_field = True _can_group_by = True + def can_represent_collaborators(self, field): + return True + def get_serializer_field(self, instance, **kwargs): required = kwargs.pop("required", False) field_serializer = CollaboratorSerializer( diff --git a/backend/src/baserow/contrib/database/fields/handler.py b/backend/src/baserow/contrib/database/fields/handler.py index 28726cbe7..f5cf767c1 100644 --- a/backend/src/baserow/contrib/database/fields/handler.py +++ b/backend/src/baserow/contrib/database/fields/handler.py @@ -208,6 +208,7 @@ class FieldHandler(metaclass=baserow_trace_methods(tracer)): queryset=User.objects.filter(profile__to_be_deleted=False).order_by( "first_name" ), + to_attr="available_collaborators", ), "select_options", ) diff --git a/backend/src/baserow/contrib/database/formula/types/formula_types.py b/backend/src/baserow/contrib/database/formula/types/formula_types.py index ba7bfcd92..d84c9fd42 100644 --- a/backend/src/baserow/contrib/database/formula/types/formula_types.py +++ b/backend/src/baserow/contrib/database/formula/types/formula_types.py @@ -1345,11 +1345,10 @@ class BaserowFormulaArrayType( @classmethod def get_serializer_field_overrides(cls): from baserow.contrib.database.api.fields.serializers import ( - CollaboratorSerializer, + AvailableCollaboratorsSerializer, SelectOptionSerializer, ) from baserow.contrib.database.api.formula.serializers import ( - BaserowFormulaCollaboratorsSerializer, BaserowFormulaSelectOptionsSerializer, ) @@ -1360,11 +1359,8 @@ class BaserowFormulaArrayType( allow_null=True, read_only=True, ), - "available_collaborators": BaserowFormulaCollaboratorsSerializer( - child=CollaboratorSerializer(), - required=False, - allow_null=True, - read_only=True, + "available_collaborators": AvailableCollaboratorsSerializer( + required=False, allow_null=True ), } @@ -1779,18 +1775,12 @@ class BaserowFormulaMultipleCollaboratorsType(BaserowJSONBObjectBaseType): @classmethod def get_serializer_field_overrides(cls): from baserow.contrib.database.api.fields.serializers import ( - CollaboratorSerializer, - ) - from baserow.contrib.database.api.formula.serializers import ( - BaserowFormulaCollaboratorsSerializer, + AvailableCollaboratorsSerializer, ) return { - "available_collaborators": BaserowFormulaCollaboratorsSerializer( - child=CollaboratorSerializer(), - allow_null=True, - required=False, - read_only=True, + "available_collaborators": AvailableCollaboratorsSerializer( + allow_null=True, required=False ) } diff --git a/backend/src/baserow/core/handler.py b/backend/src/baserow/core/handler.py index 78c8b378d..7270e3bd8 100755 --- a/backend/src/baserow/core/handler.py +++ b/backend/src/baserow/core/handler.py @@ -521,6 +521,41 @@ class CoreHandler(metaclass=baserow_trace_methods(tracer)): ), ) + def list_user_workspaces( + self, user: AbstractUser, base_queryset: QuerySet[Workspace] = None + ) -> QuerySet[Workspace]: + """ + Returns a queryset of all workspaces the user is in. + + :param user: The user for which to get the workspaces. + :param base_queryset: The base queryset from where to select the workspaces + object. This can for example be used to do a `prefetch_related`. + :return: A queryset of all workspaces the user is in. + """ + + workspace_qs = self.get_enhanced_workspace_queryset(base_queryset) + return workspace_qs.filter(workspaceuser__user=user) + + def get_enhanced_workspace_queryset( + self, queryset: QuerySet[Workspace] | None = None + ) -> QuerySet[Workspace]: + """ + Enhances the workspace queryset with additional prefetches and filters based on + the plugins registered in the plugin registry. + + :param queryset: The Workspace queryset to enhance. + :return: The enhanced queryset. + """ + + if queryset is None: + queryset = Workspace.objects.all() + + queryset = queryset.prefetch_related("workspaceuser_set", "template_set") + + for plugin in plugin_registry.registry.values(): + queryset = plugin.enhance_workspace_queryset(queryset) + return queryset + def get_workspace( self, workspace_id: int, base_queryset: QuerySet = None ) -> Workspace: @@ -535,11 +570,10 @@ class CoreHandler(metaclass=baserow_trace_methods(tracer)): :return: The requested workspace instance of the provided id. """ - if base_queryset is None: - base_queryset = Workspace.objects + workspace_qs = self.get_enhanced_workspace_queryset(base_queryset) try: - workspace = base_queryset.get(id=workspace_id) + workspace = workspace_qs.get(id=workspace_id) except Workspace.DoesNotExist: raise WorkspaceDoesNotExist( f"The workspace with id {workspace_id} does not exist." diff --git a/backend/src/baserow/core/registries.py b/backend/src/baserow/core/registries.py index 7ad2abfb5..7ed9dadd7 100755 --- a/backend/src/baserow/core/registries.py +++ b/backend/src/baserow/core/registries.py @@ -215,8 +215,22 @@ class Plugin(APIUrlsInstanceMixin, Instance): :type user: User """ + def enhance_workspace_queryset( + self, queryset: QuerySet["Workspace"] + ) -> QuerySet["Workspace"]: + """ + Optimizes the queryset by adding select and prefetch related statements. + This reduces queries and improves performance when accessing workspace-related + models in plugin views or methods. -class PluginRegistry(APIUrlsRegistryMixin, Registry): + :param queryset: The queryset to optimize. + :return: The optimized queryset. + """ + + return queryset + + +class PluginRegistry(APIUrlsRegistryMixin, Registry[Plugin]): """ With the plugin registry it is possible to register new plugins. A plugin is an abstraction made specifically for Baserow. It allows a plugin developer to diff --git a/backend/src/baserow/core/service.py b/backend/src/baserow/core/service.py index 89094e73c..03926186d 100644 --- a/backend/src/baserow/core/service.py +++ b/backend/src/baserow/core/service.py @@ -8,6 +8,7 @@ from baserow.core.handler import CoreHandler from baserow.core.models import Application, Workspace from baserow.core.operations import ( ListApplicationsWorkspaceOperationType, + ListWorkspacesOperationType, ReadApplicationOperationType, ReadWorkspaceOperationType, ) @@ -18,11 +19,26 @@ class CoreService: def __init__(self): self.handler = CoreHandler() - def _filter_specific_queryset(self, user: AbstractUser, workspace: Workspace): + def _enhance_and_filter_application_queryset( + self, user: AbstractUser, workspace: Workspace + ): return lambda model, queryset: application_type_registry.get_by_model( model ).enhance_and_filter_queryset(queryset, user, workspace) + def list_workspaces(self, user: AbstractUser) -> QuerySet[Workspace]: + """ + Get a list of all the workspaces the user has access to. + + :param user: The user trying to access the workspaces + :return: A list of workspaces. + """ + + workspace_qs = self.handler.list_user_workspaces(user) + return self.handler.filter_queryset( + user, ListWorkspacesOperationType.type, workspace_qs + ) + def get_workspace(self, user: AbstractUser, workspace_id: int) -> Workspace: """ Get the workspace associated to the given id if the user has the permission @@ -77,7 +93,7 @@ class CoreService: if specific: application_qs = self.handler.filter_specific_applications( application_qs, - per_content_type_queryset_hook=self._filter_specific_queryset( + per_content_type_queryset_hook=self._enhance_and_filter_application_queryset( user, workspace ), ) @@ -120,7 +136,7 @@ class CoreService: if specific: application = specific_iterator( [application], - per_content_type_queryset_hook=self._filter_specific_queryset( + per_content_type_queryset_hook=self._enhance_and_filter_application_queryset( user, application.workspace ), base_model=Application,