1
0
Fork 0
mirror of https://gitlab.com/bramw/baserow.git synced 2025-04-15 01:28:30 +00:00

Polymorphic applications request serializers

This commit is contained in:
Peter Evans 2024-03-15 15:19:08 +00:00
parent e74cfc67cf
commit 8886632a15
28 changed files with 252 additions and 190 deletions

View file

@ -16,3 +16,9 @@ ERROR_APPLICATION_OPERATION_NOT_SUPPORTED = (
HTTP_400_BAD_REQUEST,
"The application does not support this operation.",
)
ERROR_APPLICATION_TYPE_DOES_NOT_EXIST = (
"ERROR_APPLICATION_TYPE_DOES_NOT_EXIST",
HTTP_400_BAD_REQUEST,
"{e}",
)

View file

@ -4,6 +4,7 @@ from drf_spectacular.openapi import OpenApiTypes
from drf_spectacular.utils import extend_schema_field
from rest_framework import serializers
from baserow.api.polymorphic import PolymorphicSerializer
from baserow.api.workspaces.serializers import WorkspaceSerializer
from baserow.core.db import specific_iterator
from baserow.core.models import Application
@ -40,15 +41,13 @@ class ApplicationSerializer(serializers.ModelSerializer):
return application_type_registry.get_by_model(instance.specific_class).type
class SpecificApplicationSerializer(ApplicationSerializer):
def to_representation(self, instance):
specific_instance = instance.specific
return get_application_serializer(
specific_instance, context=self.context
).to_representation(specific_instance)
class PolymorphicApplicationResponseSerializer(PolymorphicSerializer):
base_class = ApplicationSerializer
registry = application_type_registry
request = False
class ApplicationCreateSerializer(serializers.ModelSerializer):
class BaseApplicationCreatePolymorphicSerializer(serializers.ModelSerializer):
type = serializers.ChoiceField(
choices=lazy(application_type_registry.get_types, list)()
)
@ -59,12 +58,24 @@ class ApplicationCreateSerializer(serializers.ModelSerializer):
fields = ("name", "type", "init_with_data")
class ApplicationUpdateSerializer(serializers.ModelSerializer):
class PolymorphicApplicationCreateSerializer(PolymorphicSerializer):
base_class = BaseApplicationCreatePolymorphicSerializer
registry = application_type_registry
request = True
class BaseApplicationUpdatePolymorphicSerializer(serializers.ModelSerializer):
class Meta:
model = Application
fields = ("name",)
class PolymorphicApplicationUpdateSerializer(PolymorphicSerializer):
base_class = BaseApplicationUpdatePolymorphicSerializer
registry = application_type_registry
request = True
class OrderApplicationsSerializer(serializers.Serializer):
application_ids = serializers.ListField(
child=serializers.IntegerField(),
@ -72,31 +83,6 @@ class OrderApplicationsSerializer(serializers.Serializer):
)
def get_application_serializer(instance, **kwargs):
"""
Returns an instantiated serializer based on the instance class type. Custom
serializers can be defined per application type. This function will return the one
that is set else it will return the default one.
:param instance: The instance where a serializer is needed for.
:type instance: Application
:return: An instantiated serializer for the instance.
:rtype: ApplicationSerializer
"""
application = application_type_registry.get_by_model(instance.specific_class)
serializer_class = application.instance_serializer_class
if not serializer_class:
serializer_class = ApplicationSerializer
context = kwargs.pop("context", {})
context["application"] = application
return serializer_class(instance, context=context, **kwargs)
class InstallTemplateJobApplicationsSerializer(serializers.JSONField):
def to_representation(self, value):
application_ids = super().to_representation(value)
@ -109,4 +95,6 @@ class InstallTemplateJobApplicationsSerializer(serializers.JSONField):
pk__in=application_ids, workspace__trashed=False
)
)
return [get_application_serializer(app).data for app in applications]
return [
PolymorphicApplicationResponseSerializer(app).data for app in applications
]

View file

