1
0
Fork 0
mirror of https://gitlab.com/bramw/baserow.git synced 2025-04-10 07:37:30 +00:00

adding ability to enhance workspace queryset

This commit is contained in:
Davide Silvestri 2025-04-01 18:07:14 +02:00
parent 8a679d0550
commit 09c22ae0af
9 changed files with 142 additions and 64 deletions
backend/src/baserow

View file

@ -43,7 +43,7 @@ from baserow.core.job_types import DuplicateApplicationJobType
from baserow.core.jobs.exceptions import MaxJobCountExceeded from baserow.core.jobs.exceptions import MaxJobCountExceeded
from baserow.core.jobs.handler import JobHandler from baserow.core.jobs.handler import JobHandler
from baserow.core.jobs.registries import job_type_registry 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.operations import CreateApplicationsWorkspaceOperationType
from baserow.core.service import CoreService from baserow.core.service import CoreService
from baserow.core.trash.exceptions import CannotDeleteAlreadyDeletedItem from baserow.core.trash.exceptions import CannotDeleteAlreadyDeletedItem
@ -81,13 +81,15 @@ class AllApplicationsView(APIView):
returned. returned.
""" """
workspaces = Workspace.objects.filter(users=request.user).prefetch_related( workspaces = CoreService().list_workspaces(request.user)
"workspaceuser_set", "template_set"
)
applications_qs = Application.objects.none() applications_qs = Application.objects.none()
for workspace in workspaces: for workspace in workspaces:
workspace_applications_qs = CoreService().list_applications_in_workspace(
request.user, workspace
)
applications_qs = applications_qs.union( applications_qs = applications_qs.union(
CoreService().list_applications_in_workspace(request.user, workspace) workspace_applications_qs.order_by(), all=True
) )
data = [ data = [
@ -148,7 +150,6 @@ class ApplicationsView(APIView):
""" """
workspace = CoreService().get_workspace(request.user, workspace_id) workspace = CoreService().get_workspace(request.user, workspace_id)
applications = CoreService().list_applications_in_workspace( applications = CoreService().list_applications_in_workspace(
request.user, workspace request.user, workspace
) )

View file

@ -339,6 +339,37 @@ class CollaboratorSerializer(serializers.Serializer):
name = serializers.CharField(source="first_name", read_only=True) 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): class DuplicateFieldParamsSerializer(serializers.Serializer):
duplicate_data = serializers.BooleanField( duplicate_data = serializers.BooleanField(
default=False, help_text="Indicates whether the data should be duplicated." default=False, help_text="Indicates whether the data should be duplicated."

View file

@ -39,21 +39,3 @@ class BaserowFormulaSelectOptionsSerializer(serializers.ListField):
return [self.child.to_representation(item) for item in select_options] return [self.child.to_representation(item) for item in select_options]
else: else:
return [] 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 []

View file

@ -70,6 +70,7 @@ from baserow.contrib.database.api.fields.errors import (
ERROR_WITH_FORMULA, ERROR_WITH_FORMULA,
) )
from baserow.contrib.database.api.fields.serializers import ( from baserow.contrib.database.api.fields.serializers import (
AvailableCollaboratorsSerializer,
BaserowBooleanField, BaserowBooleanField,
CollaboratorSerializer, CollaboratorSerializer,
DurationFieldSerializer, DurationFieldSerializer,
@ -1506,15 +1507,16 @@ class LastModifiedByFieldType(ReadOnlyFieldType):
source_field_name = "last_modified_by" source_field_name = "last_modified_by"
model_field_kwargs = {"sync_with": "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_names = ["available_collaborators"]
serializer_field_overrides = { serializer_field_overrides = {
"available_collaborators": serializers.ListField( "available_collaborators": AvailableCollaboratorsSerializer(),
child=CollaboratorSerializer(),
read_only=True,
source="table.database.workspace.users.all",
),
} }
def can_represent_collaborators(self, field):
return True
def get_model_field(self, instance, **kwargs): def get_model_field(self, instance, **kwargs):
kwargs["null"] = True kwargs["null"] = True
kwargs["blank"] = True kwargs["blank"] = True
@ -1731,15 +1733,16 @@ class CreatedByFieldType(ReadOnlyFieldType):
source_field_name = "created_by" source_field_name = "created_by"
model_field_kwargs = {"sync_with_add": "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_names = ["available_collaborators"]
serializer_field_overrides = { serializer_field_overrides = {
"available_collaborators": serializers.ListField( "available_collaborators": AvailableCollaboratorsSerializer(),
child=CollaboratorSerializer(),
read_only=True,
source="table.database.workspace.users.all",
),
} }
def can_represent_collaborators(self, field):
return True
def get_model_field(self, instance, **kwargs): def get_model_field(self, instance, **kwargs):
kwargs["null"] = True kwargs["null"] = True
kwargs["blank"] = True kwargs["blank"] = True
@ -6108,18 +6111,24 @@ class MultipleCollaboratorsFieldType(
model_class = MultipleCollaboratorsField model_class = MultipleCollaboratorsField
can_get_unique_values = False can_get_unique_values = False
allowed_fields = ["notify_user_when_added"] allowed_fields = ["notify_user_when_added"]
serializer_field_names = ["available_collaborators", "notify_user_when_added"] request_serializer_field_names = ["notify_user_when_added"]
serializer_field_overrides = { request_serializer_field_overrides = {
"available_collaborators": serializers.ListField(
child=CollaboratorSerializer(),
read_only=True,
source="table.database.workspace.users.all",
),
"notify_user_when_added": serializers.BooleanField(required=False), "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 is_many_to_many_field = True
_can_group_by = True _can_group_by = True
def can_represent_collaborators(self, field):
return True
def get_serializer_field(self, instance, **kwargs): def get_serializer_field(self, instance, **kwargs):
required = kwargs.pop("required", False) required = kwargs.pop("required", False)
field_serializer = CollaboratorSerializer( field_serializer = CollaboratorSerializer(

View file

@ -208,6 +208,7 @@ class FieldHandler(metaclass=baserow_trace_methods(tracer)):
queryset=User.objects.filter(profile__to_be_deleted=False).order_by( queryset=User.objects.filter(profile__to_be_deleted=False).order_by(
"first_name" "first_name"
), ),
to_attr="available_collaborators",
), ),
"select_options", "select_options",
) )

View file

@ -1345,11 +1345,10 @@ class BaserowFormulaArrayType(
@classmethod @classmethod
def get_serializer_field_overrides(cls): def get_serializer_field_overrides(cls):
from baserow.contrib.database.api.fields.serializers import ( from baserow.contrib.database.api.fields.serializers import (
CollaboratorSerializer, AvailableCollaboratorsSerializer,
SelectOptionSerializer, SelectOptionSerializer,
) )
from baserow.contrib.database.api.formula.serializers import ( from baserow.contrib.database.api.formula.serializers import (
BaserowFormulaCollaboratorsSerializer,
BaserowFormulaSelectOptionsSerializer, BaserowFormulaSelectOptionsSerializer,
) )
@ -1360,11 +1359,8 @@ class BaserowFormulaArrayType(
allow_null=True, allow_null=True,
read_only=True, read_only=True,
), ),
"available_collaborators": BaserowFormulaCollaboratorsSerializer( "available_collaborators": AvailableCollaboratorsSerializer(
child=CollaboratorSerializer(), required=False, allow_null=True
required=False,
allow_null=True,
read_only=True,
), ),
} }
@ -1779,18 +1775,12 @@ class BaserowFormulaMultipleCollaboratorsType(BaserowJSONBObjectBaseType):
@classmethod @classmethod
def get_serializer_field_overrides(cls): def get_serializer_field_overrides(cls):
from baserow.contrib.database.api.fields.serializers import ( from baserow.contrib.database.api.fields.serializers import (
CollaboratorSerializer, AvailableCollaboratorsSerializer,
)
from baserow.contrib.database.api.formula.serializers import (
BaserowFormulaCollaboratorsSerializer,
) )
return { return {
"available_collaborators": BaserowFormulaCollaboratorsSerializer( "available_collaborators": AvailableCollaboratorsSerializer(
child=CollaboratorSerializer(), allow_null=True, required=False
allow_null=True,
required=False,
read_only=True,
) )
} }

View file

@ -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( def get_workspace(
self, workspace_id: int, base_queryset: QuerySet = None self, workspace_id: int, base_queryset: QuerySet = None
) -> Workspace: ) -> Workspace:
@ -535,11 +570,10 @@ class CoreHandler(metaclass=baserow_trace_methods(tracer)):
:return: The requested workspace instance of the provided id. :return: The requested workspace instance of the provided id.
""" """
if base_queryset is None: workspace_qs = self.get_enhanced_workspace_queryset(base_queryset)
base_queryset = Workspace.objects
try: try:
workspace = base_queryset.get(id=workspace_id) workspace = workspace_qs.get(id=workspace_id)
except Workspace.DoesNotExist: except Workspace.DoesNotExist:
raise WorkspaceDoesNotExist( raise WorkspaceDoesNotExist(
f"The workspace with id {workspace_id} does not exist." f"The workspace with id {workspace_id} does not exist."

View file

@ -215,8 +215,22 @@ class Plugin(APIUrlsInstanceMixin, Instance):
:type user: User :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 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 abstraction made specifically for Baserow. It allows a plugin developer to

View file

@ -8,6 +8,7 @@ from baserow.core.handler import CoreHandler
from baserow.core.models import Application, Workspace from baserow.core.models import Application, Workspace
from baserow.core.operations import ( from baserow.core.operations import (
ListApplicationsWorkspaceOperationType, ListApplicationsWorkspaceOperationType,
ListWorkspacesOperationType,
ReadApplicationOperationType, ReadApplicationOperationType,
ReadWorkspaceOperationType, ReadWorkspaceOperationType,
) )
@ -18,11 +19,26 @@ class CoreService:
def __init__(self): def __init__(self):
self.handler = CoreHandler() 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( return lambda model, queryset: application_type_registry.get_by_model(
model model
).enhance_and_filter_queryset(queryset, user, workspace) ).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: def get_workspace(self, user: AbstractUser, workspace_id: int) -> Workspace:
""" """
Get the workspace associated to the given id if the user has the permission Get the workspace associated to the given id if the user has the permission
@ -77,7 +93,7 @@ class CoreService:
if specific: if specific:
application_qs = self.handler.filter_specific_applications( application_qs = self.handler.filter_specific_applications(
application_qs, application_qs,
per_content_type_queryset_hook=self._filter_specific_queryset( per_content_type_queryset_hook=self._enhance_and_filter_application_queryset(
user, workspace user, workspace
), ),
) )
@ -120,7 +136,7 @@ class CoreService:
if specific: if specific:
application = specific_iterator( application = specific_iterator(
[application], [application],
per_content_type_queryset_hook=self._filter_specific_queryset( per_content_type_queryset_hook=self._enhance_and_filter_application_queryset(
user, application.workspace user, application.workspace
), ),
base_model=Application, base_model=Application,