mirror of
https://gitlab.com/bramw/baserow.git
synced 2025-04-11 07:51:20 +00:00
Resolve "Create a configurable field limit"
This commit is contained in:
parent
ed34899093
commit
ffa6e8a1ee
13 changed files with 111 additions and 1 deletions
backend
src/baserow
config/settings
contrib/database
tests/baserow/contrib/database
api
field
table
web-frontend/modules/core/plugins
|
@ -265,3 +265,5 @@ APPLICATION_TEMPLATES_DIR = os.path.join(BASE_DIR, "../../../templates")
|
|||
# The template that must be selected when the user first opens the templates select
|
||||
# modal.
|
||||
DEFAULT_APPLICATION_TEMPLATE = "project-management"
|
||||
|
||||
MAX_FIELD_LIMIT = 1500
|
||||
|
|
|
@ -40,3 +40,5 @@ ERROR_INCOMPATIBLE_PRIMARY_FIELD_TYPE = (
|
|||
HTTP_400_BAD_REQUEST,
|
||||
"The field type {e.field_type} is not compatible with the primary field.",
|
||||
)
|
||||
|
||||
ERROR_MAX_FIELD_COUNT_EXCEEDED = "ERROR_MAX_FIELD_COUNT_EXCEEDED"
|
||||
|
|
|
@ -21,11 +21,13 @@ from baserow.contrib.database.api.fields.errors import (
|
|||
ERROR_CANNOT_DELETE_PRIMARY_FIELD,
|
||||
ERROR_CANNOT_CHANGE_FIELD_TYPE,
|
||||
ERROR_FIELD_DOES_NOT_EXIST,
|
||||
ERROR_MAX_FIELD_COUNT_EXCEEDED,
|
||||
)
|
||||
from baserow.contrib.database.fields.exceptions import (
|
||||
CannotDeletePrimaryField,
|
||||
CannotChangeFieldType,
|
||||
FieldDoesNotExist,
|
||||
MaxFieldLimitExceeded,
|
||||
)
|
||||
from baserow.contrib.database.fields.models import Field
|
||||
from baserow.contrib.database.fields.handler import FieldHandler
|
||||
|
@ -122,7 +124,11 @@ class FieldsView(APIView):
|
|||
field_type_registry, FieldSerializer
|
||||
),
|
||||
400: get_error_schema(
|
||||
["ERROR_USER_NOT_IN_GROUP", "ERROR_REQUEST_BODY_VALIDATION"]
|
||||
[
|
||||
"ERROR_USER_NOT_IN_GROUP",
|
||||
"ERROR_REQUEST_BODY_VALIDATION",
|
||||
"ERROR_MAX_FIELD_COUNT_EXCEEDED",
|
||||
]
|
||||
),
|
||||
404: get_error_schema(["ERROR_TABLE_DOES_NOT_EXIST"]),
|
||||
},
|
||||
|
@ -135,6 +141,7 @@ class FieldsView(APIView):
|
|||
{
|
||||
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):
|
||||
|
|
|
@ -13,6 +13,8 @@ from baserow.api.schemas import get_error_schema
|
|||
from baserow.api.applications.errors import ERROR_APPLICATION_DOES_NOT_EXIST
|
||||
from baserow.core.exceptions import UserNotInGroup, ApplicationDoesNotExist
|
||||
from baserow.core.handler import CoreHandler
|
||||
from baserow.contrib.database.api.fields.errors import ERROR_MAX_FIELD_COUNT_EXCEEDED
|
||||
from baserow.contrib.database.fields.exceptions import MaxFieldLimitExceeded
|
||||
from baserow.contrib.database.models import Database
|
||||
from baserow.contrib.database.table.models import Table
|
||||
from baserow.contrib.database.table.handler import TableHandler
|
||||
|
@ -114,6 +116,7 @@ class TablesView(APIView):
|
|||
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)
|
||||
|
|
|
@ -45,6 +45,10 @@ class LinkRowTableNotInSameDatabase(Exception):
|
|||
"""
|
||||
|
||||
|
||||
class MaxFieldLimitExceeded(Exception):
|
||||
""" Raised when the field count exceeds the limit"""
|
||||
|
||||
|
||||
class OrderByFieldNotFound(Exception):
|
||||
"""Raised when the field was not found in the table."""
|
||||
|
||||
|
|
|
@ -14,6 +14,7 @@ from .exceptions import (
|
|||
CannotChangeFieldType,
|
||||
FieldDoesNotExist,
|
||||
IncompatiblePrimaryFieldTypeError,
|
||||
MaxFieldLimitExceeded,
|
||||
)
|
||||
from .models import Field, SelectOption
|
||||
from .registries import field_type_registry, field_converter_registry
|
||||
|
@ -80,6 +81,8 @@ class FieldHandler:
|
|||
:type kwargs: object
|
||||
:raises PrimaryFieldAlreadyExists: When we try to create a primary field,
|
||||
but one already exists.
|
||||
:raises MaxFieldLimitExceeded: When we try to create a field,
|
||||
but exceeds the field limit.
|
||||
:return: The created field instance.
|
||||
:rtype: Field
|
||||
"""
|
||||
|
@ -102,6 +105,12 @@ class FieldHandler:
|
|||
field_values = extract_allowed(kwargs, allowed_fields)
|
||||
last_order = model_class.get_last_order(table)
|
||||
|
||||
num_fields = table.field_set.count()
|
||||
if (num_fields + 1) > settings.MAX_FIELD_LIMIT:
|
||||
raise MaxFieldLimitExceeded(
|
||||
f"Fields count exceeds the limit of {settings.MAX_FIELD_LIMIT}"
|
||||
)
|
||||
|
||||
field_values = field_type.prepare_values(field_values, user)
|
||||
before = field_type.before_create(
|
||||
table, primary, field_values, last_order, user
|
||||
|
|
|
@ -6,6 +6,7 @@ from baserow.contrib.database.fields.models import TextField
|
|||
from baserow.contrib.database.views.handler import ViewHandler
|
||||
from baserow.contrib.database.views.view_types import GridViewType
|
||||
from baserow.contrib.database.fields.handler import FieldHandler
|
||||
from baserow.contrib.database.fields.exceptions import MaxFieldLimitExceeded
|
||||
from baserow.contrib.database.fields.field_types import (
|
||||
LongTextFieldType,
|
||||
BooleanFieldType,
|
||||
|
@ -73,6 +74,8 @@ class TableHandler:
|
|||
:type first_row_header: bool
|
||||
:param kwargs: The fields that need to be set upon creation.
|
||||
:type kwargs: object
|
||||
:raises MaxFieldLimitExceeded: When the data contains more columns
|
||||
than the field limit.
|
||||
:return: The created table instance.
|
||||
:rtype: Table
|
||||
"""
|
||||
|
@ -81,6 +84,10 @@ class TableHandler:
|
|||
|
||||
if data is not None:
|
||||
fields, data = self.normalize_initial_table_data(data, first_row_header)
|
||||
if len(fields) > settings.MAX_FIELD_LIMIT:
|
||||
raise MaxFieldLimitExceeded(
|
||||
f"Fields count exceeds the limit of {settings.MAX_FIELD_LIMIT}"
|
||||
)
|
||||
|
||||
table_values = extract_allowed(kwargs, ["name"])
|
||||
last_order = Table.get_last_order(database)
|
||||
|
|
|
@ -8,6 +8,7 @@ from rest_framework.status import (
|
|||
)
|
||||
|
||||
from django.shortcuts import reverse
|
||||
from django.conf import settings
|
||||
|
||||
from baserow.contrib.database.fields.models import Field, TextField, NumberField
|
||||
|
||||
|
@ -109,6 +110,18 @@ def test_create_field(api_client, data_fixture):
|
|||
assert response.status_code == HTTP_400_BAD_REQUEST
|
||||
assert response.json()["error"] == "ERROR_USER_NOT_IN_GROUP"
|
||||
|
||||
field_limit = settings.MAX_FIELD_LIMIT
|
||||
settings.MAX_FIELD_LIMIT = 0
|
||||
response = api_client.post(
|
||||
reverse("api:database:fields:list", kwargs={"table_id": table.id}),
|
||||
{"name": "Test 1", "type": "text"},
|
||||
format="json",
|
||||
HTTP_AUTHORIZATION=f"JWT {token}",
|
||||
)
|
||||
assert response.status_code == HTTP_400_BAD_REQUEST
|
||||
assert response.json()["error"] == "ERROR_MAX_FIELD_COUNT_EXCEEDED"
|
||||
settings.MAX_FIELD_LIMIT = field_limit
|
||||
|
||||
url = reverse("api:database:fields:list", kwargs={"table_id": table_2.id})
|
||||
response = api_client.get(url)
|
||||
assert response.status_code == HTTP_401_UNAUTHORIZED
|
||||
|
|
|
@ -122,6 +122,20 @@ def test_create_table_with_data(api_client, data_fixture):
|
|||
assert response_json["error"] == "ERROR_INITIAL_TABLE_DATA_LIMIT_EXCEEDED"
|
||||
settings.INITIAL_TABLE_DATA_LIMIT = limit
|
||||
|
||||
field_limit = settings.MAX_FIELD_LIMIT
|
||||
settings.MAX_FIELD_LIMIT = 2
|
||||
url = reverse("api:database:tables:list", kwargs={"database_id": database.id})
|
||||
response = api_client.post(
|
||||
url,
|
||||
{"name": "Test 1", "data": [["fields"] * 3, ["rows"] * 3]},
|
||||
format="json",
|
||||
HTTP_AUTHORIZATION=f"JWT {token}",
|
||||
)
|
||||
response_json = response.json()
|
||||
assert response.status_code == HTTP_400_BAD_REQUEST
|
||||
assert response_json["error"] == "ERROR_MAX_FIELD_COUNT_EXCEEDED"
|
||||
settings.MAX_FIELD_LIMIT = field_limit
|
||||
|
||||
url = reverse("api:database:tables:list", kwargs={"database_id": database.id})
|
||||
response = api_client.post(
|
||||
url,
|
||||
|
|
|
@ -3,6 +3,7 @@ from decimal import Decimal
|
|||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
from django.conf import settings
|
||||
from django.db import models
|
||||
from faker import Faker
|
||||
|
||||
|
@ -13,6 +14,7 @@ from baserow.contrib.database.fields.exceptions import (
|
|||
FieldDoesNotExist,
|
||||
IncompatiblePrimaryFieldTypeError,
|
||||
CannotChangeFieldType,
|
||||
MaxFieldLimitExceeded,
|
||||
)
|
||||
from baserow.contrib.database.fields.field_helpers import (
|
||||
construct_all_possible_field_kwargs,
|
||||
|
@ -224,6 +226,20 @@ def test_create_field(send_mock, data_fixture):
|
|||
assert NumberField.objects.all().count() == 1
|
||||
assert BooleanField.objects.all().count() == 1
|
||||
|
||||
field_limit = settings.MAX_FIELD_LIMIT
|
||||
settings.MAX_FIELD_LIMIT = 2
|
||||
|
||||
with pytest.raises(MaxFieldLimitExceeded):
|
||||
handler.create_field(
|
||||
user=user,
|
||||
table=table,
|
||||
type_name="text",
|
||||
name="Test text field",
|
||||
text_default="Some default",
|
||||
)
|
||||
|
||||
settings.MAX_FIELD_LIMIT = field_limit
|
||||
|
||||
with pytest.raises(UserNotInGroup):
|
||||
handler.create_field(user=user_2, table=table, type_name="text")
|
||||
|
||||
|
|
|
@ -6,6 +6,7 @@ from django.conf import settings
|
|||
from decimal import Decimal
|
||||
|
||||
from baserow.core.exceptions import UserNotInGroup
|
||||
from baserow.contrib.database.fields.exceptions import MaxFieldLimitExceeded
|
||||
from baserow.contrib.database.table.models import Table
|
||||
from baserow.contrib.database.table.handler import TableHandler
|
||||
from baserow.contrib.database.table.exceptions import (
|
||||
|
@ -131,6 +132,16 @@ def test_fill_table_with_initial_data(data_fixture):
|
|||
|
||||
settings.INITIAL_TABLE_DATA_LIMIT = limit
|
||||
|
||||
field_limit = settings.MAX_FIELD_LIMIT
|
||||
settings.MAX_FIELD_LIMIT = 2
|
||||
|
||||
with pytest.raises(MaxFieldLimitExceeded):
|
||||
table_handler.create_table(
|
||||
user, database, name="Table 1", data=[["fields"] * 3, ["rows"] * 3]
|
||||
)
|
||||
|
||||
settings.MAX_FIELD_LIMIT = field_limit
|
||||
|
||||
data = [
|
||||
["A", "B", "C", "D"],
|
||||
["1-1", "1-2", "1-3", "1-4", "1-5"],
|
||||
|
@ -205,6 +216,22 @@ def test_fill_table_with_initial_data(data_fixture):
|
|||
assert getattr(results[2], f"field_{text_fields[0].id}") == "3-1"
|
||||
assert getattr(results[2], f"field_{text_fields[1].id}") == "3-2"
|
||||
|
||||
field_limit = settings.MAX_FIELD_LIMIT
|
||||
settings.MAX_FIELD_LIMIT = 5
|
||||
data = [
|
||||
["A", "B", "C", "D", "E"],
|
||||
["1-1", "1-2", "1-3", "1-4", "1-5"],
|
||||
]
|
||||
table = table_handler.create_table(
|
||||
user, database, name="Table 3", data=data, first_row_header=True
|
||||
)
|
||||
num_fields = table.field_set.count()
|
||||
|
||||
assert GridView.objects.all().count() == 3
|
||||
assert num_fields == settings.MAX_FIELD_LIMIT
|
||||
|
||||
settings.MAX_FIELD_LIMIT = field_limit
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@patch("baserow.contrib.database.table.signals.table_updated.send")
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
|
||||
# Changelog
|
||||
|
||||
## Unreleased
|
||||
|
||||
* Added configurable field limit.
|
||||
* Fixed memory leak in the `link_row` field.
|
||||
* Switch to using a celery based email backend by default.
|
||||
* Added `--add-columns` flag to the `fill_table` management command. It creates all the
|
||||
|
|
|
@ -56,6 +56,10 @@ class ErrorHandler {
|
|||
'Invalid URL',
|
||||
'The provided file URL is invalid or not allowed.'
|
||||
),
|
||||
ERROR_MAX_FIELD_COUNT_EXCEEDED: new ResponseErrorMessage(
|
||||
"Couldn't create field.",
|
||||
"The action couldn't be completed because the field count exceeds the limit"
|
||||
),
|
||||
}
|
||||
|
||||
// A temporary notFoundMap containing the error messages for when the
|
||||
|
|
Loading…
Add table
Reference in a new issue