@ -11,6 +11,7 @@ from rest_framework.views import APIView
from baserow.api.applications.errors import (
ERROR_APPLICATION_DOES_NOT_EXIST,
ERROR_APPLICATION_NOT_IN_GROUP,
ERROR_APPLICATION_TYPE_DOES_NOT_EXIST,
)
from baserow.api.decorators import map_exceptions, validate_body
from baserow.api.errors import ERROR_GROUP_DOES_NOT_EXIST, ERROR_USER_NOT_IN_GROUP
@ -22,7 +23,7 @@ from baserow.api.schemas import (
get_error_schema,
)
from baserow.api.trash.errors import ERROR_CANNOT_DELETE_ALREADY_DELETED_ITEM
from baserow.api.utils import DiscriminatorMappingSerializer
from baserow.api.utils import validate_data
from baserow.core.action.registries import action_type_registry
from baserow.core.actions import (
CreateApplicationActionType,
@ -34,6 +35,7 @@ from baserow.core.db import specific_iterator
from baserow.core.exceptions import (
ApplicationDoesNotExist,
ApplicationNotInWorkspace,
ApplicationTypeDoesNotExist,
UserNotInWorkspace,
WorkspaceDoesNotExist,
)
@ -51,20 +53,12 @@ from baserow.core.registries import application_type_registry
from baserow.core.trash.exceptions import CannotDeleteAlreadyDeletedItem
from .serializers import (
ApplicationCreateSerializer,
ApplicationSerializer,
ApplicationUpdateSerializer,
OrderApplicationsSerializer,
get_application_serializer,
PolymorphicApplicationCreateSerializer,
PolymorphicApplicationResponseSerializer,
PolymorphicApplicationUpdateSerializer,
)
application_type_serializers = {
application_type.type: (
application_type.instance_serializer_class or ApplicationSerializer
)
for application_type in application_type_registry.registry.values()
}
DuplicateApplicationJobTypeSerializer = job_type_registry.get(
DuplicateApplicationJobType.type
).get_serializer_class(
@ -86,9 +80,7 @@ class AllApplicationsView(APIView):
"workspaces that the user has access to are going to be listed here."
),
responses={
200: DiscriminatorMappingSerializer(
"Applications", application_type_serializers, many=True
),
200: PolymorphicApplicationResponseSerializer(many=True),
400: get_error_schema(["ERROR_USER_NOT_IN_GROUP"]),
},
)
@ -130,8 +122,8 @@ class AllApplicationsView(APIView):
)
data = [
get_application_serializer(
application, context={"request": request, "application": application}
PolymorphicApplicationResponseSerializer(
application, context={"request": request}
).data
for application in applications
]
@ -167,9 +159,7 @@ class ApplicationsView(APIView):
"type. An application always belongs to a single workspace."
),
responses={
200: DiscriminatorMappingSerializer(
"Applications", application_type_serializers, many=True
),
200: PolymorphicApplicationResponseSerializer(many=True),
400: get_error_schema(["ERROR_USER_NOT_IN_GROUP"]),
404: get_error_schema(["ERROR_GROUP_DOES_NOT_EXIST"]),
},
@ -221,7 +211,9 @@ class ApplicationsView(APIView):
)
data = [
get_application_serializer(application, context={"request": request}).data
PolymorphicApplicationResponseSerializer(
application, context={"request": request}
).data
for application in applications
]
return Response(data)
@ -246,11 +238,9 @@ class ApplicationsView(APIView):
"`workspace_id` parameter. If the authorized user does not belong to the workspace "
"an error will be returned."
),
request=ApplicationCreateSerializer,
request=PolymorphicApplicationCreateSerializer,
responses={
200: DiscriminatorMappingSerializer(
"Applications", application_type_serializers
),
200: PolymorphicApplicationResponseSerializer,
400: get_error_schema(
["ERROR_USER_NOT_IN_GROUP", "ERROR_REQUEST_BODY_VALIDATION"]
),
@ -258,13 +248,14 @@ class ApplicationsView(APIView):
},
)
@transaction.atomic
@validate_body(ApplicationCreateSerializer)
@map_exceptions(
{
WorkspaceDoesNotExist: ERROR_GROUP_DOES_NOT_EXIST,
UserNotInWorkspace: ERROR_USER_NOT_IN_GROUP,
ApplicationTypeDoesNotExist: ERROR_APPLICATION_TYPE_DOES_NOT_EXIST,
}
)
@validate_body(PolymorphicApplicationCreateSerializer)
def post(self, request, data, workspace_id):
"""Creates a new application for a user."""
@ -280,13 +271,14 @@ class ApplicationsView(APIView):
application = action_type_registry.get_by_type(CreateApplicationActionType).do(
request.user,
workspace,
data["type"],
name=data["name"],
init_with_data=data["init_with_data"],
application_type=data.pop("type"),
**data,
)
return Response(
get_application_serializer(application, context={"request": request}).data
PolymorphicApplicationResponseSerializer(
application, context={"request": request}
).data
)
@ -309,11 +301,9 @@ class ApplicationView(APIView):
"application's workspace. The properties that belong to the application can "
"differ per type."
),
request=ApplicationCreateSerializer,
request=PolymorphicApplicationCreateSerializer,
responses={
200: DiscriminatorMappingSerializer(
"Applications", application_type_serializers
),
200: PolymorphicApplicationResponseSerializer,
400: get_error_schema(
["ERROR_USER_NOT_IN_GROUP", "ERROR_REQUEST_BODY_VALIDATION"]
),
@ -334,7 +324,9 @@ class ApplicationView(APIView):
)
return Response(
get_application_serializer(application, context={"request": request}).data
PolymorphicApplicationResponseSerializer(
application, context={"request": request}
).data
)
@extend_schema(
@ -356,11 +348,9 @@ class ApplicationView(APIView):
"workspace. It is not possible to change the type, but properties like the "
"name can be changed."
),
request=ApplicationUpdateSerializer,
request=PolymorphicApplicationUpdateSerializer,
responses={
200: DiscriminatorMappingSerializer(
"Applications", application_type_serializers
),
200: PolymorphicApplicationResponseSerializer,
400: get_error_schema(
["ERROR_USER_NOT_IN_GROUP", "ERROR_REQUEST_BODY_VALIDATION"]
),
@ -368,14 +358,14 @@ class ApplicationView(APIView):
},
)
@transaction.atomic
@validate_body(ApplicationUpdateSerializer)
@map_exceptions(
{
ApplicationDoesNotExist: ERROR_APPLICATION_DOES_NOT_EXIST,
UserNotInWorkspace: ERROR_USER_NOT_IN_GROUP,
ApplicationTypeDoesNotExist: ERROR_APPLICATION_TYPE_DOES_NOT_EXIST,
}
)
def patch(self, request, data, application_id):
def patch(self, request, application_id):
"""Updates the application if the user belongs to the workspace."""
application = (
@ -387,12 +377,27 @@ class ApplicationView(APIView):
.specific
)
# We validate the data in the method here so that we can
# pass the application instance directly into the serializer.
# This ensures the `PolymorphicSerializer` can correctly determine
# the type of the instance, otherwise PATCH requests would need to
# include the `type` field in the request body.
data = validate_data(
PolymorphicApplicationUpdateSerializer,
request.data,
partial=True,
return_validated=True,
instance=application,
)
application = action_type_registry.get_by_type(UpdateApplicationActionType).do(
request.user, application, name=data["name"]
request.user, application, **data
)
return Response(
get_application_serializer(application, context={"request": request}).data
PolymorphicApplicationResponseSerializer(
application, context={"request": request}
).data
)
@extend_schema(

View file

@ -104,6 +104,7 @@ class PolymorphicSerializer(serializers.Serializer):
instance_type.model_class,
base_class=self.base_class,
request=self.request,
context=self.context,
)
ret = serializer.to_representation(instance)
@ -121,6 +122,7 @@ class PolymorphicSerializer(serializers.Serializer):
instance_type.model_class,
base_class=self.base_class,
request=self.request,
context=self.context,
)
return serializer.to_internal_value(data)
@ -132,6 +134,7 @@ class PolymorphicSerializer(serializers.Serializer):
instance_type.model_class,
base_class=self.base_class,
request=self.request,
context=self.context,
)
return serializer.create(validated_data)
@ -147,6 +150,7 @@ class PolymorphicSerializer(serializers.Serializer):
instance_type.model_class,
base_class=self.base_class,
request=self.request,
context=self.context,
)
return serializer.update(instance, validated_data)
@ -164,6 +168,8 @@ class PolymorphicSerializer(serializers.Serializer):
instance_type.model_class,
base_class=self.base_class,
request=self.request,
context=self.context,
data=self.data,
)
except serializers.ValidationError:
child_valid = False
@ -186,6 +192,7 @@ class PolymorphicSerializer(serializers.Serializer):
instance_type.model_class,
base_class=self.base_class,
request=self.request,
context=self.context,
)
validated_data = serializer.run_validation(data)

