mirror of
https://gitlab.com/bramw/baserow.git
synced 2025-05-13 04:41:43 +00:00
Resolve "Allow changing the primary field"
This commit is contained in:
parent
f931be08a1
commit
36890975e7
27 changed files with 892 additions and 63 deletions
backend
src/baserow
contrib
core/locale/en/LC_MESSAGES
tests/baserow/contrib/database
changelog/entries/unreleased/feature
web-frontend
|
@ -8,7 +8,7 @@ msgid ""
|
|||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2024-06-20 05:25+0000\n"
|
||||
"POT-Creation-Date: 2024-07-16 13:47+0000\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
|
@ -46,13 +46,18 @@ msgstr ""
|
|||
msgid "Last name"
|
||||
msgstr ""
|
||||
|
||||
#: src/baserow/contrib/builder/data_providers/data_provider_types.py:342
|
||||
#, python-format
|
||||
msgid "%(user_source_name)s member"
|
||||
msgstr ""
|
||||
|
||||
#: src/baserow/contrib/builder/data_sources/service.py:128
|
||||
msgid "Data source"
|
||||
msgstr ""
|
||||
|
||||
#: src/baserow/contrib/builder/elements/mixins.py:289
|
||||
#: src/baserow/contrib/builder/elements/mixins.py:294
|
||||
#: src/baserow/contrib/builder/elements/mixins.py:299
|
||||
#: src/baserow/contrib/builder/elements/mixins.py:309
|
||||
#: src/baserow/contrib/builder/elements/mixins.py:314
|
||||
#: src/baserow/contrib/builder/elements/mixins.py:319
|
||||
#, python-format
|
||||
msgid "Column %(count)s"
|
||||
msgstr ""
|
||||
|
|
|
@ -230,7 +230,7 @@ class AirtableHandler:
|
|||
baserow_field.name = column["name"]
|
||||
baserow_field.order = order
|
||||
baserow_field.primary = (
|
||||
baserow_field_type.can_be_primary_field
|
||||
baserow_field_type.can_be_primary_field(baserow_field)
|
||||
and table["primaryColumnId"] == column["id"]
|
||||
)
|
||||
|
||||
|
@ -439,7 +439,7 @@ class AirtableHandler:
|
|||
for value in field_mapping.values():
|
||||
if field_type_registry.get_by_model(
|
||||
value["baserow_field"]
|
||||
).can_be_primary_field:
|
||||
).can_be_primary_field(value["baserow_field"]):
|
||||
value["baserow_field"].primary = True
|
||||
found_existing_field = True
|
||||
break
|
||||
|
|
|
@ -152,3 +152,13 @@ ERROR_DATE_FORCE_TIMEZONE_OFFSET_ERROR = (
|
|||
"must be set to True on the field to convert values with "
|
||||
"the utc_offset provided in date_force_timezone_offset.",
|
||||
)
|
||||
ERROR_FIELD_IS_ALREADY_PRIMARY = (
|
||||
"ERROR_FIELD_IS_ALREADY_PRIMARY",
|
||||
HTTP_400_BAD_REQUEST,
|
||||
"The provided field is already the primary field.",
|
||||
)
|
||||
ERROR_TABLE_HAS_NO_PRIMARY_FIELD = (
|
||||
"ERROR_TABLE_HAS_NO_PRIMARY_FIELD",
|
||||
HTTP_400_BAD_REQUEST,
|
||||
"The provided table does not have a primary field.",
|
||||
)
|
||||
|
|
|
@ -308,6 +308,12 @@ class DuplicateFieldParamsSerializer(serializers.Serializer):
|
|||
)
|
||||
|
||||
|
||||
class ChangePrimaryFieldParamsSerializer(serializers.Serializer):
|
||||
new_primary_field_id = serializers.IntegerField(
|
||||
help_text="The ID of the new primary field."
|
||||
)
|
||||
|
||||
|
||||
class ListOrStringField(serializers.ListField):
|
||||
"""
|
||||
A serializer field that accept a List or a CSV string that will be converted to
|
||||
|
|
|
@ -4,6 +4,7 @@ from baserow.contrib.database.fields.registries import field_type_registry
|
|||
|
||||
from .views import (
|
||||
AsyncDuplicateFieldView,
|
||||
ChangePrimaryFieldView,
|
||||
FieldsView,
|
||||
FieldView,
|
||||
UniqueRowValueFieldView,
|
||||
|
@ -13,6 +14,11 @@ app_name = "baserow.contrib.database.api.fields"
|
|||
|
||||
urlpatterns = field_type_registry.api_urls + [
|
||||
re_path(r"table/(?P<table_id>[0-9]+)/$", FieldsView.as_view(), name="list"),
|
||||
re_path(
|
||||
r"table/(?P<table_id>[0-9]+)/change-primary-field/$",
|
||||
ChangePrimaryFieldView.as_view(),
|
||||
name="change_primary_field",
|
||||
),
|
||||
re_path(
|
||||
r"(?P<field_id>[0-9]+)/unique_row_values/$",
|
||||
UniqueRowValueFieldView.as_view(),
|
||||
|
|
|
@ -39,12 +39,16 @@ from baserow.contrib.database.api.fields.errors import (
|
|||
ERROR_FAILED_TO_LOCK_FIELD_DUE_TO_CONFLICT,
|
||||
ERROR_FIELD_CIRCULAR_REFERENCE,
|
||||
ERROR_FIELD_DOES_NOT_EXIST,
|
||||
ERROR_FIELD_IS_ALREADY_PRIMARY,
|
||||
ERROR_FIELD_NOT_IN_TABLE,
|
||||
ERROR_FIELD_SELF_REFERENCE,
|
||||
ERROR_FIELD_WITH_SAME_NAME_ALREADY_EXISTS,
|
||||
ERROR_INCOMPATIBLE_FIELD_TYPE_FOR_UNIQUE_VALUES,
|
||||
ERROR_INCOMPATIBLE_PRIMARY_FIELD_TYPE,
|
||||
ERROR_INVALID_BASEROW_FIELD_NAME,
|
||||
ERROR_MAX_FIELD_COUNT_EXCEEDED,
|
||||
ERROR_RESERVED_BASEROW_FIELD_NAME,
|
||||
ERROR_TABLE_HAS_NO_PRIMARY_FIELD,
|
||||
)
|
||||
from baserow.contrib.database.api.tables.errors import (
|
||||
ERROR_FAILED_TO_LOCK_TABLE_DUE_TO_CONFLICT,
|
||||
|
@ -53,6 +57,7 @@ from baserow.contrib.database.api.tables.errors import (
|
|||
from baserow.contrib.database.api.tokens.authentications import TokenAuthentication
|
||||
from baserow.contrib.database.api.tokens.errors import ERROR_NO_PERMISSION_TO_TABLE
|
||||
from baserow.contrib.database.fields.actions import (
|
||||
ChangePrimaryFieldActionType,
|
||||
CreateFieldActionType,
|
||||
DeleteFieldActionType,
|
||||
UpdateFieldActionType,
|
||||
|
@ -67,11 +72,15 @@ from baserow.contrib.database.fields.exceptions import (
|
|||
CannotDeletePrimaryField,
|
||||
FailedToLockFieldDueToConflict,
|
||||
FieldDoesNotExist,
|
||||
FieldIsAlreadyPrimary,
|
||||
FieldNotInTable,
|
||||
FieldWithSameNameAlreadyExists,
|
||||
IncompatibleFieldTypeForUniqueValues,
|
||||
IncompatiblePrimaryFieldTypeError,
|
||||
InvalidBaserowFieldName,
|
||||
MaxFieldLimitExceeded,
|
||||
ReservedBaserowFieldNameException,
|
||||
TableHasNoPrimaryField,
|
||||
)
|
||||
from baserow.contrib.database.fields.handler import FieldHandler
|
||||
from baserow.contrib.database.fields.job_types import DuplicateFieldJobType
|
||||
|
@ -99,6 +108,7 @@ from baserow.core.jobs.registries import job_type_registry
|
|||
from baserow.core.trash.exceptions import CannotDeleteAlreadyDeletedItem
|
||||
|
||||
from .serializers import (
|
||||
ChangePrimaryFieldParamsSerializer,
|
||||
CreateFieldSerializer,
|
||||
DuplicateFieldParamsSerializer,
|
||||
FieldSerializer,
|
||||
|
@ -600,3 +610,79 @@ class AsyncDuplicateFieldView(APIView):
|
|||
|
||||
serializer = job_type_registry.get_serializer(job, JobSerializer)
|
||||
return Response(serializer.data, status=status.HTTP_202_ACCEPTED)
|
||||
|
||||
|
||||
class ChangePrimaryFieldView(APIView):
|
||||
permission_classes = (IsAuthenticated,)
|
||||
|
||||
@extend_schema(
|
||||
parameters=[
|
||||
OpenApiParameter(
|
||||
name="table_id",
|
||||
location=OpenApiParameter.PATH,
|
||||
type=OpenApiTypes.INT,
|
||||
description="The table where to update the primary field in.",
|
||||
),
|
||||
CLIENT_SESSION_ID_SCHEMA_PARAMETER,
|
||||
CLIENT_UNDO_REDO_ACTION_GROUP_ID_SCHEMA_PARAMETER,
|
||||
],
|
||||
tags=["Database table fields"],
|
||||
operation_id="change_primary_field",
|
||||
description=(
|
||||
"Changes the primary field of a table to the one provided in the body "
|
||||
"payload."
|
||||
),
|
||||
request=ChangePrimaryFieldParamsSerializer,
|
||||
responses={
|
||||
200: DiscriminatorCustomFieldsMappingSerializer(
|
||||
field_type_registry, FieldSerializer
|
||||
),
|
||||
400: get_error_schema(
|
||||
[
|
||||
"ERROR_USER_NOT_IN_GROUP",
|
||||
"ERROR_REQUEST_BODY_VALIDATION",
|
||||
"ERROR_USER_NOT_IN_GROUP",
|
||||
"ERROR_FIELD_IS_ALREADY_PRIMARY",
|
||||
"ERROR_FIELD_NOT_IN_TABLE",
|
||||
"ERROR_INCOMPATIBLE_PRIMARY_FIELD_TYPE",
|
||||
"ERROR_TABLE_HAS_NO_PRIMARY_FIELD",
|
||||
]
|
||||
),
|
||||
404: get_error_schema(
|
||||
["ERROR_TABLE_DOES_NOT_EXIST", "ERROR_FIELD_DOES_NOT_EXIST"]
|
||||
),
|
||||
},
|
||||
)
|
||||
@transaction.atomic
|
||||
@map_exceptions(
|
||||
{
|
||||
TableDoesNotExist: ERROR_TABLE_DOES_NOT_EXIST,
|
||||
FieldDoesNotExist: ERROR_FIELD_DOES_NOT_EXIST,
|
||||
UserNotInWorkspace: ERROR_USER_NOT_IN_GROUP,
|
||||
FieldIsAlreadyPrimary: ERROR_FIELD_IS_ALREADY_PRIMARY,
|
||||
FieldNotInTable: ERROR_FIELD_NOT_IN_TABLE,
|
||||
IncompatiblePrimaryFieldTypeError: ERROR_INCOMPATIBLE_PRIMARY_FIELD_TYPE,
|
||||
TableHasNoPrimaryField: ERROR_TABLE_HAS_NO_PRIMARY_FIELD,
|
||||
}
|
||||
)
|
||||
@validate_body(ChangePrimaryFieldParamsSerializer)
|
||||
def post(self, request: Request, table_id: int, data: Dict[str, Any]) -> Response:
|
||||
"""Changes the primary field of the given table."""
|
||||
|
||||
# Get the table for update because we want to prevent other fields being
|
||||
# changed to the primary field.
|
||||
table = TableHandler().get_table_for_update(table_id)
|
||||
new_primary_field = FieldHandler().get_specific_field_for_update(
|
||||
data["new_primary_field_id"]
|
||||
)
|
||||
|
||||
new_primary_field, old_primary_field = action_type_registry.get_by_type(
|
||||
ChangePrimaryFieldActionType
|
||||
).do(request.user, table, new_primary_field)
|
||||
|
||||
serializer = field_type_registry.get_serializer(
|
||||
new_primary_field,
|
||||
FieldSerializerWithRelatedFields,
|
||||
related_fields=[old_primary_field],
|
||||
)
|
||||
return Response(serializer.data)
|
||||
|
|
|
@ -246,6 +246,7 @@ class DatabaseConfig(AppConfig):
|
|||
field_converter_registry.register(PasswordFieldConverter())
|
||||
|
||||
from .fields.actions import (
|
||||
ChangePrimaryFieldActionType,
|
||||
CreateFieldActionType,
|
||||
DeleteFieldActionType,
|
||||
DuplicateFieldActionType,
|
||||
|
@ -256,6 +257,7 @@ class DatabaseConfig(AppConfig):
|
|||
action_type_registry.register(DeleteFieldActionType())
|
||||
action_type_registry.register(UpdateFieldActionType())
|
||||
action_type_registry.register(DuplicateFieldActionType())
|
||||
action_type_registry.register(ChangePrimaryFieldActionType())
|
||||
|
||||
from .views.view_types import FormViewType, GalleryViewType, GridViewType
|
||||
|
||||
|
|
|
@ -17,6 +17,7 @@ from baserow.contrib.database.fields.backup_handler import (
|
|||
from baserow.contrib.database.fields.handler import FieldHandler
|
||||
from baserow.contrib.database.fields.models import Field, SpecificFieldForUpdate
|
||||
from baserow.contrib.database.fields.registries import field_type_registry
|
||||
from baserow.contrib.database.table.handler import TableHandler
|
||||
from baserow.contrib.database.table.models import Table
|
||||
from baserow.core.action.models import Action
|
||||
from baserow.core.action.registries import (
|
||||
|
@ -583,3 +584,84 @@ class DuplicateFieldActionType(UndoableActionType):
|
|||
TrashHandler.restore_item(
|
||||
user, "field", params.field_id, parent_trash_item_id=None
|
||||
)
|
||||
|
||||
|
||||
class ChangePrimaryFieldActionType(UndoableActionType):
|
||||
type = "change_primary_field"
|
||||
description = ActionTypeDescription(
|
||||
_("Change primary field"),
|
||||
_(
|
||||
"Primary field of table %(table_name)s was changed to "
|
||||
"%(new_primary_field_name)s"
|
||||
),
|
||||
TABLE_ACTION_CONTEXT,
|
||||
)
|
||||
analytics_params = [
|
||||
"table_id",
|
||||
"new_primary_field_id",
|
||||
]
|
||||
|
||||
@dataclasses.dataclass
|
||||
class Params:
|
||||
database_id: int
|
||||
database_name: str
|
||||
table_id: int
|
||||
table_name: str
|
||||
new_primary_field_id: int
|
||||
new_primary_field_name: str
|
||||
old_primary_field_id: int
|
||||
old_primary_field_name: str
|
||||
|
||||
@classmethod
|
||||
def do(
|
||||
cls,
|
||||
user: AbstractUser,
|
||||
table: Table,
|
||||
new_primary_field: Field,
|
||||
) -> Tuple[Field, Field]:
|
||||
"""
|
||||
Change the primary field of the provided table.
|
||||
|
||||
:param user: The user on whose behalf the duplicated field will be
|
||||
changed.
|
||||
:param table: The table where to change the primary field in.
|
||||
:param new_primary_field: The field that must be changed to the primary field.
|
||||
:return: The updated field object.
|
||||
"""
|
||||
|
||||
new_primary_field, old_primary_field = FieldHandler().change_primary_field(
|
||||
user, table, new_primary_field
|
||||
)
|
||||
params = cls.Params(
|
||||
table.database_id,
|
||||
table.database.name,
|
||||
table.id,
|
||||
table.name,
|
||||
new_primary_field.id,
|
||||
new_primary_field.name,
|
||||
old_primary_field.id,
|
||||
old_primary_field.name,
|
||||
)
|
||||
workspace = table.database.workspace
|
||||
cls.register_action(user, params, cls.scope(table.id), workspace)
|
||||
return new_primary_field, old_primary_field
|
||||
|
||||
@classmethod
|
||||
def scope(cls, table_id) -> ActionScopeStr:
|
||||
return TableActionScopeType.value(table_id)
|
||||
|
||||
@classmethod
|
||||
def undo(cls, user: AbstractUser, params: Params, action_being_undone: Action):
|
||||
table = TableHandler().get_table_for_update(params.table_id)
|
||||
field = FieldHandler().get_specific_field_for_update(
|
||||
params.old_primary_field_id
|
||||
)
|
||||
FieldHandler().change_primary_field(user, table, field)
|
||||
|
||||
@classmethod
|
||||
def redo(cls, user: AbstractUser, params: Params, action_being_redone: Action):
|
||||
table = TableHandler().get_table_for_update(params.table_id)
|
||||
field = FieldHandler().get_specific_field_for_update(
|
||||
params.new_primary_field_id
|
||||
)
|
||||
FieldHandler().change_primary_field(user, table, field)
|
||||
|
|
|
@ -264,3 +264,15 @@ class RichTextFieldCannotBePrimaryField(Exception):
|
|||
"""
|
||||
Raised when a rich text field is attempted to be set as the primary field.
|
||||
"""
|
||||
|
||||
|
||||
class FieldIsAlreadyPrimary(Exception):
|
||||
"""
|
||||
Raised when the new primary field is already the primary field.
|
||||
"""
|
||||
|
||||
|
||||
class TableHasNoPrimaryField(Exception):
|
||||
"""
|
||||
Raised when the table doesn't have a primary field.
|
||||
"""
|
||||
|
|
|
@ -150,7 +150,6 @@ from .exceptions import (
|
|||
InvalidRollupThroughField,
|
||||
LinkRowTableNotInSameDatabase,
|
||||
LinkRowTableNotProvided,
|
||||
RichTextFieldCannotBePrimaryField,
|
||||
SelfReferencingLinkRowCannotHaveRelatedField,
|
||||
)
|
||||
from .expressions import extract_jsonb_array_values_to_single_string
|
||||
|
@ -429,24 +428,14 @@ class LongTextFieldType(CollationSortMixin, FieldType):
|
|||
def check_can_group_by(self, field: Field) -> bool:
|
||||
return not field.long_text_enable_rich_text
|
||||
|
||||
def before_create(
|
||||
self, table, primary, allowed_field_values, order, user, field_kwargs
|
||||
):
|
||||
# Disallow rich text fields from being primary fields as they can be difficult
|
||||
# to render in some email notifications, as linked rows and possibly other
|
||||
# places.
|
||||
if primary and field_kwargs.get("long_text_enable_rich_text", False):
|
||||
raise RichTextFieldCannotBePrimaryField(
|
||||
"A rich text field cannot be the primary field."
|
||||
)
|
||||
|
||||
def before_update(self, from_field, to_field_values, user, field_kwargs):
|
||||
if from_field.primary and to_field_values.get(
|
||||
"long_text_enable_rich_text", False
|
||||
):
|
||||
raise RichTextFieldCannotBePrimaryField(
|
||||
"A rich text field cannot be the primary field."
|
||||
def can_be_primary_field(self, field_or_values: Union[Field, dict]) -> bool:
|
||||
if isinstance(field_or_values, dict):
|
||||
enable_rich_text = field_or_values.get("long_text_enable_rich_text", False)
|
||||
else:
|
||||
enable_rich_text = getattr(
|
||||
field_or_values, "long_text_enable_rich_text", False
|
||||
)
|
||||
return enable_rich_text is False
|
||||
|
||||
def get_serializer_field(self, instance, **kwargs):
|
||||
required = kwargs.get("required", False)
|
||||
|
@ -1995,7 +1984,7 @@ class LinkRowFieldType(ManyToManyFieldTypeSerializeToInputValueMixin, FieldType)
|
|||
ViewNotInTable: ERROR_VIEW_NOT_IN_TABLE,
|
||||
}
|
||||
_can_order_by = False
|
||||
can_be_primary_field = False
|
||||
_can_be_primary_field = False
|
||||
can_get_unique_values = False
|
||||
is_many_to_many_field = True
|
||||
|
||||
|
@ -6094,7 +6083,7 @@ class PasswordFieldType(FieldType):
|
|||
can_be_in_form_view = True
|
||||
keep_data_on_duplication = True
|
||||
_can_order_by = False
|
||||
can_be_primary_field = False
|
||||
_can_be_primary_field = False
|
||||
can_get_unique_values = False
|
||||
|
||||
def get_serializer_field(self, instance, **kwargs):
|
||||
|
|
|
@ -73,6 +73,8 @@ from .exceptions import (
|
|||
CannotDeletePrimaryField,
|
||||
FailedToLockFieldDueToConflict,
|
||||
FieldDoesNotExist,
|
||||
FieldIsAlreadyPrimary,
|
||||
FieldNotInTable,
|
||||
FieldWithSameNameAlreadyExists,
|
||||
IncompatibleFieldTypeForUniqueValues,
|
||||
IncompatiblePrimaryFieldTypeError,
|
||||
|
@ -81,6 +83,7 @@ from .exceptions import (
|
|||
MaxFieldNameLengthExceeded,
|
||||
PrimaryFieldAlreadyExists,
|
||||
ReservedBaserowFieldNameException,
|
||||
TableHasNoPrimaryField,
|
||||
)
|
||||
from .field_cache import FieldCache
|
||||
from .models import Field, SelectOption, SpecificFieldForUpdate
|
||||
|
@ -343,6 +346,9 @@ class FieldHandler(metaclass=baserow_trace_methods(tracer)):
|
|||
field_values = extract_allowed(kwargs, allowed_fields)
|
||||
last_order = model_class.get_last_order(table)
|
||||
|
||||
if primary and not field_type.can_be_primary_field(field_values):
|
||||
raise IncompatiblePrimaryFieldTypeError(field_type.type)
|
||||
|
||||
num_fields = table.field_set.count()
|
||||
if (num_fields + 1) > settings.MAX_FIELD_LIMIT:
|
||||
raise MaxFieldLimitExceeded(
|
||||
|
@ -494,12 +500,19 @@ class FieldHandler(metaclass=baserow_trace_methods(tracer)):
|
|||
# migrate the field to the new type.
|
||||
baserow_field_type_changed = from_field_type.type != to_field_type_name
|
||||
field_cache = FieldCache()
|
||||
|
||||
if baserow_field_type_changed:
|
||||
to_field_type = field_type_registry.get(to_field_type_name)
|
||||
else:
|
||||
to_field_type = from_field_type
|
||||
|
||||
if field.primary and not to_field_type.can_be_primary_field:
|
||||
raise IncompatiblePrimaryFieldTypeError(to_field_type_name)
|
||||
allowed_fields = ["name", "description"] + to_field_type.allowed_fields
|
||||
field_values = extract_allowed(kwargs, allowed_fields)
|
||||
|
||||
if field.primary and not to_field_type.can_be_primary_field(field_values):
|
||||
raise IncompatiblePrimaryFieldTypeError(to_field_type_name)
|
||||
|
||||
if baserow_field_type_changed:
|
||||
dependants_broken_due_to_type_change = (
|
||||
from_field_type.get_dependants_which_will_break_when_field_type_changes(
|
||||
field, to_field_type, field_cache
|
||||
|
@ -510,10 +523,6 @@ class FieldHandler(metaclass=baserow_trace_methods(tracer)):
|
|||
|
||||
else:
|
||||
dependants_broken_due_to_type_change = []
|
||||
to_field_type = from_field_type
|
||||
|
||||
allowed_fields = ["name", "description"] + to_field_type.allowed_fields
|
||||
field_values = extract_allowed(kwargs, allowed_fields)
|
||||
|
||||
self._validate_name_and_optionally_rename_if_collision(
|
||||
field, field_values, postfix_to_fix_name_collisions
|
||||
|
@ -1242,6 +1251,78 @@ class FieldHandler(metaclass=baserow_trace_methods(tracer)):
|
|||
|
||||
return [x[0] for x in res]
|
||||
|
||||
def change_primary_field(
|
||||
self, user: AbstractUser, table: Table, new_primary_field: Field
|
||||
) -> Tuple[Field, Field]:
|
||||
"""
|
||||
Changes the primary field of the given table.
|
||||
|
||||
:param user: The user on whose behalf the duplicated field will be
|
||||
changed.
|
||||
:param table: The table where to change the primary field in.
|
||||
:param new_primary_field: The field that must be changed to the primary field.
|
||||
:raises FieldNotInTable:
|
||||
:raises IncompatiblePrimaryFieldTypeError:
|
||||
:raises FieldIsAlreadyPrimary:
|
||||
:return: The updated field object.
|
||||
"""
|
||||
|
||||
if not isinstance(new_primary_field, Field):
|
||||
raise ValueError("The field is not an instance of Field.")
|
||||
|
||||
if type(new_primary_field) is Field:
|
||||
raise ValueError(
|
||||
"The field must be a specific instance of Field and not the base type "
|
||||
"Field itself."
|
||||
)
|
||||
|
||||
workspace = table.database.workspace
|
||||
CoreHandler().check_permissions(
|
||||
user,
|
||||
UpdateFieldOperationType.type,
|
||||
workspace=workspace,
|
||||
context=new_primary_field,
|
||||
)
|
||||
|
||||
if new_primary_field.table_id != table.id:
|
||||
raise FieldNotInTable(
|
||||
"The provided new primary field does not belong in the provided table."
|
||||
)
|
||||
|
||||
new_primary_field_type = field_type_registry.get_by_model(new_primary_field)
|
||||
if not new_primary_field_type.can_be_primary_field(new_primary_field):
|
||||
raise IncompatiblePrimaryFieldTypeError(new_primary_field_type.type)
|
||||
|
||||
existing_primary_fields = self.get_fields(
|
||||
table,
|
||||
Field.objects.filter(table=table, primary=True).select_for_update(),
|
||||
specific=True,
|
||||
)
|
||||
existing_primary_field = next(iter(existing_primary_fields), None)
|
||||
|
||||
if existing_primary_field is None:
|
||||
raise TableHasNoPrimaryField("The provided table has no primary field.")
|
||||
|
||||
if existing_primary_field.id == new_primary_field.id:
|
||||
raise FieldIsAlreadyPrimary("The provided field is already primary.")
|
||||
|
||||
existing_primary_field.primary = False
|
||||
existing_primary_field.save()
|
||||
|
||||
old_new_primary_field = deepcopy(new_primary_field)
|
||||
new_primary_field.primary = True
|
||||
new_primary_field.save()
|
||||
|
||||
field_updated.send(
|
||||
self,
|
||||
field=new_primary_field,
|
||||
old_field=old_new_primary_field,
|
||||
related_fields=[existing_primary_field],
|
||||
user=user,
|
||||
)
|
||||
|
||||
return new_primary_field, existing_primary_field
|
||||
|
||||
def _validate_name_and_optionally_rename_if_collision(
|
||||
self,
|
||||
field: Field,
|
||||
|
|
|
@ -105,7 +105,7 @@ class FieldType(
|
|||
_can_order_by = True
|
||||
"""Indicates whether it is possible to order by this field type."""
|
||||
|
||||
can_be_primary_field = True
|
||||
_can_be_primary_field = True
|
||||
"""Some field types cannot be the primary field."""
|
||||
|
||||
can_have_select_options = False
|
||||
|
@ -1489,6 +1489,18 @@ class FieldType(
|
|||
via_path_to_starting_table,
|
||||
)
|
||||
|
||||
def can_be_primary_field(self, field_or_values: Union[Field, dict]) -> bool:
|
||||
"""
|
||||
Override this method if this field type can't be primary.
|
||||
|
||||
:param field_or_values: The field object or the values to create/update it.
|
||||
It accepts either because in some cases the field object doesn't exist,
|
||||
but we do need to check if the field can be primary.
|
||||
:return: True if the field can be primary.
|
||||
"""
|
||||
|
||||
return self._can_be_primary_field
|
||||
|
||||
def check_can_order_by(self, field: Field) -> bool:
|
||||
"""
|
||||
Override this method if this field type can sometimes be ordered or sometimes
|
||||
|
|
|
@ -8,7 +8,7 @@ msgid ""
|
|||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2024-06-20 05:25+0000\n"
|
||||
"POT-Creation-Date: 2024-07-16 13:47+0000\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
|
@ -68,46 +68,57 @@ msgstr ""
|
|||
msgid "Table \"%(table_name)s\" (%(table_id)s) exported to %(export_type)s"
|
||||
msgstr ""
|
||||
|
||||
#: src/baserow/contrib/database/fields/actions.py:35
|
||||
#: src/baserow/contrib/database/fields/actions.py:36
|
||||
msgid "Update field"
|
||||
msgstr ""
|
||||
|
||||
#: src/baserow/contrib/database/fields/actions.py:36
|
||||
#: src/baserow/contrib/database/fields/actions.py:37
|
||||
#, python-format
|
||||
msgid "Field \"%(field_name)s\" (%(field_id)s) updated"
|
||||
msgstr ""
|
||||
|
||||
#: src/baserow/contrib/database/fields/actions.py:339
|
||||
#: src/baserow/contrib/database/fields/actions.py:340
|
||||
msgid "Create field"
|
||||
msgstr ""
|
||||
|
||||
#: src/baserow/contrib/database/fields/actions.py:340
|
||||
#: src/baserow/contrib/database/fields/actions.py:341
|
||||
#, python-format
|
||||
msgid "Field \"%(field_name)s\" (%(field_id)s) created"
|
||||
msgstr ""
|
||||
|
||||
#: src/baserow/contrib/database/fields/actions.py:433
|
||||
#: src/baserow/contrib/database/fields/actions.py:434
|
||||
msgid "Delete field"
|
||||
msgstr ""
|
||||
|
||||
#: src/baserow/contrib/database/fields/actions.py:434
|
||||
#: src/baserow/contrib/database/fields/actions.py:435
|
||||
#, python-format
|
||||
msgid "Field \"%(field_name)s\" (%(field_id)s) deleted"
|
||||
msgstr ""
|
||||
|
||||
#: src/baserow/contrib/database/fields/actions.py:506
|
||||
#: src/baserow/contrib/database/fields/actions.py:507
|
||||
msgid "Duplicate field"
|
||||
msgstr ""
|
||||
|
||||
#: src/baserow/contrib/database/fields/actions.py:508
|
||||
#: src/baserow/contrib/database/fields/actions.py:509
|
||||
#, python-format
|
||||
msgid ""
|
||||
"Field \"%(field_name)s\" (%(field_id)s) duplicated (with_data=%(with_data)s) "
|
||||
"from field \"%(original_field_name)s\" (%(original_field_id)s)"
|
||||
msgstr ""
|
||||
|
||||
#: src/baserow/contrib/database/fields/actions.py:592
|
||||
msgid "Change primary field"
|
||||
msgstr ""
|
||||
|
||||
#: src/baserow/contrib/database/fields/actions.py:594
|
||||
#, python-format
|
||||
msgid ""
|
||||
"Primary field of table %(table_name)s was changed to "
|
||||
"%(new_primary_field_name)s"
|
||||
msgstr ""
|
||||
|
||||
#: src/baserow/contrib/database/fields/models.py:371
|
||||
#: src/baserow/contrib/database/fields/models.py:510
|
||||
#: src/baserow/contrib/database/fields/models.py:519
|
||||
msgid "The format of the duration."
|
||||
msgstr ""
|
||||
|
||||
|
@ -136,8 +147,8 @@ msgstr ""
|
|||
|
||||
#: src/baserow/contrib/database/plugins.py:55
|
||||
#: src/baserow/contrib/database/plugins.py:77
|
||||
#: src/baserow/contrib/database/table/handler.py:623
|
||||
#: src/baserow/contrib/database/table/handler.py:636
|
||||
#: src/baserow/contrib/database/table/handler.py:624
|
||||
#: src/baserow/contrib/database/table/handler.py:637
|
||||
msgid "Name"
|
||||
msgstr ""
|
||||
|
||||
|
@ -146,13 +157,13 @@ msgid "Last name"
|
|||
msgstr ""
|
||||
|
||||
#: src/baserow/contrib/database/plugins.py:57
|
||||
#: src/baserow/contrib/database/table/handler.py:624
|
||||
#: src/baserow/contrib/database/table/handler.py:625
|
||||
msgid "Notes"
|
||||
msgstr ""
|
||||
|
||||
#: src/baserow/contrib/database/plugins.py:58
|
||||
#: src/baserow/contrib/database/plugins.py:79
|
||||
#: src/baserow/contrib/database/table/handler.py:625
|
||||
#: src/baserow/contrib/database/table/handler.py:626
|
||||
msgid "Active"
|
||||
msgstr ""
|
||||
|
||||
|
@ -300,11 +311,11 @@ msgid ""
|
|||
"\"%(original_table_name)s\" (%(original_table_id)s) "
|
||||
msgstr ""
|
||||
|
||||
#: src/baserow/contrib/database/table/handler.py:527
|
||||
#: src/baserow/contrib/database/table/handler.py:528
|
||||
msgid "Grid"
|
||||
msgstr ""
|
||||
|
||||
#: src/baserow/contrib/database/table/handler.py:585
|
||||
#: src/baserow/contrib/database/table/handler.py:586
|
||||
#, python-format
|
||||
msgid "Field %d"
|
||||
msgstr ""
|
||||
|
|
|
@ -8,7 +8,7 @@ msgid ""
|
|||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2024-06-20 07:22+0000\n"
|
||||
"POT-Creation-Date: 2024-07-16 13:47+0000\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
|
@ -215,16 +215,16 @@ msgstr ""
|
|||
msgid "You have %(count)d new notifications - Baserow"
|
||||
msgstr ""
|
||||
|
||||
#: src/baserow/core/fields.py:112
|
||||
#: src/baserow/core/fields.py:114
|
||||
#, python-format
|
||||
msgid "“%(value)s” value must be a decimal number."
|
||||
msgstr ""
|
||||
|
||||
#: src/baserow/core/fields.py:114
|
||||
#: src/baserow/core/fields.py:116
|
||||
msgid "Decimal number"
|
||||
msgstr ""
|
||||
|
||||
#: src/baserow/core/handler.py:2039 src/baserow/core/user/handler.py:266
|
||||
#: src/baserow/core/handler.py:2053 src/baserow/core/user/handler.py:268
|
||||
#, python-format
|
||||
msgid "%(name)s's workspace"
|
||||
msgstr ""
|
||||
|
@ -536,8 +536,3 @@ msgstr ""
|
|||
#: src/baserow/core/user/emails.py:74
|
||||
msgid "Account deletion cancelled - Baserow"
|
||||
msgstr ""
|
||||
|
||||
#: src/baserow/core/user_sources/user_source_user.py:81
|
||||
#, python-format
|
||||
msgid "%(user_source_name)s member"
|
||||
msgstr ""
|
||||
|
|
|
@ -953,3 +953,175 @@ def test_async_duplicate_field(api_client, data_fixture):
|
|||
assert field_set.count() == original_field_count + 2
|
||||
for row in response_json["results"]:
|
||||
assert row[f"{primary_field.name} 3"] == row[primary_field.name]
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_change_primary_field_different_table(api_client, data_fixture):
|
||||
user, token = data_fixture.create_user_and_token()
|
||||
table_a = data_fixture.create_database_table(user)
|
||||
field_1 = data_fixture.create_text_field(user=user, primary=True, table=table_a)
|
||||
field_2 = data_fixture.create_text_field(user=user, primary=False, table=table_a)
|
||||
table_b = data_fixture.create_database_table(user)
|
||||
|
||||
response = api_client.post(
|
||||
reverse(
|
||||
"api:database:fields:change_primary_field", kwargs={"table_id": table_b.id}
|
||||
),
|
||||
{"new_primary_field_id": field_2.id},
|
||||
format="json",
|
||||
HTTP_AUTHORIZATION=f"JWT {token}",
|
||||
)
|
||||
assert response.status_code == HTTP_400_BAD_REQUEST
|
||||
assert response.json()["error"] == "ERROR_FIELD_NOT_IN_TABLE"
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_change_primary_field_type_not_primary(api_client, data_fixture):
|
||||
user, token = data_fixture.create_user_and_token()
|
||||
table_a = data_fixture.create_database_table(user)
|
||||
field_1 = data_fixture.create_text_field(user=user, primary=True, table=table_a)
|
||||
field_2 = data_fixture.create_password_field(
|
||||
user=user, primary=False, table=table_a
|
||||
)
|
||||
|
||||
response = api_client.post(
|
||||
reverse(
|
||||
"api:database:fields:change_primary_field", kwargs={"table_id": table_a.id}
|
||||
),
|
||||
{"new_primary_field_id": field_2.id},
|
||||
format="json",
|
||||
HTTP_AUTHORIZATION=f"JWT {token}",
|
||||
)
|
||||
assert response.status_code == HTTP_400_BAD_REQUEST
|
||||
assert response.json()["error"] == "ERROR_INCOMPATIBLE_PRIMARY_FIELD_TYPE"
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_change_primary_field_field_is_already_primary(api_client, data_fixture):
|
||||
user, token = data_fixture.create_user_and_token()
|
||||
table_a = data_fixture.create_database_table(user)
|
||||
field_1 = data_fixture.create_text_field(user=user, primary=True, table=table_a)
|
||||
|
||||
response = api_client.post(
|
||||
reverse(
|
||||
"api:database:fields:change_primary_field", kwargs={"table_id": table_a.id}
|
||||
),
|
||||
{"new_primary_field_id": field_1.id},
|
||||
format="json",
|
||||
HTTP_AUTHORIZATION=f"JWT {token}",
|
||||
)
|
||||
assert response.status_code == HTTP_400_BAD_REQUEST
|
||||
assert response.json()["error"] == "ERROR_FIELD_IS_ALREADY_PRIMARY"
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_change_primary_field_field_no_update_permissions(api_client, data_fixture):
|
||||
user, token = data_fixture.create_user_and_token()
|
||||
user_2, token_2 = data_fixture.create_user_and_token()
|
||||
table_a = data_fixture.create_database_table(user)
|
||||
field_1 = data_fixture.create_text_field(user=user, primary=True, table=table_a)
|
||||
field_2 = data_fixture.create_text_field(user=user, primary=False, table=table_a)
|
||||
|
||||
response = api_client.post(
|
||||
reverse(
|
||||
"api:database:fields:change_primary_field", kwargs={"table_id": table_a.id}
|
||||
),
|
||||
{"new_primary_field_id": field_2.id},
|
||||
format="json",
|
||||
HTTP_AUTHORIZATION=f"JWT {token_2}",
|
||||
)
|
||||
assert response.status_code == HTTP_400_BAD_REQUEST
|
||||
assert response.json()["error"] == "ERROR_USER_NOT_IN_GROUP"
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_change_primary_field_field_without_primary(api_client, data_fixture):
|
||||
user, token = data_fixture.create_user_and_token()
|
||||
table_a = data_fixture.create_database_table(user)
|
||||
field_2 = data_fixture.create_text_field(user=user, primary=False, table=table_a)
|
||||
|
||||
response = api_client.post(
|
||||
reverse(
|
||||
"api:database:fields:change_primary_field", kwargs={"table_id": table_a.id}
|
||||
),
|
||||
{"new_primary_field_id": field_2.id},
|
||||
format="json",
|
||||
HTTP_AUTHORIZATION=f"JWT {token}",
|
||||
)
|
||||
assert response.status_code == HTTP_400_BAD_REQUEST
|
||||
assert response.json()["error"] == "ERROR_TABLE_HAS_NO_PRIMARY_FIELD"
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_change_primary_field_field_with_primary(api_client, data_fixture):
|
||||
user, token = data_fixture.create_user_and_token()
|
||||
table_a = data_fixture.create_database_table(user)
|
||||
field_1 = data_fixture.create_text_field(user=user, primary=True, table=table_a)
|
||||
field_2 = data_fixture.create_text_field(user=user, primary=False, table=table_a)
|
||||
|
||||
response = api_client.post(
|
||||
reverse(
|
||||
"api:database:fields:change_primary_field", kwargs={"table_id": table_a.id}
|
||||
),
|
||||
{"new_primary_field_id": field_2.id},
|
||||
format="json",
|
||||
HTTP_AUTHORIZATION=f"JWT {token}",
|
||||
)
|
||||
assert response.status_code == HTTP_200_OK
|
||||
assert response.json() == {
|
||||
"id": field_2.id,
|
||||
"table_id": table_a.id,
|
||||
"name": field_2.name,
|
||||
"order": 0,
|
||||
"type": "text",
|
||||
"primary": True,
|
||||
"read_only": False,
|
||||
"description": None,
|
||||
"related_fields": [
|
||||
{
|
||||
"id": field_1.id,
|
||||
"table_id": table_a.id,
|
||||
"name": field_1.name,
|
||||
"order": 0,
|
||||
"type": "text",
|
||||
"primary": False,
|
||||
"read_only": False,
|
||||
"description": None,
|
||||
"text_default": "",
|
||||
}
|
||||
],
|
||||
"text_default": "",
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_change_primary_field_field_and_back(api_client, data_fixture):
|
||||
user, token = data_fixture.create_user_and_token()
|
||||
table_a = data_fixture.create_database_table(user)
|
||||
field_1 = data_fixture.create_text_field(user=user, primary=True, table=table_a)
|
||||
field_2 = data_fixture.create_text_field(user=user, primary=False, table=table_a)
|
||||
|
||||
response = api_client.post(
|
||||
reverse(
|
||||
"api:database:fields:change_primary_field", kwargs={"table_id": table_a.id}
|
||||
),
|
||||
{"new_primary_field_id": field_2.id},
|
||||
format="json",
|
||||
HTTP_AUTHORIZATION=f"JWT {token}",
|
||||
)
|
||||
|
||||
assert response.status_code == HTTP_200_OK
|
||||
response = api_client.post(
|
||||
reverse(
|
||||
"api:database:fields:change_primary_field", kwargs={"table_id": table_a.id}
|
||||
),
|
||||
{"new_primary_field_id": field_1.id},
|
||||
format="json",
|
||||
HTTP_AUTHORIZATION=f"JWT {token}",
|
||||
)
|
||||
assert response.status_code == HTTP_200_OK
|
||||
|
||||
field_1.refresh_from_db()
|
||||
field_2.refresh_from_db()
|
||||
assert field_1.primary is True
|
||||
assert field_2.primary is False
|
||||
|
|
|
@ -10,6 +10,7 @@ from pytest_unordered import unordered
|
|||
from rest_framework.status import HTTP_200_OK
|
||||
|
||||
from baserow.contrib.database.fields.actions import (
|
||||
ChangePrimaryFieldActionType,
|
||||
DuplicateFieldActionType,
|
||||
UpdateFieldActionType,
|
||||
)
|
||||
|
@ -1490,3 +1491,46 @@ def test_date_field_type_undo_redo_fix_timezone_offset(api_client, data_fixture)
|
|||
assert getattr(
|
||||
row_3, f"field_{datetime_field.id}"
|
||||
) == original_datetime_3 + timedelta(minutes=utc_offset)
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@pytest.mark.undo_redo
|
||||
@pytest.mark.field_single_select
|
||||
def test_can_undo_redo_change_primary_field(data_fixture):
|
||||
session_id = "session-id"
|
||||
user = data_fixture.create_user(session_id=session_id)
|
||||
table_a = data_fixture.create_database_table(user)
|
||||
field_1 = data_fixture.create_text_field(user=user, primary=True, table=table_a)
|
||||
field_2 = data_fixture.create_text_field(user=user, primary=False, table=table_a)
|
||||
|
||||
new_primary_field, old_primary_field = action_type_registry.get_by_type(
|
||||
ChangePrimaryFieldActionType
|
||||
).do(user, table_a, field_2)
|
||||
|
||||
assert new_primary_field.id == field_2.id
|
||||
assert old_primary_field.id == field_1.id
|
||||
|
||||
field_1.refresh_from_db()
|
||||
field_2.refresh_from_db()
|
||||
assert field_1.primary is False
|
||||
assert field_2.primary is True
|
||||
|
||||
actions = ActionHandler.undo(
|
||||
user, [ChangePrimaryFieldActionType.scope(table_a.id)], session_id
|
||||
)
|
||||
assert_undo_redo_actions_are_valid(actions, [ChangePrimaryFieldActionType])
|
||||
|
||||
field_1.refresh_from_db()
|
||||
field_2.refresh_from_db()
|
||||
assert field_1.primary is True
|
||||
assert field_2.primary is False
|
||||
|
||||
actions = ActionHandler.redo(
|
||||
user, [ChangePrimaryFieldActionType.scope(table_a.id)], session_id
|
||||
)
|
||||
assert_undo_redo_actions_are_valid(actions, [ChangePrimaryFieldActionType])
|
||||
|
||||
field_1.refresh_from_db()
|
||||
field_2.refresh_from_db()
|
||||
assert field_1.primary is False
|
||||
assert field_2.primary is True
|
||||
|
|
|
@ -14,6 +14,8 @@ from baserow.contrib.database.fields.exceptions import (
|
|||
CannotChangeFieldType,
|
||||
CannotDeletePrimaryField,
|
||||
FieldDoesNotExist,
|
||||
FieldIsAlreadyPrimary,
|
||||
FieldNotInTable,
|
||||
FieldTypeDoesNotExist,
|
||||
FieldWithSameNameAlreadyExists,
|
||||
IncompatibleFieldTypeForUniqueValues,
|
||||
|
@ -22,6 +24,7 @@ from baserow.contrib.database.fields.exceptions import (
|
|||
MaxFieldNameLengthExceeded,
|
||||
PrimaryFieldAlreadyExists,
|
||||
ReservedBaserowFieldNameException,
|
||||
TableHasNoPrimaryField,
|
||||
)
|
||||
from baserow.contrib.database.fields.field_helpers import (
|
||||
construct_all_possible_field_kwargs,
|
||||
|
@ -1733,3 +1736,117 @@ def test_duplicating_link_row_field_properly_resets_pk_sequence_of_new_table(
|
|||
)
|
||||
assert table_b_row_1.id in linked_vals
|
||||
assert table_b_row_2.id in linked_vals
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_change_primary_field_different_table(data_fixture):
|
||||
user = data_fixture.create_user()
|
||||
table_a = data_fixture.create_database_table(user)
|
||||
field_1 = data_fixture.create_text_field(user=user, primary=True, table=table_a)
|
||||
field_2 = data_fixture.create_text_field(user=user, primary=False, table=table_a)
|
||||
table_b = data_fixture.create_database_table(user)
|
||||
|
||||
with pytest.raises(FieldNotInTable):
|
||||
FieldHandler().change_primary_field(user, table_b, field_2)
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_change_primary_field_type_not_primary(data_fixture):
|
||||
user = data_fixture.create_user()
|
||||
table_a = data_fixture.create_database_table(user)
|
||||
field_1 = data_fixture.create_text_field(user=user, primary=True, table=table_a)
|
||||
field_2 = data_fixture.create_password_field(
|
||||
user=user, primary=False, table=table_a
|
||||
)
|
||||
|
||||
with pytest.raises(IncompatiblePrimaryFieldTypeError):
|
||||
FieldHandler().change_primary_field(user, table_a, field_2)
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_change_primary_field_field_is_already_primary(data_fixture):
|
||||
user = data_fixture.create_user()
|
||||
table_a = data_fixture.create_database_table(user)
|
||||
field_1 = data_fixture.create_text_field(user=user, primary=True, table=table_a)
|
||||
|
||||
with pytest.raises(FieldIsAlreadyPrimary):
|
||||
FieldHandler().change_primary_field(user, table_a, field_1)
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_change_primary_field_field_no_update_permissions(data_fixture):
|
||||
user = data_fixture.create_user()
|
||||
user_2 = data_fixture.create_user()
|
||||
table_a = data_fixture.create_database_table(user)
|
||||
field_1 = data_fixture.create_text_field(user=user, primary=True, table=table_a)
|
||||
field_2 = data_fixture.create_text_field(user=user, primary=False, table=table_a)
|
||||
|
||||
with pytest.raises(UserNotInWorkspace):
|
||||
FieldHandler().change_primary_field(user_2, table_a, field_2)
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_change_primary_field_field_without_primary(data_fixture):
|
||||
user = data_fixture.create_user()
|
||||
table_a = data_fixture.create_database_table(user)
|
||||
field_2 = data_fixture.create_text_field(user=user, primary=False, table=table_a)
|
||||
|
||||
with pytest.raises(TableHasNoPrimaryField):
|
||||
FieldHandler().change_primary_field(user, table_a, field_2)
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_change_primary_field_field_with_primary(data_fixture):
|
||||
user = data_fixture.create_user()
|
||||
table_a = data_fixture.create_database_table(user)
|
||||
field_1 = data_fixture.create_text_field(user=user, primary=True, table=table_a)
|
||||
field_2 = data_fixture.create_text_field(user=user, primary=False, table=table_a)
|
||||
|
||||
new_primary, old_primary = FieldHandler().change_primary_field(
|
||||
user, table_a, field_2
|
||||
)
|
||||
|
||||
assert new_primary.id == field_2.id
|
||||
assert new_primary.primary is True
|
||||
assert old_primary.id == field_1.id
|
||||
assert old_primary.primary is False
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_change_primary_field_field_with_existing_primary_field(data_fixture):
|
||||
user = data_fixture.create_user()
|
||||
table_a = data_fixture.create_database_table(user)
|
||||
field_1 = data_fixture.create_text_field(user=user, primary=True, table=table_a)
|
||||
field_2 = data_fixture.create_text_field(user=user, primary=False, table=table_a)
|
||||
data_fixture.create_text_field(user=user, primary=True)
|
||||
|
||||
new_primary, old_primary = FieldHandler().change_primary_field(
|
||||
user, table_a, field_2
|
||||
)
|
||||
|
||||
assert new_primary.id == field_2.id
|
||||
assert new_primary.primary is True
|
||||
assert old_primary.id == field_1.id
|
||||
assert old_primary.primary is False
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@patch("baserow.contrib.database.fields.signals.field_updated.send")
|
||||
def test_change_primary_field_signal_send(send_mock, data_fixture):
|
||||
user = data_fixture.create_user()
|
||||
table_a = data_fixture.create_database_table(user)
|
||||
field_1 = data_fixture.create_text_field(user=user, primary=True, table=table_a)
|
||||
field_2 = data_fixture.create_text_field(user=user, primary=False, table=table_a)
|
||||
|
||||
new_primary, old_primary = FieldHandler().change_primary_field(
|
||||
user, table_a, field_2
|
||||
)
|
||||
|
||||
send_mock.assert_called_once()
|
||||
assert send_mock.call_args[1]["user"].id == user.id
|
||||
assert send_mock.call_args[1]["field"].id == field_2.id
|
||||
assert send_mock.call_args[1]["field"].primary is True
|
||||
assert send_mock.call_args[1]["old_field"].id == field_2.id
|
||||
assert send_mock.call_args[1]["old_field"].primary is False
|
||||
assert send_mock.call_args[1]["related_fields"][0].id == field_1.id
|
||||
assert send_mock.call_args[1]["related_fields"][0].primary is False
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import pytest
|
||||
|
||||
from baserow.contrib.database.fields.exceptions import RichTextFieldCannotBePrimaryField
|
||||
from baserow.contrib.database.fields.exceptions import IncompatiblePrimaryFieldTypeError
|
||||
from baserow.contrib.database.fields.handler import FieldHandler
|
||||
from baserow.contrib.database.rows.handler import RowHandler
|
||||
from baserow.contrib.database.table.models import RichTextFieldMention
|
||||
|
@ -15,7 +15,7 @@ def test_rich_text_field_cannot_be_primary(data_fixture):
|
|||
database = data_fixture.create_database_application(user=user)
|
||||
table = data_fixture.create_database_table(database=database)
|
||||
|
||||
with pytest.raises(RichTextFieldCannotBePrimaryField):
|
||||
with pytest.raises(IncompatiblePrimaryFieldTypeError):
|
||||
FieldHandler().create_field(
|
||||
user=user,
|
||||
table=table,
|
||||
|
@ -30,7 +30,7 @@ def test_rich_text_field_cannot_be_primary(data_fixture):
|
|||
user=user, table=table, type_name="long_text", name="Primary", primary=True
|
||||
)
|
||||
|
||||
with pytest.raises(RichTextFieldCannotBePrimaryField):
|
||||
with pytest.raises(IncompatiblePrimaryFieldTypeError):
|
||||
FieldHandler().update_field(
|
||||
user=user,
|
||||
field=primary_field,
|
||||
|
@ -38,6 +38,20 @@ def test_rich_text_field_cannot_be_primary(data_fixture):
|
|||
long_text_enable_rich_text=True,
|
||||
)
|
||||
|
||||
rich_text = FieldHandler().create_field(
|
||||
user=user,
|
||||
table=table,
|
||||
type_name="long_text",
|
||||
name="Rich text",
|
||||
primary=False,
|
||||
long_text_enable_rich_text=True,
|
||||
)
|
||||
|
||||
with pytest.raises(IncompatiblePrimaryFieldTypeError):
|
||||
FieldHandler().change_primary_field(
|
||||
user=user, table=table, new_primary_field=rich_text
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_perm_deleting_rows_delete_rich_text_mentions(data_fixture):
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"type": "feature",
|
||||
"message": "Allow changing the primary field.",
|
||||
"issue_number": 1301,
|
||||
"bullet_points": [],
|
||||
"created_at": "2024-07-16"
|
||||
}
|
|
@ -369,7 +369,11 @@
|
|||
"generateAIPromptTitle": "Prompt error",
|
||||
"generateAIPromptDescription": "Something was wrong with the constructed prompt.",
|
||||
"maxNumberOfPendingWorkspaceInvitesReachedTitle": "Max pending invites reached",
|
||||
"maxNumberOfPendingWorkspaceInvitesReachedDescription": "You've reached the maximum number of pending invites for this workspace. Please let invitees accept the invite or cancel existing ones to continue."
|
||||
"maxNumberOfPendingWorkspaceInvitesReachedDescription": "You've reached the maximum number of pending invites for this workspace. Please let invitees accept the invite or cancel existing ones to continue.",
|
||||
"fieldIsAlreadyPrimaryTitle": "Field is already primary",
|
||||
"fieldIsAlreadyPrimaryDescription": "The chose new primary field is already the primary field.",
|
||||
"incompatiblePrimaryFieldTypeTitle": "Field is not compatible",
|
||||
"incompatiblePrimaryFieldTypeDescription": "The chose field can't be the primary field because it's incompatible."
|
||||
},
|
||||
"importerType": {
|
||||
"csv": "Import a CSV file",
|
||||
|
|
|
@ -172,6 +172,14 @@ export class ClientErrorMap {
|
|||
'clientHandler.maxNumberOfPendingWorkspaceInvitesReachedDescription'
|
||||
)
|
||||
),
|
||||
ERROR_FIELD_IS_ALREADY_PRIMARY: new ResponseErrorMessage(
|
||||
app.i18n.t('clientHandler.fieldIsAlreadyPrimaryTitle'),
|
||||
app.i18n.t('clientHandler.fieldIsAlreadyPrimaryDescription')
|
||||
),
|
||||
ERROR_INCOMPATIBLE_PRIMARY_FIELD_TYPE: new ResponseErrorMessage(
|
||||
app.i18n.t('clientHandler.incompatiblePrimaryFieldTypeTitle'),
|
||||
app.i18n.t('clientHandler.incompatiblePrimaryFieldTypeDescription')
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,98 @@
|
|||
<template>
|
||||
<Modal :small="true" @show="hideError()">
|
||||
<h2 class="box__title">
|
||||
{{ $t('changePrimaryFieldModal.title') }}
|
||||
</h2>
|
||||
<Error :error="error"></Error>
|
||||
<form @submit.prevent="changePrimaryField()">
|
||||
<FormGroup
|
||||
:label="$t('changePrimaryFieldModal.primaryFieldLabel')"
|
||||
small-label
|
||||
required
|
||||
:helper-text="
|
||||
$t('changePrimaryFieldModal.existingPrimary', {
|
||||
name: fromField.name,
|
||||
})
|
||||
"
|
||||
>
|
||||
<Dropdown v-model="newPrimaryFieldId">
|
||||
<DropdownItem
|
||||
v-for="field in newPrimaryFields"
|
||||
:key="'field-' + field.id"
|
||||
:name="field.name"
|
||||
:value="field.id"
|
||||
:icon="field._.type.iconClass"
|
||||
:disabled="!field._.type.canBePrimaryField"
|
||||
></DropdownItem>
|
||||
</Dropdown>
|
||||
</FormGroup>
|
||||
<div class="actions actions--right">
|
||||
<Button
|
||||
ref="submitButton"
|
||||
type="primary"
|
||||
:loading="loading"
|
||||
:disabled="loading"
|
||||
>
|
||||
{{ $t('changePrimaryFieldModal.change') }}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import error from '@baserow/modules/core/mixins/error'
|
||||
import modal from '@baserow/modules/core/mixins/modal'
|
||||
|
||||
export default {
|
||||
name: 'ChangePrimaryFieldModal',
|
||||
mixins: [modal, error],
|
||||
props: {
|
||||
table: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
fromField: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
allFieldsInTable: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
newPrimaryFieldId: this.fromField.id,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
newPrimaryFields() {
|
||||
return this.allFieldsInTable.filter((field) => !field.primary)
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
async changePrimaryField() {
|
||||
if (this.loading || this.disabled) {
|
||||
return
|
||||
}
|
||||
this.loading = true
|
||||
this.hideError()
|
||||
const newPrimaryField = this.allFieldsInTable.find(
|
||||
(field) => field.id === this.newPrimaryFieldId
|
||||
)
|
||||
try {
|
||||
await this.$store.dispatch('field/changePrimary', {
|
||||
field: newPrimaryField,
|
||||
})
|
||||
this.newPrimaryFieldId = null
|
||||
this.hide()
|
||||
} catch (error) {
|
||||
this.handleError(error)
|
||||
}
|
||||
this.loading = false
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
|
@ -42,6 +42,31 @@
|
|||
@updated="$refs.context.hide()"
|
||||
></UpdateFieldContext>
|
||||
</li>
|
||||
<li
|
||||
v-if="
|
||||
field.primary &&
|
||||
$hasPermission(
|
||||
'database.table.field.update',
|
||||
field,
|
||||
database.workspace.id
|
||||
)
|
||||
"
|
||||
class="context__menu-item"
|
||||
>
|
||||
<a
|
||||
class="context__menu-item-link"
|
||||
@click="$refs.changePrimaryFieldModal.show()"
|
||||
>
|
||||
<i class="context__menu-item-icon iconoir-coins-swap"></i>
|
||||
{{ $t('fieldContext.changePrimaryField') }}
|
||||
</a>
|
||||
<ChangePrimaryFieldModal
|
||||
ref="changePrimaryFieldModal"
|
||||
:all-fields-in-table="allFieldsInTable"
|
||||
:from-field="field"
|
||||
:table="table"
|
||||
></ChangePrimaryFieldModal>
|
||||
</li>
|
||||
<slot></slot>
|
||||
<li
|
||||
v-if="
|
||||
|
@ -71,10 +96,12 @@
|
|||
import context from '@baserow/modules/core/mixins/context'
|
||||
import UpdateFieldContext from '@baserow/modules/database/components/field/UpdateFieldContext'
|
||||
import { notifyIf } from '@baserow/modules/core/utils/error'
|
||||
import ChangePrimaryFieldModal from '@baserow/modules/database/components/field/ChangePrimaryFieldModal.vue'
|
||||
|
||||
export default {
|
||||
name: 'FieldContext',
|
||||
components: {
|
||||
ChangePrimaryFieldModal,
|
||||
UpdateFieldContext,
|
||||
},
|
||||
mixins: [context],
|
||||
|
|
|
@ -450,6 +450,7 @@ export class FieldType extends Registerable {
|
|||
name: this.getName(),
|
||||
isReadOnly: this.isReadOnly,
|
||||
canImport: this.getCanImport(),
|
||||
canBePrimaryField: this.canBePrimaryField,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -279,7 +279,8 @@
|
|||
"editField": "Edit field",
|
||||
"deleteField": "Delete field",
|
||||
"hideField": "Hide field",
|
||||
"showField": "Show field"
|
||||
"showField": "Show field",
|
||||
"changePrimaryField": "Change primary field"
|
||||
},
|
||||
"fieldForm": {
|
||||
"name": "Name",
|
||||
|
@ -754,6 +755,12 @@
|
|||
"cloneData": "Copy data",
|
||||
"readOnlyField": "Cell values will be filled automatically."
|
||||
},
|
||||
"changePrimaryFieldModal": {
|
||||
"title": "Change the primary field",
|
||||
"change": "Change",
|
||||
"primaryFieldLabel": "Primary field",
|
||||
"existingPrimary": "\"{name}\" is currently the primary field."
|
||||
},
|
||||
"snapshotsModal": {
|
||||
"title": "snapshots",
|
||||
"description": "Snapshots are a full copy of your {applicationTypeName} of the moment when they were created. A duplication of that data will be created when restoring. Snapshots are automatically deleted after one year.",
|
||||
|
|
|
@ -46,5 +46,13 @@ export default (client) => {
|
|||
config
|
||||
)
|
||||
},
|
||||
changePrimary(tableId, newPrimaryFieldId) {
|
||||
return client.post(
|
||||
`/database/fields/table/${tableId}/change-primary-field/`,
|
||||
{
|
||||
new_primary_field_id: newPrimaryFieldId,
|
||||
}
|
||||
)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
@ -223,6 +223,31 @@ export const actions = {
|
|||
|
||||
return forceUpdate ? await forceUpdateCallback() : forceUpdateCallback
|
||||
},
|
||||
/**
|
||||
* Promote the provided field to primary field.
|
||||
*/
|
||||
async changePrimary(context, { field, forceUpdate = true }) {
|
||||
const { dispatch } = context
|
||||
|
||||
const newField = clone(field)
|
||||
const oldField = clone(field)
|
||||
newField.primary = true
|
||||
|
||||
const { data } = await FieldService(this.$client).changePrimary(
|
||||
field.table_id,
|
||||
field.id
|
||||
)
|
||||
const forceUpdateCallback = async () => {
|
||||
return await dispatch('forceUpdate', {
|
||||
field,
|
||||
oldField,
|
||||
data,
|
||||
relatedFields: data.related_fields,
|
||||
})
|
||||
}
|
||||
|
||||
return forceUpdate ? await forceUpdateCallback() : forceUpdateCallback
|
||||
},
|
||||
/**
|
||||
* Forcefully update an existing field without making a request to the backend.
|
||||
*/
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue