class TableView(APIView): permission_classes = (IsAuthenticated, ) @extend_schema( parameters=[ OpenApiParameter( name="table_id", location=OpenApiParameter.PATH, type=OpenApiTypes.INT, description="Returns the table related to the provided value.", ) ], tags=["Database tables"], operation_id="get_database_table", description= ("Returns the requested table if the authorized user has access to the " "related database's group."), responses={ 200: TableSerializer, 400: get_error_schema(["ERROR_USER_NOT_IN_GROUP"]), 404: get_error_schema(["ERROR_TABLE_DOES_NOT_EXIST"]), }, ) @map_exceptions({ TableDoesNotExist: ERROR_TABLE_DOES_NOT_EXIST, UserNotInGroup: ERROR_USER_NOT_IN_GROUP, }) def get(self, request, table_id): """Responds with a serialized table instance.""" table = TableHandler().get_table(table_id) table.database.group.has_user(request.user, raise_error=True) serializer = TableSerializer(table) return Response(serializer.data) @extend_schema( parameters=[ OpenApiParameter( name="table_id", location=OpenApiParameter.PATH, type=OpenApiTypes.INT, description="Updates the table related to the provided value.", ) ], tags=["Database tables"], operation_id="update_database_table", description= ("Updates the existing table if the authorized user has access to the " "related database's group."), request=TableUpdateSerializer, responses={ 200: TableSerializer, 400: get_error_schema( ["ERROR_USER_NOT_IN_GROUP", "ERROR_REQUEST_BODY_VALIDATION"]), 404: get_error_schema(["ERROR_TABLE_DOES_NOT_EXIST"]), }, ) @transaction.atomic @map_exceptions({ TableDoesNotExist: ERROR_TABLE_DOES_NOT_EXIST, UserNotInGroup: ERROR_USER_NOT_IN_GROUP, }) @validate_body(TableUpdateSerializer) def patch(self, request, data, table_id): """Updates the values a table instance.""" table = TableHandler().update_table( request.user, TableHandler().get_table(table_id), base_queryset=Table.objects.select_for_update(), name=data["name"], ) serializer = TableSerializer(table) return Response(serializer.data) @extend_schema( parameters=[ OpenApiParameter( name="table_id", location=OpenApiParameter.PATH, type=OpenApiTypes.INT, description="Deletes the table related to the provided value.", ) ], tags=["Database tables"], operation_id="delete_database_table", description= ("Deletes the existing table if the authorized user has access to the " "related database's group."), responses={ 204: None, 400: get_error_schema(["ERROR_USER_NOT_IN_GROUP"]), 404: get_error_schema(["ERROR_TABLE_DOES_NOT_EXIST"]), }, ) @transaction.atomic @map_exceptions({ TableDoesNotExist: ERROR_TABLE_DOES_NOT_EXIST, UserNotInGroup: ERROR_USER_NOT_IN_GROUP, }) def delete(self, request, table_id): """Deletes an existing table.""" TableHandler().delete_table(request.user, TableHandler().get_table(table_id)) return Response(status=204)
class ViewsView(APIView): permission_classes = (IsAuthenticated,) @extend_schema( parameters=[ OpenApiParameter( name='table_id', location=OpenApiParameter.PATH, type=OpenApiTypes.INT, description='Returns only views of the table related to the provided ' 'value.' ) ], tags=['Database table views'], operation_id='list_database_table_views', description=( 'Lists all views of the table related to the provided `table_id` if the ' 'user has access to the related database\'s group. A table can have ' 'multiple views. Each view can display the data in a different way. For ' 'example the `grid` view shows the in a spreadsheet like way. That type ' 'has custom endpoints for data retrieval and manipulation. In the future ' 'other views types like a calendar or Kanban are going to be added. Each ' 'type can have different properties.' ), responses={ 200: PolymorphicCustomFieldRegistrySerializer( view_type_registry, ViewSerializer, many=True ), 400: get_error_schema(['ERROR_USER_NOT_IN_GROUP']), 404: get_error_schema(['ERROR_TABLE_DOES_NOT_EXIST']) } ) @map_exceptions({ TableDoesNotExist: ERROR_TABLE_DOES_NOT_EXIST, UserNotInGroupError: ERROR_USER_NOT_IN_GROUP }) @allowed_includes('filters', 'sortings') def get(self, request, table_id, filters, sortings): """ Responds with a list of serialized views that belong to the table if the user has access to that group. """ table = TableHandler().get_table(request.user, table_id) views = View.objects.filter(table=table).select_related('content_type') if filters: views = views.prefetch_related('viewfilter_set') if sortings: views = views.prefetch_related('viewsort_set') data = [ view_type_registry.get_serializer( view, ViewSerializer, filters=filters, sortings=sortings ).data for view in views ] return Response(data) @extend_schema( parameters=[ OpenApiParameter( name='table_id', location=OpenApiParameter.PATH, type=OpenApiTypes.INT, description='Creates a view for the table related to the provided ' 'value.' ) ], tags=['Database table views'], operation_id='create_database_table_view', description=( 'Creates a new view for the table related to the provided `table_id` ' 'parameter if the authorized user has access to the related database\'s ' 'group. Depending on the type, different properties can optionally be ' 'set.' ), request=PolymorphicCustomFieldRegistrySerializer( view_type_registry, CreateViewSerializer ), responses={ 200: PolymorphicCustomFieldRegistrySerializer( view_type_registry, ViewSerializer ), 400: get_error_schema([ 'ERROR_USER_NOT_IN_GROUP', 'ERROR_REQUEST_BODY_VALIDATION' ]), 404: get_error_schema(['ERROR_TABLE_DOES_NOT_EXIST']) } ) @transaction.atomic @validate_body_custom_fields( view_type_registry, base_serializer_class=CreateViewSerializer) @map_exceptions({ TableDoesNotExist: ERROR_TABLE_DOES_NOT_EXIST, UserNotInGroupError: ERROR_USER_NOT_IN_GROUP }) @allowed_includes('filters', 'sortings') def post(self, request, data, table_id, filters, sortings): """Creates a new view for a user.""" table = TableHandler().get_table(request.user, table_id) view = ViewHandler().create_view( request.user, table, data.pop('type'), **data) serializer = view_type_registry.get_serializer( view, ViewSerializer, filters=filters, sortings=sortings ) return Response(serializer.data)
class ViewSortView(APIView): permission_classes = (IsAuthenticated,) @extend_schema( parameters=[ OpenApiParameter( name='view_sort_id', location=OpenApiParameter.PATH, type=OpenApiTypes.INT, description='Returns the view sort related to the provided value.' ) ], tags=['Database table view sortings'], operation_id='get_database_table_view_sort', description=( 'Returns the existing view sort if the authorized user has access to the' ' related database\'s group.' ), responses={ 200: ViewSortSerializer(), 400: get_error_schema(['ERROR_USER_NOT_IN_GROUP']), 404: get_error_schema(['ERROR_VIEW_SORT_DOES_NOT_EXIST']) } ) @map_exceptions({ ViewSortDoesNotExist: ERROR_VIEW_SORT_DOES_NOT_EXIST, UserNotInGroupError: ERROR_USER_NOT_IN_GROUP }) def get(self, request, view_sort_id): """Selects a single sort and responds with a serialized version.""" view_sort = ViewHandler().get_sort(request.user, view_sort_id) serializer = ViewSortSerializer(view_sort) return Response(serializer.data) @extend_schema( parameters=[ OpenApiParameter( name='view_sort_id', location=OpenApiParameter.PATH, type=OpenApiTypes.INT, description='Updates the view sort related to the provided value.' ) ], tags=['Database table view sortings'], operation_id='update_database_table_view_sort', description=( 'Updates the existing sort if the authorized user has access to the ' 'related database\'s group.' ), request=UpdateViewSortSerializer(), responses={ 200: ViewSortSerializer(), 400: get_error_schema([ 'ERROR_USER_NOT_IN_GROUP', 'ERROR_FIELD_NOT_IN_TABLE', 'ERROR_VIEW_SORT_FIELD_ALREADY_EXISTS' ]), 404: get_error_schema(['ERROR_VIEW_SORT_DOES_NOT_EXIST']) } ) @transaction.atomic @validate_body(UpdateViewSortSerializer) @map_exceptions({ ViewSortDoesNotExist: ERROR_VIEW_SORT_DOES_NOT_EXIST, UserNotInGroupError: ERROR_USER_NOT_IN_GROUP, FieldNotInTable: ERROR_FIELD_NOT_IN_TABLE, ViewSortFieldAlreadyExist: ERROR_VIEW_SORT_FIELD_ALREADY_EXISTS, ViewSortFieldNotSupported: ERROR_VIEW_SORT_FIELD_NOT_SUPPORTED, }) def patch(self, request, data, view_sort_id): """Updates the view sort if the user belongs to the group.""" handler = ViewHandler() view_sort = handler.get_sort( request.user, view_sort_id, base_queryset=ViewSort.objects.select_for_update() ) if 'field' in data: # We can safely assume the field exists because the # UpdateViewSortSerializer has already checked that. data['field'] = Field.objects.get(pk=data['field']) view_sort = handler.update_sort(request.user, view_sort, **data) serializer = ViewSortSerializer(view_sort) return Response(serializer.data) @extend_schema( parameters=[ OpenApiParameter( name='view_sort_id', location=OpenApiParameter.PATH, type=OpenApiTypes.INT, description='Deletes the sort related to the provided value.' ) ], tags=['Database table view sortings'], operation_id='delete_database_table_view_sort', description=( 'Deletes the existing sort if the authorized user has access to the ' 'related database\'s group.' ), responses={ 204: None, 400: get_error_schema(['ERROR_USER_NOT_IN_GROUP']), 404: get_error_schema(['ERROR_VIEW_SORT_DOES_NOT_EXIST']) } ) @transaction.atomic @map_exceptions({ ViewSortDoesNotExist: ERROR_VIEW_SORT_DOES_NOT_EXIST, UserNotInGroupError: ERROR_USER_NOT_IN_GROUP }) def delete(self, request, view_sort_id): """Deletes an existing sort if the user belongs to the group.""" view = ViewHandler().get_sort(request.user, view_sort_id) ViewHandler().delete_sort(request.user, view) return Response(status=204)
class FieldView(APIView): permission_classes = (IsAuthenticated, ) @extend_schema( parameters=[ OpenApiParameter( name='field_id', location=OpenApiParameter.PATH, type=OpenApiTypes.INT, description='Returns the field related to the provided value.') ], tags=['Database table fields'], operation_id='get_database_table_field', description= ('Returns the existing field if the authorized user has access to the ' 'related database\'s group. Depending on the type different properties' 'could be returned.'), responses={ 200: PolymorphicCustomFieldRegistrySerializer(field_type_registry, FieldSerializer), 400: get_error_schema(['ERROR_USER_NOT_IN_GROUP']), 404: get_error_schema(['ERROR_FIELD_DOES_NOT_EXIST']) }) @map_exceptions({ FieldDoesNotExist: ERROR_FIELD_DOES_NOT_EXIST, UserNotInGroupError: ERROR_USER_NOT_IN_GROUP }) def get(self, request, field_id): """Selects a single field and responds with a serialized version.""" field = FieldHandler().get_field(field_id) field.table.database.group.has_user(request.user, raise_error=True) serializer = field_type_registry.get_serializer(field, FieldSerializer) return Response(serializer.data) @extend_schema( parameters=[ OpenApiParameter( name='field_id', location=OpenApiParameter.PATH, type=OpenApiTypes.INT, description='Updates the field related to the provided value.') ], tags=['Database table fields'], operation_id='update_database_table_field', description= ('Updates the existing field if the authorized user has access to the ' 'related database\'s group. The type can also be changed and depending on ' 'that type, different additional properties can optionally be set. If you ' 'change the field type it could happen that the data conversion fails, in ' 'that case the `ERROR_CANNOT_CHANGE_FIELD_TYPE` is returned, but this ' 'rarely happens. If a data value cannot be converted it is set to `null` ' 'so data might go lost.'), request=PolymorphicCustomFieldRegistrySerializer( field_type_registry, UpdateFieldSerializer), responses={ 200: PolymorphicCustomFieldRegistrySerializer(field_type_registry, FieldSerializer), 400: get_error_schema([ 'ERROR_USER_NOT_IN_GROUP', 'ERROR_CANNOT_CHANGE_FIELD_TYPE', 'ERROR_REQUEST_BODY_VALIDATION' ]), 404: get_error_schema(['ERROR_FIELD_DOES_NOT_EXIST']) }) @transaction.atomic @map_exceptions({ FieldDoesNotExist: ERROR_FIELD_DOES_NOT_EXIST, UserNotInGroupError: ERROR_USER_NOT_IN_GROUP, CannotChangeFieldType: ERROR_CANNOT_CHANGE_FIELD_TYPE }) def patch(self, request, field_id): """Updates the field if the user belongs to the group.""" field = FieldHandler().get_field( field_id, base_queryset=Field.objects.select_for_update()).specific type_name = type_from_data_or_registry(request.data, field_type_registry, field) field_type = field_type_registry.get(type_name) data = validate_data_custom_fields( type_name, field_type_registry, request.data, base_serializer_class=UpdateFieldSerializer) # Because each field type can raise custom exceptions at while updating the # field we need to be able to map those to the correct API exceptions which are # defined in the type. with field_type.map_api_exceptions(): field = FieldHandler().update_field(request.user, field, type_name, **data) serializer = field_type_registry.get_serializer(field, FieldSerializer) return Response(serializer.data) @extend_schema( parameters=[ OpenApiParameter( name='field_id', location=OpenApiParameter.PATH, type=OpenApiTypes.INT, description='Deletes the field related to the provided value.') ], tags=['Database table fields'], operation_id='delete_database_table_field', description= ('Deletes the existing field if the authorized user has access to the ' 'related database\'s group. Note that all the related data to that field ' 'is also deleted. Primary fields cannot be deleted because their value ' 'represents the row.'), responses={ 204: None, 400: get_error_schema([ 'ERROR_USER_NOT_IN_GROUP', 'ERROR_CANNOT_DELETE_PRIMARY_FIELD' ]), 404: get_error_schema(['ERROR_FIELD_DOES_NOT_EXIST']) }) @transaction.atomic @map_exceptions({ FieldDoesNotExist: ERROR_FIELD_DOES_NOT_EXIST, UserNotInGroupError: ERROR_USER_NOT_IN_GROUP, CannotDeletePrimaryField: ERROR_CANNOT_DELETE_PRIMARY_FIELD }) def delete(self, request, field_id): """Deletes an existing field if the user belongs to the group.""" field = FieldHandler().get_field(field_id) FieldHandler().delete_field(request.user, field) return Response(status=204)
class ViewView(APIView): permission_classes = (IsAuthenticated,) @extend_schema( parameters=[ OpenApiParameter( name='view_id', location=OpenApiParameter.PATH, type=OpenApiTypes.INT, description='Returns the view related to the provided value.' ) ], tags=['Database table views'], operation_id='get_database_table_view', description=( 'Returns the existing view if the authorized user has access to the ' 'related database\'s group. Depending on the type different properties' 'could be returned.' ), responses={ 200: PolymorphicCustomFieldRegistrySerializer( view_type_registry, ViewSerializer ), 400: get_error_schema(['ERROR_USER_NOT_IN_GROUP']), 404: get_error_schema(['ERROR_VIEW_DOES_NOT_EXIST']) } ) @map_exceptions({ ViewDoesNotExist: ERROR_VIEW_DOES_NOT_EXIST, UserNotInGroupError: ERROR_USER_NOT_IN_GROUP }) @allowed_includes('filters', 'sortings') def get(self, request, view_id, filters, sortings): """Selects a single view and responds with a serialized version.""" view = ViewHandler().get_view(request.user, view_id) serializer = view_type_registry.get_serializer( view, ViewSerializer, filters=filters, sortings=sortings ) return Response(serializer.data) @extend_schema( parameters=[ OpenApiParameter( name='view_id', location=OpenApiParameter.PATH, type=OpenApiTypes.INT, description='Updates the view related to the provided value.' ) ], tags=['Database table views'], operation_id='update_database_table_view', description=( 'Updates the existing view if the authorized user has access to the ' 'related database\'s group. The type cannot be changed. It depends on the ' 'existing type which properties can be changed.' ), request=PolymorphicCustomFieldRegistrySerializer( view_type_registry, UpdateViewSerializer ), responses={ 200: PolymorphicCustomFieldRegistrySerializer( view_type_registry, ViewSerializer ), 400: get_error_schema([ 'ERROR_USER_NOT_IN_GROUP', 'ERROR_REQUEST_BODY_VALIDATION' ]), 404: get_error_schema(['ERROR_VIEW_DOES_NOT_EXIST']) } ) @transaction.atomic @map_exceptions({ ViewDoesNotExist: ERROR_VIEW_DOES_NOT_EXIST, UserNotInGroupError: ERROR_USER_NOT_IN_GROUP }) @allowed_includes('filters', 'sortings') def patch(self, request, view_id, filters, sortings): """Updates the view if the user belongs to the group.""" view = ViewHandler().get_view(request.user, view_id).specific view_type = view_type_registry.get_by_model(view) data = validate_data_custom_fields( view_type.type, view_type_registry, request.data, base_serializer_class=UpdateViewSerializer ) view = ViewHandler().update_view(request.user, view, **data) serializer = view_type_registry.get_serializer( view, ViewSerializer, filters=filters, sortings=sortings ) return Response(serializer.data) @extend_schema( parameters=[ OpenApiParameter( name='view_id', location=OpenApiParameter.PATH, type=OpenApiTypes.INT, description='Deletes the view related to the provided value.' ) ], tags=['Database table views'], operation_id='delete_database_table_view', description=( 'Deletes the existing view if the authorized user has access to the ' 'related database\'s group. Note that all the related settings of the ' 'view are going to be deleted also. The data stays intact after deleting ' 'the view because this is related to the table and not the view.' ), responses={ 204: None, 400: get_error_schema(['ERROR_USER_NOT_IN_GROUP']), 404: get_error_schema(['ERROR_VIEW_DOES_NOT_EXIST']) } ) @transaction.atomic @map_exceptions({ ViewDoesNotExist: ERROR_VIEW_DOES_NOT_EXIST, UserNotInGroupError: ERROR_USER_NOT_IN_GROUP }) def delete(self, request, view_id): """Deletes an existing view if the user belongs to the group.""" view = ViewHandler().get_view(request.user, view_id) ViewHandler().delete_view(request.user, view) return Response(status=204)
class ApplicationsView(APIView): permission_classes = (IsAuthenticated, ) def get_permissions(self): if self.request.method == 'GET': return [AllowAny()] return super().get_permissions() @extend_schema( parameters=[ OpenApiParameter( name='group_id', location=OpenApiParameter.PATH, type=OpenApiTypes.INT, description= 'Returns only applications that are in the group related ' 'to the provided value.') ], tags=['Applications'], operation_id='list_applications', description= ('Lists all the applications of the group related to the provided ' '`group_id` parameter if the authorized user is in that group. If the' 'group is related to a template, then this endpoint will be publicly ' 'accessible. The properties that belong to the application can differ per ' 'type. An application always belongs to a single group.'), responses={ 200: PolymorphicMappingSerializer('Applications', application_type_serializers, many=True), 400: get_error_schema(['ERROR_USER_NOT_IN_GROUP']), 404: get_error_schema(['ERROR_GROUP_DOES_NOT_EXIST']) }) @map_exceptions({ GroupDoesNotExist: ERROR_GROUP_DOES_NOT_EXIST, UserNotInGroupError: ERROR_USER_NOT_IN_GROUP }) def get(self, request, group_id): """ Responds with a list of serialized applications that belong to the user. If a group id is provided only the applications of that group are going to be returned. """ group = CoreHandler().get_group(group_id) group.has_user(request.user, raise_error=True, allow_if_template=True) applications = Application.objects.select_related( 'content_type', 'group').filter(group=group) data = [ get_application_serializer(application).data for application in applications ] return Response(data) @extend_schema( parameters=[ OpenApiParameter( name='group_id', location=OpenApiParameter.PATH, type=OpenApiTypes.INT, description= 'Creates an application for the group related to the ' 'provided value.') ], tags=['Applications'], operation_id='create_application', description= ('Creates a new application based on the provided type. The newly created ' 'application is going to be added to the group related to the provided ' '`group_id` parameter. If the authorized user does not belong to the group ' 'an error will be returned.'), request=ApplicationCreateSerializer, responses={ 200: PolymorphicMappingSerializer('Applications', application_type_serializers), 400: get_error_schema( ['ERROR_USER_NOT_IN_GROUP', 'ERROR_REQUEST_BODY_VALIDATION']), 404: get_error_schema(['ERROR_GROUP_DOES_NOT_EXIST']) }, ) @transaction.atomic @validate_body(ApplicationCreateSerializer) @map_exceptions({ GroupDoesNotExist: ERROR_GROUP_DOES_NOT_EXIST, UserNotInGroupError: ERROR_USER_NOT_IN_GROUP }) def post(self, request, data, group_id): """Creates a new application for a user.""" group = CoreHandler().get_group(group_id) application = CoreHandler().create_application(request.user, group, data['type'], name=data['name']) return Response(get_application_serializer(application).data)
class RowsView(APIView): authentication_classes = APIView.authentication_classes + [ TokenAuthentication ] permission_classes = (IsAuthenticated, ) @extend_schema( parameters=[ OpenApiParameter( name='table_id', location=OpenApiParameter.PATH, type=OpenApiTypes.INT, description= 'Returns the rows of the table related to the provided ' 'value.'), OpenApiParameter( name='page', location=OpenApiParameter.QUERY, type=OpenApiTypes.INT, description='Defines which page of rows should be returned.'), OpenApiParameter( name='size', location=OpenApiParameter.QUERY, type=OpenApiTypes.INT, description='Defines how many rows should be returned per page.' ), OpenApiParameter( name='search', location=OpenApiParameter.QUERY, type=OpenApiTypes.STR, description= 'If provided only rows with data that matches the search ' 'query are going to be returned.'), OpenApiParameter( name='order_by', location=OpenApiParameter.QUERY, type=OpenApiTypes.STR, description= 'Optionally the rows can be ordered by provided field ids ' 'separated by comma. By default a field is ordered in ' 'ascending (A-Z) order, but by prepending the field with ' 'a \'-\' it can be ordered descending (Z-A). '), OpenApiParameter( name='filter__{field}__{filter}', location=OpenApiParameter.QUERY, type=OpenApiTypes.STR, description= (f'The rows can optionally be filtered by the same view filters ' f'available for the views. Multiple filters can be provided if ' f'they follow the same format. The field and filter variable ' f'indicate how to filter and the value indicates where to filter ' f'on.\n\n' f'For example if you provide the following GET parameter ' f'`filter__field_1__equal=test` then only rows where the value of ' f'field_1 is equal to test are going to be returned.\n\n' f'The following filters are available: ' f'{", ".join(view_filter_type_registry.get_types())}.')), OpenApiParameter( name='filter_type', location= OpenApiParameter.QUERY, type=OpenApiTypes.STR, description= ('`AND`: Indicates that the rows must match all the provided ' 'filters.\n' '`OR`: Indicates that the rows only have to match one of the ' 'filters.\n\n' 'This works only if two or more filters are provided.')), OpenApiParameter( name='include', location= OpenApiParameter.QUERY, type=OpenApiTypes.STR, description= ('All the fields are included in the response by default. You can ' 'select a subset of fields by providing the include query ' 'parameter. If you for example provide the following GET ' 'parameter `include=field_1,field_2` then only the fields with' 'id `1` and id `2` are going to be selected and included in the ' 'response. ')), OpenApiParameter( name='exclude', location= OpenApiParameter.QUERY, type=OpenApiTypes.STR, description= ('All the fields are included in the response by default. You can ' 'select a subset of fields by providing the exclude query ' 'parameter. If you for example provide the following GET ' 'parameter `exclude=field_1,field_2` then the fields with id `1` ' 'and id `2` are going to be excluded from the selection and ' 'response.')), ], tags=['Database table rows'], operation_id='list_database_table_rows', description = ('Lists all the rows of the table related to the provided parameter if the ' 'user has access to the related database\'s group. The response is ' 'paginated by a page/size style. It is also possible to provide an ' 'optional search query, only rows where the data matches the search query ' 'are going to be returned then. The properties of the returned rows ' 'depends on which fields the table has. For a complete overview of fields ' 'use the **list_database_table_fields** endpoint to list them all. In the ' 'example all field types are listed, but normally the number in ' 'field_{id} key is going to be the id of the field. The value is what the ' 'user has provided and the format of it depends on the fields type.'), responses={ 200: example_pagination_row_serializer_class, 400: get_error_schema([ 'ERROR_USER_NOT_IN_GROUP', 'ERROR_REQUEST_BODY_VALIDATION', 'ERROR_PAGE_SIZE_LIMIT', 'ERROR_INVALID_PAGE', 'ERROR_ORDER_BY_FIELD_NOT_FOUND', 'ERROR_ORDER_BY_FIELD_NOT_POSSIBLE', 'ERROR_FILTER_FIELD_NOT_FOUND', 'ERROR_VIEW_FILTER_TYPE_DOES_NOT_EXIST', 'ERROR_VIEW_FILTER_TYPE_NOT_ALLOWED_FOR_FIELD' ]), 401: get_error_schema(['ERROR_NO_PERMISSION_TO_TABLE']), 404: get_error_schema(['ERROR_TABLE_DOES_NOT_EXIST']) }) @map_exceptions({ UserNotInGroupError: ERROR_USER_NOT_IN_GROUP, TableDoesNotExist: ERROR_TABLE_DOES_NOT_EXIST, NoPermissionToTable: ERROR_NO_PERMISSION_TO_TABLE, OrderByFieldNotFound: ERROR_ORDER_BY_FIELD_NOT_FOUND, OrderByFieldNotPossible: ERROR_ORDER_BY_FIELD_NOT_POSSIBLE, FilterFieldNotFound: ERROR_FILTER_FIELD_NOT_FOUND, ViewFilterTypeDoesNotExist: ERROR_VIEW_FILTER_TYPE_DOES_NOT_EXIST, ViewFilterTypeNotAllowedForField: ERROR_VIEW_FILTER_TYPE_NOT_ALLOWED_FOR_FIELD }) def get(self, request, table_id): """ Lists all the rows of the given table id paginated. It is also possible to provide a search query. """ table = TableHandler().get_table(table_id) table.database.group.has_user(request.user, raise_error=True) TokenHandler().check_table_permissions(request, 'read', table, False) search = request.GET.get('search') order_by = request.GET.get('order_by') include = request.GET.get('include') exclude = request.GET.get('exclude') fields = RowHandler().get_include_exclude_fields( table, include, exclude) model = table.get_model(fields=fields, field_ids=[] if fields else None) queryset = model.objects.all().enhance_by_fields() if search: queryset = queryset.search_all_fields(search) if order_by: queryset = queryset.order_by_fields_string(order_by) filter_type = (FILTER_TYPE_OR if str(request.GET.get('filter_type')).upper() == 'OR' else FILTER_TYPE_AND) filter_object = { key: request.GET.getlist(key) for key in request.GET.keys() } queryset = queryset.filter_by_fields_object(filter_object, filter_type) paginator = PageNumberPagination( limit_page_size=settings.ROW_PAGE_SIZE_LIMIT) page = paginator.paginate_queryset(queryset, request, self) serializer_class = get_row_serializer_class(model, RowSerializer, is_response=True) serializer = serializer_class(page, many=True) return paginator.get_paginated_response(serializer.data) @extend_schema( parameters=[ OpenApiParameter( name='table_id', location=OpenApiParameter.PATH, type=OpenApiTypes.INT, description='Creates a row in the table related to the provided ' 'value.'), OpenApiParameter( name='before', location=OpenApiParameter.QUERY, type=OpenApiTypes.INT, description='If provided then the newly created row will be ' 'positioned before the row with the provided id.') ], tags=['Database table rows'], operation_id='create_database_table_row', description= ('Creates a new row in the table if the user has access to the related ' 'table\'s group. The accepted body fields are depending on the fields ' 'that the table has. For a complete overview of fields use the ' '**list_database_table_fields** to list them all. None of the fields are ' 'required, if they are not provided the value is going to be `null` or ' '`false` or some default value is that is set. If you want to add a value ' 'for the field with for example id `10`, the key must be named ' '`field_10`. Of course multiple fields can be provided in one request. In ' 'the examples below you will find all the different field types, the ' 'numbers/ids in the example are just there for example purposes, the ' 'field_ID must be replaced with the actual id of the field.'), request=get_example_row_serializer_class(False), responses={ 200: get_example_row_serializer_class(True), 400: get_error_schema( ['ERROR_USER_NOT_IN_GROUP', 'ERROR_REQUEST_BODY_VALIDATION']), 401: get_error_schema(['ERROR_NO_PERMISSION_TO_TABLE']), 404: get_error_schema( ['ERROR_TABLE_DOES_NOT_EXIST', 'ERROR_ROW_DOES_NOT_EXIST']) }) @transaction.atomic @map_exceptions({ UserNotInGroupError: ERROR_USER_NOT_IN_GROUP, TableDoesNotExist: ERROR_TABLE_DOES_NOT_EXIST, NoPermissionToTable: ERROR_NO_PERMISSION_TO_TABLE, UserFileDoesNotExist: ERROR_USER_FILE_DOES_NOT_EXIST, RowDoesNotExist: ERROR_ROW_DOES_NOT_EXIST, }) def post(self, request, table_id): """ Creates a new row for the given table_id. Also the post data is validated according to the tables field types. """ table = TableHandler().get_table(table_id) TokenHandler().check_table_permissions(request, 'create', table, False) model = table.get_model() validation_serializer = get_row_serializer_class(model) data = validate_data(validation_serializer, request.data) before_id = request.GET.get('before') before = (RowHandler().get_row(request.user, table, before_id, model) if before_id else None) row = RowHandler().create_row(request.user, table, data, model, before=before) serializer_class = get_row_serializer_class(model, RowSerializer, is_response=True) serializer = serializer_class(row) return Response(serializer.data)
class ViewFilterView(APIView): permission_classes = (IsAuthenticated,) @extend_schema( parameters=[ OpenApiParameter( name="view_filter_id", location=OpenApiParameter.PATH, type=OpenApiTypes.INT, description="Returns the view filter related to the provided value.", ) ], tags=["Database table view filters"], operation_id="get_database_table_view_filter", description=( "Returns the existing view filter if the authorized user has access to the" " related database's group." ), responses={ 200: ViewFilterSerializer(), 400: get_error_schema(["ERROR_USER_NOT_IN_GROUP"]), 404: get_error_schema(["ERROR_VIEW_FILTER_DOES_NOT_EXIST"]), }, ) @map_exceptions( { ViewFilterDoesNotExist: ERROR_VIEW_FILTER_DOES_NOT_EXIST, UserNotInGroup: ERROR_USER_NOT_IN_GROUP, } ) def get(self, request, view_filter_id): """Selects a single filter and responds with a serialized version.""" view_filter = ViewHandler().get_filter(request.user, view_filter_id) serializer = ViewFilterSerializer(view_filter) return Response(serializer.data) @extend_schema( parameters=[ OpenApiParameter( name="view_filter_id", location=OpenApiParameter.PATH, type=OpenApiTypes.INT, description="Updates the view filter related to the provided value.", ) ], tags=["Database table view filters"], operation_id="update_database_table_view_filter", description=( "Updates the existing filter if the authorized user has access to the " "related database's group." ), request=UpdateViewFilterSerializer(), responses={ 200: ViewFilterSerializer(), 400: get_error_schema( [ "ERROR_USER_NOT_IN_GROUP", "ERROR_FIELD_NOT_IN_TABLE", "ERROR_VIEW_FILTER_NOT_SUPPORTED", "ERROR_VIEW_FILTER_TYPE_UNSUPPORTED_FIELD", ] ), 404: get_error_schema(["ERROR_VIEW_FILTER_DOES_NOT_EXIST"]), }, ) @transaction.atomic @validate_body(UpdateViewFilterSerializer) @map_exceptions( { ViewFilterDoesNotExist: ERROR_VIEW_FILTER_DOES_NOT_EXIST, UserNotInGroup: ERROR_USER_NOT_IN_GROUP, FieldNotInTable: ERROR_FIELD_NOT_IN_TABLE, ViewFilterTypeNotAllowedForField: ERROR_VIEW_FILTER_TYPE_UNSUPPORTED_FIELD, } ) def patch(self, request, data, view_filter_id): """Updates the view filter if the user belongs to the group.""" handler = ViewHandler() view_filter = handler.get_filter( request.user, view_filter_id, base_queryset=ViewFilter.objects.select_for_update(), ) if "field" in data: # We can safely assume the field exists because the # UpdateViewFilterSerializer has already checked that. data["field"] = Field.objects.get(pk=data["field"]) if "type" in data: data["type_name"] = data.pop("type") view_filter = handler.update_filter(request.user, view_filter, **data) serializer = ViewFilterSerializer(view_filter) return Response(serializer.data) @extend_schema( parameters=[ OpenApiParameter( name="view_filter_id", location=OpenApiParameter.PATH, type=OpenApiTypes.INT, description="Deletes the filter related to the provided value.", ) ], tags=["Database table view filters"], operation_id="delete_database_table_view_filter", description=( "Deletes the existing filter if the authorized user has access to the " "related database's group." ), responses={ 204: None, 400: get_error_schema(["ERROR_USER_NOT_IN_GROUP"]), 404: get_error_schema(["ERROR_VIEW_FILTER_DOES_NOT_EXIST"]), }, ) @transaction.atomic @map_exceptions( { ViewFilterDoesNotExist: ERROR_VIEW_FILTER_DOES_NOT_EXIST, UserNotInGroup: ERROR_USER_NOT_IN_GROUP, } ) def delete(self, request, view_filter_id): """Deletes an existing filter if the user belongs to the group.""" view = ViewHandler().get_filter(request.user, view_filter_id) ViewHandler().delete_filter(request.user, view) return Response(status=204)
class ViewsView(APIView): permission_classes = (IsAuthenticated,) def get_permissions(self): if self.request.method == "GET": return [AllowAny()] return super().get_permissions() @extend_schema( parameters=[ OpenApiParameter( name="table_id", location=OpenApiParameter.PATH, type=OpenApiTypes.INT, description="Returns only views of the table related to the provided " "value.", ), OpenApiParameter( name="include", location=OpenApiParameter.QUERY, type=OpenApiTypes.STR, description=( "A comma separated list of extra attributes to include on each " "view in the response. The supported attributes are `filters` and " "`sortings`. For example `include=filters,sortings` will add the " "attributes `filters` and `sortings` to every returned view, " "containing a list of the views filters and sortings respectively." ), ), ], tags=["Database table views"], operation_id="list_database_table_views", description=( "Lists all views of the table related to the provided `table_id` if the " "user has access to the related database's group. If the group is " "related to a template, then this endpoint will be publicly accessible. A " "table can have multiple views. Each view can display the data in a " "different way. For example the `grid` view shows the in a spreadsheet " "like way. That type has custom endpoints for data retrieval and " "manipulation. In the future other views types like a calendar or Kanban " "are going to be added. Each type can have different properties." ), responses={ 200: PolymorphicCustomFieldRegistrySerializer( view_type_registry, ViewSerializer, many=True ), 400: get_error_schema(["ERROR_USER_NOT_IN_GROUP"]), 404: get_error_schema(["ERROR_TABLE_DOES_NOT_EXIST"]), }, ) @map_exceptions( { TableDoesNotExist: ERROR_TABLE_DOES_NOT_EXIST, UserNotInGroup: ERROR_USER_NOT_IN_GROUP, } ) @allowed_includes("filters", "sortings") def get(self, request, table_id, filters, sortings): """ Responds with a list of serialized views that belong to the table if the user has access to that group. """ table = TableHandler().get_table(table_id) table.database.group.has_user( request.user, raise_error=True, allow_if_template=True ) views = View.objects.filter(table=table).select_related("content_type") if filters: views = views.prefetch_related("viewfilter_set") if sortings: views = views.prefetch_related("viewsort_set") data = [ view_type_registry.get_serializer( view, ViewSerializer, filters=filters, sortings=sortings ).data for view in views ] return Response(data) @extend_schema( parameters=[ OpenApiParameter( name="table_id", location=OpenApiParameter.PATH, type=OpenApiTypes.INT, description="Creates a view for the table related to the provided " "value.", ), OpenApiParameter( name="include", location=OpenApiParameter.QUERY, type=OpenApiTypes.STR, description=( "A comma separated list of extra attributes to include on each " "view in the response. The supported attributes are `filters` and " "`sortings`. " "For example `include=filters,sortings` will add the attributes " "`filters` and `sortings` to every returned view, containing " "a list of the views filters and sortings respectively." ), ), ], tags=["Database table views"], operation_id="create_database_table_view", description=( "Creates a new view for the table related to the provided `table_id` " "parameter if the authorized user has access to the related database's " "group. Depending on the type, different properties can optionally be " "set." ), request=PolymorphicCustomFieldRegistrySerializer( view_type_registry, CreateViewSerializer ), responses={ 200: PolymorphicCustomFieldRegistrySerializer( view_type_registry, ViewSerializer ), 400: get_error_schema( ["ERROR_USER_NOT_IN_GROUP", "ERROR_REQUEST_BODY_VALIDATION"] ), 404: get_error_schema(["ERROR_TABLE_DOES_NOT_EXIST"]), }, ) @transaction.atomic @validate_body_custom_fields( view_type_registry, base_serializer_class=CreateViewSerializer ) @map_exceptions( { TableDoesNotExist: ERROR_TABLE_DOES_NOT_EXIST, UserNotInGroup: ERROR_USER_NOT_IN_GROUP, } ) @allowed_includes("filters", "sortings") def post(self, request, data, table_id, filters, sortings): """Creates a new view for a user.""" table = TableHandler().get_table(table_id) view = ViewHandler().create_view(request.user, table, data.pop("type"), **data) serializer = view_type_registry.get_serializer( view, ViewSerializer, filters=filters, sortings=sortings ) return Response(serializer.data)
class ViewView(APIView): permission_classes = (IsAuthenticated,) @extend_schema( parameters=[ OpenApiParameter( name="view_id", location=OpenApiParameter.PATH, type=OpenApiTypes.INT, description="Returns the view related to the provided value.", ), OpenApiParameter( name="include", location=OpenApiParameter.QUERY, type=OpenApiTypes.STR, description=( "A comma separated list of extra attributes to include on the " "returned view. The supported attributes are are `filters` and " "`sortings`. " "For example `include=filters,sortings` will add the attributes " "`filters` and `sortings` to every returned view, containing " "a list of the views filters and sortings respectively." ), ), ], tags=["Database table views"], operation_id="get_database_table_view", description=( "Returns the existing view if the authorized user has access to the " "related database's group. Depending on the type different properties" "could be returned." ), responses={ 200: PolymorphicCustomFieldRegistrySerializer( view_type_registry, ViewSerializer ), 400: get_error_schema(["ERROR_USER_NOT_IN_GROUP"]), 404: get_error_schema(["ERROR_VIEW_DOES_NOT_EXIST"]), }, ) @map_exceptions( { ViewDoesNotExist: ERROR_VIEW_DOES_NOT_EXIST, UserNotInGroup: ERROR_USER_NOT_IN_GROUP, } ) @allowed_includes("filters", "sortings") def get(self, request, view_id, filters, sortings): """Selects a single view and responds with a serialized version.""" view = ViewHandler().get_view(view_id) view.table.database.group.has_user(request.user, raise_error=True) serializer = view_type_registry.get_serializer( view, ViewSerializer, filters=filters, sortings=sortings ) return Response(serializer.data) @extend_schema( parameters=[ OpenApiParameter( name="view_id", location=OpenApiParameter.PATH, type=OpenApiTypes.INT, description="Updates the view related to the provided value.", ), OpenApiParameter( name="include", location=OpenApiParameter.QUERY, type=OpenApiTypes.STR, description=( "A comma separated list of extra attributes to include on the " "returned view. The supported attributes are are `filters` and " "`sortings`. " "For example `include=filters,sortings` will add the attributes " "`filters` and `sortings` to every returned view, containing " "a list of the views filters and sortings respectively." ), ), ], tags=["Database table views"], operation_id="update_database_table_view", description=( "Updates the existing view if the authorized user has access to the " "related database's group. The type cannot be changed. It depends on the " "existing type which properties can be changed." ), request=PolymorphicCustomFieldRegistrySerializer( view_type_registry, UpdateViewSerializer ), responses={ 200: PolymorphicCustomFieldRegistrySerializer( view_type_registry, ViewSerializer ), 400: get_error_schema( ["ERROR_USER_NOT_IN_GROUP", "ERROR_REQUEST_BODY_VALIDATION"] ), 404: get_error_schema(["ERROR_VIEW_DOES_NOT_EXIST"]), }, ) @transaction.atomic @map_exceptions( { ViewDoesNotExist: ERROR_VIEW_DOES_NOT_EXIST, UserNotInGroup: ERROR_USER_NOT_IN_GROUP, } ) @allowed_includes("filters", "sortings") def patch(self, request, view_id, filters, sortings): """Updates the view if the user belongs to the group.""" view = ViewHandler().get_view(view_id).specific view_type = view_type_registry.get_by_model(view) data = validate_data_custom_fields( view_type.type, view_type_registry, request.data, base_serializer_class=UpdateViewSerializer, ) view = ViewHandler().update_view(request.user, view, **data) serializer = view_type_registry.get_serializer( view, ViewSerializer, filters=filters, sortings=sortings ) return Response(serializer.data) @extend_schema( parameters=[ OpenApiParameter( name="view_id", location=OpenApiParameter.PATH, type=OpenApiTypes.INT, description="Deletes the view related to the provided value.", ) ], tags=["Database table views"], operation_id="delete_database_table_view", description=( "Deletes the existing view if the authorized user has access to the " "related database's group. Note that all the related settings of the " "view are going to be deleted also. The data stays intact after deleting " "the view because this is related to the table and not the view." ), responses={ 204: None, 400: get_error_schema(["ERROR_USER_NOT_IN_GROUP"]), 404: get_error_schema(["ERROR_VIEW_DOES_NOT_EXIST"]), }, ) @transaction.atomic @map_exceptions( { ViewDoesNotExist: ERROR_VIEW_DOES_NOT_EXIST, UserNotInGroup: ERROR_USER_NOT_IN_GROUP, } ) def delete(self, request, view_id): """Deletes an existing view if the user belongs to the group.""" view = ViewHandler().get_view(view_id) ViewHandler().delete_view(request.user, view) return Response(status=204)
class ViewFiltersView(APIView): permission_classes = (IsAuthenticated,) @extend_schema( parameters=[ OpenApiParameter( name="view_id", location=OpenApiParameter.PATH, type=OpenApiTypes.INT, description="Returns only filters of the view related to the provided " "value.", ) ], tags=["Database table view filters"], operation_id="list_database_table_view_filters", description=( "Lists all filters of the view related to the provided `view_id` if the " "user has access to the related database's group. A view can have " "multiple filters. When all the rows are requested for the view only those " "that apply to the filters are returned." ), responses={ 200: ViewFilterSerializer(many=True), 400: get_error_schema(["ERROR_USER_NOT_IN_GROUP"]), 404: get_error_schema(["ERROR_VIEW_DOES_NOT_EXIST"]), }, ) @map_exceptions( { ViewDoesNotExist: ERROR_VIEW_DOES_NOT_EXIST, UserNotInGroup: ERROR_USER_NOT_IN_GROUP, } ) def get(self, request, view_id): """ Responds with a list of serialized filters that belong to the view if the user has access to that group. """ view = ViewHandler().get_view(view_id) view.table.database.group.has_user(request.user, raise_error=True) filters = ViewFilter.objects.filter(view=view) serializer = ViewFilterSerializer(filters, many=True) return Response(serializer.data) @extend_schema( parameters=[ OpenApiParameter( name="view_id", location=OpenApiParameter.PATH, type=OpenApiTypes.INT, description="Creates a filter for the view related to the provided " "value.", ) ], tags=["Database table view filters"], operation_id="create_database_table_view_filter", description=( "Creates a new filter for the view related to the provided `view_id` " "parameter if the authorized user has access to the related database's " "group. When the rows of a view are requested, for example via the " "`list_database_table_grid_view_rows` endpoint, then only the rows that " "apply to all the filters are going to be returned. A filter compares the " "value of a field to the value of a filter. It depends on the type how " "values are going to be compared." ), request=CreateViewFilterSerializer(), responses={ 200: ViewFilterSerializer(), 400: get_error_schema( [ "ERROR_USER_NOT_IN_GROUP", "ERROR_REQUEST_BODY_VALIDATION", "ERROR_FIELD_NOT_IN_TABLE", "ERROR_VIEW_FILTER_NOT_SUPPORTED", "ERROR_VIEW_FILTER_TYPE_UNSUPPORTED_FIELD", ] ), 404: get_error_schema(["ERROR_VIEW_DOES_NOT_EXIST"]), }, ) @transaction.atomic @validate_body(CreateViewFilterSerializer) @map_exceptions( { ViewDoesNotExist: ERROR_VIEW_DOES_NOT_EXIST, UserNotInGroup: ERROR_USER_NOT_IN_GROUP, FieldNotInTable: ERROR_FIELD_NOT_IN_TABLE, ViewFilterNotSupported: ERROR_VIEW_FILTER_NOT_SUPPORTED, ViewFilterTypeNotAllowedForField: ERROR_VIEW_FILTER_TYPE_UNSUPPORTED_FIELD, } ) def post(self, request, data, view_id): """Creates a new filter for the provided view.""" view_handler = ViewHandler() view = view_handler.get_view(view_id) # We can safely assume the field exists because the CreateViewFilterSerializer # has already checked that. field = Field.objects.get(pk=data["field"]) view_filter = view_handler.create_filter( request.user, view, field, data["type"], data["value"] ) serializer = ViewFilterSerializer(view_filter) return Response(serializer.data)
class GroupUserView(APIView): permission_classes = (IsAuthenticated, ) @extend_schema( parameters=[ OpenApiParameter( name="group_user_id", location=OpenApiParameter.PATH, type=OpenApiTypes.INT, description= "Updates the group user related to the provided value.", ) ], tags=["Groups"], operation_id="update_group_user", description=( "Updates the existing group user related to the provided " "`group_user_id` param if the authorized user has admin rights to " "the related group."), request=UpdateGroupUserSerializer, responses={ 200: GroupUserGroupSerializer, 400: get_error_schema([ "ERROR_USER_NOT_IN_GROUP", "ERROR_USER_INVALID_GROUP_PERMISSIONS", "ERROR_REQUEST_BODY_VALIDATION", ]), 404: get_error_schema(["ERROR_GROUP_USER_DOES_NOT_EXIST"]), }, ) @transaction.atomic @validate_body(UpdateGroupUserSerializer) @map_exceptions({ GroupUserDoesNotExist: ERROR_GROUP_USER_DOES_NOT_EXIST, UserNotInGroup: ERROR_USER_NOT_IN_GROUP, UserInvalidGroupPermissionsError: ERROR_USER_INVALID_GROUP_PERMISSIONS, }) def patch(self, request, data, group_user_id): """Updates the group user if the user has admin permissions to the group.""" group_user = CoreHandler().get_group_user( group_user_id, base_queryset=GroupUser.objects.select_for_update()) group_user = CoreHandler().update_group_user(request.user, group_user, **data) return Response(GroupUserGroupSerializer(group_user).data) @extend_schema( parameters=[ OpenApiParameter( name="group_user_id", location=OpenApiParameter.PATH, type=OpenApiTypes.INT, description="Deletes the group user related to the provided " "value.", ) ], tags=["Groups"], operation_id="delete_group_user", description=( "Deletes a group user if the authorized user has admin rights to " "the related group."), responses={ 204: None, 400: get_error_schema([ "ERROR_USER_NOT_IN_GROUP", "ERROR_USER_INVALID_GROUP_PERMISSIONS" ]), 404: get_error_schema(["ERROR_GROUP_INVITATION_DOES_NOT_EXIST"]), }, ) @transaction.atomic @map_exceptions({ GroupUserDoesNotExist: ERROR_GROUP_USER_DOES_NOT_EXIST, UserNotInGroup: ERROR_USER_NOT_IN_GROUP, UserInvalidGroupPermissionsError: ERROR_USER_INVALID_GROUP_PERMISSIONS, }) def delete(self, request, group_user_id): """Deletes an existing group_user if the user belongs to the group.""" group_user = CoreHandler().get_group_user( group_user_id, base_queryset=GroupUser.objects.select_for_update()) CoreHandler().delete_group_user(request.user, group_user) return Response(status=204)
class RowView(APIView): permission_classes = (IsAuthenticated, ) @extend_schema( parameters=[ OpenApiParameter( name='table_id', location=OpenApiParameter.PATH, type=OpenApiTypes.INT, description='Updates the row in the table related to the value.' ), OpenApiParameter( name='row_id', location=OpenApiParameter.PATH, type=OpenApiTypes.INT, description='Updates the row related to the value.') ], tags=['Database table rows'], operation_id='update_database_table_row', description= ('Updates an existing row in the table if the user has access to the ' 'related table\'s group. The accepted body fields are depending on the ' 'fields that the table has. For a complete overview of fields use the ' '**list_database_table_fields** endpoint to list them all. None of the ' 'fields are required, if they are not provided the value is not going to ' 'be updated. If you want to update a value for the field with for example ' 'id `10`, the key must be named `field_10`. Of course multiple fields can ' 'be provided in one request. In the examples below you will find all the ' 'different field types, the numbers/ids in the example are just there for ' 'example purposes, the field_ID must be replaced with the actual id of the ' 'field.'), request=get_example_row_serializer_class(False), responses={ 200: get_example_row_serializer_class(True), 400: get_error_schema( ['ERROR_USER_NOT_IN_GROUP', 'ERROR_REQUEST_BODY_VALIDATION']), 404: get_error_schema( ['ERROR_TABLE_DOES_NOT_EXIST', 'ERROR_ROW_DOES_NOT_EXIST']) }) @transaction.atomic @map_exceptions({ UserNotInGroupError: ERROR_USER_NOT_IN_GROUP, TableDoesNotExist: ERROR_TABLE_DOES_NOT_EXIST, RowDoesNotExist: ERROR_ROW_DOES_NOT_EXIST }) def patch(self, request, table_id, row_id): """ Updates the row with the given row_id for the table with the given table_id. Also the post data is validated according to the tables field types. """ table = TableHandler().get_table(request.user, table_id) # Small side effect of generating the model for only the fields that need to # change is that the response it not going to contain the other fields. It is # however much faster because it doesn't need to get the specific version of # all the field objects. field_ids = RowHandler().extract_field_ids_from_dict(request.data) model = table.get_model(field_ids=field_ids) validation_serializer = get_row_serializer_class(model) data = validate_data(validation_serializer, request.data) row = RowHandler().update_row(request.user, table, row_id, data, model) serializer_class = get_row_serializer_class(model, RowSerializer, is_response=True) serializer = serializer_class(row) return Response(serializer.data) @extend_schema( parameters=[ OpenApiParameter( name='table_id', location=OpenApiParameter.PATH, type=OpenApiTypes.INT, description='Deletes the row in the table related to the value.' ), OpenApiParameter( name='row_id', location=OpenApiParameter.PATH, type=OpenApiTypes.INT, description='Deletes the row related to the value.') ], tags=['Database table rows'], operation_id='delete_database_table_row', description= ('Deletes an existing row in the table if the user has access to the ' 'table\'s group.'), responses={ 204: None, 400: get_error_schema(['ERROR_USER_NOT_IN_GROUP']), 404: get_error_schema( ['ERROR_TABLE_DOES_NOT_EXIST', 'ERROR_ROW_DOES_NOT_EXIST']) }) @transaction.atomic @map_exceptions({ UserNotInGroupError: ERROR_USER_NOT_IN_GROUP, TableDoesNotExist: ERROR_TABLE_DOES_NOT_EXIST, RowDoesNotExist: ERROR_ROW_DOES_NOT_EXIST }) def delete(self, request, table_id, row_id): """ Deletes an existing row with the given row_id for table with the given table_id. """ table = TableHandler().get_table(request.user, table_id) RowHandler().delete_row(request.user, table, row_id) return Response(status=204)
class TablesView(APIView): permission_classes = (IsAuthenticated, ) @extend_schema( parameters=[ OpenApiParameter( name="database_id", location=OpenApiParameter.PATH, type=OpenApiTypes.INT, description= "Returns only tables that are related to the provided " "value.", ) ], tags=["Database tables"], operation_id="list_database_tables", description= ("Lists all the tables that are in the database related to the " "`database_id` parameter if the user has access to the database's group. " "A table is exactly as the name suggests. It can hold multiple fields, " "each having their own type and multiple rows. They can be added via the " "**create_database_table_field** and **create_database_table_row** " "endpoints."), responses={ 200: TableSerializer(many=True), 400: get_error_schema(["ERROR_USER_NOT_IN_GROUP"]), 404: get_error_schema(["ERROR_APPLICATION_DOES_NOT_EXIST"]), }, ) @map_exceptions({ ApplicationDoesNotExist: ERROR_APPLICATION_DOES_NOT_EXIST, UserNotInGroup: ERROR_USER_NOT_IN_GROUP, }) def get(self, request, database_id): """Lists all the tables of a database.""" database = CoreHandler().get_application( database_id, base_queryset=Database.objects) database.group.has_user(request.user, raise_error=True) tables = Table.objects.filter(database=database) serializer = TableSerializer(tables, many=True) return Response(serializer.data) @extend_schema( parameters=[ OpenApiParameter( name="database_id", location=OpenApiParameter.PATH, type=OpenApiTypes.INT, description= "Creates a table for the database related to the provided " "value.", ) ], tags=["Database tables"], operation_id="create_database_table", description=( "Creates a new table for the database related to the provided " "`database_id` parameter if the authorized user has access to the " "database's group."), request=TableCreateSerializer, responses={ 200: TableSerializer, 400: get_error_schema([ "ERROR_USER_NOT_IN_GROUP", "ERROR_REQUEST_BODY_VALIDATION", "ERROR_INVALID_INITIAL_TABLE_DATA", "ERROR_INITIAL_TABLE_DATA_LIMIT_EXCEEDED", ]), 404: get_error_schema(["ERROR_APPLICATION_DOES_NOT_EXIST"]), }, ) @transaction.atomic @map_exceptions({ ApplicationDoesNotExist: ERROR_APPLICATION_DOES_NOT_EXIST, UserNotInGroup: ERROR_USER_NOT_IN_GROUP, InvalidInitialTableData: ERROR_INVALID_INITIAL_TABLE_DATA, InitialTableDataLimitExceeded: ERROR_INITIAL_TABLE_DATA_LIMIT_EXCEEDED, MaxFieldLimitExceeded: ERROR_MAX_FIELD_COUNT_EXCEEDED, }) @validate_body(TableCreateSerializer) def post(self, request, data, database_id): """Creates a new table in a database.""" database = CoreHandler().get_application( database_id, base_queryset=Database.objects) table = TableHandler().create_table(request.user, database, fill_example=True, **data) serializer = TableSerializer(table) return Response(serializer.data)
class ViewFilterView(APIView): permission_classes = (IsAuthenticated, ) @extend_schema( parameters=[ OpenApiParameter( name='view_filter_id', location=OpenApiParameter.PATH, type=OpenApiTypes.INT, description= 'Returns the view filter related to the provided value.') ], tags=['Database table view filters'], operation_id='get_database_table_view_filter', description= ('Returns the existing view filter if the authorized user has access to the' ' related database\'s group.'), responses={ 200: ViewFilterSerializer(), 400: get_error_schema(['ERROR_USER_NOT_IN_GROUP']), 404: get_error_schema(['ERROR_VIEW_FILTER_DOES_NOT_EXIST']) }) @map_exceptions({ ViewFilterDoesNotExist: ERROR_VIEW_FILTER_DOES_NOT_EXIST, UserNotInGroupError: ERROR_USER_NOT_IN_GROUP }) def get(self, request, view_filter_id): """Selects a single filter and responds with a serialized version.""" view_filter = ViewHandler().get_filter(request.user, view_filter_id) serializer = ViewFilterSerializer(view_filter) return Response(serializer.data) @extend_schema( parameters=[ OpenApiParameter( name='view_filter_id', location=OpenApiParameter.PATH, type=OpenApiTypes.INT, description= 'Updates the view filter related to the provided value.') ], tags=['Database table view filters'], operation_id='update_database_table_view_filter', description= ('Updates the existing filter if the authorized user has access to the ' 'related database\'s group.'), request=UpdateViewFilterSerializer(), responses={ 200: ViewFilterSerializer(), 400: get_error_schema([ 'ERROR_USER_NOT_IN_GROUP', 'ERROR_FIELD_NOT_IN_TABLE', 'ERROR_VIEW_FILTER_NOT_SUPPORTED', 'ERROR_VIEW_FILTER_TYPE_NOT_ALLOWED_FOR_FIELD' ]), 404: get_error_schema(['ERROR_VIEW_FILTER_DOES_NOT_EXIST']) }) @transaction.atomic @validate_body(UpdateViewFilterSerializer) @map_exceptions({ ViewFilterDoesNotExist: ERROR_VIEW_FILTER_DOES_NOT_EXIST, UserNotInGroupError: ERROR_USER_NOT_IN_GROUP, FieldNotInTable: ERROR_FIELD_NOT_IN_TABLE, ViewFilterTypeNotAllowedForField: ERROR_VIEW_FILTER_TYPE_NOT_ALLOWED_FOR_FIELD }) def patch(self, request, data, view_filter_id): """Updates the view filter if the user belongs to the group.""" handler = ViewHandler() view_filter = handler.get_filter(request.user, view_filter_id) if 'field' in data: # We can safely assume the field exists because the # UpdateViewFilterSerializer has already checked that. data['field'] = Field.objects.get(pk=data['field']) if 'type' in data: data['type_name'] = data.pop('type') view_filter = handler.update_filter(request.user, view_filter, **data) serializer = ViewFilterSerializer(view_filter) return Response(serializer.data) @extend_schema( parameters=[ OpenApiParameter( name='view_filter_id', location=OpenApiParameter.PATH, type=OpenApiTypes.INT, description='Deletes the filter related to the provided value.' ) ], tags=['Database table view filters'], operation_id='delete_database_table_view_filter', description= ('Deletes the existing filter if the authorized user has access to the ' 'related database\'s group.'), responses={ 204: None, 400: get_error_schema(['ERROR_USER_NOT_IN_GROUP']), 404: get_error_schema(['ERROR_VIEW_FILTER_DOES_NOT_EXIST']) }) @transaction.atomic @map_exceptions({ ViewFilterDoesNotExist: ERROR_VIEW_FILTER_DOES_NOT_EXIST, UserNotInGroupError: ERROR_USER_NOT_IN_GROUP }) def delete(self, request, view_filter_id): """Deletes an existing filter if the user belongs to the group.""" view = ViewHandler().get_filter(request.user, view_filter_id) ViewHandler().delete_filter(request.user, view) return Response(status=204)
class ViewSortingsView(APIView): permission_classes = (IsAuthenticated,) @extend_schema( parameters=[ OpenApiParameter( name="view_id", location=OpenApiParameter.PATH, type=OpenApiTypes.INT, description="Returns only sortings of the view related to the provided " "value.", ) ], tags=["Database table view sortings"], operation_id="list_database_table_view_sortings", description=( "Lists all sortings of the view related to the provided `view_id` if the " "user has access to the related database's group. A view can have " "multiple sortings. When all the rows are requested they will be in the " "desired order." ), responses={ 200: ViewSortSerializer(many=True), 400: get_error_schema(["ERROR_USER_NOT_IN_GROUP"]), 404: get_error_schema(["ERROR_VIEW_DOES_NOT_EXIST"]), }, ) @map_exceptions( { ViewDoesNotExist: ERROR_VIEW_DOES_NOT_EXIST, UserNotInGroup: ERROR_USER_NOT_IN_GROUP, } ) def get(self, request, view_id): """ Responds with a list of serialized sortings that belong to the view if the user has access to that group. """ view = ViewHandler().get_view(view_id) view.table.database.group.has_user(request.user, raise_error=True) sortings = ViewSort.objects.filter(view=view) serializer = ViewSortSerializer(sortings, many=True) return Response(serializer.data) @extend_schema( parameters=[ OpenApiParameter( name="view_id", location=OpenApiParameter.PATH, type=OpenApiTypes.INT, description="Creates a sort for the view related to the provided " "value.", ) ], tags=["Database table view sortings"], operation_id="create_database_table_view_sort", description=( "Creates a new sort for the view related to the provided `view_id` " "parameter if the authorized user has access to the related database's " "group. When the rows of a view are requested, for example via the " "`list_database_table_grid_view_rows` endpoint, they will be returned in " "the respected order defined by all the sortings." ), request=CreateViewSortSerializer(), responses={ 200: ViewSortSerializer(), 400: get_error_schema( [ "ERROR_USER_NOT_IN_GROUP", "ERROR_REQUEST_BODY_VALIDATION", "ERROR_VIEW_SORT_NOT_SUPPORTED", "ERROR_FIELD_NOT_IN_TABLE", "ERROR_VIEW_SORT_FIELD_ALREADY_EXISTS", "ERROR_VIEW_SORT_FIELD_NOT_SUPPORTED", ] ), 404: get_error_schema(["ERROR_VIEW_DOES_NOT_EXIST"]), }, ) @transaction.atomic @validate_body(CreateViewSortSerializer) @map_exceptions( { ViewDoesNotExist: ERROR_VIEW_DOES_NOT_EXIST, UserNotInGroup: ERROR_USER_NOT_IN_GROUP, FieldNotInTable: ERROR_FIELD_NOT_IN_TABLE, ViewSortNotSupported: ERROR_VIEW_SORT_NOT_SUPPORTED, ViewSortFieldAlreadyExist: ERROR_VIEW_SORT_FIELD_ALREADY_EXISTS, ViewSortFieldNotSupported: ERROR_VIEW_SORT_FIELD_NOT_SUPPORTED, } ) def post(self, request, data, view_id): """Creates a new sort for the provided view.""" view_handler = ViewHandler() view = view_handler.get_view(view_id) # We can safely assume the field exists because the CreateViewSortSerializer # has already checked that. field = Field.objects.get(pk=data["field"]) view_sort = view_handler.create_sort(request.user, view, field, data["order"]) serializer = ViewSortSerializer(view_sort) return Response(serializer.data)
class ApplicationView(APIView): permission_classes = (IsAuthenticated, ) @extend_schema( parameters=[ OpenApiParameter( name='application_id', location=OpenApiParameter.PATH, type=OpenApiTypes.INT, description= 'Returns the application related to the provided value.') ], tags=['Applications'], operation_id='get_application', description= ('Returns the requested application if the authorized user is in the ' 'application\'s group. The properties that belong to the application can ' 'differ per type.'), request=ApplicationCreateSerializer, responses={ 200: PolymorphicMappingSerializer('Applications', application_type_serializers), 400: get_error_schema( ['ERROR_USER_NOT_IN_GROUP', 'ERROR_REQUEST_BODY_VALIDATION']), 404: get_error_schema(['ERROR_APPLICATION_DOES_NOT_EXIST']) }, ) @map_exceptions({ ApplicationDoesNotExist: ERROR_APPLICATION_DOES_NOT_EXIST, UserNotInGroupError: ERROR_USER_NOT_IN_GROUP }) def get(self, request, application_id): """Selects a single application and responds with a serialized version.""" application = CoreHandler().get_application(application_id) application.group.has_user(request.user, raise_error=True) return Response(get_application_serializer(application).data) @extend_schema( parameters=[ OpenApiParameter( name='application_id', location=OpenApiParameter.PATH, type=OpenApiTypes.INT, description= 'Updates the application related to the provided value.') ], tags=['Applications'], operation_id='update_application', description= ('Updates the existing application related to the provided ' '`application_id` param if the authorized user is in the application\'s ' 'group. It is not possible to change the type, but properties like the ' 'name can be changed.'), request=ApplicationUpdateSerializer, responses={ 200: PolymorphicMappingSerializer('Applications', application_type_serializers), 400: get_error_schema( ['ERROR_USER_NOT_IN_GROUP', 'ERROR_REQUEST_BODY_VALIDATION']), 404: get_error_schema(['ERROR_APPLICATION_DOES_NOT_EXIST']) }, ) @transaction.atomic @validate_body(ApplicationUpdateSerializer) @map_exceptions({ ApplicationDoesNotExist: ERROR_APPLICATION_DOES_NOT_EXIST, UserNotInGroupError: ERROR_USER_NOT_IN_GROUP }) def patch(self, request, data, application_id): """Updates the application if the user belongs to the group.""" application = CoreHandler().get_application( application_id, base_queryset=Application.objects.select_for_update()) application = CoreHandler().update_application(request.user, application, name=data['name']) return Response(get_application_serializer(application).data) @extend_schema( parameters=[ OpenApiParameter( name='application_id', location=OpenApiParameter.PATH, type=OpenApiTypes.INT, description= 'Deletes the application related to the provided value.') ], tags=['Applications'], operation_id='delete_application', description= ('Deletes an application if the authorized user is in the application\'s ' 'group. All the related children are also going to be deleted. For example ' 'in case of a database application all the underlying tables, fields, ' 'views and rows are going to be deleted.'), responses={ 204: None, 400: get_error_schema(['ERROR_USER_NOT_IN_GROUP']), 404: get_error_schema(['ERROR_APPLICATION_DOES_NOT_EXIST']) }, ) @transaction.atomic @map_exceptions({ ApplicationDoesNotExist: ERROR_APPLICATION_DOES_NOT_EXIST, UserNotInGroupError: ERROR_USER_NOT_IN_GROUP }) def delete(self, request, application_id): """Deletes an existing application if the user belongs to the group.""" application = CoreHandler().get_application( application_id, base_queryset=Application.objects.select_for_update()) CoreHandler().delete_application(request.user, application) return Response(status=204)
class FieldsView(APIView): permission_classes = (IsAuthenticated, ) def get_permissions(self): if self.request.method == "GET": return [AllowAny()] return super().get_permissions() @extend_schema( parameters=[ OpenApiParameter( name="table_id", location=OpenApiParameter.PATH, type=OpenApiTypes.INT, description= "Returns only the fields of the table related to the " "provided value.", ) ], tags=["Database table fields"], operation_id="list_database_table_fields", description= ("Lists all the fields of the table related to the provided parameter if " "the user has access to the related database's group. If the group is " "related to a template, then this endpoint will be publicly accessible. A " "table consists of fields and each field can have a different type. Each " "type can have different properties. A field is comparable with a regular " "table's column."), responses={ 200: PolymorphicCustomFieldRegistrySerializer(field_type_registry, FieldSerializer, many=True), 400: get_error_schema(["ERROR_USER_NOT_IN_GROUP"]), 404: get_error_schema(["ERROR_TABLE_DOES_NOT_EXIST"]), }, ) @map_exceptions({ TableDoesNotExist: ERROR_TABLE_DOES_NOT_EXIST, UserNotInGroup: ERROR_USER_NOT_IN_GROUP, }) @method_permission_classes([AllowAny]) def get(self, request, table_id): """ Responds with a list of serialized fields that belong to the table if the user has access to that group. """ table = TableHandler().get_table(table_id) table.database.group.has_user(request.user, raise_error=True, allow_if_template=True) fields = Field.objects.filter( table=table).select_related("content_type") data = [ field_type_registry.get_serializer(field, FieldSerializer).data for field in fields ] return Response(data) @extend_schema( parameters=[ OpenApiParameter( name="table_id", location=OpenApiParameter.PATH, type=OpenApiTypes.INT, description= "Creates a new field for the provided table related to the " "value.", ) ], tags=["Database table fields"], operation_id="create_database_table_field", description= ("Creates a new field for the table related to the provided `table_id` " "parameter if the authorized user has access to the related database's " "group. Depending on the type, different properties can optionally be " "set."), request=PolymorphicCustomFieldRegistrySerializer( field_type_registry, CreateFieldSerializer), responses={ 200: PolymorphicCustomFieldRegistrySerializer(field_type_registry, FieldSerializer), 400: get_error_schema([ "ERROR_USER_NOT_IN_GROUP", "ERROR_REQUEST_BODY_VALIDATION", "ERROR_MAX_FIELD_COUNT_EXCEEDED", ]), 404: get_error_schema(["ERROR_TABLE_DOES_NOT_EXIST"]), }, ) @transaction.atomic @validate_body_custom_fields(field_type_registry, base_serializer_class=CreateFieldSerializer) @map_exceptions({ TableDoesNotExist: ERROR_TABLE_DOES_NOT_EXIST, UserNotInGroup: ERROR_USER_NOT_IN_GROUP, MaxFieldLimitExceeded: ERROR_MAX_FIELD_COUNT_EXCEEDED, }) def post(self, request, data, table_id): """Creates a new field for a table.""" type_name = data.pop("type") field_type = field_type_registry.get(type_name) table = TableHandler().get_table(table_id) table.database.group.has_user(request.user, raise_error=True) # Because each field type can raise custom exceptions while creating the # field we need to be able to map those to the correct API exceptions which are # defined in the type. with field_type.map_api_exceptions(): field = FieldHandler().create_field(request.user, table, type_name, **data) serializer = field_type_registry.get_serializer(field, FieldSerializer) return Response(serializer.data)
class RowView(APIView): authentication_classes = APIView.authentication_classes + [ TokenAuthentication ] permission_classes = (IsAuthenticated, ) @extend_schema( parameters=[ OpenApiParameter( name='table_id', location=OpenApiParameter.PATH, type=OpenApiTypes.INT, description= 'Returns the row of the table related to the provided ' 'value.'), OpenApiParameter( name='row_id', location=OpenApiParameter.PATH, type=OpenApiTypes.INT, description='Returns the row related the provided value.') ], tags=['Database table rows'], operation_id='get_database_table_row', description= ('Fetches an existing row from the table if the user has access to the ' 'related table\'s group. The properties of the returned row depend on ' 'which fields the table has. For a complete overview of fields use the ' '**list_database_table_fields** endpoint to list them all. In the example ' 'all field types are listed, but normally the number in field_{id} key is ' 'going to be the id of the field. The value is what the user has provided ' 'and the format of it depends on the fields type.'), responses={ 200: get_example_row_serializer_class(True), 400: get_error_schema( ['ERROR_USER_NOT_IN_GROUP', 'ERROR_REQUEST_BODY_VALIDATION']), 401: get_error_schema(['ERROR_NO_PERMISSION_TO_TABLE']), 404: get_error_schema( ['ERROR_TABLE_DOES_NOT_EXIST', 'ERROR_ROW_DOES_NOT_EXIST']) }) @map_exceptions({ UserNotInGroupError: ERROR_USER_NOT_IN_GROUP, TableDoesNotExist: ERROR_TABLE_DOES_NOT_EXIST, RowDoesNotExist: ERROR_ROW_DOES_NOT_EXIST, NoPermissionToTable: ERROR_NO_PERMISSION_TO_TABLE }) def get(self, request, table_id, row_id): """ Responds with a serializer version of the row related to the provided row_id and table_id. """ table = TableHandler().get_table(table_id) TokenHandler().check_table_permissions(request, 'read', table, False) model = table.get_model() row = RowHandler().get_row(request.user, table, row_id, model) serializer_class = get_row_serializer_class(model, RowSerializer, is_response=True) serializer = serializer_class(row) return Response(serializer.data) @extend_schema( parameters=[ OpenApiParameter( name='table_id', location=OpenApiParameter.PATH, type=OpenApiTypes.INT, description='Updates the row in the table related to the value.' ), OpenApiParameter( name='row_id', location=OpenApiParameter.PATH, type=OpenApiTypes.INT, description='Updates the row related to the value.') ], tags=['Database table rows'], operation_id='update_database_table_row', description= ('Updates an existing row in the table if the user has access to the ' 'related table\'s group. The accepted body fields are depending on the ' 'fields that the table has. For a complete overview of fields use the ' '**list_database_table_fields** endpoint to list them all. None of the ' 'fields are required, if they are not provided the value is not going to ' 'be updated. If you want to update a value for the field with for example ' 'id `10`, the key must be named `field_10`. Of course multiple fields can ' 'be provided in one request. In the examples below you will find all the ' 'different field types, the numbers/ids in the example are just there for ' 'example purposes, the field_ID must be replaced with the actual id of the ' 'field.'), request=get_example_row_serializer_class(False), responses={ 200: get_example_row_serializer_class(True), 400: get_error_schema( ['ERROR_USER_NOT_IN_GROUP', 'ERROR_REQUEST_BODY_VALIDATION']), 401: get_error_schema(['ERROR_NO_PERMISSION_TO_TABLE']), 404: get_error_schema( ['ERROR_TABLE_DOES_NOT_EXIST', 'ERROR_ROW_DOES_NOT_EXIST']) }) @transaction.atomic @map_exceptions({ UserNotInGroupError: ERROR_USER_NOT_IN_GROUP, TableDoesNotExist: ERROR_TABLE_DOES_NOT_EXIST, RowDoesNotExist: ERROR_ROW_DOES_NOT_EXIST, NoPermissionToTable: ERROR_NO_PERMISSION_TO_TABLE, UserFileDoesNotExist: ERROR_USER_FILE_DOES_NOT_EXIST }) def patch(self, request, table_id, row_id): """ Updates the row with the given row_id for the table with the given table_id. Also the post data is validated according to the tables field types. """ table = TableHandler().get_table(table_id) TokenHandler().check_table_permissions(request, 'update', table, False) field_ids = RowHandler().extract_field_ids_from_dict(request.data) model = table.get_model() validation_serializer = get_row_serializer_class(model, field_ids=field_ids) data = validate_data(validation_serializer, request.data) row = RowHandler().update_row(request.user, table, row_id, data, model) serializer_class = get_row_serializer_class(model, RowSerializer, is_response=True) serializer = serializer_class(row) return Response(serializer.data) @extend_schema( parameters=[ OpenApiParameter( name='table_id', location=OpenApiParameter.PATH, type=OpenApiTypes.INT, description='Deletes the row in the table related to the value.' ), OpenApiParameter( name='row_id', location=OpenApiParameter.PATH, type=OpenApiTypes.INT, description='Deletes the row related to the value.') ], tags=['Database table rows'], operation_id='delete_database_table_row', description= ('Deletes an existing row in the table if the user has access to the ' 'table\'s group.'), responses={ 204: None, 400: get_error_schema(['ERROR_USER_NOT_IN_GROUP']), 404: get_error_schema( ['ERROR_TABLE_DOES_NOT_EXIST', 'ERROR_ROW_DOES_NOT_EXIST']) }) @transaction.atomic @map_exceptions({ UserNotInGroupError: ERROR_USER_NOT_IN_GROUP, TableDoesNotExist: ERROR_TABLE_DOES_NOT_EXIST, RowDoesNotExist: ERROR_ROW_DOES_NOT_EXIST, NoPermissionToTable: ERROR_NO_PERMISSION_TO_TABLE }) def delete(self, request, table_id, row_id): """ Deletes an existing row with the given row_id for table with the given table_id. """ table = TableHandler().get_table(table_id) TokenHandler().check_table_permissions(request, 'delete', table, False) RowHandler().delete_row(request.user, table, row_id) return Response(status=204)
class ViewsView(APIView): permission_classes = (IsAuthenticated, ) def get_permissions(self): if self.request.method == 'GET': return [AllowAny()] return super().get_permissions() @extend_schema( parameters=[ OpenApiParameter( name='table_id', location=OpenApiParameter.PATH, type=OpenApiTypes.INT, description= 'Returns only views of the table related to the provided ' 'value.'), OpenApiParameter( name='include', location=OpenApiParameter.QUERY, type=OpenApiTypes.STR, description= ('A comma separated list of extra attributes to include on each ' 'view in the response. The supported attributes are `filters` and ' '`sortings`. For example `include=filters,sortings` will add the ' 'attributes `filters` and `sortings` to every returned view, ' 'containing a list of the views filters and sortings respectively.' )), ], tags=['Database table views'], operation_id='list_database_table_views', description = ('Lists all views of the table related to the provided `table_id` if the ' 'user has access to the related database\'s group. If the group is ' 'related to a template, then this endpoint will be publicly accessible. A ' 'table can have multiple views. Each view can display the data in a ' 'different way. For example the `grid` view shows the in a spreadsheet ' 'like way. That type has custom endpoints for data retrieval and ' 'manipulation. In the future other views types like a calendar or Kanban ' 'are going to be added. Each type can have different properties.'), responses={ 200: PolymorphicCustomFieldRegistrySerializer(view_type_registry, ViewSerializer, many=True), 400: get_error_schema(['ERROR_USER_NOT_IN_GROUP']), 404: get_error_schema(['ERROR_TABLE_DOES_NOT_EXIST']) }) @map_exceptions({ TableDoesNotExist: ERROR_TABLE_DOES_NOT_EXIST, UserNotInGroupError: ERROR_USER_NOT_IN_GROUP }) @allowed_includes('filters', 'sortings') def get(self, request, table_id, filters, sortings): """ Responds with a list of serialized views that belong to the table if the user has access to that group. """ table = TableHandler().get_table(table_id) table.database.group.has_user(request.user, raise_error=True, allow_if_template=True) views = View.objects.filter(table=table).select_related('content_type') if filters: views = views.prefetch_related('viewfilter_set') if sortings: views = views.prefetch_related('viewsort_set') data = [ view_type_registry.get_serializer(view, ViewSerializer, filters=filters, sortings=sortings).data for view in views ] return Response(data) @extend_schema( parameters=[ OpenApiParameter( name='table_id', location=OpenApiParameter.PATH, type=OpenApiTypes.INT, description= 'Creates a view for the table related to the provided ' 'value.'), OpenApiParameter( name='include', location=OpenApiParameter.QUERY, type=OpenApiTypes.STR, description= ('A comma separated list of extra attributes to include on each ' 'view in the response. The supported attributes are `filters` and ' '`sortings`. ' 'For example `include=filters,sortings` will add the attributes ' '`filters` and `sortings` to every returned view, containing ' 'a list of the views filters and sortings respectively.')), ], tags=['Database table views'], operation_id='create_database_table_view', description = ('Creates a new view for the table related to the provided `table_id` ' 'parameter if the authorized user has access to the related database\'s ' 'group. Depending on the type, different properties can optionally be ' 'set.'), request=PolymorphicCustomFieldRegistrySerializer( view_type_registry, CreateViewSerializer), responses={ 200: PolymorphicCustomFieldRegistrySerializer(view_type_registry, ViewSerializer), 400: get_error_schema( ['ERROR_USER_NOT_IN_GROUP', 'ERROR_REQUEST_BODY_VALIDATION']), 404: get_error_schema(['ERROR_TABLE_DOES_NOT_EXIST']) }) @transaction.atomic @validate_body_custom_fields(view_type_registry, base_serializer_class=CreateViewSerializer) @map_exceptions({ TableDoesNotExist: ERROR_TABLE_DOES_NOT_EXIST, UserNotInGroupError: ERROR_USER_NOT_IN_GROUP }) @allowed_includes('filters', 'sortings') def post(self, request, data, table_id, filters, sortings): """Creates a new view for a user.""" table = TableHandler().get_table(table_id) view = ViewHandler().create_view(request.user, table, data.pop('type'), **data) serializer = view_type_registry.get_serializer(view, ViewSerializer, filters=filters, sortings=sortings) return Response(serializer.data)
class GridViewView(APIView): permission_classes = (IsAuthenticated, ) @extend_schema( parameters=[ OpenApiParameter( name='view_id', location=OpenApiParameter.PATH, type=OpenApiTypes.INT, description= 'Returns only rows that belong to the related view\'s ' 'table.'), OpenApiParameter( name='count', location=OpenApiParameter.PATH, type=OpenApiTypes.NONE, description='If provided only the count will be returned.'), OpenApiParameter( name='include', location=OpenApiParameter.QUERY, type=OpenApiTypes.STR, description= ('Can contain `field_options` which will add an object with the ' 'same name to the response if included. That object contains ' 'user defined view settings for each field. For example the ' 'field\'s width is included in here.')), OpenApiParameter( name='limit', location=OpenApiParameter.QUERY, type=OpenApiTypes.INT, description='Defines how many rows should be returned.'), OpenApiParameter( name='offset', location=OpenApiParameter.QUERY, type=OpenApiTypes.INT, description='Can only be used in combination with the `limit` ' 'parameter and defines from which offset the rows should ' 'be returned.'), OpenApiParameter( name='page', location=OpenApiParameter.QUERY, type=OpenApiTypes.INT, description= 'Defines which page of rows should be returned. Either ' 'the `page` or `limit` can be provided, not both.'), OpenApiParameter( name='size', location= OpenApiParameter.QUERY, type=OpenApiTypes.INT, description= 'Can only be used in combination with the `page` parameter ' 'and defines how many rows should be returned.') ], tags=['Database table grid view'], operation_id='list_database_table_grid_view_rows', description = ('Lists the requested rows of the view\'s table related to the provided ' '`view_id` if the authorized user has access to the database\'s group. ' 'The response is paginated either by a limit/offset or page/size style. ' 'The style depends on the provided GET parameters. The properties of the ' 'returned rows depends on which fields the table has. For a complete ' 'overview of fields use the **list_database_table_fields** endpoint to ' 'list them all. In the example all field types are listed, but normally ' 'the number in field_{id} key is going to be the id of the field. ' 'The value is what the user has provided and the format of it depends on ' 'the fields type.\n' '\n' 'The filters and sortings are automatically applied. To get a full ' 'overview of the applied filters and sortings you can use the ' '`list_database_table_view_filters` and ' '`list_database_table_view_sortings` endpoints.'), responses={ 200: example_pagination_row_serializer_class, 400: get_error_schema(['ERROR_USER_NOT_IN_GROUP']), 404: get_error_schema(['ERROR_GRID_DOES_NOT_EXIST']) }) @map_exceptions({ UserNotInGroupError: ERROR_USER_NOT_IN_GROUP, ViewDoesNotExist: ERROR_GRID_DOES_NOT_EXIST }) @allowed_includes('field_options') def get(self, request, view_id, field_options): """ Lists all the rows of a grid view, paginated either by a page or offset/limit. If the limit get parameter is provided the limit/offset pagination will be used else the page number pagination. Optionally the field options can also be included in the response if the the `field_options` are provided in the includes GET parameter. """ view_handler = ViewHandler() view = view_handler.get_view(view_id, GridView) view.table.database.group.has_user(request.user, raise_error=True) model = view.table.get_model() queryset = model.objects.all().enhance_by_fields() # Applies the view filters and sortings to the queryset if there are any. queryset = view_handler.apply_filters(view, queryset) queryset = view_handler.apply_sorting(view, queryset) if 'count' in request.GET: return Response({'count': queryset.count()}) if LimitOffsetPagination.limit_query_param in request.GET: paginator = LimitOffsetPagination() else: paginator = PageNumberPagination() page = paginator.paginate_queryset(queryset, request, self) serializer_class = get_row_serializer_class(model, RowSerializer, is_response=True) serializer = serializer_class(page, many=True) response = paginator.get_paginated_response(serializer.data) if field_options: # The serializer has the GridViewFieldOptionsField which fetches the # field options from the database and creates them if they don't exist, # but when added to the context the fields don't have to be fetched from # the database again when checking if they exist. context = { 'fields': [o['field'] for o in model._field_objects.values()] } response.data.update( **GridViewSerializer(view, context=context).data) return response @extend_schema( parameters=[ OpenApiParameter( name='view_id', location=OpenApiParameter.PATH, type=OpenApiTypes.INT, required=False, description= 'Returns only rows that belong to the related view\'s ' 'table.') ], tags=['Database table grid view'], operation_id='filter_database_table_grid_view_rows', description= ('Lists only the rows and fields that match the request. Only the rows ' 'with the ids that are in the `row_ids` list are going to be returned. ' 'Same goes for the fields, only the fields with the ids in the ' '`field_ids` are going to be returned. This endpoint could be used to ' 'refresh data after changes something. For example in the web frontend ' 'after changing a field type, the data of the related cells will be ' 'refreshed using this endpoint. In the example all field types are listed, ' 'but normally the number in field_{id} key is going to be the id of the ' 'field. The value is what the user has provided and the format of it ' 'depends on the fields type.'), request=GridViewFilterSerializer, responses={ 200: get_example_row_serializer_class(True)(many=True), 400: get_error_schema( ['ERROR_USER_NOT_IN_GROUP', 'ERROR_REQUEST_BODY_VALIDATION']), 404: get_error_schema(['ERROR_GRID_DOES_NOT_EXIST']) }) @map_exceptions({ UserNotInGroupError: ERROR_USER_NOT_IN_GROUP, ViewDoesNotExist: ERROR_GRID_DOES_NOT_EXIST }) @validate_body(GridViewFilterSerializer) def post(self, request, view_id, data): """ Row filter endpoint that only lists the requested rows and optionally only the requested fields. """ view = ViewHandler().get_view(view_id, GridView) view.table.database.group.has_user(request.user, raise_error=True) model = view.table.get_model(field_ids=data['field_ids']) results = model.objects.filter(pk__in=data['row_ids']) serializer_class = get_row_serializer_class(model, RowSerializer, is_response=True) serializer = serializer_class(results, many=True) return Response(serializer.data) @extend_schema( parameters=[ OpenApiParameter( name='view_id', location=OpenApiParameter.PATH, type=OpenApiTypes.INT, required=False, description= 'Updates the field related to the provided `view_id` ' 'parameter.') ], tags=['Database table grid view'], operation_id='update_database_table_grid_view_field_options', description= ('Updates the field options of a `grid` view. The field options are unique ' 'options per field for a view. This could for example be used to update ' 'the field width if the user changes it.'), request=GridViewSerializer, responses={ 200: GridViewSerializer, 400: get_error_schema([ 'ERROR_USER_NOT_IN_GROUP', 'ERROR_UNRELATED_FIELD', 'ERROR_REQUEST_BODY_VALIDATION' ]), 404: get_error_schema(['ERROR_GRID_DOES_NOT_EXIST']) }) @map_exceptions({ UserNotInGroupError: ERROR_USER_NOT_IN_GROUP, ViewDoesNotExist: ERROR_GRID_DOES_NOT_EXIST, UnrelatedFieldError: ERROR_UNRELATED_FIELD }) @validate_body(GridViewSerializer) def patch(self, request, view_id, data): """ Updates the field options for the provided grid view. The following example body data will only update the width of the FIELD_ID and leaves the others untouched. { FIELD_ID: { 'width': 200 } } """ handler = ViewHandler() view = handler.get_view(view_id, GridView) handler.update_grid_view_field_options(request.user, view, data['field_options']) return Response(GridViewSerializer(view).data)
class RowsView(APIView): authentication_classes = APIView.authentication_classes + [ TokenAuthentication ] permission_classes = (IsAuthenticated, ) @extend_schema( parameters=[ OpenApiParameter( name='table_id', location=OpenApiParameter.PATH, type=OpenApiTypes.INT, description= 'Returns the rows of the table related to the provided ' 'value.'), OpenApiParameter( name='page', location=OpenApiParameter.QUERY, type=OpenApiTypes.INT, description='Defines which page of rows should be returned.'), OpenApiParameter( name='size', location=OpenApiParameter.QUERY, type=OpenApiTypes.INT, description='Defines how many rows should be returned per page.' ), OpenApiParameter( name='search', location=OpenApiParameter.QUERY, type=OpenApiTypes.STR, description= 'If provided only rows with data that matches the search ' 'query are going to be returned.'), OpenApiParameter( name='order_by', location=OpenApiParameter.QUERY, type=OpenApiTypes.STR, description= 'Optionally the rows can be ordered by provided field ids ' 'separated by comma. By default a field is ordered in ' 'ascending (A-Z) order, but by prepending the field with ' 'a \'-\' it can be ordered descending (Z-A). ') ], tags=['Database table rows'], operation_id='list_database_table_rows', description= ('Lists all the rows of the table related to the provided parameter if the ' 'user has access to the related database\'s group. The response is ' 'paginated by a page/size style. It is also possible to provide an ' 'optional search query, only rows where the data matches the search query ' 'are going to be returned then. The properties of the returned rows ' 'depends on which fields the table has. For a complete overview of fields ' 'use the **list_database_table_fields** endpoint to list them all. In the ' 'example all field types are listed, but normally the number in ' 'field_{id} key is going to be the id of the field. The value is what the ' 'user has provided and the format of it depends on the fields type.'), responses={ 200: example_pagination_row_serializer_class, 400: get_error_schema([ 'ERROR_USER_NOT_IN_GROUP', 'ERROR_REQUEST_BODY_VALIDATION', 'ERROR_PAGE_SIZE_LIMIT', 'ERROR_INVALID_PAGE', 'ERROR_ORDER_BY_FIELD_NOT_FOUND', 'ERROR_ORDER_BY_FIELD_NOT_POSSIBLE' ]), 401: get_error_schema(['ERROR_NO_PERMISSION_TO_TABLE']), 404: get_error_schema(['ERROR_TABLE_DOES_NOT_EXIST']) }) @map_exceptions({ UserNotInGroupError: ERROR_USER_NOT_IN_GROUP, TableDoesNotExist: ERROR_TABLE_DOES_NOT_EXIST, NoPermissionToTable: ERROR_NO_PERMISSION_TO_TABLE, OrderByFieldNotFound: ERROR_ORDER_BY_FIELD_NOT_FOUND, OrderByFieldNotPossible: ERROR_ORDER_BY_FIELD_NOT_POSSIBLE }) def get(self, request, table_id): """ Lists all the rows of the given table id paginated. It is also possible to provide a search query. """ table = TableHandler().get_table(request.user, table_id) TokenHandler().check_table_permissions(request, 'read', table, False) model = table.get_model() search = request.GET.get('search') order_by = request.GET.get('order_by') queryset = model.objects.all().enhance_by_fields().order_by('id') if search: queryset = queryset.search_all_fields(search) if order_by: queryset = queryset.order_by_fields_string(order_by) paginator = PageNumberPagination( limit_page_size=settings.ROW_PAGE_SIZE_LIMIT) page = paginator.paginate_queryset(queryset, request, self) serializer_class = get_row_serializer_class(model, RowSerializer, is_response=True) serializer = serializer_class(page, many=True) return paginator.get_paginated_response(serializer.data) @extend_schema( parameters=[ OpenApiParameter( name='table_id', location=OpenApiParameter.PATH, type=OpenApiTypes.INT, description='Creates a row in the table related to the provided ' 'value.') ], tags=['Database table rows'], operation_id='create_database_table_row', description= ('Creates a new row in the table if the user has access to the related ' 'table\'s group. The accepted body fields are depending on the fields ' 'that the table has. For a complete overview of fields use the ' '**list_database_table_fields** to list them all. None of the fields are ' 'required, if they are not provided the value is going to be `null` or ' '`false` or some default value is that is set. If you want to add a value ' 'for the field with for example id `10`, the key must be named ' '`field_10`. Of course multiple fields can be provided in one request. In ' 'the examples below you will find all the different field types, the ' 'numbers/ids in the example are just there for example purposes, the ' 'field_ID must be replaced with the actual id of the field.'), request=get_example_row_serializer_class(False), responses={ 200: get_example_row_serializer_class(True), 400: get_error_schema( ['ERROR_USER_NOT_IN_GROUP', 'ERROR_REQUEST_BODY_VALIDATION']), 401: get_error_schema(['ERROR_NO_PERMISSION_TO_TABLE']), 404: get_error_schema(['ERROR_TABLE_DOES_NOT_EXIST']) }) @transaction.atomic @map_exceptions({ UserNotInGroupError: ERROR_USER_NOT_IN_GROUP, TableDoesNotExist: ERROR_TABLE_DOES_NOT_EXIST, NoPermissionToTable: ERROR_NO_PERMISSION_TO_TABLE, UserFileDoesNotExist: ERROR_USER_FILE_DOES_NOT_EXIST }) def post(self, request, table_id): """ Creates a new row for the given table_id. Also the post data is validated according to the tables field types. """ table = TableHandler().get_table(request.user, table_id) TokenHandler().check_table_permissions(request, 'create', table, False) model = table.get_model() validation_serializer = get_row_serializer_class(model) data = validate_data(validation_serializer, request.data) row = RowHandler().create_row(request.user, table, data, model) serializer_class = get_row_serializer_class(model, RowSerializer, is_response=True) serializer = serializer_class(row) return Response(serializer.data)
class FieldsView(APIView): permission_classes = (IsAuthenticated, ) @extend_schema( parameters=[ OpenApiParameter( name='table_id', location=OpenApiParameter.PATH, type=OpenApiTypes.INT, description= 'Returns only the fields of the table related to the ' 'provided value.') ], tags=['Database table fields'], operation_id='list_database_table_fields', description= ('Lists all the fields of the table related to the provided parameter if ' 'the user has access to the related database\'s group. A table consists of ' 'fields and each field can have a different type. Each type can have ' 'different properties. A field is comparable with a regular table\'s ' 'column.'), responses={ 200: PolymorphicCustomFieldRegistrySerializer(field_type_registry, FieldSerializer, many=True), 400: get_error_schema(['ERROR_USER_NOT_IN_GROUP']), 404: get_error_schema(['ERROR_TABLE_DOES_NOT_EXIST']) }) @map_exceptions({ TableDoesNotExist: ERROR_TABLE_DOES_NOT_EXIST, UserNotInGroupError: ERROR_USER_NOT_IN_GROUP }) def get(self, request, table_id): """ Responds with a list of serialized fields that belong to the table if the user has access to that group. """ table = TableHandler().get_table(table_id) table.database.group.has_user(request.user, raise_error=True) fields = Field.objects.filter( table=table).select_related('content_type') data = [ field_type_registry.get_serializer(field, FieldSerializer).data for field in fields ] return Response(data) @extend_schema( parameters=[ OpenApiParameter( name='table_id', location=OpenApiParameter.PATH, type=OpenApiTypes.INT, description= 'Creates a new field for the provided table related to the ' 'value.') ], tags=['Database table fields'], operation_id='create_database_table_field', description= ('Creates a new field for the table related to the provided `table_id` ' 'parameter if the authorized user has access to the related database\'s ' 'group. Depending on the type, different properties can optionally be ' 'set.'), request=PolymorphicCustomFieldRegistrySerializer( field_type_registry, CreateFieldSerializer), responses={ 200: PolymorphicCustomFieldRegistrySerializer(field_type_registry, FieldSerializer), 400: get_error_schema( ['ERROR_USER_NOT_IN_GROUP', 'ERROR_REQUEST_BODY_VALIDATION']), 404: get_error_schema(['ERROR_TABLE_DOES_NOT_EXIST']) }) @transaction.atomic @validate_body_custom_fields(field_type_registry, base_serializer_class=CreateFieldSerializer) @map_exceptions({ TableDoesNotExist: ERROR_TABLE_DOES_NOT_EXIST, UserNotInGroupError: ERROR_USER_NOT_IN_GROUP }) def post(self, request, data, table_id): """Creates a new field for a table.""" type_name = data.pop('type') field_type = field_type_registry.get(type_name) table = TableHandler().get_table(table_id) table.database.group.has_user(request.user, raise_error=True) # Because each field type can raise custom exceptions while creating the # field we need to be able to map those to the correct API exceptions which are # defined in the type. with field_type.map_api_exceptions(): field = FieldHandler().create_field(request.user, table, type_name, **data) serializer = field_type_registry.get_serializer(field, FieldSerializer) return Response(serializer.data)
class GroupInvitationView(APIView): permission_classes = (IsAuthenticated, ) @extend_schema( parameters=[ OpenApiParameter( name='group_invitation_id', location=OpenApiParameter.PATH, type=OpenApiTypes.INT, description= 'Returns the group invitation related to the provided ' 'value.') ], tags=['Group invitations'], operation_id='get_group_invitation', description= ('Returns the requested group invitation if the authorized user has admin ' 'right to the related group'), responses={ 200: GroupInvitationSerializer, 400: get_error_schema([ 'ERROR_USER_NOT_IN_GROUP', 'ERROR_USER_INVALID_GROUP_PERMISSIONS_ERROR' ]), 404: get_error_schema(['ERROR_GROUP_INVITATION_DOES_NOT_EXIST']) }, ) @map_exceptions({ GroupInvitationDoesNotExist: ERROR_GROUP_INVITATION_DOES_NOT_EXIST, UserNotInGroupError: ERROR_USER_NOT_IN_GROUP, UserInvalidGroupPermissionsError: ERROR_USER_INVALID_GROUP_PERMISSIONS_ERROR, }) def get(self, request, group_invitation_id): """Selects a single group invitation and responds with a serialized version.""" group_invitation = CoreHandler().get_group_invitation( request.user, group_invitation_id) return Response(GroupInvitationSerializer(group_invitation).data) @extend_schema( parameters=[ OpenApiParameter( name='group_invitation_id', location=OpenApiParameter.PATH, type=OpenApiTypes.INT, description= 'Updates the group invitation related to the provided ' 'value.') ], tags=['Group invitations'], operation_id='update_group_invitation', description= ('Updates the existing group invitation related to the provided ' '`group_invitation_id` param if the authorized user has admin rights to ' 'the related group.'), request=UpdateGroupInvitationSerializer, responses={ 200: GroupInvitationSerializer, 400: get_error_schema([ 'ERROR_USER_NOT_IN_GROUP', 'ERROR_USER_INVALID_GROUP_PERMISSIONS_ERROR', 'ERROR_REQUEST_BODY_VALIDATION' ]), 404: get_error_schema(['ERROR_GROUP_INVITATION_DOES_NOT_EXIST']) }, ) @transaction.atomic @validate_body(UpdateGroupInvitationSerializer) @map_exceptions({ GroupInvitationDoesNotExist: ERROR_GROUP_INVITATION_DOES_NOT_EXIST, UserNotInGroupError: ERROR_USER_NOT_IN_GROUP, UserInvalidGroupPermissionsError: ERROR_USER_INVALID_GROUP_PERMISSIONS_ERROR }) def patch(self, request, data, group_invitation_id): """Updates the group invitation if the user belongs to the group.""" group_invitation = CoreHandler().get_group_invitation( request.user, group_invitation_id, base_queryset=GroupInvitation.objects.select_for_update()) group_invitation = CoreHandler().update_group_invitation( request.user, group_invitation, **data) return Response(GroupInvitationSerializer(group_invitation).data) @extend_schema( parameters=[ OpenApiParameter( name='group_invitation_id', location=OpenApiParameter.PATH, type=OpenApiTypes.INT, description= 'Deletes the group invitation related to the provided ' 'value.') ], tags=['Group invitations'], operation_id='delete_group_invitation', description= ('Deletes a group invitation if the authorized user has admin rights to ' 'the related group.'), responses={ 204: None, 400: get_error_schema([ 'ERROR_USER_NOT_IN_GROUP', 'ERROR_USER_INVALID_GROUP_PERMISSIONS_ERROR' ]), 404: get_error_schema(['ERROR_GROUP_INVITATION_DOES_NOT_EXIST']) }, ) @transaction.atomic @map_exceptions({ GroupInvitationDoesNotExist: ERROR_GROUP_INVITATION_DOES_NOT_EXIST, UserNotInGroupError: ERROR_USER_NOT_IN_GROUP, UserInvalidGroupPermissionsError: ERROR_USER_INVALID_GROUP_PERMISSIONS_ERROR, }) def delete(self, request, group_invitation_id): """Deletes an existing group_invitation if the user belongs to the group.""" group_invitation = CoreHandler().get_group_invitation( request.user, group_invitation_id, base_queryset=GroupInvitation.objects.select_for_update()) CoreHandler().delete_group_invitation(request.user, group_invitation) return Response(status=204)
class ViewFiltersView(APIView): permission_classes = (IsAuthenticated,) @extend_schema( parameters=[ OpenApiParameter( name='view_id', location=OpenApiParameter.PATH, type=OpenApiTypes.INT, description='Returns only filters of the view related to the provided ' 'value.' ) ], tags=['Database table view filters'], operation_id='list_database_table_view_filters', description=( 'Lists all filters of the view related to the provided `view_id` if the ' 'user has access to the related database\'s group. A view can have ' 'multiple filters. When all the rows are requested for the view only those ' 'that apply to the filters are returned.' ), responses={ 200: ViewFilterSerializer(many=True), 400: get_error_schema(['ERROR_USER_NOT_IN_GROUP']), 404: get_error_schema(['ERROR_VIEW_DOES_NOT_EXIST']) } ) @map_exceptions({ ViewDoesNotExist: ERROR_VIEW_DOES_NOT_EXIST, UserNotInGroupError: ERROR_USER_NOT_IN_GROUP }) def get(self, request, view_id): """ Responds with a list of serialized filters that belong to the view if the user has access to that group. """ view = ViewHandler().get_view(request.user, view_id) filters = ViewFilter.objects.filter(view=view) serializer = ViewFilterSerializer(filters, many=True) return Response(serializer.data) @extend_schema( parameters=[ OpenApiParameter( name='view_id', location=OpenApiParameter.PATH, type=OpenApiTypes.INT, description='Creates a filter for the view related to the provided ' 'value.' ) ], tags=['Database table view filters'], operation_id='create_database_table_view_filter', description=( 'Creates a new filter for the view related to the provided `view_id` ' 'parameter if the authorized user has access to the related database\'s ' 'group. When the rows of a view are requested, for example via the ' '`list_database_table_grid_view_rows` endpoint, then only the rows that ' 'apply to all the filters are going to be returned. A filters compares the ' 'value of a field to the value of a filter. It depends on the type how ' 'values are going to be compared.' ), request=CreateViewFilterSerializer(), responses={ 200: ViewFilterSerializer(), 400: get_error_schema([ 'ERROR_USER_NOT_IN_GROUP', 'ERROR_REQUEST_BODY_VALIDATION', 'ERROR_FIELD_NOT_IN_TABLE', 'ERROR_VIEW_FILTER_NOT_SUPPORTED', 'ERROR_VIEW_FILTER_TYPE_NOT_ALLOWED_FOR_FIELD' ]), 404: get_error_schema(['ERROR_VIEW_DOES_NOT_EXIST']) } ) @transaction.atomic @validate_body(CreateViewFilterSerializer) @map_exceptions({ ViewDoesNotExist: ERROR_VIEW_DOES_NOT_EXIST, UserNotInGroupError: ERROR_USER_NOT_IN_GROUP, FieldNotInTable: ERROR_FIELD_NOT_IN_TABLE, ViewFilterNotSupported: ERROR_VIEW_FILTER_NOT_SUPPORTED, ViewFilterTypeNotAllowedForField: ERROR_VIEW_FILTER_TYPE_NOT_ALLOWED_FOR_FIELD }) def post(self, request, data, view_id): """Creates a new filter for the provided view.""" view_handler = ViewHandler() view = view_handler.get_view(request.user, view_id) # We can safely assume the field exists because the CreateViewFilterSerializer # has already checked that. field = Field.objects.get(pk=data['field']) view_filter = view_handler.create_filter(request.user, view, field, data['type'], data['value']) serializer = ViewFilterSerializer(view_filter) return Response(serializer.data)
class GroupInvitationsView(APIView): permission_classes = (IsAuthenticated, ) @extend_schema( parameters=[ OpenApiParameter( name='group_id', location=OpenApiParameter.PATH, type=OpenApiTypes.INT, description= 'Returns only invitations that are in the group related ' 'to the provided value.') ], tags=['Group invitations'], operation_id='list_group_invitations', description= ('Lists all the group invitations of the group related to the provided ' '`group_id` parameter if the authorized user has admin rights to that ' 'group.'), responses={ 200: GroupInvitationSerializer(many=True), 400: get_error_schema([ 'ERROR_USER_NOT_IN_GROUP', 'ERROR_USER_INVALID_GROUP_PERMISSIONS_ERROR' ]), 404: get_error_schema(['ERROR_GROUP_DOES_NOT_EXIST']) }) @map_exceptions({ GroupDoesNotExist: ERROR_GROUP_DOES_NOT_EXIST, UserNotInGroupError: ERROR_USER_NOT_IN_GROUP, UserInvalidGroupPermissionsError: ERROR_USER_INVALID_GROUP_PERMISSIONS_ERROR }) def get(self, request, group_id): """Lists all the invitations of the provided group id.""" group = CoreHandler().get_group(group_id) group.has_user(request.user, 'ADMIN', raise_error=True) group_invitations = GroupInvitation.objects.filter(group=group) serializer = GroupInvitationSerializer(group_invitations, many=True) return Response(serializer.data) @extend_schema( parameters=[ OpenApiParameter( name='group_id', location=OpenApiParameter.PATH, type=OpenApiTypes.INT, description= 'Creates a group invitation to the group related to the ' 'provided value.') ], tags=['Group invitations'], operation_id='create_group_invitation', description= ('Creates a new group invitations for an email address if the authorized ' 'user has admin rights to the related group. An email containing a sign ' 'up link will be send to the user.'), request=CreateGroupInvitationSerializer, responses={ 200: GroupInvitationSerializer, 400: get_error_schema([ 'ERROR_USER_NOT_IN_GROUP', 'ERROR_USER_INVALID_GROUP_PERMISSIONS_ERROR', 'ERROR_REQUEST_BODY_VALIDATION' ]), 404: get_error_schema(['ERROR_GROUP_DOES_NOT_EXIST']) }, ) @transaction.atomic @validate_body(CreateGroupInvitationSerializer) @map_exceptions({ GroupDoesNotExist: ERROR_GROUP_DOES_NOT_EXIST, UserNotInGroupError: ERROR_USER_NOT_IN_GROUP, UserInvalidGroupPermissionsError: ERROR_USER_INVALID_GROUP_PERMISSIONS_ERROR, GroupUserAlreadyExists: ERROR_GROUP_USER_ALREADY_EXISTS, BaseURLHostnameNotAllowed: ERROR_HOSTNAME_IS_NOT_ALLOWED }) def post(self, request, data, group_id): """Creates a new group invitation and sends it the provided email.""" group = CoreHandler().get_group(group_id) group_invitation = CoreHandler().create_group_invitation( request.user, group, **data) return Response(GroupInvitationSerializer(group_invitation).data)
class ViewSortingsView(APIView): permission_classes = (IsAuthenticated,) @extend_schema( parameters=[ OpenApiParameter( name='view_id', location=OpenApiParameter.PATH, type=OpenApiTypes.INT, description='Returns only sortings of the view related to the provided ' 'value.' ) ], tags=['Database table view sortings'], operation_id='list_database_table_view_sortings', description=( 'Lists all sortings of the view related to the provided `view_id` if the ' 'user has access to the related database\'s group. A view can have ' 'multiple sortings. When all the rows are requested they will be in the ' 'desired order.' ), responses={ 200: ViewSortSerializer(many=True), 400: get_error_schema(['ERROR_USER_NOT_IN_GROUP']), 404: get_error_schema(['ERROR_VIEW_DOES_NOT_EXIST']) } ) @map_exceptions({ ViewDoesNotExist: ERROR_VIEW_DOES_NOT_EXIST, UserNotInGroupError: ERROR_USER_NOT_IN_GROUP }) def get(self, request, view_id): """ Responds with a list of serialized sortings that belong to the view if the user has access to that group. """ view = ViewHandler().get_view(request.user, view_id) sortings = ViewSort.objects.filter(view=view) serializer = ViewSortSerializer(sortings, many=True) return Response(serializer.data) @extend_schema( parameters=[ OpenApiParameter( name='view_id', location=OpenApiParameter.PATH, type=OpenApiTypes.INT, description='Creates a sort for the view related to the provided ' 'value.' ) ], tags=['Database table view sortings'], operation_id='create_database_table_view_sort', description=( 'Creates a new sort for the view related to the provided `view_id` ' 'parameter if the authorized user has access to the related database\'s ' 'group. When the rows of a view are requested, for example via the ' '`list_database_table_grid_view_rows` endpoint, they will be returned in ' 'the respected order defined by all the sortings.' ), request=CreateViewSortSerializer(), responses={ 200: ViewSortSerializer(), 400: get_error_schema([ 'ERROR_USER_NOT_IN_GROUP', 'ERROR_REQUEST_BODY_VALIDATION', 'ERROR_VIEW_SORT_NOT_SUPPORTED', 'ERROR_FIELD_NOT_IN_TABLE', 'ERROR_VIEW_SORT_FIELD_ALREADY_EXISTS', 'ERROR_VIEW_SORT_FIELD_NOT_SUPPORTED' ]), 404: get_error_schema(['ERROR_VIEW_DOES_NOT_EXIST']) } ) @transaction.atomic @validate_body(CreateViewSortSerializer) @map_exceptions({ ViewDoesNotExist: ERROR_VIEW_DOES_NOT_EXIST, UserNotInGroupError: ERROR_USER_NOT_IN_GROUP, FieldNotInTable: ERROR_FIELD_NOT_IN_TABLE, ViewSortNotSupported: ERROR_VIEW_SORT_NOT_SUPPORTED, ViewSortFieldAlreadyExist: ERROR_VIEW_SORT_FIELD_ALREADY_EXISTS, ViewSortFieldNotSupported: ERROR_VIEW_SORT_FIELD_NOT_SUPPORTED, }) def post(self, request, data, view_id): """Creates a new sort for the provided view.""" view_handler = ViewHandler() view = view_handler.get_view(request.user, view_id) # We can safely assume the field exists because the CreateViewSortSerializer # has already checked that. field = Field.objects.get(pk=data['field']) view_sort = view_handler.create_sort(request.user, view, field, data['order']) serializer = ViewSortSerializer(view_sort) return Response(serializer.data)
class TokenView(APIView): permission_classes = (IsAuthenticated,) @extend_schema( parameters=[ OpenApiParameter( name='token_id', location=OpenApiParameter.PATH, type=OpenApiTypes.INT, description='Returns the token related to the provided value.' ) ], tags=['Database tokens'], operation_id='get_database_token', description=( 'Returns the requested token if it is owned by the authorized user and' 'if the user has access to the related group.' ), responses={ 200: TokenSerializer, 400: get_error_schema(['ERROR_USER_NOT_IN_GROUP']), 404: get_error_schema(['ERROR_TOKEN_DOES_NOT_EXIST']) } ) @map_exceptions({ TokenDoesNotExist: ERROR_TOKEN_DOES_NOT_EXIST, UserNotInGroupError: ERROR_USER_NOT_IN_GROUP }) def get(self, request, token_id): """Responds with a serialized token instance.""" token = TokenHandler().get_token(request.user, token_id) serializer = TokenSerializer(token) return Response(serializer.data) @extend_schema( parameters=[ OpenApiParameter( name='token_id', location=OpenApiParameter.PATH, type=OpenApiTypes.INT, description='Updates the token related to the provided value.' ) ], tags=['Database tokens'], operation_id='update_database_token', description=( 'Updates the existing token if it is owned by the authorized user and if' 'the user has access to the related group.' ), request=TokenUpdateSerializer, responses={ 200: TokenSerializer, 400: get_error_schema([ 'ERROR_USER_NOT_IN_GROUP', 'ERROR_REQUEST_BODY_VALIDATION', 'ERROR_DATABASE_DOES_NOT_BELONG_TO_GROUP', 'ERROR_TABLE_DOES_NOT_BELONG_TO_GROUP' ]), 404: get_error_schema(['ERROR_TOKEN_DOES_NOT_EXIST']) } ) @transaction.atomic @map_exceptions({ TokenDoesNotExist: ERROR_TOKEN_DOES_NOT_EXIST, UserNotInGroupError: ERROR_USER_NOT_IN_GROUP, DatabaseDoesNotBelongToGroup: ERROR_DATABASE_DOES_NOT_BELONG_TO_GROUP, TableDoesNotBelongToGroup: ERROR_TABLE_DOES_NOT_BELONG_TO_GROUP }) @validate_body(TokenUpdateSerializer) def patch(self, request, data, token_id): """Updates the values of a token.""" token = TokenHandler().get_token(request.user, token_id) permissions = data.pop('permissions', None) rotate_key = data.pop('rotate_key', False) if len(data) > 0: token = TokenHandler().update_token(request.user, token, **data) if permissions: TokenHandler().update_token_permissions(request.user, token, **permissions) if rotate_key: token = TokenHandler().rotate_token_key(request.user, token) serializer = TokenSerializer(token) return Response(serializer.data) @extend_schema( parameters=[ OpenApiParameter( name='token_id', location=OpenApiParameter.PATH, type=OpenApiTypes.INT, description='Deletes the token related to the provided value.' ) ], tags=['Database tokens'], operation_id='delete_database_token', description=( 'Deletes the existing token if it is owned by the authorized user and if' 'the user has access to the related group.' ), responses={ 204: None, 400: get_error_schema(['ERROR_USER_NOT_IN_GROUP']), 404: get_error_schema(['ERROR_TOKEN_DOES_NOT_EXIST']) } ) @transaction.atomic @map_exceptions({ TokenDoesNotExist: ERROR_TOKEN_DOES_NOT_EXIST, UserNotInGroupError: ERROR_USER_NOT_IN_GROUP }) def delete(self, request, token_id): """Deletes an existing token.""" token = TokenHandler().get_token(request.user, token_id) TokenHandler().delete_token(request.user, token) return Response(status=204)
class TableView(APIView): permission_classes = (IsAuthenticated, ) @extend_schema( parameters=[ OpenApiParameter( name='table_id', location=OpenApiParameter.PATH, type=OpenApiTypes.INT, description='Returns the table related to the provided value.') ], tags=['Database tables'], operation_id='get_database_table', description= ('Returns the requested table if the authorized user has access to the ' 'related database\'s group.'), responses={ 200: TableSerializer, 400: get_error_schema(['ERROR_USER_NOT_IN_GROUP']), 404: get_error_schema(['ERROR_TABLE_DOES_NOT_EXIST']) }) @map_exceptions({ TableDoesNotExist: ERROR_TABLE_DOES_NOT_EXIST, UserNotInGroupError: ERROR_USER_NOT_IN_GROUP }) def get(self, request, table_id): """Responds with a serialized table instance.""" table = TableHandler().get_table(request.user, table_id) serializer = TableSerializer(table) return Response(serializer.data) @extend_schema( parameters=[ OpenApiParameter( name='table_id', location=OpenApiParameter.PATH, type=OpenApiTypes.INT, description='Updates the table related to the provided value.') ], tags=['Database tables'], operation_id='update_database_table', description= ('Updates the existing table if the authorized user has access to the ' 'related database\'s group.'), request=TableCreateUpdateSerializer, responses={ 200: TableSerializer, 400: get_error_schema( ['ERROR_USER_NOT_IN_GROUP', 'ERROR_REQUEST_BODY_VALIDATION']), 404: get_error_schema(['ERROR_TABLE_DOES_NOT_EXIST']) }) @transaction.atomic @map_exceptions({ TableDoesNotExist: ERROR_TABLE_DOES_NOT_EXIST, UserNotInGroupError: ERROR_USER_NOT_IN_GROUP }) @validate_body(TableCreateUpdateSerializer) def patch(self, request, data, table_id): """Updates the values a table instance.""" table = TableHandler().update_table(request.user, TableHandler().get_table( request.user, table_id), name=data['name']) serializer = TableSerializer(table) return Response(serializer.data) @extend_schema( parameters=[ OpenApiParameter( name='table_id', location=OpenApiParameter.PATH, type=OpenApiTypes.INT, description='Deletes the table related to the provided value.') ], tags=['Database tables'], operation_id='delete_database_table', description= ('Deletes the existing table if the authorized user has access to the ' 'related database\'s group.'), responses={ 204: None, 400: get_error_schema(['ERROR_USER_NOT_IN_GROUP']), 404: get_error_schema(['ERROR_TABLE_DOES_NOT_EXIST']) }) @transaction.atomic @map_exceptions({ TableDoesNotExist: ERROR_TABLE_DOES_NOT_EXIST, UserNotInGroupError: ERROR_USER_NOT_IN_GROUP }) def delete(self, request, table_id): """Deletes an existing table.""" TableHandler().delete_table( request.user, TableHandler().get_table(request.user, table_id)) return Response(status=204)
class GridViewView(APIView): permission_classes = (IsAuthenticated,) def get_permissions(self): if self.request.method == "GET": return [AllowAny()] return super().get_permissions() @extend_schema( parameters=[ OpenApiParameter( name="view_id", location=OpenApiParameter.PATH, type=OpenApiTypes.INT, description="Returns only rows that belong to the related view's " "table.", ), OpenApiParameter( name="count", location=OpenApiParameter.QUERY, type=OpenApiTypes.NONE, description="If provided only the count will be returned.", ), OpenApiParameter( name="include", location=OpenApiParameter.QUERY, type=OpenApiTypes.STR, description=( "Can contain `field_options` which will add an object with the " "same name to the response if included. That object contains " "user defined view settings for each field. For example the " "field's width is included in here." ), ), OpenApiParameter( name="limit", location=OpenApiParameter.QUERY, type=OpenApiTypes.INT, description="Defines how many rows should be returned.", ), OpenApiParameter( name="offset", location=OpenApiParameter.QUERY, type=OpenApiTypes.INT, description="Can only be used in combination with the `limit` " "parameter and defines from which offset the rows should " "be returned.", ), OpenApiParameter( name="page", location=OpenApiParameter.QUERY, type=OpenApiTypes.INT, description="Defines which page of rows should be returned. Either " "the `page` or `limit` can be provided, not both.", ), OpenApiParameter( name="size", location=OpenApiParameter.QUERY, type=OpenApiTypes.INT, description="Can only be used in combination with the `page` parameter " "and defines how many rows should be returned.", ), OpenApiParameter( name="search", location=OpenApiParameter.QUERY, type=OpenApiTypes.STR, description="If provided only rows with data that matches the search " "query are going to be returned.", ), ], tags=["Database table grid view"], operation_id="list_database_table_grid_view_rows", description=( "Lists the requested rows of the view's table related to the provided " "`view_id` if the authorized user has access to the database's group. " "The response is paginated either by a limit/offset or page/size style. " "The style depends on the provided GET parameters. The properties of the " "returned rows depends on which fields the table has. For a complete " "overview of fields use the **list_database_table_fields** endpoint to " "list them all. In the example all field types are listed, but normally " "the number in field_{id} key is going to be the id of the field. " "The value is what the user has provided and the format of it depends on " "the fields type.\n" "\n" "The filters and sortings are automatically applied. To get a full " "overview of the applied filters and sortings you can use the " "`list_database_table_view_filters` and " "`list_database_table_view_sortings` endpoints." ), responses={ 200: example_pagination_row_serializer_class_with_field_options, 400: get_error_schema(["ERROR_USER_NOT_IN_GROUP"]), 404: get_error_schema(["ERROR_GRID_DOES_NOT_EXIST"]), }, ) @map_exceptions( { UserNotInGroup: ERROR_USER_NOT_IN_GROUP, ViewDoesNotExist: ERROR_GRID_DOES_NOT_EXIST, } ) @allowed_includes("field_options") def get(self, request, view_id, field_options): """ Lists all the rows of a grid view, paginated either by a page or offset/limit. If the limit get parameter is provided the limit/offset pagination will be used else the page number pagination. Optionally the field options can also be included in the response if the the `field_options` are provided in the include GET parameter. """ search = request.GET.get("search") view_handler = ViewHandler() view = view_handler.get_view(view_id, GridView) view.table.database.group.has_user( request.user, raise_error=True, allow_if_template=True ) model = view.table.get_model() queryset = view_handler.get_queryset(view, search, model) if "count" in request.GET: return Response({"count": queryset.count()}) if LimitOffsetPagination.limit_query_param in request.GET: paginator = LimitOffsetPagination() else: paginator = PageNumberPagination() page = paginator.paginate_queryset(queryset, request, self) serializer_class = get_row_serializer_class( model, RowSerializer, is_response=True ) serializer = serializer_class(page, many=True) response = paginator.get_paginated_response(serializer.data) if field_options: # The serializer has the GridViewFieldOptionsField which fetches the # field options from the database and creates them if they don't exist, # but when added to the context the fields don't have to be fetched from # the database again when checking if they exist. context = {"fields": [o["field"] for o in model._field_objects.values()]} serialized_view = GridViewSerializer(view, context=context).data response.data["field_options"] = serialized_view["field_options"] return response @extend_schema( parameters=[ OpenApiParameter( name="view_id", location=OpenApiParameter.PATH, type=OpenApiTypes.INT, required=False, description="Returns only rows that belong to the related view's " "table.", ) ], tags=["Database table grid view"], operation_id="filter_database_table_grid_view_rows", description=( "Lists only the rows and fields that match the request. Only the rows " "with the ids that are in the `row_ids` list are going to be returned. " "Same goes for the fields, only the fields with the ids in the " "`field_ids` are going to be returned. This endpoint could be used to " "refresh data after changes something. For example in the web frontend " "after changing a field type, the data of the related cells will be " "refreshed using this endpoint. In the example all field types are listed, " "but normally the number in field_{id} key is going to be the id of the " "field. The value is what the user has provided and the format of it " "depends on the fields type." ), request=GridViewFilterSerializer, responses={ 200: get_example_row_serializer_class(True)(many=True), 400: get_error_schema( ["ERROR_USER_NOT_IN_GROUP", "ERROR_REQUEST_BODY_VALIDATION"] ), 404: get_error_schema(["ERROR_GRID_DOES_NOT_EXIST"]), }, ) @map_exceptions( { UserNotInGroup: ERROR_USER_NOT_IN_GROUP, ViewDoesNotExist: ERROR_GRID_DOES_NOT_EXIST, } ) @validate_body(GridViewFilterSerializer) def post(self, request, view_id, data): """ Row filter endpoint that only lists the requested rows and optionally only the requested fields. """ view = ViewHandler().get_view(view_id, GridView) view.table.database.group.has_user(request.user, raise_error=True) model = view.table.get_model(field_ids=data["field_ids"]) results = model.objects.filter(pk__in=data["row_ids"]) serializer_class = get_row_serializer_class( model, RowSerializer, is_response=True ) serializer = serializer_class(results, many=True) return Response(serializer.data) @extend_schema( parameters=[ OpenApiParameter( name="view_id", location=OpenApiParameter.PATH, type=OpenApiTypes.INT, required=False, description="Updates the field related to the provided `view_id` " "parameter.", ) ], tags=["Database table grid view"], operation_id="update_database_table_grid_view_field_options", description=( "Updates the field options of a `grid` view. The field options are unique " "options per field for a view. This could for example be used to update " "the field width if the user changes it." ), request=GridViewSerializer, responses={ 200: GridViewSerializer, 400: get_error_schema( [ "ERROR_USER_NOT_IN_GROUP", "ERROR_UNRELATED_FIELD", "ERROR_REQUEST_BODY_VALIDATION", ] ), 404: get_error_schema(["ERROR_GRID_DOES_NOT_EXIST"]), }, ) @map_exceptions( { UserNotInGroup: ERROR_USER_NOT_IN_GROUP, ViewDoesNotExist: ERROR_GRID_DOES_NOT_EXIST, UnrelatedFieldError: ERROR_UNRELATED_FIELD, } ) @validate_body(GridViewSerializer) def patch(self, request, view_id, data): """ Updates the field options for the provided grid view. The following example body data will only update the width of the FIELD_ID and leaves the others untouched. { FIELD_ID: { 'width': 200 } } """ handler = ViewHandler() view = handler.get_view(view_id, GridView) handler.update_grid_view_field_options( request.user, view, data["field_options"] ) return Response(GridViewSerializer(view).data)