View file

@ -7,8 +7,9 @@ from rest_framework.permissions import AllowAny, IsAuthenticated
from rest_framework.response import Response
from rest_framework.views import APIView
from baserow.api.applications.serializers import get_application_serializer
from baserow.api.applications.views import application_type_serializers
from baserow.api.applications.serializers import (
PolymorphicApplicationResponseSerializer,
)
from baserow.api.decorators import map_exceptions
from baserow.api.errors import ERROR_GROUP_DOES_NOT_EXIST, ERROR_USER_NOT_IN_GROUP
from baserow.api.jobs.errors import ERROR_MAX_JOB_COUNT_EXCEEDED
@ -19,7 +20,6 @@ from baserow.api.schemas import (
get_error_schema,
)
from baserow.api.templates.serializers import TemplateCategoriesSerializer
from baserow.api.utils import DiscriminatorMappingSerializer
from baserow.core.action.registries import action_type_registry
from baserow.core.actions import InstallTemplateActionType
from baserow.core.exceptions import (
@ -97,9 +97,7 @@ class InstallTemplateView(APIView):
),
request=None,
responses={
200: DiscriminatorMappingSerializer(
"Applications", application_type_serializers, many=True
),
200: PolymorphicApplicationResponseSerializer(many=True),
400: get_error_schema(
["ERROR_USER_NOT_IN_GROUP", "ERROR_TEMPLATE_FILE_DOES_NOT_EXIST"]
),
@ -128,7 +126,7 @@ class InstallTemplateView(APIView):
).do(request.user, workspace, template)
data = [
get_application_serializer(application).data
PolymorphicApplicationResponseSerializer(application).data
for application in installed_applications
]
return Response(data)

View file

@ -173,6 +173,7 @@ def validate_data(
exception_to_raise: Type[Exception] = RequestBodyValidationException,
many: bool = False,
return_validated: bool = False,
instance=None,
) -> Dict[str, Any]:
"""
Validates the provided data via the provided serializer class. If the data doesn't
@ -186,10 +187,11 @@ def validate_data(
invalid.
:param many: Indicates whether the serializer should be constructed as a list.
:param return_validated: Returns validated_data from DRF serializer
:param instance: The instance that is being updated.
:return: The data after being validated by the serializer.
"""
serializer = serializer_class(data=data, partial=partial, many=many)
serializer = serializer_class(instance, data=data, partial=partial, many=many)
if not serializer.is_valid():
detail = serialize_validation_errors_recursive(serializer.errors)
raise exception_to_raise(detail)

View file

@ -5,9 +5,9 @@ from drf_spectacular.utils import extend_schema
from baserow.api.applications.errors import ERROR_APPLICATION_NOT_IN_GROUP
from baserow.api.applications.serializers import (
ApplicationCreateSerializer,
ApplicationSerializer,
OrderApplicationsSerializer,
PolymorphicApplicationCreateSerializer,
PolymorphicApplicationResponseSerializer,
)
from baserow.api.applications.views import ApplicationsView, OrderApplicationsView
from baserow.api.decorators import map_exceptions, validate_body
@ -17,7 +17,6 @@ from baserow.api.schemas import (
CLIENT_UNDO_REDO_ACTION_GROUP_ID_SCHEMA_PARAMETER,
get_error_schema,
)
from baserow.api.utils import DiscriminatorMappingSerializer
from baserow.compat.api.conf import (
APPLICATION_DEPRECATION_PREFIXES as DEPRECATION_PREFIXES,
)
@ -26,14 +25,6 @@ from baserow.core.exceptions import (
UserNotInWorkspace,
WorkspaceDoesNotExist,
)
from baserow.core.registries import application_type_registry
application_type_serializers = {
application_type.type: (
application_type.instance_serializer_class or ApplicationSerializer
)
for application_type in application_type_registry.registry.values()
}
class ApplicationsCompatView(ApplicationsView):
@ -59,9 +50,7 @@ class ApplicationsCompatView(ApplicationsView):
"belongs to a single group."
),
responses={
200: DiscriminatorMappingSerializer(
"Applications", application_type_serializers, many=True
),
200: PolymorphicApplicationResponseSerializer(many=True),
400: get_error_schema(["ERROR_USER_NOT_IN_GROUP"]),
404: get_error_schema(["ERROR_GROUP_DOES_NOT_EXIST"]),
},
@ -103,11 +92,9 @@ class ApplicationsCompatView(ApplicationsView):
"parameter. If the authorized user does not belong to the group an "
"error will be returned."
),
request=ApplicationCreateSerializer,
request=PolymorphicApplicationCreateSerializer,
responses={
200: DiscriminatorMappingSerializer(
"Applications", application_type_serializers
),
200: PolymorphicApplicationResponseSerializer(),
400: get_error_schema(
["ERROR_USER_NOT_IN_GROUP", "ERROR_REQUEST_BODY_VALIDATION"]
),
@ -115,7 +102,7 @@ class ApplicationsCompatView(ApplicationsView):
},
)
@transaction.atomic
@validate_body(ApplicationCreateSerializer)
@validate_body(PolymorphicApplicationCreateSerializer)
@map_exceptions(
{
WorkspaceDoesNotExist: ERROR_GROUP_DOES_NOT_EXIST,

View file

@ -3,7 +3,9 @@ from django.db import transaction
from drf_spectacular.openapi import OpenApiParameter, OpenApiTypes
from drf_spectacular.utils import extend_schema
from baserow.api.applications.views import application_type_serializers
from baserow.api.applications.serializers import (
PolymorphicApplicationResponseSerializer,
)
from baserow.api.decorators import map_exceptions
from baserow.api.errors import ERROR_GROUP_DOES_NOT_EXIST, ERROR_USER_NOT_IN_GROUP
from baserow.api.jobs.errors import ERROR_MAX_JOB_COUNT_EXCEEDED
@ -23,7 +25,6 @@ from baserow.api.templates.views import (
InstallTemplateView,
TemplatesView,
)
from baserow.api.utils import DiscriminatorMappingSerializer
from baserow.compat.api.conf import (
TEMPLATES_DEPRECATION_PREFIXES as DEPRECATION_PREFIXES,
)
@ -86,9 +87,7 @@ class InstallTemplateCompatView(InstallTemplateView):
),
request=None,
responses={
200: DiscriminatorMappingSerializer(
"Applications", application_type_serializers, many=True
),
200: PolymorphicApplicationResponseSerializer(many=True),
400: get_error_schema(
["ERROR_USER_NOT_IN_GROUP", "ERROR_TEMPLATE_FILE_DOES_NOT_EXIST"]
),

View file

@ -3,7 +3,6 @@ from typing import List
from drf_spectacular.utils import extend_schema_field
from rest_framework import serializers
from baserow.api.applications.serializers import ApplicationSerializer
from baserow.api.user_sources.serializers import PolymorphicUserSourceSerializer
from baserow.contrib.builder.api.pages.serializers import PageSerializer
from baserow.contrib.builder.api.theme.serializers import (
@ -16,7 +15,7 @@ from baserow.core.handler import CoreHandler
from baserow.core.user_sources.operations import ListUserSourcesApplicationOperationType
class BuilderSerializer(ApplicationSerializer):
class BuilderSerializer(serializers.ModelSerializer):
"""
The builder serializer.
@ -37,9 +36,10 @@ class BuilderSerializer(ApplicationSerializer):
"the theme settings."
)
class Meta(ApplicationSerializer.Meta):
class Meta:
model = Builder
ref_name = "BuilderApplication"
fields = ApplicationSerializer.Meta.fields + ("pages", "user_sources", "theme")
fields = ("id", "name", "pages", "theme", "user_sources")
@extend_schema_field(PageSerializer(many=True))
def get_pages(self, instance: Builder) -> List:

View file

@ -25,21 +25,29 @@ from baserow.core.user_sources.handler import UserSourceHandler
from baserow.core.utils import ChildProgressBuilder
# This lazy loads the serializer, which is needed because the `BuilderSerializer`
# needs to decorate the `get_theme` with the `extend_schema_field` using a
# generated serializer that needs the registry to be populated.
def lazy_get_instance_serializer_class():
from baserow.contrib.builder.api.serializers import BuilderSerializer
return BuilderSerializer
class BuilderApplicationType(ApplicationType):
type = "builder"
model_class = Builder
supports_actions = False
supports_integrations = True
supports_user_sources = True
# This lazy loads the serializer, which is needed because the `BuilderSerializer`
# needs to decorate the `get_theme` with the `extend_schema_field` using a
# generated serializer that needs the registry to be populated.
@property
def instance_serializer_class(self):
from baserow.contrib.builder.api.serializers import BuilderSerializer
return BuilderSerializer
serializer_field_names = [
"name",
"pages",
"user_sources",
"theme",
]
request_serializer_field_names = []
serializer_mixins = [lazy_get_instance_serializer_class]
def get_api_urls(self):
from .api import urls as api_urls

View file

@ -3,22 +3,25 @@ from typing import List
from drf_spectacular.utils import extend_schema_field
from rest_framework import serializers
from baserow.api.applications.serializers import ApplicationSerializer
from baserow.contrib.database.api.tables.serializers import TableSerializer
from baserow.contrib.database.models import Database
from baserow.contrib.database.operations import ListTablesDatabaseTableOperationType
from baserow.core.handler import CoreHandler
class DatabaseSerializer(ApplicationSerializer):
class DatabaseSerializer(serializers.ModelSerializer):
tables = serializers.SerializerMethodField(
help_text="This field is specific to the `database` application and contains "
"an array of tables that are in the database."
)
class Meta(ApplicationSerializer.Meta):
ref_name = "DatabaseApplication"
fields = ApplicationSerializer.Meta.fields + ("tables",)
class Meta:
model = Database
fields = (
"id",
"name",
"tables",
)
@extend_schema_field(TableSerializer(many=True))
def get_tables(self, instance: Database) -> List:

View file

@ -45,7 +45,12 @@ from .table.models import Table
class DatabaseApplicationType(ApplicationType):
type = "database"
model_class = Database
serializer_mixins = [DatabaseSerializer]
instance_serializer_class = DatabaseSerializer
serializer_field_names = ["tables"]
# Mark the request serializer field names as empty, otherwise
# the polymorphic request serializer will try and serialize tables.
request_serializer_field_names = []
def pre_delete(self, database):
"""

View file

@ -390,12 +390,7 @@ class CreateApplicationActionType(UndoableActionType):
@classmethod
def do(
cls,
user: AbstractUser,
workspace: Workspace,
application_type: str,
name: str,
init_with_data: bool = False,
cls, user: AbstractUser, workspace: Workspace, application_type: str, **kwargs
) -> Any:
"""
Creates a new application based on the provided type. See
@ -405,14 +400,13 @@ class CreateApplicationActionType(UndoableActionType):
:param user: The user creating the application.
:param workspace: The workspace to create the application in.
:param application_type: The type of application to create.
:param name: The name of the new application.
:param init_with_data: Whether the application should be initialized with
some default data. Defaults to False.
:param kwargs: Additional parameters to pass to the application creation.
:return: The created Application model instance.
"""
init_with_data = kwargs.get("init_with_data", False)
application = CoreHandler().create_application(
user, workspace, application_type, name=name, init_with_data=init_with_data
user, workspace, application_type, **kwargs
)
application_type = application_type_registry.get_by_model(
@ -548,7 +542,7 @@ class UpdateApplicationActionType(UndoableActionType):
original_application_name: str
@classmethod
def do(cls, user: AbstractUser, application: Application, name: str) -> Application:
def do(cls, user: AbstractUser, application: Application, **kwargs) -> Application:
"""
Updates an existing application instance.
See baserow.core.handler.CoreHandler.update_application for further details.
@ -556,30 +550,34 @@ class UpdateApplicationActionType(UndoableActionType):
:param user: The user on whose behalf the application is updated.
:param application: The application instance that needs to be updated.
:param name: The new name of the application.
:param kwargs: Additional parameters to pass to the application update.
:raises ValueError: If one of the provided parameters is invalid.
:return: The updated application instance.
"""
original_name = application.name
application = CoreHandler().update_application(user, application, name)
application = CoreHandler().update_application(user, application, **kwargs)
application_type = application_type_registry.get_by_model(
application.specific_class
)
workspace = application.workspace
params = cls.Params(
workspace.id,
workspace.name,
application_type.type,
application.id,
name,
original_name,
)
cls.register_action(
user, params, scope=cls.scope(workspace.id), workspace=workspace
)
# Only register an action if this application type supports actions.
# At the moment, the builder application doesn't use actions and need
# to bypass registering.
if application_type.supports_actions:
params = cls.Params(
workspace.id,
workspace.name,
application_type.type,
application.id,
kwargs["name"],
original_name,
)
cls.register_action(
user, params, scope=cls.scope(workspace.id), workspace=workspace
)
return application
@ -591,13 +589,15 @@ class UpdateApplicationActionType(UndoableActionType):
def undo(cls, user: AbstractUser, params: Params, action_being_undone: Action):
application = CoreHandler().get_application(params.application_id).specific
CoreHandler().update_application(
user, application, params.original_application_name
user, application, name=params.original_application_name
)
@classmethod
def redo(cls, user: AbstractUser, params: Params, action_being_redone: Action):
application = CoreHandler().get_application(params.application_id).specific
CoreHandler().update_application(user, application, params.application_name)
CoreHandler().update_application(
user, application, name=params.application_name
)
class DuplicateApplicationActionType(UndoableActionType):

View file

@ -110,6 +110,7 @@ from .types import (
from .utils import (
ChildProgressBuilder,
atomic_if_not_already,
extract_allowed,
find_unused_name,
set_allowed_attrs,
)
@ -122,6 +123,9 @@ tracer = trace.get_tracer(__name__)
class CoreHandler(metaclass=baserow_trace_methods(tracer)):
default_create_allowed_fields = ["name", "init_with_data"]
default_update_allowed_fields = ["name"]
def get_settings(self):
"""
Returns a settings model instance containing all the admin configured settings.
@ -1306,8 +1310,8 @@ class CoreHandler(metaclass=baserow_trace_methods(tracer)):
user: AbstractUser,
workspace: Workspace,
type_name: str,
name: str,
init_with_data: bool = False,
**kwargs,
) -> Application:
"""
Creates a new application based on the provided type.
@ -1316,9 +1320,9 @@ class CoreHandler(metaclass=baserow_trace_methods(tracer)):
:param workspace: The workspace that the application instance belongs to.
:param type_name: The type name of the application. ApplicationType can be
registered via the ApplicationTypeRegistry.
:param name: The name of the application.
:param init_with_data: Whether the application should be initialized with
some default data. Defaults to False.
:param kwargs: Additional parameters to pass to the application creation.
:return: The created application instance.
"""
@ -1330,8 +1334,11 @@ class CoreHandler(metaclass=baserow_trace_methods(tracer)):
)
application_type = application_type_registry.get(type_name)
allowed_values = extract_allowed(
kwargs, self.default_create_allowed_fields + application_type.allowed_fields
)
application = application_type.create_application(
user, workspace, name, init_with_data
user, workspace, init_with_data=init_with_data, **allowed_values
)
application_created.send(self, application=application, user=user)
@ -1356,14 +1363,14 @@ class CoreHandler(metaclass=baserow_trace_methods(tracer)):
)
def update_application(
self, user: AbstractUser, application: Application, name: str
self, user: AbstractUser, application: Application, **kwargs
) -> Application:
"""
Updates an existing application instance.
:param user: The user on whose behalf the application is updated.
:param application: The application instance that needs to be updated.
:param name: The new name of the application.
:param kwargs: Additional parameters to pass to the application update.
:return: The updated application instance.
"""
@ -1374,7 +1381,14 @@ class CoreHandler(metaclass=baserow_trace_methods(tracer)):
context=application,
)
application.name = name
application_type = application_type_registry.get_by_model(application)
allowed_updates = extract_allowed(
kwargs, self.default_update_allowed_fields + application_type.allowed_fields
)
for key, value in allowed_updates.items():
setattr(application, key, value)
application.save()
application_updated.send(self, application=application, user=user)

View file

@ -7,7 +7,7 @@ from rest_framework.exceptions import ValidationError
from baserow.api.applications.serializers import (
InstallTemplateJobApplicationsSerializer,
SpecificApplicationSerializer,
PolymorphicApplicationResponseSerializer,
)
from baserow.api.errors import (
ERROR_GROUP_DOES_NOT_EXIST,
@ -65,8 +65,12 @@ class DuplicateApplicationJobType(JobType):
serializer_field_names = ["original_application", "duplicated_application"]
serializer_field_overrides = {
"original_application": SpecificApplicationSerializer(read_only=True),
"duplicated_application": SpecificApplicationSerializer(read_only=True),
"original_application": PolymorphicApplicationResponseSerializer(
read_only=True
),
"duplicated_application": PolymorphicApplicationResponseSerializer(
read_only=True
),
}
def transaction_atomic_context(self, job: "DuplicateApplicationJob"):

View file

@ -32,6 +32,8 @@ from .export_serialized import CoreExportSerializedStructure
from .registry import (
APIUrlsInstanceMixin,
APIUrlsRegistryMixin,
CustomFieldsInstanceMixin,
CustomFieldsRegistryMixin,
Instance,
ModelInstanceMixin,
ModelRegistryMixin,
@ -211,6 +213,7 @@ class PluginRegistry(APIUrlsRegistryMixin, Registry):
class ApplicationType(
APIUrlsInstanceMixin,
ModelInstanceMixin["Application"],
CustomFieldsInstanceMixin,
Instance,
):
"""
@ -280,25 +283,24 @@ class ApplicationType(
)
def create_application(
self, user, workspace: "Workspace", name: str, init_with_data: bool = False
self, user, workspace: "Workspace", init_with_data: bool = False, **kwargs
) -> "Application":
"""
Creates a new application instance of this type and returns it.
:param user: The user that is creating the application.
:param workspace: The workspace that the application will be created in.
:param name: The name of the application.
:param init_with_data: Whether the application should be created with some
initial data. Defaults to False.
:param kwargs: Additional parameters to pass to the application creation,
these values have already been validated by the view and are allowed.
:return: The newly created application instance.
"""
model = self.model_class
last_order = model.get_last_order(workspace)
instance = model.objects.create(
workspace=workspace, order=last_order, name=name
)
instance = model.objects.create(workspace=workspace, order=last_order, **kwargs)
if init_with_data:
self.init_application(user, instance)
return instance
@ -486,6 +488,7 @@ class ApplicationTypeRegistry(
APIUrlsRegistryMixin,
ModelRegistryMixin[ApplicationSubClassInstance, ApplicationType],
Registry[ApplicationType],
CustomFieldsRegistryMixin,
):
"""
With the application registry it is possible to register new applications. An

View file

@ -2,6 +2,7 @@ import contextlib
import typing
from abc import ABC, abstractmethod
from functools import lru_cache
from types import FunctionType
from typing import (
Any,
Dict,
@ -171,11 +172,22 @@ class CustomFieldsInstanceMixin:
request_serializer, extra_params, **kwargs
)
# Build a list of serializers, using two methods:
# 1) Serializers can provide a function (note: we can't test with callable()
# as serializers are callable) which lazy loads a serializer mixin, or
# 2) Serializers can provide a serializer mixin directly.
dynamic_serializer_mixins = []
for serializer_mixin in self.serializer_mixins:
if isinstance(serializer_mixin, FunctionType):
dynamic_serializer_mixins.append(serializer_mixin())
else:
dynamic_serializer_mixins.append(serializer_mixin)
return get_serializer_class(
self.model_class,
field_names,
field_overrides=field_overrides,
base_mixins=self.serializer_mixins,
base_mixins=dynamic_serializer_mixins,
meta_extra_kwargs=self.serializer_extra_kwargs,
meta_ref_name=meta_ref_name,
base_class=base_class,

View file

@ -4,7 +4,7 @@ from django.dispatch import receiver
from baserow.api.applications.serializers import (
ApplicationSerializer,
get_application_serializer,
PolymorphicApplicationResponseSerializer,
)
from baserow.api.user.serializers import PublicUserSerializer
from baserow.api.workspaces.invitations.serializers import (
@ -222,7 +222,7 @@ def workspace_restored(sender, workspace_user, user, **kwargs):
)
applications_qs = specific_iterator(applications_qs)
applications = [
get_application_serializer(
PolymorphicApplicationResponseSerializer(
application, context={"user": workspace_user.user}
).data
for application in applications_qs

View file

@ -303,7 +303,9 @@ def broadcast_application_created(
:return:
"""
from baserow.api.applications.serializers import get_application_serializer
from baserow.api.applications.serializers import (
PolymorphicApplicationResponseSerializer,
)
from baserow.core.handler import CoreHandler
from baserow.core.models import Application, WorkspaceUser
from baserow.core.operations import ReadApplicationOperationType
@ -332,7 +334,7 @@ def broadcast_application_created(
payload_map = {}
for user_id in user_ids:
user = users_in_workspace_id_map[user_id]
application_serialized = get_application_serializer(
application_serialized = PolymorphicApplicationResponseSerializer(
application, context={"user": user}
).data

View file

@ -208,8 +208,10 @@ def test_create_application(api_client, data_fixture):
)
response_json = response.json()
assert response.status_code == HTTP_400_BAD_REQUEST
assert response_json["error"] == "ERROR_REQUEST_BODY_VALIDATION"
assert response_json["detail"]["type"][0]["code"] == "invalid_choice"
assert response_json["error"] == "ERROR_APPLICATION_TYPE_DOES_NOT_EXIST"
assert (
response_json["detail"] == "The application type NOT_EXISTING does not exist."
)
response = api_client.post(
reverse("api:applications:list", kwargs={"workspace_id": 99999}),

View file

@ -186,8 +186,10 @@ def test_create_application(api_client, data_fixture, group_compat_timebomb):
)
response_json = response.json()
assert response.status_code == HTTP_400_BAD_REQUEST
assert response_json["error"] == "ERROR_REQUEST_BODY_VALIDATION"
assert response_json["detail"]["type"][0]["code"] == "invalid_choice"
assert response_json["error"] == "ERROR_APPLICATION_TYPE_DOES_NOT_EXIST"
assert (
response_json["detail"] == "The application type NOT_EXISTING does not exist."
)
response = api_client.post(
reverse("api:applications_compat:list", kwargs={"group_id": 99999}),

View file

@ -134,12 +134,12 @@ def test_create_application_and_init_with_data(data_fixture):
user = data_fixture.create_user()
workspace = data_fixture.create_workspace(user=user)
database_1 = core_handler.create_application(
user, workspace, "database", "Database 1"
user, workspace, "database", name="Database 1"
)
assert Table.objects.filter(database=database_1).count() == 0
database_2 = core_handler.create_application(
user, workspace, "database", "Database 2", init_with_data=True
user, workspace, "database", True, name="Database 2"
)
assert Table.objects.filter(database=database_2).count() == 1

View file

@ -160,7 +160,7 @@ def test_can_undo_update_application(data_fixture, django_assert_num_queries):
)
action_type_registry.get_by_type(UpdateApplicationActionType).do(
user, application, application_name_new
user, application, name=application_name_new
)
assert Application.objects.get(pk=application.id).name == application_name_new
@ -187,7 +187,7 @@ def test_can_undo_redo_update_application(data_fixture, django_assert_num_querie
)
action_type_registry.get_by_type(UpdateApplicationActionType).do(
user, application, application_name_new
user, application, name=application_name_new
)
assert Application.objects.get(pk=application.id).name == application_name_new

View file

@ -778,16 +778,16 @@ def test_undo_redo_action_group_with_interleaved_actions(data_fixture):
def _interleave_actions():
user_1_app = action_type_registry.get_by_type(CreateApplicationActionType).do(
user_1, workspace=workspace, application_type="database", name="u1_a1"
user_1, workspace, application_type="database", name="u1_a1"
)
user_2_app = action_type_registry.get_by_type(CreateApplicationActionType).do(
user_2, workspace=workspace, application_type="database", name="u2_a1"
user_2, workspace, application_type="database", name="u2_a1"
)
action_type_registry.get_by_type(UpdateApplicationActionType).do(
user_1, application=user_1_app, name="u1_a2"
user_1, user_1_app, name="u1_a2"
)
action_type_registry.get_by_type(UpdateApplicationActionType).do(
user_2, application=user_2_app, name="u2_a2"
user_2, user_2_app, name="u2_a2"
)
_interleave_actions()

View file

@ -52,7 +52,7 @@ def test_can_submit_duplicate_application_job(data_fixture):
application_name = "My Application"
application = CoreHandler().create_application(
user, workspace, application_type, application_name
user, workspace, application_type, name=application_name
)
assert Application.objects.count() == 1
@ -110,7 +110,7 @@ def test_can_undo_duplicate_application_job(data_fixture):
application_name = "My Application"
application = CoreHandler().create_application(
user, workspace, application_type, application_name
user, workspace, application_type, name=application_name
)
JobHandler().create_and_start_job(

View file

@ -0,0 +1,9 @@
{
"type": "breaking_change",
"message": "Made the application request serializers polymorphic.",
"issue_number": null,
"bullet_points": [
"Breaking change: when an application is created or updated via the API with an invalid `type` we will now return an `ERROR_APPLICATION_TYPE_DOES_NOT_EXIST` error code, rather than `ERROR_REQUEST_BODY_VALIDATION`."
],
"created_at": "2024-03-15"
}

View file

@ -232,10 +232,14 @@ def test_audit_log_export_workspace_csv_correctly(
workspace = enterprise_data_fixture.create_workspace(user=user)
with freeze_time("2023-01-01 12:00:00"):
app_1 = CreateApplicationActionType.do(user, workspace, "database", "App 1")
app_1 = CreateApplicationActionType.do(
user, workspace, "database", name="App 1"
)
with freeze_time("2023-01-01 12:00:10"):
app_2 = CreateApplicationActionType.do(user, workspace, "database", "App 2")
app_2 = CreateApplicationActionType.do(
user, workspace, "database", name="App 2"
)
csv_settings = {
"csv_column_separator": ",",

View file

@ -2,7 +2,9 @@ import pytest
from asgiref.sync import sync_to_async
from channels.testing import WebsocketCommunicator
from baserow.api.applications.serializers import get_application_serializer
from baserow.api.applications.serializers import (
PolymorphicApplicationResponseSerializer,
)
from baserow.config.asgi import application
from baserow.core.handler import CoreHandler
from baserow.core.trash.handler import TrashHandler
@ -158,6 +160,6 @@ async def test_workspace_restored_applications_arent_leaked(data_fixture):
workspace_restored_message = await get_message(communicator, "group_restored")
assert workspace_restored_message is not None
assert workspace_restored_message["applications"] == [
get_application_serializer(database).data
PolymorphicApplicationResponseSerializer(database).data
]
await communicator.disconnect()