mirror of
https://gitlab.com/bramw/baserow.git
synced 2025-04-14 17:18:33 +00:00
Resolve "Sorting by link_row
(Link to table) field"
This commit is contained in:
parent
a364f72c4a
commit
d78f6b6906
38 changed files with 1425 additions and 325 deletions
backend
src/baserow/contrib/database
api/fields
fields
formula/types
table
views
tests/baserow/contrib
database
api
fields
rows
views
field
test_count_field_type.pytest_formula_field_type.pytest_link_row_field_type.pytest_lookup_field_type.pytest_rollup_field_type.py
rows
table
view
integrations/local_baserow
changelog/entries/unreleased/feature
premium/backend
src/baserow_premium/fields
tests/baserow_premium_tests/api/views/views
web-frontend
modules/database
test/unit/database
|
@ -198,6 +198,11 @@ class LinkRowValueSerializer(serializers.Serializer):
|
|||
required=False,
|
||||
source="*",
|
||||
)
|
||||
order = serializers.DecimalField(
|
||||
max_digits=40,
|
||||
decimal_places=20,
|
||||
required=False,
|
||||
)
|
||||
|
||||
|
||||
class FileFieldRequestSerializer(serializers.ListField):
|
||||
|
@ -458,3 +463,17 @@ class PasswordSerializer(serializers.CharField):
|
|||
return None
|
||||
|
||||
return make_password(data)
|
||||
|
||||
|
||||
class LinkRowFieldSerializerMixin(serializers.ModelSerializer):
|
||||
link_row_table_primary_field = serializers.SerializerMethodField(
|
||||
help_text="The primary field of the table that is linked to."
|
||||
)
|
||||
|
||||
def get_link_row_table_primary_field(self, instance):
|
||||
related_field = instance.link_row_table_primary_field
|
||||
if related_field is None:
|
||||
return None
|
||||
return field_type_registry.get_serializer(
|
||||
related_field.specific, FieldSerializer
|
||||
).data
|
||||
|
|
|
@ -193,7 +193,12 @@ class FieldsView(APIView):
|
|||
fields = specific_iterator(
|
||||
Field.objects.filter(table=table)
|
||||
.select_related("content_type")
|
||||
.prefetch_related("select_options")
|
||||
.prefetch_related("select_options"),
|
||||
per_content_type_queryset_hook=(
|
||||
lambda field, queryset: field_type_registry.get_by_model(
|
||||
field
|
||||
).enhance_field_queryset(queryset, field)
|
||||
),
|
||||
)
|
||||
|
||||
data = [
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
from dataclasses import dataclass
|
||||
from typing import Any, Dict, Optional
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from django.db.models.expressions import OrderBy
|
||||
|
||||
|
@ -18,7 +18,7 @@ class OptionallyAnnotatedOrderBy:
|
|||
order by expression.
|
||||
"""
|
||||
|
||||
order: OrderBy
|
||||
order: OrderBy | List[OrderBy]
|
||||
annotation: Optional[Dict[str, Any]] = None
|
||||
can_be_indexed: bool = False
|
||||
|
||||
|
@ -37,6 +37,10 @@ class OptionallyAnnotatedOrderBy:
|
|||
else:
|
||||
return self.order.expression.name
|
||||
|
||||
@property
|
||||
def order_bys(self) -> List[OrderBy]:
|
||||
return self.order if isinstance(self.order, (list, tuple)) else [self.order]
|
||||
|
||||
@property
|
||||
def collation(self) -> str:
|
||||
return getattr(self.order.expression, "collation", None)
|
||||
|
|
|
@ -7,13 +7,24 @@ from datetime import date, datetime, timedelta, timezone
|
|||
from decimal import Decimal, InvalidOperation
|
||||
from itertools import cycle
|
||||
from random import randint, randrange, sample
|
||||
from typing import TYPE_CHECKING, Any, Callable, Dict, List, Optional, Set, Tuple, Union
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Any,
|
||||
Callable,
|
||||
Dict,
|
||||
List,
|
||||
Optional,
|
||||
Set,
|
||||
Tuple,
|
||||
Type,
|
||||
Union,
|
||||
)
|
||||
from zipfile import ZipFile
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.contrib.auth.models import AbstractUser
|
||||
from django.contrib.postgres.aggregates import StringAgg
|
||||
from django.contrib.postgres.aggregates import ArrayAgg, StringAgg
|
||||
from django.contrib.postgres.fields import JSONField
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.files.storage import Storage
|
||||
|
@ -35,7 +46,7 @@ from django.db.models import (
|
|||
Window,
|
||||
)
|
||||
from django.db.models.fields.related import ManyToManyField
|
||||
from django.db.models.functions import Coalesce, RowNumber
|
||||
from django.db.models.functions import Coalesce, Extract, RowNumber
|
||||
|
||||
from dateutil import parser
|
||||
from dateutil.parser import ParserError
|
||||
|
@ -64,6 +75,7 @@ from baserow.contrib.database.api.fields.serializers import (
|
|||
FileFieldRequestSerializer,
|
||||
FileFieldResponseSerializer,
|
||||
IntegerOrStringField,
|
||||
LinkRowFieldSerializerMixin,
|
||||
LinkRowRequestSerializer,
|
||||
LinkRowValueSerializer,
|
||||
ListOrStringField,
|
||||
|
@ -113,6 +125,7 @@ from baserow.contrib.database.views.models import OWNERSHIP_TYPE_COLLABORATIVE,
|
|||
from baserow.core.db import (
|
||||
CombinedForeignKeyAndManyToManyMultipleFieldPrefetch,
|
||||
collate_expression,
|
||||
specific_queryset,
|
||||
)
|
||||
from baserow.core.expressions import DateTrunc
|
||||
from baserow.core.fields import SyncedDateTimeField
|
||||
|
@ -234,7 +247,7 @@ if TYPE_CHECKING:
|
|||
|
||||
class CollationSortMixin:
|
||||
def get_order(
|
||||
self, field, field_name, order_direction
|
||||
self, field, field_name, order_direction, table_model=None
|
||||
) -> OptionallyAnnotatedOrderBy:
|
||||
field_expr = collate_expression(F(field_name))
|
||||
|
||||
|
@ -1476,15 +1489,14 @@ class LastModifiedByFieldType(ReadOnlyFieldType):
|
|||
return user.email if user else None
|
||||
|
||||
def get_order(
|
||||
self, field, field_name, order_direction
|
||||
self, field, field_name, order_direction, table_model=None
|
||||
) -> OptionallyAnnotatedOrderBy:
|
||||
"""
|
||||
If the user wants to sort the results they expect them to be ordered
|
||||
alphabetically based on the user's name.
|
||||
"""
|
||||
|
||||
name = f"{field_name}__first_name"
|
||||
order = collate_expression(F(name))
|
||||
order = collate_expression(self.get_sortable_column_expression(field_name))
|
||||
|
||||
if order_direction == "ASC":
|
||||
order = order.asc(nulls_first=True)
|
||||
|
@ -1549,6 +1561,9 @@ class LastModifiedByFieldType(ReadOnlyFieldType):
|
|||
connection, from_field, to_field
|
||||
)
|
||||
|
||||
def get_sortable_column_expression(self, field_name: str) -> Expression | F:
|
||||
return F(f"{field_name}__first_name")
|
||||
|
||||
|
||||
class CreatedByFieldType(ReadOnlyFieldType):
|
||||
type = "created_by"
|
||||
|
@ -1681,15 +1696,14 @@ class CreatedByFieldType(ReadOnlyFieldType):
|
|||
return user.email if user else None
|
||||
|
||||
def get_order(
|
||||
self, field, field_name, order_direction
|
||||
self, field, field_name, order_direction, table_model=None
|
||||
) -> OptionallyAnnotatedOrderBy:
|
||||
"""
|
||||
If the user wants to sort the results they expect them to be ordered
|
||||
alphabetically based on the user's name.
|
||||
"""
|
||||
|
||||
name = f"{field_name}__first_name"
|
||||
order = collate_expression(F(name))
|
||||
order = collate_expression(self.get_sortable_column_expression(field_name))
|
||||
|
||||
if order_direction == "ASC":
|
||||
order = order.asc(nulls_first=True)
|
||||
|
@ -1754,6 +1768,9 @@ class CreatedByFieldType(ReadOnlyFieldType):
|
|||
connection, from_field, to_field
|
||||
)
|
||||
|
||||
def get_sortable_column_expression(self, field_name: str) -> Expression | F:
|
||||
return F(f"{field_name}__first_name")
|
||||
|
||||
|
||||
class DurationFieldType(FieldType):
|
||||
type = "duration"
|
||||
|
@ -1908,6 +1925,9 @@ class DurationFieldType(FieldType):
|
|||
|
||||
setattr(row, field_name, value)
|
||||
|
||||
def get_sortable_column_expression(self, field_name: str) -> Expression | F:
|
||||
return Extract(F(f"{field_name}"), "epoch")
|
||||
|
||||
|
||||
class LinkRowFieldType(ManyToManyFieldTypeSerializeToInputValueMixin, FieldType):
|
||||
"""
|
||||
|
@ -1932,7 +1952,9 @@ class LinkRowFieldType(ManyToManyFieldTypeSerializeToInputValueMixin, FieldType)
|
|||
"link_row_table",
|
||||
"link_row_related_field",
|
||||
"link_row_limit_selection_view_id",
|
||||
"link_row_table_primary_field",
|
||||
]
|
||||
serializer_mixins = [LinkRowFieldSerializerMixin]
|
||||
serializer_field_overrides = {
|
||||
"link_row_table_id": serializers.IntegerField(
|
||||
required=False,
|
||||
|
@ -2001,12 +2023,111 @@ class LinkRowFieldType(ManyToManyFieldTypeSerializeToInputValueMixin, FieldType)
|
|||
ViewDoesNotExist: ERROR_VIEW_DOES_NOT_EXIST,
|
||||
ViewNotInTable: ERROR_VIEW_NOT_IN_TABLE,
|
||||
}
|
||||
_can_order_by = False
|
||||
_can_be_primary_field = False
|
||||
can_get_unique_values = False
|
||||
is_many_to_many_field = True
|
||||
can_be_target_of_adhoc_lookup = False
|
||||
|
||||
def _check_related_field_can_order_by(
|
||||
self, related_primary_field: Type[Field]
|
||||
) -> bool:
|
||||
related_primary_field_type = field_type_registry.get_by_model(
|
||||
related_primary_field
|
||||
)
|
||||
return related_primary_field_type.check_can_order_by(related_primary_field)
|
||||
|
||||
def check_can_order_by(self, field: Field) -> bool:
|
||||
related_primary_field = field.specific.link_row_table_primary_field
|
||||
if related_primary_field is None:
|
||||
return False
|
||||
return self._check_related_field_can_order_by(related_primary_field.specific)
|
||||
|
||||
def get_value_for_filter(self, row: "GeneratedTableModel", field):
|
||||
related_primary_field = field.link_row_table_primary_field
|
||||
if related_primary_field is None:
|
||||
return None
|
||||
related_primary_field = related_primary_field.specific
|
||||
related_primary_field_type = field_type_registry.get_by_model(
|
||||
related_primary_field
|
||||
)
|
||||
return related_primary_field_type.get_value_for_filter(
|
||||
row, related_primary_field
|
||||
)
|
||||
|
||||
def get_order(self, field, field_name, order_direction, table_model=None):
|
||||
# If provided, use the table_model to find the related_primary_field to avoid
|
||||
# potential unnecessary queries.
|
||||
if table_model is not None:
|
||||
remote_table_model = getattr(
|
||||
table_model, field_name
|
||||
).field.remote_field.model
|
||||
related_primary_field = remote_table_model.get_primary_field()
|
||||
else:
|
||||
related_primary_field = field.link_row_table_primary_field
|
||||
if related_primary_field is None:
|
||||
raise ValueError("Cannot find the related primary field.")
|
||||
|
||||
related_primary_field = related_primary_field.specific
|
||||
if not self._check_related_field_can_order_by(related_primary_field):
|
||||
raise ValueError(
|
||||
"The primary field for the related table cannot be ordered by."
|
||||
)
|
||||
related_primary_field_type = field_type_registry.get_by_model(
|
||||
related_primary_field
|
||||
)
|
||||
sortable_column_expr = (
|
||||
related_primary_field_type.get_sortable_column_expression(
|
||||
f"{field_name}__{related_primary_field.db_column}",
|
||||
)
|
||||
)
|
||||
|
||||
def get_array_agg(expr):
|
||||
return ArrayAgg(
|
||||
expr,
|
||||
filter=Q(
|
||||
**{f"{field_name}__isnull": False, f"{field_name}__trashed": False}
|
||||
),
|
||||
ordering=(f"{field_name}__order", f"{field_name}__id"),
|
||||
)
|
||||
|
||||
value_query = get_array_agg(sortable_column_expr)
|
||||
order_query = get_array_agg(F(f"{field_name}__order"))
|
||||
id_query = get_array_agg(F(f"{field_name}__id"))
|
||||
|
||||
linked_value_column_name = f"{field_name}_{related_primary_field.db_column}"
|
||||
# If the value are the same for multiple rows, we don't want Postgres to return
|
||||
# them randomly, so we add the order and id of the rows in the linked table to
|
||||
# make the ordering deterministic.
|
||||
linked_order_column_name = (
|
||||
f"{field_name}_{related_primary_field.db_column}_order"
|
||||
)
|
||||
linked_id_column_name = f"{field_name}_{related_primary_field.db_column}_id"
|
||||
annotation = {
|
||||
linked_value_column_name: value_query,
|
||||
linked_order_column_name: order_query,
|
||||
linked_id_column_name: id_query,
|
||||
}
|
||||
|
||||
linked_value = F(linked_value_column_name)
|
||||
linked_order = F(linked_order_column_name)
|
||||
linked_id = F(linked_id_column_name)
|
||||
if isinstance(related_primary_field_type, CollationSortMixin):
|
||||
linked_value = collate_expression(linked_value)
|
||||
|
||||
if order_direction == "DESC":
|
||||
linked_value = linked_value.desc(nulls_first=True)
|
||||
linked_order = linked_order.desc()
|
||||
linked_id = linked_id.desc()
|
||||
else:
|
||||
linked_value = linked_value.asc(nulls_first=True)
|
||||
linked_order = linked_order.asc()
|
||||
linked_id = linked_id.asc()
|
||||
|
||||
return OptionallyAnnotatedOrderBy(
|
||||
annotation=annotation,
|
||||
order=[linked_value, linked_order, linked_id],
|
||||
)
|
||||
|
||||
def get_search_expression(self, field: Field, queryset: QuerySet) -> Expression:
|
||||
remote_field = queryset.model._meta.get_field(field.db_column).remote_field
|
||||
remote_model = remote_field.model
|
||||
|
@ -2077,7 +2198,7 @@ class LinkRowFieldType(ManyToManyFieldTypeSerializeToInputValueMixin, FieldType)
|
|||
]
|
||||
|
||||
if len(target_field_names) > 0:
|
||||
related_queryset = related_queryset.only(*target_field_names)
|
||||
related_queryset = related_queryset.only("order", *target_field_names)
|
||||
for target_field_name in target_field_names:
|
||||
field_obj = remote_model.get_field_object(target_field_name)
|
||||
related_queryset = field_obj["type"].enhance_queryset(
|
||||
|
@ -2090,6 +2211,21 @@ class LinkRowFieldType(ManyToManyFieldTypeSerializeToInputValueMixin, FieldType)
|
|||
models.Prefetch(name, queryset=related_queryset)
|
||||
)
|
||||
|
||||
def enhance_field_queryset(
|
||||
self, queryset: QuerySet[Field], field: Field
|
||||
) -> QuerySet[Field]:
|
||||
return queryset.prefetch_related(
|
||||
models.Prefetch(
|
||||
"link_row_table__field_set",
|
||||
queryset=specific_queryset(
|
||||
Field.objects.filter(primary=True)
|
||||
.select_related("content_type")
|
||||
.prefetch_related("select_options")
|
||||
),
|
||||
to_attr=LinkRowField.RELATED_PPRIMARY_FIELD_ATTR,
|
||||
)
|
||||
)
|
||||
|
||||
def prepare_value_for_db(self, instance, value):
|
||||
return self.prepare_value_for_db_in_bulk(
|
||||
instance, {0: value}, continue_on_error=False
|
||||
|
@ -3000,7 +3136,7 @@ class LinkRowFieldType(ManyToManyFieldTypeSerializeToInputValueMixin, FieldType)
|
|||
return fields
|
||||
|
||||
def to_baserow_formula_type(self, field) -> BaserowFormulaType:
|
||||
primary_field = field.get_related_primary_field()
|
||||
primary_field = field.link_row_table_primary_field
|
||||
if primary_field is None:
|
||||
return BaserowFormulaInvalidType("references unknown or deleted table")
|
||||
else:
|
||||
|
@ -3011,7 +3147,7 @@ class LinkRowFieldType(ManyToManyFieldTypeSerializeToInputValueMixin, FieldType)
|
|||
def to_baserow_formula_expression(
|
||||
self, field
|
||||
) -> BaserowExpression[BaserowFormulaType]:
|
||||
primary_field = field.get_related_primary_field()
|
||||
primary_field = field.link_row_table_primary_field
|
||||
return FormulaHandler.get_lookup_field_reference_expression(
|
||||
field, primary_field, self.to_baserow_formula_type(field)
|
||||
)
|
||||
|
@ -3019,7 +3155,7 @@ class LinkRowFieldType(ManyToManyFieldTypeSerializeToInputValueMixin, FieldType)
|
|||
def get_field_dependencies(
|
||||
self, field_instance: LinkRowField, field_cache: "FieldCache"
|
||||
) -> FieldDependencies:
|
||||
primary_related_field = field_instance.get_related_primary_field()
|
||||
primary_related_field = field_instance.link_row_table_primary_field
|
||||
if primary_related_field is not None:
|
||||
return [
|
||||
FieldDependency(
|
||||
|
@ -3185,6 +3321,7 @@ class FileFieldType(FieldType):
|
|||
model_class = FileField
|
||||
can_be_in_form_view = True
|
||||
can_get_unique_values = False
|
||||
_can_order_by = False
|
||||
|
||||
def to_baserow_formula_type(self, field) -> BaserowFormulaType:
|
||||
return BaserowFormulaArrayType(BaserowFormulaSingleFileType(nullable=True))
|
||||
|
@ -3557,8 +3694,11 @@ class SelectOptionBaseFieldType(FieldType):
|
|||
|
||||
return queryset
|
||||
|
||||
def get_sortable_column_expression(self, field_name: str) -> Expression | F:
|
||||
return F(f"{field_name}__value")
|
||||
|
||||
class SingleSelectFieldType(SelectOptionBaseFieldType):
|
||||
|
||||
class SingleSelectFieldType(CollationSortMixin, SelectOptionBaseFieldType):
|
||||
type = "single_select"
|
||||
model_class = SingleSelectField
|
||||
|
||||
|
@ -3815,7 +3955,7 @@ class SingleSelectFieldType(SelectOptionBaseFieldType):
|
|||
)
|
||||
|
||||
def get_order(
|
||||
self, field, field_name, order_direction
|
||||
self, field, field_name, order_direction, table_model=None
|
||||
) -> OptionallyAnnotatedOrderBy:
|
||||
"""
|
||||
If the user wants to sort the results they expect them to be ordered
|
||||
|
@ -3824,8 +3964,7 @@ class SingleSelectFieldType(SelectOptionBaseFieldType):
|
|||
to the correct position.
|
||||
"""
|
||||
|
||||
name = f"{field_name}__value"
|
||||
order = collate_expression(F(name))
|
||||
order = collate_expression(self.get_sortable_column_expression(field_name))
|
||||
|
||||
if order_direction == "ASC":
|
||||
order = order.asc(nulls_first=True)
|
||||
|
@ -3893,6 +4032,7 @@ class SingleSelectFieldType(SelectOptionBaseFieldType):
|
|||
|
||||
|
||||
class MultipleSelectFieldType(
|
||||
CollationSortMixin,
|
||||
ManyToManyFieldTypeSerializeToInputValueMixin,
|
||||
ManyToManyGroupByMixin,
|
||||
SelectOptionBaseFieldType,
|
||||
|
@ -4246,17 +4386,23 @@ class MultipleSelectFieldType(
|
|||
q={f"select_option_value_{field_name}__iregex": rf"\m{value}\M"},
|
||||
)
|
||||
|
||||
def get_order(self, field, field_name, order_direction):
|
||||
def get_order(self, field, field_name, order_direction, table_model=None):
|
||||
"""
|
||||
If the user wants to sort the results they expect them to be ordered
|
||||
alphabetically based on the select option value and not in the id which is
|
||||
stored in the table. This method generates a Case expression which maps the id
|
||||
to the correct position.
|
||||
Order by the concatenated values of the select options, separated by a comma.
|
||||
"""
|
||||
|
||||
# FIXME: this is broken because the field sort items by insertion order with the
|
||||
# id in the through table. It's fixable here using a subquery on the m2m table
|
||||
# instead of a `StringAgg`, but it will be very difficult to fix in the formula
|
||||
# language. Also the frontend is not matching exactly the backend sorting and we
|
||||
# should also consider the possibility that a comma can be part of the value.
|
||||
sort_column_name = f"{field_name}_agg_sort"
|
||||
query = Coalesce(
|
||||
StringAgg(f"{field_name}__value", ",", output_field=models.TextField()),
|
||||
StringAgg(
|
||||
self.get_sortable_column_expression(field_name),
|
||||
",",
|
||||
output_field=models.TextField(),
|
||||
),
|
||||
Value(""),
|
||||
output_field=models.TextField(),
|
||||
)
|
||||
|
@ -4877,10 +5023,10 @@ class FormulaFieldType(FormulaArrayFilterSupport, ReadOnlyFieldType):
|
|||
return self.to_baserow_formula_type(field.specific).can_group_by
|
||||
|
||||
def get_order(
|
||||
self, field, field_name, order_direction
|
||||
self, field, field_name, order_direction, table_model=None
|
||||
) -> OptionallyAnnotatedOrderBy:
|
||||
return self.to_baserow_formula_type(field.specific).get_order(
|
||||
field, field_name, order_direction
|
||||
field, field_name, order_direction, table_model=table_model
|
||||
)
|
||||
|
||||
def get_value_for_filter(self, row: "GeneratedTableModel", field):
|
||||
|
@ -5543,7 +5689,7 @@ class LookupFieldType(FormulaFieldType):
|
|||
|
||||
|
||||
class MultipleCollaboratorsFieldType(
|
||||
ManyToManyFieldTypeSerializeToInputValueMixin, FieldType
|
||||
CollationSortMixin, ManyToManyFieldTypeSerializeToInputValueMixin, FieldType
|
||||
):
|
||||
type = "multiple_collaborators"
|
||||
model_class = MultipleCollaboratorsField
|
||||
|
@ -5861,7 +6007,7 @@ class MultipleCollaboratorsFieldType(
|
|||
def random_to_input_value(self, field, value):
|
||||
return [{"id": user_id} for user_id in value]
|
||||
|
||||
def get_order(self, field, field_name, order_direction):
|
||||
def get_order(self, field, field_name, order_direction, table_model=None):
|
||||
"""
|
||||
If the user wants to sort the results they expect them to be ordered
|
||||
alphabetically based on the user's name and not in the id which is
|
||||
|
@ -5871,7 +6017,11 @@ class MultipleCollaboratorsFieldType(
|
|||
|
||||
sort_column_name = f"{field_name}_agg_sort"
|
||||
query = Coalesce(
|
||||
StringAgg(f"{field_name}__first_name", "", output_field=models.TextField()),
|
||||
StringAgg(
|
||||
self.get_sortable_column_expression(field_name),
|
||||
"",
|
||||
output_field=models.TextField(),
|
||||
),
|
||||
Value(""),
|
||||
output_field=models.TextField(),
|
||||
)
|
||||
|
@ -5892,6 +6042,9 @@ class MultipleCollaboratorsFieldType(
|
|||
value = list_to_comma_separated_string(values)
|
||||
return value
|
||||
|
||||
def get_sortable_column_expression(self, field_name: str) -> Expression | F:
|
||||
return F(f"{field_name}__first_name")
|
||||
|
||||
|
||||
class UUIDFieldType(ReadOnlyFieldType):
|
||||
"""
|
||||
|
|
|
@ -389,6 +389,8 @@ class DurationField(Field):
|
|||
|
||||
class LinkRowField(Field):
|
||||
THROUGH_DATABASE_TABLE_PREFIX = LINK_ROW_THROUGH_TABLE_PREFIX
|
||||
RELATED_PPRIMARY_FIELD_ATTR = "primary_fields"
|
||||
|
||||
link_row_table = models.ForeignKey(
|
||||
"database.Table",
|
||||
on_delete=models.CASCADE,
|
||||
|
@ -427,7 +429,15 @@ class LinkRowField(Field):
|
|||
|
||||
return f"{self.THROUGH_DATABASE_TABLE_PREFIX}{self.link_row_relation_id}"
|
||||
|
||||
def get_related_primary_field(self):
|
||||
@property
|
||||
def link_row_table_primary_field(self):
|
||||
# LinkRowFieldType.enhance_field_queryset prefetches the primary field
|
||||
# into RELATED_PPRIMARY_FIELD_ATTR. Let's check if it's already there first.
|
||||
if related_primary_field_set := getattr(
|
||||
self.link_row_table, self.RELATED_PPRIMARY_FIELD_ATTR, None
|
||||
):
|
||||
return related_primary_field_set[0]
|
||||
|
||||
try:
|
||||
return self.link_row_table.field_set.get(primary=True)
|
||||
except Field.DoesNotExist:
|
||||
|
|
|
@ -1,5 +1,16 @@
|
|||
from functools import cached_property
|
||||
from typing import TYPE_CHECKING, Any, Dict, List, NoReturn, Optional, Set, Tuple, Union
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Any,
|
||||
Dict,
|
||||
List,
|
||||
NoReturn,
|
||||
Optional,
|
||||
Set,
|
||||
Tuple,
|
||||
Type,
|
||||
Union,
|
||||
)
|
||||
from zipfile import ZipFile
|
||||
|
||||
from django.contrib.auth.models import AbstractUser
|
||||
|
@ -15,6 +26,7 @@ from django.db.models import (
|
|||
Count,
|
||||
DurationField,
|
||||
Expression,
|
||||
F,
|
||||
IntegerField,
|
||||
JSONField,
|
||||
Model,
|
||||
|
@ -317,6 +329,18 @@ class FieldType(
|
|||
|
||||
return queryset
|
||||
|
||||
def enhance_field_queryset(
|
||||
self, queryset: QuerySet[Field], field: Field
|
||||
) -> QuerySet[Field]:
|
||||
"""
|
||||
This hook can be used to enhance a queryset when fetching multiple fields of a
|
||||
table. This is used when retrieving the fields of a table in the table view
|
||||
to, for example, retrieve the primary field of the related table for the link
|
||||
row field.
|
||||
"""
|
||||
|
||||
return queryset
|
||||
|
||||
def enhance_queryset_in_bulk(
|
||||
self, queryset: QuerySet, field_objects: List[dict], **kwargs
|
||||
) -> QuerySet:
|
||||
|
@ -808,7 +832,11 @@ class FieldType(
|
|||
"""
|
||||
|
||||
def get_order(
|
||||
self, field, field_name, order_direction
|
||||
self,
|
||||
field: Type[Field],
|
||||
field_name: str,
|
||||
order_direction: str,
|
||||
table_model: Optional["GeneratedTableModel"] = None,
|
||||
) -> OptionallyAnnotatedOrderBy:
|
||||
"""
|
||||
This hook can be called to generate a different order by expression.
|
||||
|
@ -822,17 +850,15 @@ class FieldType(
|
|||
get_value_for_filter method.
|
||||
|
||||
:param field: The related field object instance.
|
||||
:type field: Field
|
||||
:param field_name: The name of the field.
|
||||
:type field_name: str
|
||||
:param order_direction: The sort order direction.
|
||||
:type order_direction: str (Either "ASC" or "DESC")
|
||||
:param order_direction: The sort order direction (either "ASC" or "DESC").
|
||||
:param table_model: The table model instance that the field is part of,
|
||||
if available.
|
||||
:return: Either the expression that is added directly to the
|
||||
model.objects.order(), an AnnotatedOrderBy class or None.
|
||||
:rtype: Optional[Expression, AnnotatedOrderBy, None]
|
||||
"""
|
||||
|
||||
field_expr = django_models.F(field_name)
|
||||
field_expr = self.get_sortable_column_expression(field_name)
|
||||
|
||||
if order_direction == "ASC":
|
||||
field_order_by = field_expr.asc(nulls_first=True)
|
||||
|
@ -1589,6 +1615,18 @@ class FieldType(
|
|||
|
||||
return self._can_group_by
|
||||
|
||||
def get_sortable_column_expression(self, field_name: str) -> Expression | F:
|
||||
"""
|
||||
Returns the expression that can be used to sort the field in the database.
|
||||
By default it will just return the field name, but for example for a
|
||||
SingleSelectField, the select option value should be returned.
|
||||
|
||||
:param field_name: The name of the field in the table.
|
||||
:return: The expression that can be used to sort the field in the database.
|
||||
"""
|
||||
|
||||
return F(field_name)
|
||||
|
||||
def get_group_by_field_unique_value(
|
||||
self, field: Field, field_name: str, value: Any
|
||||
) -> Any:
|
||||
|
|
|
@ -203,7 +203,7 @@ class BaserowFormulaType(abc.ABC):
|
|||
pass
|
||||
|
||||
def get_order(
|
||||
self, field, field_name, order_direction
|
||||
self, field, field_name, order_direction, table_model=None
|
||||
) -> OptionallyAnnotatedOrderBy:
|
||||
"""
|
||||
Returns OptionallyAnnotatedOrderBy with desired order and optional
|
||||
|
|
|
@ -1312,7 +1312,7 @@ class BaserowFormulaArrayType(
|
|||
return self.sub_type.can_order_by_in_array
|
||||
|
||||
def get_order(
|
||||
self, field, field_name, order_direction
|
||||
self, field, field_name, order_direction, table_model=None
|
||||
) -> OptionallyAnnotatedOrderBy:
|
||||
expr = self.sub_type.get_order_by_in_array_expr(
|
||||
field, field_name, order_direction
|
||||
|
@ -1474,7 +1474,7 @@ class BaserowFormulaSingleSelectType(
|
|||
return True
|
||||
|
||||
def get_order(
|
||||
self, field, field_name, order_direction
|
||||
self, field, field_name, order_direction, table_model=None
|
||||
) -> OptionallyAnnotatedOrderBy:
|
||||
field_expr = F(f"{field_name}__value")
|
||||
|
||||
|
|
|
@ -146,7 +146,7 @@ class FieldDependencyExtractingVisitor(
|
|||
# We don't have a target field so we are not a lookup and must be
|
||||
# a field() reference of a link row field.
|
||||
|
||||
primary_field_in_other_table = via_field.get_related_primary_field()
|
||||
primary_field_in_other_table = via_field.link_row_table_primary_field
|
||||
if primary_field_in_other_table is None:
|
||||
return []
|
||||
else:
|
||||
|
@ -289,7 +289,7 @@ class FormulaTypingVisitor(
|
|||
)
|
||||
# If we are looking up a link row field we need to do an
|
||||
# extra relational jump to that primary field.
|
||||
related_primary_field = target_field.get_related_primary_field()
|
||||
related_primary_field = target_field.link_row_table_primary_field
|
||||
if related_primary_field is None:
|
||||
return field_reference.with_invalid_type(
|
||||
"references a deleted or unknown table"
|
||||
|
|
|
@ -308,7 +308,7 @@ class TableModelQuerySet(MultiFieldPrefetchQuerysetMixin, models.QuerySet):
|
|||
:rtype: QuerySet
|
||||
"""
|
||||
|
||||
order_by = split_comma_separated_string(order_string)
|
||||
order_by_fields = split_comma_separated_string(order_string)
|
||||
|
||||
if user_field_names:
|
||||
field_object_dict = {
|
||||
|
@ -318,7 +318,8 @@ class TableModelQuerySet(MultiFieldPrefetchQuerysetMixin, models.QuerySet):
|
|||
field_object_dict = self.model._field_objects
|
||||
|
||||
annotations = {}
|
||||
for index, order in enumerate(order_by):
|
||||
order_by = []
|
||||
for order in order_by_fields:
|
||||
if user_field_names:
|
||||
field_name_or_id = self._get_field_name(order)
|
||||
else:
|
||||
|
@ -346,13 +347,14 @@ class TableModelQuerySet(MultiFieldPrefetchQuerysetMixin, models.QuerySet):
|
|||
)
|
||||
|
||||
field_annotated_order_by = field_type.get_order(
|
||||
field, field_name, order_direction
|
||||
field, field_name, order_direction, table_model=self.model
|
||||
)
|
||||
|
||||
if field_annotated_order_by.annotation is not None:
|
||||
annotations = {**annotations, **field_annotated_order_by.annotation}
|
||||
field_order_by = field_annotated_order_by.order
|
||||
order_by[index] = field_order_by
|
||||
field_order_bys = field_annotated_order_by.order_bys
|
||||
for field_order_by in field_order_bys:
|
||||
order_by.append(field_order_by)
|
||||
|
||||
order_by.append("order")
|
||||
order_by.append("id")
|
||||
|
@ -679,6 +681,18 @@ class GeneratedTableModel(HierarchicalModelMixin, models.Model):
|
|||
def get_fields(cls, include_trash=False):
|
||||
return [o["field"] for o in cls.get_field_objects(include_trash)]
|
||||
|
||||
@classmethod
|
||||
def get_primary_field(self):
|
||||
field_objects = self.get_field_objects()
|
||||
|
||||
try:
|
||||
field_object = next(
|
||||
filter(lambda f: f["field"].primary is True, field_objects)
|
||||
)
|
||||
return field_object["field"]
|
||||
except StopIteration:
|
||||
return None
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
|
|
|
@ -32,7 +32,7 @@ from baserow.contrib.database.fields.field_filters import (
|
|||
FilterBuilder,
|
||||
)
|
||||
from baserow.contrib.database.fields.field_sortings import OptionallyAnnotatedOrderBy
|
||||
from baserow.contrib.database.fields.models import Field
|
||||
from baserow.contrib.database.fields.models import Field, LinkRowField
|
||||
from baserow.contrib.database.fields.operations import ReadFieldOperationType
|
||||
from baserow.contrib.database.fields.registries import field_type_registry
|
||||
from baserow.contrib.database.rows.handler import RowHandler
|
||||
|
@ -311,7 +311,10 @@ class ViewIndexingHandler(metaclass=baserow_trace_methods(tracer)):
|
|||
for view_sort_or_group_by in view.get_all_sorts():
|
||||
field_object = model._field_objects[view_sort_or_group_by.field_id]
|
||||
annotated_order_by = field_object["type"].get_order(
|
||||
field_object["field"], field_object["name"], view_sort_or_group_by.order
|
||||
field_object["field"],
|
||||
field_object["name"],
|
||||
view_sort_or_group_by.order,
|
||||
table_model=model,
|
||||
)
|
||||
|
||||
# It's enough to have one field that cannot be indexed to make the DB
|
||||
|
@ -321,7 +324,7 @@ class ViewIndexingHandler(metaclass=baserow_trace_methods(tracer)):
|
|||
|
||||
field_order_bys.append(annotated_order_by)
|
||||
|
||||
index_fields = [order_by.order for order_by in field_order_bys]
|
||||
index_fields = [o for ob in field_order_bys for o in ob.order_bys]
|
||||
|
||||
if not index_fields:
|
||||
return None
|
||||
|
@ -1315,6 +1318,19 @@ class ViewHandler(metaclass=baserow_trace_methods(tracer)):
|
|||
if deleted_count > 0:
|
||||
ViewIndexingHandler.after_field_changed_or_deleted(field)
|
||||
|
||||
# If it's a primary field, we also need to remove any sortings on the
|
||||
# link row fields pointing to this table.
|
||||
if field.primary:
|
||||
related_fields = LinkRowField.objects.filter(
|
||||
link_row_table_id=field.table_id
|
||||
)
|
||||
deleted_count, _ = ViewSort.objects.filter(
|
||||
field__in=related_fields
|
||||
).delete()
|
||||
if deleted_count > 0:
|
||||
for field in related_fields:
|
||||
ViewIndexingHandler.after_field_changed_or_deleted(field)
|
||||
|
||||
# If the new field type does not support grouping then all group bys will be
|
||||
# removed.
|
||||
if not field_type.check_can_group_by(field):
|
||||
|
@ -1829,15 +1845,19 @@ class ViewHandler(metaclass=baserow_trace_methods(tracer)):
|
|||
field_type = model._field_objects[view_sort_or_group_by.field_id]["type"]
|
||||
|
||||
field_annotated_order_by = field_type.get_order(
|
||||
field, field_name, view_sort_or_group_by.order
|
||||
field,
|
||||
field_name,
|
||||
view_sort_or_group_by.order,
|
||||
table_model=queryset.model,
|
||||
)
|
||||
field_annotation = field_annotated_order_by.annotation
|
||||
field_order_by = field_annotated_order_by.order
|
||||
field_order_bys = field_annotated_order_by.order_bys
|
||||
|
||||
if field_annotation is not None:
|
||||
queryset = queryset.annotate(**field_annotation)
|
||||
|
||||
order_by.append(field_order_by)
|
||||
for fob in field_order_bys:
|
||||
order_by.append(fob)
|
||||
|
||||
order_by.append(F("order").asc(nulls_first=True))
|
||||
order_by.append(F("id").asc(nulls_first=True))
|
||||
|
|
|
@ -1319,7 +1319,9 @@ class LinkRowContainsViewFilterType(ViewFilterType):
|
|||
compatible_field_types = [LinkRowFieldType.type]
|
||||
|
||||
def get_filter(self, field_name, value, model_field, field) -> OptionallyAnnotatedQ:
|
||||
related_primary_field = field.get_related_primary_field().specific
|
||||
related_primary_field = field.link_row_table_primary_field.specific
|
||||
if related_primary_field is None:
|
||||
return Q()
|
||||
related_primary_field_type = field_type_registry.get_by_model(
|
||||
related_primary_field
|
||||
)
|
||||
|
|
|
@ -4,6 +4,7 @@ import pytest
|
|||
from rest_framework.status import HTTP_200_OK, HTTP_400_BAD_REQUEST
|
||||
|
||||
from baserow.contrib.database.fields.handler import FieldHandler
|
||||
from baserow.test_utils.helpers import AnyStr
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
|
@ -46,14 +47,16 @@ def test_batch_create_rows_link_row_field(api_client, data_fixture):
|
|||
"items": [
|
||||
{
|
||||
f"id": 1,
|
||||
f"field_{link_field.id}": [{"id": linked_row_3.id, "value": "Row 3"}],
|
||||
f"field_{link_field.id}": [
|
||||
{"id": linked_row_3.id, "value": "Row 3", "order": AnyStr()}
|
||||
],
|
||||
"order": "1.00000000000000000000",
|
||||
},
|
||||
{
|
||||
f"id": 2,
|
||||
f"field_{link_field.id}": [
|
||||
{"id": linked_row_2.id, "value": "Row 2"},
|
||||
{"id": linked_row_3.id, "value": "Row 3"},
|
||||
{"id": linked_row_2.id, "value": "Row 2", "order": AnyStr()},
|
||||
{"id": linked_row_3.id, "value": "Row 3", "order": AnyStr()},
|
||||
],
|
||||
"order": "2.00000000000000000000",
|
||||
},
|
||||
|
@ -128,14 +131,16 @@ def test_batch_create_rows_link_row_field_with_other_value_types(
|
|||
"items": [
|
||||
{
|
||||
f"id": 1,
|
||||
f"field_{link_field.id}": [{"id": linked_row_3.id, "value": "Row 3"}],
|
||||
f"field_{link_field.id}": [
|
||||
{"id": linked_row_3.id, "value": "Row 3", "order": AnyStr()}
|
||||
],
|
||||
"order": "1.00000000000000000000",
|
||||
},
|
||||
{
|
||||
f"id": 2,
|
||||
f"field_{link_field.id}": [
|
||||
{"id": linked_row_2.id, "value": "Row 2"},
|
||||
{"id": linked_row_3.id, "value": "Row 3"},
|
||||
{"id": linked_row_2.id, "value": "Row 2", "order": AnyStr()},
|
||||
{"id": linked_row_3.id, "value": "Row 3", "order": AnyStr()},
|
||||
],
|
||||
"order": "2.00000000000000000000",
|
||||
},
|
||||
|
@ -147,15 +152,15 @@ def test_batch_create_rows_link_row_field_with_other_value_types(
|
|||
{
|
||||
f"id": 4,
|
||||
f"field_{link_field.id}": [
|
||||
{"id": linked_row_2.id, "value": "Row 2"},
|
||||
{"id": linked_row_3.id, "value": "Row 3"},
|
||||
{"id": linked_row_2.id, "value": "Row 2", "order": AnyStr()},
|
||||
{"id": linked_row_3.id, "value": "Row 3", "order": AnyStr()},
|
||||
],
|
||||
"order": "4.00000000000000000000",
|
||||
},
|
||||
{
|
||||
f"id": 5,
|
||||
f"field_{link_field.id}": [
|
||||
{"id": linked_row_2.id, "value": "Row 2"},
|
||||
{"id": linked_row_2.id, "value": "Row 2", "order": AnyStr()},
|
||||
],
|
||||
"order": "5.00000000000000000000",
|
||||
},
|
||||
|
@ -339,15 +344,17 @@ def test_batch_create_rows_link_same_table_row_field(api_client, data_fixture):
|
|||
{
|
||||
"id": 4,
|
||||
f"field_{primary_field.id}": "Row 4",
|
||||
f"field_{link_field.id}": [{"id": row_3.id, "value": "Row 3"}],
|
||||
f"field_{link_field.id}": [
|
||||
{"id": row_3.id, "value": "Row 3", "order": AnyStr()}
|
||||
],
|
||||
"order": "2.00000000000000000000",
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
f"field_{primary_field.id}": "Row 5",
|
||||
f"field_{link_field.id}": [
|
||||
{"id": row_1.id, "value": "Row 1"},
|
||||
{"id": row_2.id, "value": "Row 2"},
|
||||
{"id": row_1.id, "value": "Row 1", "order": AnyStr()},
|
||||
{"id": row_2.id, "value": "Row 2", "order": AnyStr()},
|
||||
],
|
||||
"order": "3.00000000000000000000",
|
||||
},
|
||||
|
@ -421,14 +428,16 @@ def test_batch_update_rows_link_row_field(api_client, data_fixture):
|
|||
"items": [
|
||||
{
|
||||
f"id": row_1.id,
|
||||
f"field_{link_field.id}": [{"id": linked_row_3.id, "value": "Row 3"}],
|
||||
f"field_{link_field.id}": [
|
||||
{"id": linked_row_3.id, "value": "Row 3", "order": AnyStr()}
|
||||
],
|
||||
"order": "1.00000000000000000000",
|
||||
},
|
||||
{
|
||||
f"id": row_2.id,
|
||||
f"field_{link_field.id}": [
|
||||
{"id": linked_row_2.id, "value": "Row 2"},
|
||||
{"id": linked_row_3.id, "value": "Row 3"},
|
||||
{"id": linked_row_2.id, "value": "Row 2", "order": AnyStr()},
|
||||
{"id": linked_row_3.id, "value": "Row 3", "order": AnyStr()},
|
||||
],
|
||||
"order": "1.00000000000000000000",
|
||||
},
|
||||
|
@ -495,15 +504,17 @@ def test_batch_update_rows_link_same_table_row_field(api_client, data_fixture):
|
|||
{
|
||||
"id": row_1.id,
|
||||
f"field_{primary_field.id}": "Row 1",
|
||||
f"field_{link_field.id}": [{"id": row_3.id, "value": "Row 3"}],
|
||||
f"field_{link_field.id}": [
|
||||
{"id": row_3.id, "value": "Row 3", "order": AnyStr()}
|
||||
],
|
||||
"order": "1.00000000000000000000",
|
||||
},
|
||||
{
|
||||
"id": row_2.id,
|
||||
f"field_{primary_field.id}": "Row 2",
|
||||
f"field_{link_field.id}": [
|
||||
{"id": row_2.id, "value": "Row 2"},
|
||||
{"id": row_3.id, "value": "Row 3"},
|
||||
{"id": row_2.id, "value": "Row 2", "order": AnyStr()},
|
||||
{"id": row_3.id, "value": "Row 3", "order": AnyStr()},
|
||||
],
|
||||
"order": "1.00000000000000000000",
|
||||
},
|
||||
|
|
|
@ -19,7 +19,7 @@ from rest_framework.status import (
|
|||
from baserow.contrib.database.fields.handler import FieldHandler
|
||||
from baserow.contrib.database.fields.models import SelectOption
|
||||
from baserow.contrib.database.tokens.handler import TokenHandler
|
||||
from baserow.test_utils.helpers import is_dict_subset
|
||||
from baserow.test_utils.helpers import AnyStr, is_dict_subset
|
||||
|
||||
# Create
|
||||
|
||||
|
@ -1485,7 +1485,9 @@ def test_batch_update_rows_different_manytomany_provided(api_client, data_fixtur
|
|||
"id": 1,
|
||||
"order": "1.00000000000000000000",
|
||||
f"field_{primary.id}": "row 1",
|
||||
f"field_{link_row_field_1.id}": [{"id": 1, "value": "row A"}],
|
||||
f"field_{link_row_field_1.id}": [
|
||||
{"id": 1, "value": "row A", "order": AnyStr()}
|
||||
],
|
||||
f"field_{link_row_field_2.id}": [],
|
||||
},
|
||||
{
|
||||
|
@ -1493,19 +1495,21 @@ def test_batch_update_rows_different_manytomany_provided(api_client, data_fixtur
|
|||
"order": "1.00000000000000000000",
|
||||
f"field_{primary.id}": "row 2",
|
||||
f"field_{link_row_field_1.id}": [],
|
||||
f"field_{link_row_field_2.id}": [{"id": 2, "value": "row B"}],
|
||||
f"field_{link_row_field_2.id}": [
|
||||
{"id": 2, "value": "row B", "order": AnyStr()}
|
||||
],
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"order": "1.00000000000000000000",
|
||||
f"field_{primary.id}": "row 3",
|
||||
f"field_{link_row_field_1.id}": [
|
||||
{"id": 1, "value": "row A"},
|
||||
{"id": 2, "value": "row B"},
|
||||
{"id": 1, "value": "row A", "order": AnyStr()},
|
||||
{"id": 2, "value": "row B", "order": AnyStr()},
|
||||
],
|
||||
f"field_{link_row_field_2.id}": [
|
||||
{"id": 1, "value": "row A"},
|
||||
{"id": 2, "value": "row B"},
|
||||
{"id": 1, "value": "row A", "order": AnyStr()},
|
||||
{"id": 2, "value": "row B", "order": AnyStr()},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
@ -1536,27 +1540,35 @@ def test_batch_update_rows_different_manytomany_provided(api_client, data_fixtur
|
|||
"id": 1,
|
||||
"order": "1.00000000000000000000",
|
||||
f"field_{primary.id}": "row 1",
|
||||
f"field_{link_row_field_1.id}": [{"id": 1, "value": "row A"}],
|
||||
f"field_{link_row_field_2.id}": [{"id": 1, "value": "row A"}],
|
||||
f"field_{link_row_field_1.id}": [
|
||||
{"id": 1, "value": "row A", "order": AnyStr()}
|
||||
],
|
||||
f"field_{link_row_field_2.id}": [
|
||||
{"id": 1, "value": "row A", "order": AnyStr()}
|
||||
],
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"order": "1.00000000000000000000",
|
||||
f"field_{primary.id}": "row 2",
|
||||
f"field_{link_row_field_1.id}": [{"id": 2, "value": "row B"}],
|
||||
f"field_{link_row_field_2.id}": [{"id": 2, "value": "row B"}],
|
||||
f"field_{link_row_field_1.id}": [
|
||||
{"id": 2, "value": "row B", "order": AnyStr()}
|
||||
],
|
||||
f"field_{link_row_field_2.id}": [
|
||||
{"id": 2, "value": "row B", "order": AnyStr()}
|
||||
],
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"order": "1.00000000000000000000",
|
||||
f"field_{primary.id}": "row 3",
|
||||
f"field_{link_row_field_1.id}": [
|
||||
{"id": 1, "value": "row A"},
|
||||
{"id": 2, "value": "row B"},
|
||||
{"id": 1, "value": "row A", "order": AnyStr()},
|
||||
{"id": 2, "value": "row B", "order": AnyStr()},
|
||||
],
|
||||
f"field_{link_row_field_2.id}": [
|
||||
{"id": 1, "value": "row A"},
|
||||
{"id": 2, "value": "row B"},
|
||||
{"id": 1, "value": "row A", "order": AnyStr()},
|
||||
{"id": 2, "value": "row B", "order": AnyStr()},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
|
|
@ -12,7 +12,7 @@ from baserow.contrib.database.api.rows.serializers import (
|
|||
from baserow.contrib.database.fields.handler import FieldHandler
|
||||
from baserow.contrib.database.fields.models import SelectOption
|
||||
from baserow.contrib.database.fields.registries import field_type_registry
|
||||
from baserow.test_utils.helpers import setup_interesting_test_table
|
||||
from baserow.test_utils.helpers import AnyStr, setup_interesting_test_table
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
|
@ -218,7 +218,7 @@ def test_get_row_serializer_with_user_field_names(data_fixture):
|
|||
model, RowSerializer, is_response=True, user_field_names=True
|
||||
)
|
||||
serializer_instance = serializer_class([row], many=True)
|
||||
assert json.loads(json.dumps(serializer_instance.data[0])) == json.loads(
|
||||
expected_result = json.loads(
|
||||
json.dumps(
|
||||
{
|
||||
"boolean": True,
|
||||
|
@ -241,9 +241,9 @@ def test_get_row_serializer_with_user_field_names(data_fixture):
|
|||
"created_on_datetime_us": "2021-01-02T12:00:00Z",
|
||||
"created_on_datetime_eu_tzone": "2021-01-02T12:00:00Z",
|
||||
"decimal_link_row": [
|
||||
{"id": 1, "value": "1.234"},
|
||||
{"id": 2, "value": "-123.456"},
|
||||
{"id": 3, "value": ""},
|
||||
{"id": 1, "value": "1.234", "order": "1.00000000000000000000"},
|
||||
{"id": 2, "value": "-123.456", "order": "2.00000000000000000000"},
|
||||
{"id": 3, "value": "", "order": "3.00000000000000000000"},
|
||||
],
|
||||
"duration_hm": 3660.0,
|
||||
"duration_hms": 3666.0,
|
||||
|
@ -281,19 +281,37 @@ def test_get_row_serializer_with_user_field_names(data_fixture):
|
|||
},
|
||||
],
|
||||
"file_link_row": [
|
||||
{"id": 1, "value": "name.txt"},
|
||||
{"id": 2, "value": ""},
|
||||
{"id": 1, "value": "name.txt", "order": "1.00000000000000000000"},
|
||||
{"id": 2, "value": "", "order": "2.00000000000000000000"},
|
||||
],
|
||||
"id": 2,
|
||||
"link_row": [
|
||||
{"id": 1, "value": "linked_row_1"},
|
||||
{"id": 2, "value": "linked_row_2"},
|
||||
{"id": 3, "value": ""},
|
||||
{
|
||||
"id": 1,
|
||||
"value": "linked_row_1",
|
||||
"order": "1.00000000000000000000",
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"value": "linked_row_2",
|
||||
"order": "2.00000000000000000000",
|
||||
},
|
||||
{"id": 3, "value": "", "order": "3.00000000000000000000"},
|
||||
],
|
||||
"self_link_row": [
|
||||
{"id": 1, "value": "", "order": "1.00000000000000000000"},
|
||||
],
|
||||
"self_link_row": [{"id": 1, "value": ""}],
|
||||
"link_row_without_related": [
|
||||
{"id": 1, "value": "linked_row_1"},
|
||||
{"id": 2, "value": "linked_row_2"},
|
||||
{
|
||||
"id": 1,
|
||||
"value": "linked_row_1",
|
||||
"order": "1.00000000000000000000",
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"value": "linked_row_2",
|
||||
"order": "2.00000000000000000000",
|
||||
},
|
||||
],
|
||||
"long_text": "long_text",
|
||||
"negative_decimal": "-1.2",
|
||||
|
@ -386,6 +404,8 @@ def test_get_row_serializer_with_user_field_names(data_fixture):
|
|||
}
|
||||
)
|
||||
)
|
||||
test_result = json.loads(json.dumps(serializer_instance.data[0]))
|
||||
assert test_result == expected_result
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
|
@ -432,6 +452,8 @@ def test_remap_serialized_row_to_user_field_names(data_fixture):
|
|||
assert remapped_row == {
|
||||
"id": 1,
|
||||
"order": "1.00000000000000000000",
|
||||
"Link": [{"id": 1, "value": "Lookup 1"}],
|
||||
"Link": [
|
||||
{"id": 1, "value": "Lookup 1", "order": AnyStr()},
|
||||
],
|
||||
"Test 1": "Test value",
|
||||
}
|
||||
|
|
|
@ -1032,8 +1032,18 @@ def test_list_rows_join_lookup(api_client, data_fixture, user_field_names):
|
|||
"results": [
|
||||
{
|
||||
f"{link_row_ref}": [
|
||||
{"id": linked_blank_row.id, "value": "", **looked_up_fields_blank},
|
||||
{"id": linked_row.id, "value": "text", **looked_up_fields_row},
|
||||
{
|
||||
"id": linked_blank_row.id,
|
||||
"value": "",
|
||||
**looked_up_fields_blank,
|
||||
"order": AnyStr(),
|
||||
},
|
||||
{
|
||||
"id": linked_row.id,
|
||||
"value": "text",
|
||||
**looked_up_fields_row,
|
||||
"order": AnyStr(),
|
||||
},
|
||||
],
|
||||
"id": row.id,
|
||||
"order": AnyStr(),
|
||||
|
@ -1129,6 +1139,7 @@ def test_list_rows_join_lookup_field_to_same_table(data_fixture, api_client):
|
|||
{
|
||||
"id": table_row.id,
|
||||
"value": "unnamed row 1",
|
||||
"order": AnyStr(),
|
||||
f"field_{linked_table_text_field.id}": "Text 1",
|
||||
f"field_{linked_table_multiselect.id}": [
|
||||
{
|
||||
|
@ -1214,6 +1225,7 @@ def test_list_rows_join_lookup_multiple_link_row_fields(data_fixture, api_client
|
|||
{
|
||||
"id": table_row.id,
|
||||
"value": "unnamed row 1",
|
||||
"order": AnyStr(),
|
||||
f"field_{linked_table_text_field.id}": "Text 1",
|
||||
f"field_{linked_table_text_field_2.id}": "Text 2",
|
||||
},
|
||||
|
@ -1222,6 +1234,7 @@ def test_list_rows_join_lookup_multiple_link_row_fields(data_fixture, api_client
|
|||
{
|
||||
"id": table_row.id,
|
||||
"value": "unnamed row 1",
|
||||
"order": AnyStr(),
|
||||
f"field_{linked_table_2_text_field.id}": "Table 2 Text 1",
|
||||
},
|
||||
],
|
||||
|
@ -1368,6 +1381,7 @@ def test_list_rows_join_lookup_field_multiple_lookups_user_field_names(
|
|||
{
|
||||
"id": table_row.id,
|
||||
"value": "unnamed row 1",
|
||||
"order": AnyStr(),
|
||||
f"{linked_table_text_field.name}": "Text 1",
|
||||
f"{linked_table_text_field_2.name}": "Text 2",
|
||||
},
|
||||
|
@ -1375,6 +1389,7 @@ def test_list_rows_join_lookup_field_multiple_lookups_user_field_names(
|
|||
f"{link_row_field_2.name}": [
|
||||
{
|
||||
"id": table_row.id,
|
||||
"order": AnyStr(),
|
||||
"value": "unnamed row 1",
|
||||
f"{linked_table_2_text_field.name}": "Table 2 Text 1",
|
||||
},
|
||||
|
@ -2717,6 +2732,7 @@ def test_list_rows_with_attribute_names(api_client, data_fixture):
|
|||
link_field = FieldHandler().create_field(
|
||||
user, table, "link_row", link_row_table=table_to_link_with, name="Link"
|
||||
)
|
||||
password_field = data_fixture.create_password_field(name="Password", table=table)
|
||||
|
||||
model = table.get_model()
|
||||
row_1 = model.objects.create(
|
||||
|
@ -2742,6 +2758,7 @@ def test_list_rows_with_attribute_names(api_client, data_fixture):
|
|||
"id": 1,
|
||||
"order": "1.00000000000000000000",
|
||||
"Link": [],
|
||||
"Password": None,
|
||||
}
|
||||
]
|
||||
|
||||
|
@ -2762,6 +2779,7 @@ def test_list_rows_with_attribute_names(api_client, data_fixture):
|
|||
"order": "1.00000000000000000000",
|
||||
"Price,": "2",
|
||||
"Link": [],
|
||||
"Password": None,
|
||||
}
|
||||
|
||||
url = reverse("api:database:rows:list", kwargs={"table_id": table.id})
|
||||
|
@ -2795,6 +2813,7 @@ def test_list_rows_with_attribute_names(api_client, data_fixture):
|
|||
"id": 1,
|
||||
"order": "1.00000000000000000000",
|
||||
"Link": [],
|
||||
"Password": None,
|
||||
}
|
||||
]
|
||||
|
||||
|
@ -2808,7 +2827,7 @@ def test_list_rows_with_attribute_names(api_client, data_fixture):
|
|||
|
||||
url = reverse("api:database:rows:list", kwargs={"table_id": table.id})
|
||||
response = api_client.get(
|
||||
f"{url}?user_field_names=true&order_by={link_field.name}",
|
||||
f"{url}?user_field_names=true&order_by={password_field.name}",
|
||||
format="json",
|
||||
HTTP_AUTHORIZATION=f"JWT {jwt_token}",
|
||||
)
|
||||
|
@ -2816,8 +2835,8 @@ def test_list_rows_with_attribute_names(api_client, data_fixture):
|
|||
assert response.status_code == HTTP_400_BAD_REQUEST
|
||||
assert (
|
||||
response_json["detail"]
|
||||
== "It is not possible to order by Link because the field type "
|
||||
"link_row does not support filtering."
|
||||
== "It is not possible to order by Password because the field type "
|
||||
"password does not support filtering."
|
||||
)
|
||||
|
||||
url = reverse("api:database:rows:list", kwargs={"table_id": table.id})
|
||||
|
@ -2836,6 +2855,7 @@ def test_list_rows_with_attribute_names(api_client, data_fixture):
|
|||
"id": 1,
|
||||
"order": "1.00000000000000000000",
|
||||
"Link": [],
|
||||
"Password": None,
|
||||
},
|
||||
{
|
||||
'"Name, 2"': True,
|
||||
|
@ -2844,6 +2864,7 @@ def test_list_rows_with_attribute_names(api_client, data_fixture):
|
|||
"id": 2,
|
||||
"order": "1.00000000000000000000",
|
||||
"Link": [],
|
||||
"Password": None,
|
||||
},
|
||||
]
|
||||
|
||||
|
@ -2863,6 +2884,7 @@ def test_list_rows_with_attribute_names(api_client, data_fixture):
|
|||
"id": 2,
|
||||
"order": "1.00000000000000000000",
|
||||
"Link": [],
|
||||
"Password": None,
|
||||
},
|
||||
{
|
||||
'"Name, 2"': False,
|
||||
|
@ -2871,6 +2893,7 @@ def test_list_rows_with_attribute_names(api_client, data_fixture):
|
|||
"id": 1,
|
||||
"order": "1.00000000000000000000",
|
||||
"Link": [],
|
||||
"Password": None,
|
||||
},
|
||||
]
|
||||
|
||||
|
@ -2909,6 +2932,7 @@ def test_list_rows_with_attribute_names(api_client, data_fixture):
|
|||
f"field_{field_3.id}": False,
|
||||
f"field_{field_2.id}": "2",
|
||||
f"field_{link_field.id}": [],
|
||||
f"field_{password_field.id}": None,
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
|
@ -2917,6 +2941,7 @@ def test_list_rows_with_attribute_names(api_client, data_fixture):
|
|||
f"field_{field_3.id}": True,
|
||||
f"field_{field_2.id}": "1",
|
||||
f"field_{link_field.id}": [],
|
||||
f"field_{password_field.id}": None,
|
||||
},
|
||||
]
|
||||
|
||||
|
|
|
@ -939,7 +939,6 @@ def test_form_view_link_row_lookup_view(api_client, data_fixture):
|
|||
assert len(response_json["results"]) == 3
|
||||
assert response_json["results"][0]["id"] == i1.id
|
||||
assert response_json["results"][0]["value"] == "Test 1"
|
||||
assert len(response_json["results"][0]) == 2
|
||||
assert response_json["results"][1]["id"] == i2.id
|
||||
assert response_json["results"][2]["id"] == i3.id
|
||||
|
||||
|
|
|
@ -11,7 +11,6 @@ from rest_framework.status import (
|
|||
)
|
||||
|
||||
from baserow.contrib.database.api.constants import PUBLIC_PLACEHOLDER_ENTITY_ID
|
||||
from baserow.contrib.database.fields.handler import FieldHandler
|
||||
from baserow.contrib.database.rows.handler import RowHandler
|
||||
from baserow.contrib.database.search.handler import ALL_SEARCH_MODES, SearchHandler
|
||||
|
||||
|
@ -970,16 +969,9 @@ def test_list_rows_public_with_query_param_filter(api_client, data_fixture):
|
|||
def test_list_rows_public_with_query_param_order(api_client, data_fixture):
|
||||
user, token = data_fixture.create_user_and_token()
|
||||
table = data_fixture.create_database_table(user=user)
|
||||
table_2 = data_fixture.create_database_table(database=table.database)
|
||||
public_field = data_fixture.create_text_field(table=table, name="public")
|
||||
hidden_field = data_fixture.create_text_field(table=table, name="hidden")
|
||||
link_row_field = FieldHandler().create_field(
|
||||
user=user,
|
||||
table=table,
|
||||
type_name="link_row",
|
||||
name="Link",
|
||||
link_row_table=table_2,
|
||||
)
|
||||
password_field = data_fixture.create_password_field(table=table, name="password")
|
||||
gallery_view = data_fixture.create_gallery_view(table=table, user=user, public=True)
|
||||
data_fixture.create_gallery_view_field_option(
|
||||
gallery_view, public_field, hidden=False
|
||||
|
@ -988,7 +980,7 @@ def test_list_rows_public_with_query_param_order(api_client, data_fixture):
|
|||
gallery_view, hidden_field, hidden=True
|
||||
)
|
||||
data_fixture.create_gallery_view_field_option(
|
||||
gallery_view, link_row_field, hidden=False
|
||||
gallery_view, password_field, hidden=False
|
||||
)
|
||||
|
||||
first_row = RowHandler().create_row(
|
||||
|
@ -1024,7 +1016,7 @@ def test_list_rows_public_with_query_param_order(api_client, data_fixture):
|
|||
"api:database:views:gallery:public_rows", kwargs={"slug": gallery_view.slug}
|
||||
)
|
||||
response = api_client.get(
|
||||
f"{url}?order_by=field_{link_row_field.id}",
|
||||
f"{url}?order_by=field_{password_field.id}",
|
||||
)
|
||||
response_json = response.json()
|
||||
assert response.status_code == HTTP_400_BAD_REQUEST
|
||||
|
@ -1313,16 +1305,10 @@ def test_list_rows_public_only_searches_by_visible_columns(
|
|||
def test_list_rows_with_query_param_order(api_client, data_fixture):
|
||||
user, token = data_fixture.create_user_and_token()
|
||||
table = data_fixture.create_database_table(user=user)
|
||||
table_2 = data_fixture.create_database_table(database=table.database)
|
||||
text_field = data_fixture.create_text_field(table=table, name="text")
|
||||
hidden_field = data_fixture.create_text_field(table=table, name="hidden")
|
||||
link_row_field = FieldHandler().create_field(
|
||||
user=user,
|
||||
table=table,
|
||||
type_name="link_row",
|
||||
name="Link",
|
||||
link_row_table=table_2,
|
||||
)
|
||||
password_field = data_fixture.create_password_field(table=table, name="password")
|
||||
|
||||
gallery_view = data_fixture.create_gallery_view(
|
||||
table=table, user=user, create_options=False
|
||||
)
|
||||
|
@ -1333,7 +1319,7 @@ def test_list_rows_with_query_param_order(api_client, data_fixture):
|
|||
gallery_view, hidden_field, hidden=True
|
||||
)
|
||||
data_fixture.create_gallery_view_field_option(
|
||||
gallery_view, link_row_field, hidden=False
|
||||
gallery_view, password_field, hidden=False
|
||||
)
|
||||
first_row = RowHandler().create_row(
|
||||
user, table, values={"text": "a", "hidden": "a"}, user_field_names=True
|
||||
|
@ -1369,7 +1355,7 @@ def test_list_rows_with_query_param_order(api_client, data_fixture):
|
|||
|
||||
# sorting on unsupported field
|
||||
response = api_client.get(
|
||||
f"{url}?order_by=field_{link_row_field.id}",
|
||||
f"{url}?order_by=field_{password_field.id}",
|
||||
**{"HTTP_AUTHORIZATION": f"JWT {token}"},
|
||||
)
|
||||
response_json = response.json()
|
||||
|
|
|
@ -3673,22 +3673,15 @@ def test_list_rows_public_with_query_param_advanced_filters(api_client, data_fix
|
|||
def test_list_rows_with_query_param_order(api_client, data_fixture):
|
||||
user, token = data_fixture.create_user_and_token()
|
||||
table = data_fixture.create_database_table(user=user)
|
||||
table_2 = data_fixture.create_database_table(database=table.database)
|
||||
text_field = data_fixture.create_text_field(table=table, name="text")
|
||||
hidden_field = data_fixture.create_text_field(table=table, name="hidden")
|
||||
link_row_field = FieldHandler().create_field(
|
||||
user=user,
|
||||
table=table,
|
||||
type_name="link_row",
|
||||
name="Link",
|
||||
link_row_table=table_2,
|
||||
)
|
||||
password_field = data_fixture.create_password_field(table=table, name="password")
|
||||
grid_view = data_fixture.create_grid_view(
|
||||
table=table, user=user, create_options=False
|
||||
)
|
||||
data_fixture.create_grid_view_field_option(grid_view, text_field, hidden=False)
|
||||
data_fixture.create_grid_view_field_option(grid_view, hidden_field, hidden=True)
|
||||
data_fixture.create_grid_view_field_option(grid_view, link_row_field, hidden=False)
|
||||
data_fixture.create_grid_view_field_option(grid_view, password_field, hidden=False)
|
||||
first_row = RowHandler().create_row(
|
||||
user, table, values={"text": "a", "hidden": "a"}, user_field_names=True
|
||||
)
|
||||
|
@ -3721,7 +3714,7 @@ def test_list_rows_with_query_param_order(api_client, data_fixture):
|
|||
|
||||
# sorting on unsupported field
|
||||
response = api_client.get(
|
||||
f"{url}?order_by=field_{link_row_field.id}",
|
||||
f"{url}?order_by=field_{password_field.id}",
|
||||
**{"HTTP_AUTHORIZATION": f"JWT {token}"},
|
||||
)
|
||||
response_json = response.json()
|
||||
|
@ -3733,22 +3726,15 @@ def test_list_rows_with_query_param_order(api_client, data_fixture):
|
|||
def test_list_rows_public_with_query_param_order(api_client, data_fixture):
|
||||
user, token = data_fixture.create_user_and_token()
|
||||
table = data_fixture.create_database_table(user=user)
|
||||
table_2 = data_fixture.create_database_table(database=table.database)
|
||||
public_field = data_fixture.create_text_field(table=table, name="public")
|
||||
hidden_field = data_fixture.create_text_field(table=table, name="hidden")
|
||||
link_row_field = FieldHandler().create_field(
|
||||
user=user,
|
||||
table=table,
|
||||
type_name="link_row",
|
||||
name="Link",
|
||||
link_row_table=table_2,
|
||||
)
|
||||
password_field = data_fixture.create_password_field(table=table, name="password")
|
||||
grid_view = data_fixture.create_grid_view(
|
||||
table=table, user=user, public=True, create_options=False
|
||||
)
|
||||
data_fixture.create_grid_view_field_option(grid_view, public_field, hidden=False)
|
||||
data_fixture.create_grid_view_field_option(grid_view, hidden_field, hidden=True)
|
||||
data_fixture.create_grid_view_field_option(grid_view, link_row_field, hidden=False)
|
||||
data_fixture.create_grid_view_field_option(grid_view, password_field, hidden=False)
|
||||
|
||||
first_row = RowHandler().create_row(
|
||||
user, table, values={"public": "a", "hidden": "y"}, user_field_names=True
|
||||
|
@ -3783,7 +3769,7 @@ def test_list_rows_public_with_query_param_order(api_client, data_fixture):
|
|||
"api:database:views:grid:public_rows", kwargs={"slug": grid_view.slug}
|
||||
)
|
||||
response = api_client.get(
|
||||
f"{url}?order_by=field_{link_row_field.id}",
|
||||
f"{url}?order_by=field_{password_field.id}",
|
||||
)
|
||||
response_json = response.json()
|
||||
assert response.status_code == HTTP_400_BAD_REQUEST
|
||||
|
@ -3794,24 +3780,17 @@ def test_list_rows_public_with_query_param_order(api_client, data_fixture):
|
|||
def test_list_rows_public_with_query_param_group_by(api_client, data_fixture):
|
||||
user, token = data_fixture.create_user_and_token()
|
||||
table = data_fixture.create_database_table(user=user)
|
||||
table_2 = data_fixture.create_database_table(database=table.database)
|
||||
public_field = data_fixture.create_text_field(table=table, name="public")
|
||||
public_field_2 = data_fixture.create_text_field(table=table, name="public2")
|
||||
hidden_field = data_fixture.create_text_field(table=table, name="hidden")
|
||||
link_row_field = FieldHandler().create_field(
|
||||
user=user,
|
||||
table=table,
|
||||
type_name="link_row",
|
||||
name="Link",
|
||||
link_row_table=table_2,
|
||||
)
|
||||
password_field = data_fixture.create_password_field(table=table, name="password")
|
||||
grid_view = data_fixture.create_grid_view(
|
||||
table=table, user=user, public=True, create_options=False
|
||||
)
|
||||
data_fixture.create_grid_view_field_option(grid_view, public_field, hidden=False)
|
||||
data_fixture.create_grid_view_field_option(grid_view, public_field_2, hidden=False)
|
||||
data_fixture.create_grid_view_field_option(grid_view, hidden_field, hidden=True)
|
||||
data_fixture.create_grid_view_field_option(grid_view, link_row_field, hidden=False)
|
||||
data_fixture.create_grid_view_field_option(grid_view, password_field, hidden=False)
|
||||
|
||||
first_row = RowHandler().create_row(
|
||||
user,
|
||||
|
@ -3880,7 +3859,7 @@ def test_list_rows_public_with_query_param_group_by(api_client, data_fixture):
|
|||
"api:database:views:grid:public_rows", kwargs={"slug": grid_view.slug}
|
||||
)
|
||||
response = api_client.get(
|
||||
f"{url}?group_by=field_{link_row_field.id}",
|
||||
f"{url}?group_by=field_{password_field.id}",
|
||||
)
|
||||
response_json = response.json()
|
||||
assert response.status_code == HTTP_400_BAD_REQUEST
|
||||
|
@ -4234,7 +4213,6 @@ def test_grid_view_link_row_lookup_view(api_client, data_fixture):
|
|||
assert len(response_json["results"]) == 3
|
||||
assert response_json["results"][0]["id"] == i1.id
|
||||
assert response_json["results"][0]["value"] == "Test 1"
|
||||
assert len(response_json["results"][0]) == 2
|
||||
assert response_json["results"][1]["id"] == i2.id
|
||||
assert response_json["results"][2]["id"] == i3.id
|
||||
|
||||
|
|
|
@ -310,8 +310,9 @@ def test_update_view_group_by(api_client, data_fixture):
|
|||
group_by_1 = data_fixture.create_view_group_by(user=user, order="DESC")
|
||||
group_by_2 = data_fixture.create_view_group_by()
|
||||
group_by_3 = data_fixture.create_view_group_by(view=group_by_1.view, order="ASC")
|
||||
field_1 = data_fixture.create_text_field(table=group_by_1.view.table)
|
||||
link_row_field = data_fixture.create_link_row_field(table=group_by_1.view.table)
|
||||
table = group_by_1.view.table
|
||||
field_1 = data_fixture.create_text_field(table=table)
|
||||
password_field = data_fixture.create_password_field(table=table)
|
||||
field_2 = data_fixture.create_text_field()
|
||||
|
||||
response = api_client.patch(
|
||||
|
@ -371,7 +372,7 @@ def test_update_view_group_by(api_client, data_fixture):
|
|||
"api:database:views:group_by_item",
|
||||
kwargs={"view_group_by_id": group_by_1.id},
|
||||
),
|
||||
{"field": link_row_field.id},
|
||||
{"field": password_field.id},
|
||||
format="json",
|
||||
HTTP_AUTHORIZATION=f"JWT {token}",
|
||||
)
|
||||
|
|
|
@ -80,7 +80,7 @@ def test_create_view_sort(api_client, data_fixture):
|
|||
field_2 = data_fixture.create_text_field(table=table_2)
|
||||
field_3 = data_fixture.create_text_field(table=table_1)
|
||||
field_4 = data_fixture.create_text_field(table=table_1)
|
||||
link_row_field = data_fixture.create_link_row_field(table=table_1)
|
||||
password_field = data_fixture.create_password_field(table=table_1)
|
||||
view_1 = data_fixture.create_grid_view(table=table_1)
|
||||
view_2 = data_fixture.create_grid_view(table=table_2)
|
||||
|
||||
|
@ -145,7 +145,7 @@ def test_create_view_sort(api_client, data_fixture):
|
|||
|
||||
response = api_client.post(
|
||||
reverse("api:database:views:list_sortings", kwargs={"view_id": view_1.id}),
|
||||
{"field": link_row_field.id, "order": "ASC"},
|
||||
{"field": password_field.id, "order": "ASC"},
|
||||
format="json",
|
||||
HTTP_AUTHORIZATION=f"JWT {token}",
|
||||
)
|
||||
|
@ -260,7 +260,7 @@ def test_update_view_sort(api_client, data_fixture):
|
|||
sort_2 = data_fixture.create_view_sort()
|
||||
sort_3 = data_fixture.create_view_sort(view=sort_1.view, order="ASC")
|
||||
field_1 = data_fixture.create_text_field(table=sort_1.view.table)
|
||||
link_row_field = data_fixture.create_link_row_field(table=sort_1.view.table)
|
||||
password_field = data_fixture.create_password_field(table=sort_1.view.table)
|
||||
field_2 = data_fixture.create_text_field()
|
||||
|
||||
response = api_client.patch(
|
||||
|
@ -308,7 +308,7 @@ def test_update_view_sort(api_client, data_fixture):
|
|||
|
||||
response = api_client.patch(
|
||||
reverse("api:database:views:sort_item", kwargs={"view_sort_id": sort_1.id}),
|
||||
{"field": link_row_field.id},
|
||||
{"field": password_field.id},
|
||||
format="json",
|
||||
HTTP_AUTHORIZATION=f"JWT {token}",
|
||||
)
|
||||
|
|
|
@ -11,6 +11,7 @@ from baserow.contrib.database.formula import BaserowFormulaNumberType
|
|||
from baserow.contrib.database.rows.handler import RowHandler
|
||||
from baserow.core.handler import CoreHandler
|
||||
from baserow.core.registries import ImportExportConfig
|
||||
from baserow.test_utils.helpers import AnyStr
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
|
@ -182,8 +183,8 @@ def test_can_update_count_field_value(
|
|||
{
|
||||
f"field_{table_primary_field.id}": None,
|
||||
f"field_{linkrowfield.id}": [
|
||||
{"id": a.id, "value": "primary a"},
|
||||
{"id": b.id, "value": "primary b"},
|
||||
{"id": a.id, "value": "primary a", "order": AnyStr()},
|
||||
{"id": b.id, "value": "primary b", "order": AnyStr()},
|
||||
],
|
||||
f"field_{count_field.id}": "2",
|
||||
"id": table_row.id,
|
||||
|
@ -246,7 +247,9 @@ def test_can_batch_create_count_field_value(
|
|||
"id": 1,
|
||||
"order": "1.00000000000000000000",
|
||||
f"field_{table_primary_field.id}": "row 1",
|
||||
f"field_{link_row_field.id}": [{"id": 1, "value": "row A"}],
|
||||
f"field_{link_row_field.id}": [
|
||||
{"id": 1, "value": "row A", "order": AnyStr()}
|
||||
],
|
||||
f"field_{count_field.id}": "1",
|
||||
}
|
||||
]
|
||||
|
@ -305,7 +308,9 @@ def test_can_batch_update_count_field_value(
|
|||
"id": 1,
|
||||
"order": "1.00000000000000000000",
|
||||
f"field_{table_primary_field.id}": "row 1",
|
||||
f"field_{link_row_field.id}": [{"id": 1, "value": "row A"}],
|
||||
f"field_{link_row_field.id}": [
|
||||
{"id": 1, "value": "row A", "order": AnyStr()}
|
||||
],
|
||||
f"field_{count_field.id}": "1",
|
||||
}
|
||||
]
|
||||
|
|
|
@ -37,6 +37,7 @@ from baserow.contrib.database.views.exceptions import (
|
|||
from baserow.contrib.database.views.handler import ViewHandler
|
||||
from baserow.contrib.database.views.models import SORT_ORDER_ASC, SORT_ORDER_DESC
|
||||
from baserow.contrib.database.views.registries import view_filter_type_registry
|
||||
from baserow.test_utils.helpers import AnyStr
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
|
@ -366,8 +367,8 @@ def test_can_update_lookup_field_value(
|
|||
{
|
||||
f"field_{table_primary_field.id}": None,
|
||||
f"field_{linkrowfield.id}": [
|
||||
{"id": a.id, "value": "primary a"},
|
||||
{"id": b.id, "value": "primary b"},
|
||||
{"id": a.id, "value": "primary a", "order": AnyStr()},
|
||||
{"id": b.id, "value": "primary b", "order": AnyStr()},
|
||||
],
|
||||
f"field_{formulafield.id}": [
|
||||
{"value": "yes", "id": a.id},
|
||||
|
@ -401,8 +402,8 @@ def test_can_update_lookup_field_value(
|
|||
{
|
||||
f"field_{table_primary_field.id}": None,
|
||||
f"field_{linkrowfield.id}": [
|
||||
{"id": a.id, "value": "primary a"},
|
||||
{"id": b.id, "value": "primary b"},
|
||||
{"id": a.id, "value": "primary a", "order": AnyStr()},
|
||||
{"id": b.id, "value": "primary b", "order": AnyStr()},
|
||||
],
|
||||
f"field_{formulafield.id}": [
|
||||
{"value": "no", "id": a.id},
|
||||
|
@ -486,8 +487,8 @@ def test_nested_lookup_with_formula(
|
|||
{
|
||||
f"field_{table_primary_field.id}": table1_x.p,
|
||||
f"field_{linkrowfield.id}": [
|
||||
{"id": table2_1.id, "value": table2_1.p},
|
||||
{"id": table2_2.id, "value": table2_2.p},
|
||||
{"id": table2_1.id, "value": table2_1.p, "order": AnyStr()},
|
||||
{"id": table2_2.id, "value": table2_2.p, "order": AnyStr()},
|
||||
],
|
||||
f"field_{lookup_field.id}": [
|
||||
{
|
||||
|
@ -503,7 +504,9 @@ def test_nested_lookup_with_formula(
|
|||
},
|
||||
{
|
||||
f"field_{table_primary_field.id}": table1_y.p,
|
||||
f"field_{linkrowfield.id}": [{"id": table2_3.id, "value": table2_3.p}],
|
||||
f"field_{linkrowfield.id}": [
|
||||
{"id": table2_3.id, "value": table2_3.p, "order": AnyStr()}
|
||||
],
|
||||
f"field_{lookup_field.id}": [
|
||||
{
|
||||
"value": table3_c.p,
|
||||
|
@ -586,8 +589,8 @@ def test_can_delete_lookup_field_value(
|
|||
{
|
||||
f"field_{table_primary_field.id}": "table row 1",
|
||||
f"field_{linkrowfield.id}": [
|
||||
{"id": a.id, "value": "primary a"},
|
||||
{"id": b.id, "value": "primary b"},
|
||||
{"id": a.id, "value": "primary a", "order": AnyStr()},
|
||||
{"id": b.id, "value": "primary b", "order": AnyStr()},
|
||||
],
|
||||
f"field_{formulafield.id}": [
|
||||
{"value": "yes", "id": a.id},
|
||||
|
@ -620,7 +623,7 @@ def test_can_delete_lookup_field_value(
|
|||
{
|
||||
f"field_{table_primary_field.id}": "table row 1",
|
||||
f"field_{linkrowfield.id}": [
|
||||
{"id": b.id, "value": "primary b"},
|
||||
{"id": b.id, "value": "primary b", "order": AnyStr()},
|
||||
],
|
||||
f"field_{formulafield.id}": [
|
||||
{"value": "no", "id": b.id},
|
||||
|
@ -699,8 +702,8 @@ def test_can_delete_double_link_lookup_field_value(
|
|||
{
|
||||
f"field_{table_primary_field.id}": "table row 1",
|
||||
f"field_{linkrowfield.id}": [
|
||||
{"id": table2_a.id, "value": "primary a"},
|
||||
{"id": table2_b.id, "value": "primary b"},
|
||||
{"id": table2_a.id, "value": "primary a", "order": AnyStr()},
|
||||
{"id": table2_b.id, "value": "primary b", "order": AnyStr()},
|
||||
],
|
||||
f"field_{formulafield.id}": [
|
||||
{
|
||||
|
@ -745,7 +748,7 @@ def test_can_delete_double_link_lookup_field_value(
|
|||
{
|
||||
f"field_{table_primary_field.id}": "table row 1",
|
||||
f"field_{linkrowfield.id}": [
|
||||
{"id": table2_b.id, "value": "primary b"},
|
||||
{"id": table2_b.id, "value": "primary b", "order": AnyStr()},
|
||||
],
|
||||
f"field_{formulafield.id}": [
|
||||
{
|
||||
|
@ -784,7 +787,7 @@ def test_can_delete_double_link_lookup_field_value(
|
|||
{
|
||||
f"field_{table_primary_field.id}": "table row 1",
|
||||
f"field_{linkrowfield.id}": [
|
||||
{"id": table2_b.id, "value": "primary b"},
|
||||
{"id": table2_b.id, "value": "primary b", "order": AnyStr()},
|
||||
],
|
||||
f"field_{formulafield.id}": [],
|
||||
"id": table_row.id,
|
||||
|
|
|
@ -1,8 +1,14 @@
|
|||
import os
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime, timedelta
|
||||
from io import BytesIO
|
||||
from typing import List, Optional, Type
|
||||
from unittest.mock import patch
|
||||
|
||||
from django.apps.registry import apps
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.db import connection, connections
|
||||
from django.db.models import F
|
||||
from django.shortcuts import reverse
|
||||
from django.test.utils import CaptureQueriesContext
|
||||
|
||||
|
@ -27,16 +33,23 @@ from baserow.contrib.database.fields.exceptions import (
|
|||
)
|
||||
from baserow.contrib.database.fields.field_types import LinkRowFieldType
|
||||
from baserow.contrib.database.fields.handler import FieldHandler
|
||||
from baserow.contrib.database.fields.models import Field, LinkRowField, TextField
|
||||
from baserow.contrib.database.fields.models import (
|
||||
Field,
|
||||
LinkRowField,
|
||||
SelectOption,
|
||||
TextField,
|
||||
)
|
||||
from baserow.contrib.database.fields.registries import field_type_registry
|
||||
from baserow.contrib.database.fields.utils.duration import H_M_S
|
||||
from baserow.contrib.database.rows.handler import RowHandler
|
||||
from baserow.contrib.database.table.handler import TableHandler
|
||||
from baserow.contrib.database.table.models import GeneratedTableModel, Table
|
||||
from baserow.contrib.database.views.handler import ViewHandler
|
||||
from baserow.core.handler import CoreHandler
|
||||
from baserow.core.models import TrashEntry
|
||||
from baserow.core.models import TrashEntry, WorkspaceUser
|
||||
from baserow.core.registries import ImportExportConfig
|
||||
from baserow.core.trash.handler import TrashHandler
|
||||
from baserow.test_utils.helpers import AnyInt
|
||||
from baserow.test_utils.helpers import AnyInt, AnyStr
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
|
@ -1480,8 +1493,8 @@ def test_link_row_field_can_link_same_table(api_client, data_fixture):
|
|||
response_json = response.json()
|
||||
assert response.status_code == HTTP_200_OK
|
||||
assert response_json[f"field_{link_row.id}"] == [
|
||||
{"id": row_1.id, "value": "Tesla"},
|
||||
{"id": row_2.id, "value": "Amazon"},
|
||||
{"id": row_1.id, "value": "Tesla", "order": AnyStr()},
|
||||
{"id": row_2.id, "value": "Amazon", "order": AnyStr()},
|
||||
]
|
||||
|
||||
# can be trashed and restored
|
||||
|
@ -2281,3 +2294,312 @@ def test_dont_export_deleted_relations(data_fixture):
|
|||
)
|
||||
serialized_table_a = get_serialized_table_a(serialized)
|
||||
assert serialized_table_a["rows"][0][link_field.db_column] == [row_b2.id]
|
||||
|
||||
|
||||
@dataclass
|
||||
class LinkRowOrderSetup:
|
||||
table: Table
|
||||
primary_field: Field
|
||||
rows: List[GeneratedTableModel]
|
||||
comparable_field: Optional[Type[Field]] = None
|
||||
|
||||
|
||||
def read_all_chars():
|
||||
dir_path = os.path.dirname(os.path.realpath(__file__))
|
||||
with open(
|
||||
dir_path + "/../../../../../../tests/all_chars.txt", mode="r", encoding="utf-8"
|
||||
) as f:
|
||||
all_chars = f.read()
|
||||
return all_chars
|
||||
|
||||
|
||||
def setup_table_with_single_select_pk(user, data_fixture):
|
||||
table = data_fixture.create_database_table(user=user)
|
||||
primary_field = data_fixture.create_single_select_field(
|
||||
table=table, order=1, name="Single select", primary=True
|
||||
)
|
||||
comparable_field = data_fixture.create_text_field(table=table, order=2, name="f")
|
||||
|
||||
all_chars = read_all_chars()
|
||||
options = SelectOption.objects.bulk_create(
|
||||
[
|
||||
SelectOption(field=primary_field, value=char, order=i)
|
||||
for (i, char) in enumerate(all_chars)
|
||||
]
|
||||
)
|
||||
rows_values = [
|
||||
{
|
||||
f"{primary_field.db_column}": opt.id,
|
||||
f"{comparable_field.db_column}": char,
|
||||
}
|
||||
for (char, opt) in zip(all_chars, options)
|
||||
]
|
||||
|
||||
rows = RowHandler().force_create_rows(user, table, rows_values)
|
||||
return LinkRowOrderSetup(table, primary_field, rows, comparable_field)
|
||||
|
||||
|
||||
def setup_table_with_multiple_select_pk(user, data_fixture):
|
||||
table = data_fixture.create_database_table(user=user)
|
||||
primary_field = data_fixture.create_multiple_select_field(
|
||||
table=table, order=1, name="Multiple select", primary=True
|
||||
)
|
||||
comparable_field = data_fixture.create_text_field(table=table, order=2, name="f")
|
||||
|
||||
all_chars = read_all_chars()
|
||||
opts = SelectOption.objects.bulk_create(
|
||||
[
|
||||
SelectOption(field=primary_field, value=char, order=i)
|
||||
for (i, char) in enumerate(all_chars)
|
||||
]
|
||||
)
|
||||
|
||||
rows_values = [
|
||||
{
|
||||
f"{primary_field.db_column}": [opts[i].id, opts[i - 1 if i > 0 else -1].id],
|
||||
f"{comparable_field.db_column}": char,
|
||||
}
|
||||
for (i, char) in enumerate(all_chars)
|
||||
]
|
||||
|
||||
rows = RowHandler().force_create_rows(user, table, rows_values)
|
||||
return LinkRowOrderSetup(table, primary_field, rows, comparable_field)
|
||||
|
||||
|
||||
def setup_table_with_text_pk(user, data_fixture):
|
||||
table = data_fixture.create_database_table(user=user)
|
||||
primary_field = data_fixture.create_text_field(
|
||||
table=table, order=1, name="Text", primary=True
|
||||
)
|
||||
|
||||
all_chars = read_all_chars()
|
||||
|
||||
model = table.get_model()
|
||||
rows = model.objects.bulk_create(
|
||||
[model(**{primary_field.db_column: char}) for char in all_chars]
|
||||
)
|
||||
return LinkRowOrderSetup(table, primary_field, rows, primary_field)
|
||||
|
||||
|
||||
def setup_table_with_collaborator_pk(user, data_fixture):
|
||||
table = data_fixture.create_database_table(user=user)
|
||||
primary_field = data_fixture.create_multiple_collaborators_field(
|
||||
table=table, order=1, name="collab", primary=True
|
||||
)
|
||||
comparable_field = data_fixture.create_text_field(table=table, order=2, name="f")
|
||||
|
||||
all_chars = read_all_chars()
|
||||
workspace = table.database.workspace
|
||||
|
||||
User = get_user_model()
|
||||
users = User.objects.bulk_create(
|
||||
[
|
||||
User(
|
||||
first_name=char,
|
||||
username=f"user{i}@test.it",
|
||||
email=f"user{i}@test.it",
|
||||
)
|
||||
for (i, char) in enumerate(all_chars, start=1)
|
||||
]
|
||||
)
|
||||
WorkspaceUser.objects.bulk_create(
|
||||
[
|
||||
WorkspaceUser(workspace=workspace, user=usr, order=i)
|
||||
for (i, usr) in enumerate(users, start=1)
|
||||
]
|
||||
)
|
||||
|
||||
rows = RowHandler().force_create_rows(
|
||||
user,
|
||||
table,
|
||||
[
|
||||
{
|
||||
f"{primary_field.db_column}": [{"id": usr.id, "name": usr.first_name}],
|
||||
f"{comparable_field.db_column}": usr.first_name,
|
||||
}
|
||||
for usr in users
|
||||
],
|
||||
)
|
||||
return LinkRowOrderSetup(table, primary_field, rows, comparable_field)
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@pytest.mark.parametrize(
|
||||
"setup_func",
|
||||
[
|
||||
setup_table_with_single_select_pk,
|
||||
setup_table_with_multiple_select_pk,
|
||||
setup_table_with_text_pk,
|
||||
setup_table_with_collaborator_pk,
|
||||
],
|
||||
)
|
||||
def test_text_field_type_get_order_with_collation(setup_func, data_fixture):
|
||||
user = data_fixture.create_user()
|
||||
res = setup_func(user, data_fixture)
|
||||
table_b = res.table
|
||||
|
||||
table_a, table_b, link_field = data_fixture.create_two_linked_tables(
|
||||
user=user, table_b=table_b
|
||||
)
|
||||
|
||||
dir_path = os.path.dirname(os.path.realpath(__file__))
|
||||
with open(
|
||||
dir_path + "/../../../../../../tests/sorted_chars.txt",
|
||||
mode="r",
|
||||
encoding="utf-8",
|
||||
) as f:
|
||||
sorted_chars = f.read()
|
||||
|
||||
model_a = table_a.get_model()
|
||||
RowHandler().force_create_rows(
|
||||
user,
|
||||
table_a,
|
||||
[{link_field.db_column: [row_b.id]} for row_b in res.rows],
|
||||
model=model_a,
|
||||
)
|
||||
|
||||
result = "".join(
|
||||
model_a.objects.all()
|
||||
.order_by_fields_string(link_field.db_column)
|
||||
.annotate(res=F(f"{link_field.db_column}__{res.comparable_field.db_column}"))
|
||||
.values_list("res", flat=True)
|
||||
)
|
||||
|
||||
assert result == sorted_chars
|
||||
|
||||
|
||||
def setup_table_with_duration_pk(user, data_fixture):
|
||||
table = data_fixture.create_database_table(user=user)
|
||||
primary_field = data_fixture.create_duration_field(
|
||||
table=table, order=1, primary=True, duration_format=H_M_S
|
||||
)
|
||||
comparable_field = data_fixture.create_number_field(table=table, order=2)
|
||||
|
||||
values = [
|
||||
(1, 100),
|
||||
(2, 50),
|
||||
(3, 25),
|
||||
(4, None),
|
||||
(5, 75),
|
||||
(6, 25),
|
||||
]
|
||||
|
||||
model = table.get_model()
|
||||
rows = model.objects.bulk_create(
|
||||
[
|
||||
model(
|
||||
id=index,
|
||||
**{
|
||||
primary_field.db_column: timedelta(seconds=value) if value else None
|
||||
},
|
||||
)
|
||||
for (index, value) in values
|
||||
]
|
||||
)
|
||||
return LinkRowOrderSetup(table, primary_field, comparable_field, rows)
|
||||
|
||||
|
||||
def setup_table_with_number_pk(user, data_fixture):
|
||||
table = data_fixture.create_database_table(user=user)
|
||||
primary_field = data_fixture.create_number_field(
|
||||
table=table, order=1, primary=True, number_negative=True
|
||||
)
|
||||
|
||||
values = [
|
||||
(1, 100),
|
||||
(2, 50),
|
||||
(3, 25),
|
||||
(4, None),
|
||||
(5, 75),
|
||||
(6, 25),
|
||||
]
|
||||
|
||||
model = table.get_model()
|
||||
rows = model.objects.bulk_create(
|
||||
[
|
||||
model(id=index, **{primary_field.db_column: value})
|
||||
for (index, value) in values
|
||||
]
|
||||
)
|
||||
return LinkRowOrderSetup(table, primary_field, rows)
|
||||
|
||||
|
||||
def setup_table_with_date_pk(user, data_fixture):
|
||||
table = data_fixture.create_database_table(user=user)
|
||||
primary_field = data_fixture.create_date_field(
|
||||
table=table, order=1, primary=True, date_include_time=True
|
||||
)
|
||||
|
||||
values = [
|
||||
(1, "2024-12-06T11:30:00Z"),
|
||||
(2, "2024-12-06T01:00:00Z"),
|
||||
(3, "2024-12-05T07:00:00Z"),
|
||||
(4, None),
|
||||
(5, "2024-12-06T09:00:00Z"),
|
||||
(6, "2024-12-05T07:00:00Z"),
|
||||
]
|
||||
|
||||
model = table.get_model()
|
||||
rows = model.objects.bulk_create(
|
||||
[
|
||||
model(
|
||||
id=index,
|
||||
**{
|
||||
primary_field.db_column: datetime.fromisoformat(value)
|
||||
if value
|
||||
else None
|
||||
},
|
||||
)
|
||||
for (index, value) in values
|
||||
]
|
||||
)
|
||||
return LinkRowOrderSetup(table, primary_field, rows)
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@pytest.mark.parametrize(
|
||||
"setup_func",
|
||||
[
|
||||
setup_table_with_duration_pk,
|
||||
setup_table_with_number_pk,
|
||||
setup_table_with_date_pk,
|
||||
],
|
||||
)
|
||||
def test_text_field_type_get_order_without_collation(setup_func, data_fixture):
|
||||
user = data_fixture.create_user()
|
||||
res = setup_func(user, data_fixture)
|
||||
table_b = res.table
|
||||
|
||||
table_a, table_b, link_field = data_fixture.create_two_linked_tables(
|
||||
user=user, table_b=table_b
|
||||
)
|
||||
|
||||
values = [
|
||||
(1, [1, 2]),
|
||||
(2, [3]),
|
||||
(3, [4]),
|
||||
(4, []),
|
||||
(5, [5]),
|
||||
(6, [6]),
|
||||
(7, [1, 6]),
|
||||
]
|
||||
|
||||
model_a = table_a.get_model()
|
||||
rows = model_a.objects.bulk_create([model_a(id=index) for (index, _) in values])
|
||||
for row, (_, linked_ids) in zip(rows, values):
|
||||
for linked_id in linked_ids:
|
||||
getattr(row, link_field.db_column).add(linked_id)
|
||||
|
||||
result = list(
|
||||
model_a.objects.all()
|
||||
.order_by_fields_string(link_field.db_column)
|
||||
.values_list("id", flat=True)
|
||||
)
|
||||
assert result == [4, 2, 6, 5, 7, 1, 3]
|
||||
|
||||
result = list(
|
||||
model_a.objects.all()
|
||||
.order_by_fields_string(f"-{link_field.db_column}")
|
||||
.values_list("id", flat=True)
|
||||
)
|
||||
assert result == [4, 3, 1, 7, 5, 6, 2]
|
||||
|
|
|
@ -22,6 +22,7 @@ from baserow.contrib.database.views.handler import ViewHandler
|
|||
from baserow.core.db import specific_iterator
|
||||
from baserow.core.handler import CoreHandler
|
||||
from baserow.core.registries import ImportExportConfig
|
||||
from baserow.test_utils.helpers import AnyStr
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
|
@ -82,8 +83,8 @@ def test_can_update_lookup_field_value(
|
|||
{
|
||||
f"field_{table_primary_field.id}": None,
|
||||
f"field_{linkrowfield.id}": [
|
||||
{"id": a.id, "value": "primary a"},
|
||||
{"id": b.id, "value": "primary b"},
|
||||
{"id": a.id, "value": "primary a", "order": AnyStr()},
|
||||
{"id": b.id, "value": "primary b", "order": AnyStr()},
|
||||
],
|
||||
f"field_{lookup_field.id}": [
|
||||
{"id": a.id, "value": "2021-02-01"},
|
||||
|
@ -117,8 +118,8 @@ def test_can_update_lookup_field_value(
|
|||
{
|
||||
f"field_{table_primary_field.id}": None,
|
||||
f"field_{linkrowfield.id}": [
|
||||
{"id": a.id, "value": "primary a"},
|
||||
{"id": b.id, "value": "primary b"},
|
||||
{"id": a.id, "value": "primary a", "order": AnyStr()},
|
||||
{"id": b.id, "value": "primary b", "order": AnyStr()},
|
||||
],
|
||||
f"field_{lookup_field.id}": [
|
||||
{"id": a.id, "value": "2000-02-01"},
|
||||
|
@ -186,7 +187,9 @@ def test_can_batch_create_lookup_field_value(
|
|||
"id": 1,
|
||||
"order": "1.00000000000000000000",
|
||||
f"field_{table_primary_field.id}": "row 1",
|
||||
f"field_{link_row_field.id}": [{"id": 1, "value": "row A"}],
|
||||
f"field_{link_row_field.id}": [
|
||||
{"id": 1, "value": "row A", "order": AnyStr()}
|
||||
],
|
||||
f"field_{lookup_field.id}": [{"id": 1, "value": "row A"}],
|
||||
}
|
||||
]
|
||||
|
@ -248,7 +251,9 @@ def test_can_batch_update_lookup_field_value(
|
|||
"id": 1,
|
||||
"order": "1.00000000000000000000",
|
||||
f"field_{table_primary_field.id}": "row 1",
|
||||
f"field_{link_row_field.id}": [{"id": 1, "value": "row A"}],
|
||||
f"field_{link_row_field.id}": [
|
||||
{"id": 1, "value": "row A", "order": AnyStr()}
|
||||
],
|
||||
f"field_{lookup_field.id}": [{"id": 1, "value": "row A"}],
|
||||
}
|
||||
]
|
||||
|
@ -312,8 +317,8 @@ def test_can_set_sub_type_options_for_lookup_field(
|
|||
{
|
||||
f"field_{table_primary_field.id}": None,
|
||||
f"field_{linkrowfield.id}": [
|
||||
{"id": a.id, "value": "primary a"},
|
||||
{"id": b.id, "value": "primary b"},
|
||||
{"id": a.id, "value": "primary a", "order": AnyStr()},
|
||||
{"id": b.id, "value": "primary b", "order": AnyStr()},
|
||||
],
|
||||
f"field_{lookup_field.id}": [
|
||||
{"id": a.id, "value": "1.00"},
|
||||
|
@ -385,8 +390,8 @@ def test_can_lookup_single_select(data_fixture, api_client, django_assert_num_qu
|
|||
{
|
||||
f"field_{table_primary_field.id}": None,
|
||||
f"field_{linkrowfield.id}": [
|
||||
{"id": a.id, "value": "primary a"},
|
||||
{"id": b.id, "value": "primary b"},
|
||||
{"id": a.id, "value": "primary a", "order": AnyStr()},
|
||||
{"id": b.id, "value": "primary b", "order": AnyStr()},
|
||||
],
|
||||
f"field_{lookup_field.id}": [
|
||||
{
|
||||
|
@ -738,8 +743,8 @@ def test_moving_a_looked_up_row_updates_the_order(
|
|||
{
|
||||
f"field_{table_primary_field.id}": None,
|
||||
f"field_{linkrowfield.id}": [
|
||||
{"id": a.id, "value": "primary a"},
|
||||
{"id": b.id, "value": "primary b"},
|
||||
{"id": a.id, "value": "primary a", "order": AnyStr()},
|
||||
{"id": b.id, "value": "primary b", "order": AnyStr()},
|
||||
],
|
||||
f"field_{lookup_field.id}": [
|
||||
{"id": a.id, "value": "2021-02-01"},
|
||||
|
@ -773,8 +778,8 @@ def test_moving_a_looked_up_row_updates_the_order(
|
|||
{
|
||||
f"field_{table_primary_field.id}": None,
|
||||
f"field_{linkrowfield.id}": [
|
||||
{"id": b.id, "value": "primary b"},
|
||||
{"id": a.id, "value": "primary a"},
|
||||
{"id": b.id, "value": "primary b", "order": AnyStr()},
|
||||
{"id": a.id, "value": "primary a", "order": AnyStr()},
|
||||
],
|
||||
f"field_{lookup_field.id}": [
|
||||
{"id": b.id, "value": "2022-02-03"},
|
||||
|
@ -872,8 +877,8 @@ def test_can_modify_row_containing_lookup(
|
|||
f"field_{table_primary_field.id}": None,
|
||||
f"field_{table_long_field.id}": None,
|
||||
f"field_{linkrowfield.id}": [
|
||||
{"id": a.id, "value": "primary a"},
|
||||
{"id": b.id, "value": "primary b"},
|
||||
{"id": a.id, "value": "primary a", "order": AnyStr()},
|
||||
{"id": b.id, "value": "primary b", "order": AnyStr()},
|
||||
],
|
||||
f"field_{lookup_field.id}": [
|
||||
{"id": a.id, "value": "2021-02-01"},
|
||||
|
@ -919,8 +924,8 @@ def test_can_modify_row_containing_lookup(
|
|||
f"field_{table_primary_field.id}": "other",
|
||||
f"field_{table_long_field.id}": "other",
|
||||
f"field_{linkrowfield.id}": [
|
||||
{"id": a.id, "value": "primary a"},
|
||||
{"id": b.id, "value": "primary b"},
|
||||
{"id": a.id, "value": "primary a", "order": AnyStr()},
|
||||
{"id": b.id, "value": "primary b", "order": AnyStr()},
|
||||
],
|
||||
f"field_{lookup_field.id}": [
|
||||
{"id": a.id, "value": "2021-02-01"},
|
||||
|
@ -1005,8 +1010,8 @@ def test_deleting_restoring_lookup_target_works(
|
|||
{
|
||||
f"field_{table_primary_field.id}": None,
|
||||
f"field_{linkrowfield.id}": [
|
||||
{"id": a.id, "value": "primary a"},
|
||||
{"id": b.id, "value": "primary b"},
|
||||
{"id": a.id, "value": "primary a", "order": AnyStr()},
|
||||
{"id": b.id, "value": "primary b", "order": AnyStr()},
|
||||
],
|
||||
f"field_{lookup_field.id}": [
|
||||
{"id": a.id, "value": "2021-02-01"},
|
||||
|
@ -1042,8 +1047,8 @@ def test_deleting_restoring_lookup_target_works(
|
|||
{
|
||||
f"field_{table_primary_field.id}": None,
|
||||
f"field_{linkrowfield.id}": [
|
||||
{"id": a.id, "value": "primary a"},
|
||||
{"id": b.id, "value": "primary b"},
|
||||
{"id": a.id, "value": "primary a", "order": AnyStr()},
|
||||
{"id": b.id, "value": "primary b", "order": AnyStr()},
|
||||
],
|
||||
f"field_{lookup_field.id}": None,
|
||||
f"field_{string_agg.id}": None,
|
||||
|
@ -1095,8 +1100,8 @@ def test_deleting_restoring_lookup_target_works(
|
|||
{
|
||||
f"field_{table_primary_field.id}": None,
|
||||
f"field_{linkrowfield.id}": [
|
||||
{"id": a.id, "value": "primary a"},
|
||||
{"id": b.id, "value": "primary b"},
|
||||
{"id": a.id, "value": "primary a", "order": AnyStr()},
|
||||
{"id": b.id, "value": "primary b", "order": AnyStr()},
|
||||
],
|
||||
f"field_{lookup_field.id}": [
|
||||
{"id": a.id, "value": "2021-02-01"},
|
||||
|
@ -1271,8 +1276,8 @@ def test_deleting_related_link_row_field_dep_breaks_deps(
|
|||
{
|
||||
f"field_{table_primary_field.id}": None,
|
||||
f"field_{linkrowfield.id}": [
|
||||
{"id": a.id, "value": "primary a"},
|
||||
{"id": b.id, "value": "primary b"},
|
||||
{"id": a.id, "value": "primary a", "order": AnyStr()},
|
||||
{"id": b.id, "value": "primary b", "order": AnyStr()},
|
||||
],
|
||||
f"field_{lookup_field.id}": [
|
||||
{"id": a.id, "value": "2021-02-01"},
|
||||
|
@ -1494,8 +1499,8 @@ def test_deleting_table_with_dependants_works(
|
|||
{
|
||||
f"field_{table_primary_field.id}": None,
|
||||
f"field_{linkrowfield.id}": [
|
||||
{"id": a.id, "value": "primary a"},
|
||||
{"id": b.id, "value": "primary b"},
|
||||
{"id": a.id, "value": "primary a", "order": AnyStr()},
|
||||
{"id": b.id, "value": "primary b", "order": AnyStr()},
|
||||
],
|
||||
f"field_{lookup_field.id}": [
|
||||
{"id": a.id, "value": "2021-02-01"},
|
||||
|
@ -1901,7 +1906,11 @@ def test_can_modify_row_containing_lookup_diamond_dep(
|
|||
{
|
||||
"id": table2_row1.id,
|
||||
"linkrowfield": [
|
||||
{"id": starting_row.id, "value": "table1_primary_row_1"}
|
||||
{
|
||||
"id": starting_row.id,
|
||||
"value": "table1_primary_row_1",
|
||||
"order": AnyStr(),
|
||||
}
|
||||
],
|
||||
"middle_lookup": "table1_primary_row_1",
|
||||
"order": "1.00000000000000000000",
|
||||
|
@ -1909,7 +1918,11 @@ def test_can_modify_row_containing_lookup_diamond_dep(
|
|||
{
|
||||
"id": table2_row2.id,
|
||||
"linkrowfield": [
|
||||
{"id": starting_row.id, "value": "table1_primary_row_1"}
|
||||
{
|
||||
"id": starting_row.id,
|
||||
"value": "table1_primary_row_1",
|
||||
"order": AnyStr(),
|
||||
}
|
||||
],
|
||||
"middle_lookup": "table1_primary_row_1",
|
||||
"order": "2.00000000000000000000",
|
||||
|
@ -1928,7 +1941,9 @@ def test_can_modify_row_containing_lookup_diamond_dep(
|
|||
"previous": None,
|
||||
"results": [
|
||||
{
|
||||
"a": [{"id": table2_row1.id, "value": "table2_row1"}],
|
||||
"a": [
|
||||
{"id": table2_row1.id, "value": "table2_row1", "order": AnyStr()}
|
||||
],
|
||||
"b": [],
|
||||
"final_lookup": "table1_primary_row_1",
|
||||
"id": 1,
|
||||
|
@ -1936,7 +1951,9 @@ def test_can_modify_row_containing_lookup_diamond_dep(
|
|||
},
|
||||
{
|
||||
"a": [],
|
||||
"b": [{"id": table2_row2.id, "value": "table2_row2"}],
|
||||
"b": [
|
||||
{"id": table2_row2.id, "value": "table2_row2", "order": AnyStr()}
|
||||
],
|
||||
"final_lookup": "table1_primary_row_1",
|
||||
"id": 2,
|
||||
"order": "2.00000000000000000000",
|
||||
|
@ -1967,7 +1984,9 @@ def test_can_modify_row_containing_lookup_diamond_dep(
|
|||
"previous": None,
|
||||
"results": [
|
||||
{
|
||||
"a": [{"id": table2_row1.id, "value": "table2_row1"}],
|
||||
"a": [
|
||||
{"id": table2_row1.id, "value": "table2_row1", "order": AnyStr()}
|
||||
],
|
||||
"b": [],
|
||||
"final_lookup": "changed",
|
||||
"id": table3_row1.id,
|
||||
|
@ -1975,7 +1994,9 @@ def test_can_modify_row_containing_lookup_diamond_dep(
|
|||
},
|
||||
{
|
||||
"a": [],
|
||||
"b": [{"id": table2_row2.id, "value": "table2_row2"}],
|
||||
"b": [
|
||||
{"id": table2_row2.id, "value": "table2_row2", "order": AnyStr()}
|
||||
],
|
||||
"final_lookup": "changed",
|
||||
"id": table3_row2.id,
|
||||
"order": "2.00000000000000000000",
|
||||
|
|
|
@ -16,6 +16,7 @@ from baserow.contrib.database.rows.handler import RowHandler
|
|||
from baserow.core.formula.parser.exceptions import FormulaFunctionTypeDoesNotExist
|
||||
from baserow.core.handler import CoreHandler
|
||||
from baserow.core.registries import ImportExportConfig
|
||||
from baserow.test_utils.helpers import AnyStr
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
|
@ -363,8 +364,8 @@ def test_can_update_rollup_field_value(data_fixture, api_client):
|
|||
{
|
||||
f"field_{table_primary_field.id}": None,
|
||||
f"field_{link_row_field.id}": [
|
||||
{"id": a.id, "value": "primary a"},
|
||||
{"id": b.id, "value": "primary b"},
|
||||
{"id": a.id, "value": "primary a", "order": AnyStr()},
|
||||
{"id": b.id, "value": "primary b", "order": AnyStr()},
|
||||
],
|
||||
f"field_{rollup_field.id}": "3",
|
||||
"id": table_row.id,
|
||||
|
@ -395,8 +396,8 @@ def test_can_update_rollup_field_value(data_fixture, api_client):
|
|||
{
|
||||
f"field_{table_primary_field.id}": None,
|
||||
f"field_{link_row_field.id}": [
|
||||
{"id": a.id, "value": "primary a"},
|
||||
{"id": b.id, "value": "primary b"},
|
||||
{"id": a.id, "value": "primary a", "order": AnyStr()},
|
||||
{"id": b.id, "value": "primary b", "order": AnyStr()},
|
||||
],
|
||||
f"field_{rollup_field.id}": "7",
|
||||
"id": table_row.id,
|
||||
|
@ -459,7 +460,9 @@ def test_can_create_rollup_field_value(data_fixture, api_client):
|
|||
"id": 1,
|
||||
"order": "1.00000000000000000000",
|
||||
f"field_{table_primary_field.id}": "row 1",
|
||||
f"field_{link_row_field.id}": [{"id": 1, "value": "row A"}],
|
||||
f"field_{link_row_field.id}": [
|
||||
{"id": 1, "value": "row A", "order": AnyStr()}
|
||||
],
|
||||
f"field_{rollup_field.id}": "5",
|
||||
}
|
||||
]
|
||||
|
@ -520,7 +523,9 @@ def test_can_create_rollup_field_with_formula_properties(data_fixture, api_clien
|
|||
"id": 1,
|
||||
"order": "1.00000000000000000000",
|
||||
f"field_{table_primary_field.id}": "row 1",
|
||||
f"field_{link_row_field.id}": [{"id": 1, "value": "row A"}],
|
||||
f"field_{link_row_field.id}": [
|
||||
{"id": 1, "value": "row A", "order": AnyStr()}
|
||||
],
|
||||
f"field_{rollup_field.id}": "5.00",
|
||||
}
|
||||
]
|
||||
|
|
|
@ -8,6 +8,7 @@ from baserow.contrib.database.fields.handler import FieldHandler
|
|||
from baserow.contrib.database.rows.handler import RowHandler
|
||||
from baserow.contrib.database.webhooks.registries import webhook_event_type_registry
|
||||
from baserow.contrib.database.ws.rows.signals import serialize_rows_values
|
||||
from baserow.test_utils.helpers import AnyStr
|
||||
|
||||
|
||||
@pytest.mark.django_db()
|
||||
|
@ -188,7 +189,9 @@ def test_rows_updated_event_type(data_fixture):
|
|||
"id": 1,
|
||||
"order": "1.00000000000000000000",
|
||||
f"field_{text_field.id}": "New Test value",
|
||||
f"field_{link_row_field.id}": [{"id": 1, "value": "Lookup 1"}],
|
||||
f"field_{link_row_field.id}": [
|
||||
{"id": 1, "value": "Lookup 1", "order": AnyStr()}
|
||||
],
|
||||
}
|
||||
],
|
||||
"old_items": [
|
||||
|
@ -196,7 +199,9 @@ def test_rows_updated_event_type(data_fixture):
|
|||
"id": 1,
|
||||
"order": "1.00000000000000000000",
|
||||
f"field_{text_field.id}": "Old Test value",
|
||||
f"field_{link_row_field.id}": [{"id": 1, "value": "Lookup 1"}],
|
||||
f"field_{link_row_field.id}": [
|
||||
{"id": 1, "value": "Lookup 1", "order": AnyStr()}
|
||||
],
|
||||
}
|
||||
],
|
||||
}
|
||||
|
@ -223,7 +228,9 @@ def test_rows_updated_event_type(data_fixture):
|
|||
"id": 1,
|
||||
"order": "1.00000000000000000000",
|
||||
f"{text_field.name}": "New Test value",
|
||||
f"{link_row_field.name}": [{"id": 1, "value": "Lookup 1"}],
|
||||
f"{link_row_field.name}": [
|
||||
{"id": 1, "value": "Lookup 1", "order": AnyStr()}
|
||||
],
|
||||
}
|
||||
],
|
||||
"old_items": [
|
||||
|
@ -231,7 +238,9 @@ def test_rows_updated_event_type(data_fixture):
|
|||
"id": 1,
|
||||
"order": "1.00000000000000000000",
|
||||
f"{text_field.name}": "Old Test value",
|
||||
f"{link_row_field.name}": [{"id": 1, "value": "Lookup 1"}],
|
||||
f"{link_row_field.name}": [
|
||||
{"id": 1, "value": "Lookup 1", "order": AnyStr()}
|
||||
],
|
||||
}
|
||||
],
|
||||
}
|
||||
|
|
|
@ -455,14 +455,15 @@ def test_search_all_fields_queryset(data_fixture, search_mode):
|
|||
@pytest.mark.django_db
|
||||
def test_order_by_fields_string_queryset(data_fixture):
|
||||
table = data_fixture.create_database_table(name="Cars")
|
||||
table_2 = data_fixture.create_database_table(database=table.database)
|
||||
name_field = data_fixture.create_text_field(table=table, order=0, name="Name")
|
||||
color_field = data_fixture.create_text_field(table=table, order=1, name="Color")
|
||||
price_field = data_fixture.create_number_field(table=table, order=2, name="Price")
|
||||
description_field = data_fixture.create_long_text_field(
|
||||
table=table, order=3, name="Description"
|
||||
)
|
||||
link_field = data_fixture.create_link_row_field(table=table, link_row_table=table_2)
|
||||
password_field = data_fixture.create_password_field(
|
||||
table=table, order=4, name="Password"
|
||||
)
|
||||
single_select_field = data_fixture.create_single_select_field(
|
||||
table=table, name="Single"
|
||||
)
|
||||
|
@ -477,36 +478,48 @@ def test_order_by_fields_string_queryset(data_fixture):
|
|||
field=single_select_field, value="B", color="red"
|
||||
)
|
||||
option_c = data_fixture.create_select_option(
|
||||
field=single_select_field, value="C", color="blue"
|
||||
field=multiple_select_field, value="C", color="blue"
|
||||
)
|
||||
option_d = data_fixture.create_select_option(
|
||||
field=single_select_field, value="D", color="red"
|
||||
field=multiple_select_field, value="D", color="red"
|
||||
)
|
||||
|
||||
model = table.get_model(attribute_names=True)
|
||||
row_1 = model.objects.create(
|
||||
name="BMW",
|
||||
color="Blue",
|
||||
price=10000,
|
||||
description="Sports car.",
|
||||
single=option_a,
|
||||
)
|
||||
getattr(row_1, "multi").set([option_c.id])
|
||||
row_2 = model.objects.create(
|
||||
name="Audi",
|
||||
color="Orange",
|
||||
price=20000,
|
||||
description="This is the most expensive car we have.",
|
||||
single=option_b,
|
||||
)
|
||||
getattr(row_2, "multi").set([option_d.id])
|
||||
row_3 = model.objects.create(
|
||||
name="Volkswagen", color="White", price=5000, description="A very old car."
|
||||
)
|
||||
row_4 = model.objects.create(
|
||||
name="Volkswagen", color="Green", price=4000, description="Strange color."
|
||||
row_1, row_2, row_3, row_4 = RowHandler().force_create_rows(
|
||||
user=None,
|
||||
table=table,
|
||||
rows_values=[
|
||||
{
|
||||
name_field.db_column: "BMW",
|
||||
color_field.db_column: "Blue",
|
||||
price_field.db_column: 10000,
|
||||
description_field.db_column: "Sports car.",
|
||||
single_select_field.db_column: option_a.id,
|
||||
multiple_select_field.db_column: [option_c.id],
|
||||
},
|
||||
{
|
||||
name_field.db_column: "Audi",
|
||||
color_field.db_column: "Orange",
|
||||
price_field.db_column: 20000,
|
||||
description_field.db_column: "This is the most expensive car we have.",
|
||||
single_select_field.db_column: option_b.id,
|
||||
multiple_select_field.db_column: [option_d.id],
|
||||
},
|
||||
{
|
||||
name_field.db_column: "Volkswagen",
|
||||
color_field.db_column: "White",
|
||||
price_field.db_column: 5000,
|
||||
description_field.db_column: "A very old car.",
|
||||
},
|
||||
{
|
||||
name_field.db_column: "Volkswagen",
|
||||
color_field.db_column: "Green",
|
||||
price_field.db_column: 4000,
|
||||
description_field.db_column: "Strange color.",
|
||||
},
|
||||
],
|
||||
)
|
||||
|
||||
model = table.get_model()
|
||||
with pytest.raises(OrderByFieldNotFound):
|
||||
model.objects.all().order_by_fields_string("xxxx")
|
||||
|
||||
|
@ -525,7 +538,7 @@ def test_order_by_fields_string_queryset(data_fixture):
|
|||
)
|
||||
|
||||
with pytest.raises(OrderByFieldNotPossible):
|
||||
model.objects.all().order_by_fields_string(f"field_{link_field.id}")
|
||||
model.objects.all().order_by_fields_string(f"field_{password_field.id}")
|
||||
|
||||
results = model.objects.all().order_by_fields_string(f"-field_{price_field.id}")
|
||||
assert results[0].id == row_2.id
|
||||
|
@ -549,12 +562,16 @@ def test_order_by_fields_string_queryset(data_fixture):
|
|||
assert results[2].id == row_4.id
|
||||
assert results[3].id == row_2.id
|
||||
|
||||
row_5 = model.objects.create(
|
||||
name="Audi",
|
||||
color="Red",
|
||||
price=2000,
|
||||
description="Old times",
|
||||
order=Decimal("0.1"),
|
||||
row_5 = RowHandler().force_create_row(
|
||||
user=None,
|
||||
table=table,
|
||||
values={
|
||||
name_field.db_column: "Audi",
|
||||
color_field.db_column: "Red",
|
||||
price_field.db_column: 2000,
|
||||
description_field.db_column: "Old times",
|
||||
},
|
||||
before=row_1,
|
||||
)
|
||||
|
||||
row_2.order = Decimal("0.1")
|
||||
|
@ -616,10 +633,7 @@ def test_order_by_fields_string_queryset_with_user_field_names(data_fixture):
|
|||
["Volkswagen", "Green", 4000, "Strange color."],
|
||||
],
|
||||
)
|
||||
table_2 = data_fixture.create_database_table(database=table.database)
|
||||
link_field = data_fixture.create_link_row_field(
|
||||
table=table, link_row_table=table_2, name="Link Field"
|
||||
)
|
||||
password_field = data_fixture.create_password_field(table=table, name="Password")
|
||||
|
||||
model = table.get_model()
|
||||
|
||||
|
@ -633,7 +647,7 @@ def test_order_by_fields_string_queryset_with_user_field_names(data_fixture):
|
|||
|
||||
with pytest.raises(OrderByFieldNotPossible):
|
||||
model.objects.all().order_by_fields_string(
|
||||
link_field.name, user_field_names=True
|
||||
password_field.name, user_field_names=True
|
||||
)
|
||||
|
||||
results = model.objects.all().order_by_fields_string(
|
||||
|
|
|
@ -761,7 +761,6 @@ def test_enable_form_view_file_field(data_fixture):
|
|||
def test_field_type_changed(data_fixture):
|
||||
user = data_fixture.create_user()
|
||||
table = data_fixture.create_database_table(user=user)
|
||||
table_2 = data_fixture.create_database_table(user=user, database=table.database)
|
||||
text_field = data_fixture.create_text_field(table=table)
|
||||
grid_view = data_fixture.create_grid_view(table=table)
|
||||
data_fixture.create_view_filter(
|
||||
|
@ -786,10 +785,7 @@ def test_field_type_changed(data_fixture):
|
|||
assert ViewGroupBy.objects.all().count() == 1
|
||||
|
||||
field_handler.update_field(
|
||||
user=user,
|
||||
field=long_text_field,
|
||||
new_type_name="link_row",
|
||||
link_row_table=table_2,
|
||||
user=user, field=long_text_field, new_type_name="password"
|
||||
)
|
||||
assert ViewFilter.objects.all().count() == 0
|
||||
assert ViewSort.objects.all().count() == 0
|
||||
|
@ -1341,7 +1337,7 @@ def test_create_sort(send_mock, data_fixture):
|
|||
grid_view = data_fixture.create_grid_view(user=user)
|
||||
text_field = data_fixture.create_text_field(table=grid_view.table)
|
||||
text_field_2 = data_fixture.create_text_field(table=grid_view.table)
|
||||
link_row_field = data_fixture.create_link_row_field(table=grid_view.table)
|
||||
password_field = data_fixture.create_password_field(table=grid_view.table)
|
||||
other_field = data_fixture.create_text_field()
|
||||
|
||||
handler = ViewHandler()
|
||||
|
@ -1357,7 +1353,7 @@ def test_create_sort(send_mock, data_fixture):
|
|||
|
||||
with pytest.raises(ViewSortFieldNotSupported):
|
||||
handler.create_sort(
|
||||
user=user, view=grid_view, field=link_row_field, order="ASC"
|
||||
user=user, view=grid_view, field=password_field, order="ASC"
|
||||
)
|
||||
|
||||
with pytest.raises(FieldNotInTable):
|
||||
|
@ -1399,7 +1395,7 @@ def test_update_sort(send_mock, data_fixture):
|
|||
grid_view = data_fixture.create_grid_view(user=user)
|
||||
text_field = data_fixture.create_text_field(table=grid_view.table)
|
||||
long_text_field = data_fixture.create_long_text_field(table=grid_view.table)
|
||||
link_row_field = data_fixture.create_link_row_field(table=grid_view.table)
|
||||
password_field = data_fixture.create_password_field(table=grid_view.table)
|
||||
other_field = data_fixture.create_text_field()
|
||||
view_sort = data_fixture.create_view_sort(
|
||||
view=grid_view,
|
||||
|
@ -1413,7 +1409,7 @@ def test_update_sort(send_mock, data_fixture):
|
|||
handler.update_sort(user=user_2, view_sort=view_sort)
|
||||
|
||||
with pytest.raises(ViewSortFieldNotSupported):
|
||||
handler.update_sort(user=user, view_sort=view_sort, field=link_row_field)
|
||||
handler.update_sort(user=user, view_sort=view_sort, field=password_field)
|
||||
|
||||
with pytest.raises(FieldNotInTable):
|
||||
handler.update_sort(user=user, view_sort=view_sort, field=other_field)
|
||||
|
@ -3358,7 +3354,7 @@ def test_update_group_by(send_mock, data_fixture):
|
|||
grid_view = data_fixture.create_grid_view(user=user)
|
||||
text_field = data_fixture.create_text_field(table=grid_view.table)
|
||||
long_text_field = data_fixture.create_long_text_field(table=grid_view.table)
|
||||
link_row_field = data_fixture.create_link_row_field(table=grid_view.table)
|
||||
password_field = data_fixture.create_password_field(table=grid_view.table)
|
||||
other_field = data_fixture.create_text_field()
|
||||
view_group_by = data_fixture.create_view_group_by(
|
||||
view=grid_view,
|
||||
|
@ -3373,7 +3369,7 @@ def test_update_group_by(send_mock, data_fixture):
|
|||
|
||||
with pytest.raises(ViewGroupByFieldNotSupported):
|
||||
handler.update_group_by(
|
||||
user=user, view_group_by=view_group_by, field=link_row_field
|
||||
user=user, view_group_by=view_group_by, field=password_field
|
||||
)
|
||||
|
||||
with pytest.raises(FieldNotInTable):
|
||||
|
|
|
@ -18,6 +18,7 @@ from baserow.contrib.integrations.local_baserow.service_types import (
|
|||
from baserow.core.handler import CoreHandler
|
||||
from baserow.core.registries import ImportExportConfig
|
||||
from baserow.core.services.exceptions import ServiceImproperlyConfigured
|
||||
from baserow.test_utils.helpers import AnyStr
|
||||
from baserow.test_utils.pytest_conftest import FakeDispatchContext
|
||||
|
||||
|
||||
|
@ -479,7 +480,7 @@ def test_local_baserow_upsert_row_service_dispatch_data_convert_value(data_fixtu
|
|||
table.field_set.get(name="text").db_column: "text",
|
||||
# The string '1' is converted to a list with a single item
|
||||
table.field_set.get(name="array").db_column: [
|
||||
{"id": 1, "value": "unnamed row 1"}
|
||||
{"id": 1, "value": "unnamed row 1", "order": AnyStr()}
|
||||
],
|
||||
}
|
||||
|
||||
|
|
|
@ -409,7 +409,7 @@ def test_local_baserow_table_service_generate_schema_with_interesting_test_table
|
|||
"title": "link_row",
|
||||
"default": None,
|
||||
"searchable": True,
|
||||
"sortable": False,
|
||||
"sortable": True,
|
||||
"filterable": False,
|
||||
"original_type": "link_row",
|
||||
"metadata": {},
|
||||
|
@ -419,6 +419,7 @@ def test_local_baserow_table_service_generate_schema_with_interesting_test_table
|
|||
"properties": {
|
||||
"id": {"title": "id", "type": "number"},
|
||||
"value": {"title": "value", "type": "string"},
|
||||
"order": {"title": "order", "type": "string"},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -426,7 +427,7 @@ def test_local_baserow_table_service_generate_schema_with_interesting_test_table
|
|||
"title": "self_link_row",
|
||||
"default": None,
|
||||
"searchable": True,
|
||||
"sortable": False,
|
||||
"sortable": True,
|
||||
"filterable": False,
|
||||
"original_type": "link_row",
|
||||
"metadata": {},
|
||||
|
@ -436,6 +437,7 @@ def test_local_baserow_table_service_generate_schema_with_interesting_test_table
|
|||
"properties": {
|
||||
"id": {"title": "id", "type": "number"},
|
||||
"value": {"title": "value", "type": "string"},
|
||||
"order": {"title": "order", "type": "string"},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -443,7 +445,7 @@ def test_local_baserow_table_service_generate_schema_with_interesting_test_table
|
|||
"title": "link_row_without_related",
|
||||
"default": None,
|
||||
"searchable": True,
|
||||
"sortable": False,
|
||||
"sortable": True,
|
||||
"filterable": False,
|
||||
"original_type": "link_row",
|
||||
"metadata": {},
|
||||
|
@ -453,6 +455,7 @@ def test_local_baserow_table_service_generate_schema_with_interesting_test_table
|
|||
"properties": {
|
||||
"id": {"title": "id", "type": "number"},
|
||||
"value": {"title": "value", "type": "string"},
|
||||
"order": {"title": "order", "type": "string"},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -460,7 +463,7 @@ def test_local_baserow_table_service_generate_schema_with_interesting_test_table
|
|||
"title": "decimal_link_row",
|
||||
"default": None,
|
||||
"searchable": True,
|
||||
"sortable": False,
|
||||
"sortable": True,
|
||||
"filterable": False,
|
||||
"original_type": "link_row",
|
||||
"metadata": {},
|
||||
|
@ -470,6 +473,7 @@ def test_local_baserow_table_service_generate_schema_with_interesting_test_table
|
|||
"properties": {
|
||||
"id": {"title": "id", "type": "number"},
|
||||
"value": {"title": "value", "type": "string"},
|
||||
"order": {"title": "order", "type": "string"},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -487,6 +491,7 @@ def test_local_baserow_table_service_generate_schema_with_interesting_test_table
|
|||
"properties": {
|
||||
"id": {"title": "id", "type": "number"},
|
||||
"value": {"title": "value", "type": "string"},
|
||||
"order": {"title": "order", "type": "string"},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -494,7 +499,7 @@ def test_local_baserow_table_service_generate_schema_with_interesting_test_table
|
|||
"title": "file",
|
||||
"default": None,
|
||||
"searchable": True,
|
||||
"sortable": True,
|
||||
"sortable": False,
|
||||
"filterable": True,
|
||||
"original_type": "file",
|
||||
"metadata": {},
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"type": "feature",
|
||||
"message": "Add ability to sort by link_row ('Link to table') field",
|
||||
"issue_number": 506,
|
||||
"bullet_points": [],
|
||||
"created_at": "2024-12-06"
|
||||
}
|
|
@ -187,9 +187,11 @@ class AIFieldType(CollationSortMixin, SelectOptionBaseFieldType):
|
|||
baserow_field_type = self.get_baserow_field_type(field)
|
||||
return baserow_field_type.enhance_queryset(queryset, field, name)
|
||||
|
||||
def get_order(self, field, field_name, order_direction):
|
||||
def get_order(self, field, field_name, order_direction, table_name=None):
|
||||
baserow_field_type = self.get_baserow_field_type(field)
|
||||
return baserow_field_type.get_order(field, field_name, order_direction)
|
||||
return baserow_field_type.get_order(
|
||||
field, field_name, order_direction, table_model=table_name
|
||||
)
|
||||
|
||||
def serialize_to_input_value(self, field, value):
|
||||
baserow_field_type = self.get_baserow_field_type(field)
|
||||
|
|
|
@ -14,7 +14,6 @@ from rest_framework.status import (
|
|||
)
|
||||
|
||||
from baserow.contrib.database.api.constants import PUBLIC_PLACEHOLDER_ENTITY_ID
|
||||
from baserow.contrib.database.fields.handler import FieldHandler
|
||||
from baserow.contrib.database.rows.handler import RowHandler
|
||||
from baserow.contrib.database.search.handler import ALL_SEARCH_MODES, SearchHandler
|
||||
from baserow.test_utils.helpers import is_dict_subset
|
||||
|
@ -1332,15 +1331,10 @@ def test_list_rows_public_with_query_param_filter(api_client, premium_data_fixtu
|
|||
def test_list_rows_public_with_query_param_order(api_client, premium_data_fixture):
|
||||
user, token = premium_data_fixture.create_user_and_token()
|
||||
table = premium_data_fixture.create_database_table(user=user)
|
||||
table_2 = premium_data_fixture.create_database_table(database=table.database)
|
||||
public_field = premium_data_fixture.create_text_field(table=table, name="public")
|
||||
hidden_field = premium_data_fixture.create_text_field(table=table, name="hidden")
|
||||
link_row_field = FieldHandler().create_field(
|
||||
user=user,
|
||||
table=table,
|
||||
type_name="link_row",
|
||||
name="Link",
|
||||
link_row_table=table_2,
|
||||
password_field = premium_data_fixture.create_password_field(
|
||||
table=table, name="password"
|
||||
)
|
||||
timeline_view = premium_data_fixture.create_timeline_view(
|
||||
table=table, user=user, public=True
|
||||
|
@ -1352,7 +1346,7 @@ def test_list_rows_public_with_query_param_order(api_client, premium_data_fixtur
|
|||
timeline_view, hidden_field, hidden=True
|
||||
)
|
||||
premium_data_fixture.create_timeline_view_field_option(
|
||||
timeline_view, link_row_field, hidden=False
|
||||
timeline_view, password_field, hidden=False
|
||||
)
|
||||
|
||||
first_row = RowHandler().create_row(
|
||||
|
@ -1388,7 +1382,7 @@ def test_list_rows_public_with_query_param_order(api_client, premium_data_fixtur
|
|||
"api:database:views:timeline:public_rows", kwargs={"slug": timeline_view.slug}
|
||||
)
|
||||
response = api_client.get(
|
||||
f"{url}?order_by=field_{link_row_field.id}",
|
||||
f"{url}?order_by=field_{password_field.id}",
|
||||
)
|
||||
response_json = response.json()
|
||||
assert response.status_code == HTTP_400_BAD_REQUEST
|
||||
|
@ -1695,15 +1689,10 @@ def test_list_rows_with_query_param_order(api_client, premium_data_fixture):
|
|||
has_active_premium_license=True,
|
||||
)
|
||||
table = premium_data_fixture.create_database_table(user=user)
|
||||
table_2 = premium_data_fixture.create_database_table(database=table.database)
|
||||
text_field = premium_data_fixture.create_text_field(table=table, name="text")
|
||||
hidden_field = premium_data_fixture.create_text_field(table=table, name="hidden")
|
||||
link_row_field = FieldHandler().create_field(
|
||||
user=user,
|
||||
table=table,
|
||||
type_name="link_row",
|
||||
name="Link",
|
||||
link_row_table=table_2,
|
||||
password_field = premium_data_fixture.create_password_field(
|
||||
table=table, name="password"
|
||||
)
|
||||
timeline_view = premium_data_fixture.create_timeline_view(
|
||||
table=table, user=user, create_options=False
|
||||
|
@ -1715,7 +1704,7 @@ def test_list_rows_with_query_param_order(api_client, premium_data_fixture):
|
|||
timeline_view, hidden_field, hidden=True
|
||||
)
|
||||
premium_data_fixture.create_timeline_view_field_option(
|
||||
timeline_view, link_row_field, hidden=False
|
||||
timeline_view, password_field, hidden=False
|
||||
)
|
||||
first_row = RowHandler().create_row(
|
||||
user, table, values={"text": "a", "hidden": "a"}, user_field_names=True
|
||||
|
@ -1751,7 +1740,7 @@ def test_list_rows_with_query_param_order(api_client, premium_data_fixture):
|
|||
|
||||
# sorting on unsupported field
|
||||
response = api_client.get(
|
||||
f"{url}?order_by=field_{link_row_field.id}",
|
||||
f"{url}?order_by=field_{password_field.id}",
|
||||
**{"HTTP_AUTHORIZATION": f"JWT {token}"},
|
||||
)
|
||||
response_json = response.json()
|
||||
|
|
|
@ -796,6 +796,15 @@ export class FieldType extends Registerable {
|
|||
return value
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a value of for the field type from a linked row item value. This can be
|
||||
* used to convert values provided by a linked row item to the format that is used
|
||||
* by the field type to sort, filter, etc. in the frontend.
|
||||
*/
|
||||
parseFromLinkedRowItemValue(field, value) {
|
||||
return value
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates whether it's possible to select the field type when creating or updating the field.
|
||||
*/
|
||||
|
@ -1091,7 +1100,99 @@ export class LinkRowFieldType extends FieldType {
|
|||
}
|
||||
|
||||
getCanSortInView(field) {
|
||||
return false
|
||||
const relatedField = field.link_row_table_primary_field
|
||||
const relatedFieldType = this.app.$registry.get('field', relatedField.type)
|
||||
return relatedFieldType.getCanSortInView(relatedField)
|
||||
}
|
||||
|
||||
getSort(name, order, field) {
|
||||
const relatedPrimaryField = field.link_row_table_primary_field
|
||||
const relatedPrimaryFieldType = this.app.$registry.get(
|
||||
'field',
|
||||
relatedPrimaryField.type
|
||||
)
|
||||
const relatedSortFunc = relatedPrimaryFieldType.getSort(
|
||||
name,
|
||||
order,
|
||||
relatedPrimaryField
|
||||
)
|
||||
const relatedParseFunc = (item) => {
|
||||
return relatedPrimaryFieldType.parseFromLinkedRowItemValue(
|
||||
relatedPrimaryField,
|
||||
item?.value
|
||||
)
|
||||
}
|
||||
|
||||
return (a, b) => {
|
||||
const valuesA = a[name].map(relatedParseFunc)
|
||||
const valuesB = b[name].map(relatedParseFunc)
|
||||
const lenA = valuesA.length
|
||||
const lenB = valuesB.length
|
||||
|
||||
// nulls (empty arrays) first
|
||||
if (lenA === 0 && lenB !== 0) {
|
||||
return -1
|
||||
} else if (lenA !== 0 && lenB === 0) {
|
||||
return 1
|
||||
}
|
||||
|
||||
for (let i = 0; i < Math.max(valuesA.length, valuesB.length); i++) {
|
||||
let compared = 0
|
||||
|
||||
const isAdefined = valuesA[i] !== undefined
|
||||
const isBdefined = valuesB[i] !== undefined
|
||||
|
||||
if (isAdefined && isBdefined) {
|
||||
const isAnull = valuesA[i] === null
|
||||
const isBnull = valuesB[i] === null
|
||||
if (!isAnull && !isBnull) {
|
||||
compared = relatedSortFunc(
|
||||
{ [name]: valuesA[i] },
|
||||
{ [name]: valuesB[i] }
|
||||
)
|
||||
} else if (!isAnull) {
|
||||
// Postgres sort nulls last in arrays, so we do the same here.
|
||||
compared = order === 'ASC' ? -1 : 1
|
||||
} else if (!isBnull) {
|
||||
compared = order === 'ASC' ? 1 : -1
|
||||
}
|
||||
} else if (isAdefined) {
|
||||
// Different lengths with the same initial values, the shorter array comes first.
|
||||
compared = order === 'ASC' ? 1 : -1
|
||||
} else if (isBdefined) {
|
||||
compared = order === 'ASC' ? -1 : 1
|
||||
}
|
||||
if (compared !== 0) {
|
||||
return compared
|
||||
}
|
||||
}
|
||||
|
||||
// The arrays have the same length and all values are the same.
|
||||
// Let's compare the order and the id of the linked row items.
|
||||
for (let i = 0; i < a[name].length; i++) {
|
||||
const orderA = new BigNumber(a[name][i].order)
|
||||
const orderB = new BigNumber(b[name][i].order)
|
||||
if (!orderA.isEqualTo(orderB)) {
|
||||
return order === 'ASC'
|
||||
? orderA.minus(orderB).toNumber()
|
||||
: orderB.minus(orderA).toNumber()
|
||||
}
|
||||
}
|
||||
|
||||
// If the order is the same, we compare the id of the linked row items to
|
||||
// match the backend behavior.
|
||||
for (let i = 0; i < a[name].length; i++) {
|
||||
const aId = a[name][i].id
|
||||
const bId = b[name][i].id
|
||||
if (aId !== bId) {
|
||||
return order === 'ASC' ? aId - bId : bId - aId
|
||||
}
|
||||
}
|
||||
|
||||
// Exactly the same items. The order will be determined by the next
|
||||
// order by in the list, either another field or rows' order and id.
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
getCanBePrimaryField() {
|
||||
|
@ -1479,6 +1580,13 @@ export class NumberFieldType extends FieldType {
|
|||
parseInputValue(field, value) {
|
||||
return parseFloat(value)
|
||||
}
|
||||
|
||||
parseFromLinkedRowItemValue(field, value) {
|
||||
if (value === '') {
|
||||
return null
|
||||
}
|
||||
return parseFloat(value)
|
||||
}
|
||||
}
|
||||
|
||||
export class RatingFieldType extends FieldType {
|
||||
|
@ -1693,6 +1801,10 @@ export class BooleanFieldType extends FieldType {
|
|||
return this._prepareValue(value)
|
||||
}
|
||||
|
||||
parseFromLinkedRowItemValue(field, value) {
|
||||
return this._prepareValue(value)
|
||||
}
|
||||
|
||||
getDocsDataType(field) {
|
||||
return 'boolean'
|
||||
}
|
||||
|
@ -1769,6 +1881,10 @@ class BaseDateFieldType extends FieldType {
|
|||
|
||||
getSort(name, order) {
|
||||
return (a, b) => {
|
||||
if (moment.isMoment(a[name]) && moment.isMoment(b[name])) {
|
||||
return order === 'ASC' ? a[name].diff(b[name]) : b[name].diff(a[name])
|
||||
}
|
||||
|
||||
if (a[name] === b[name]) {
|
||||
return 0
|
||||
}
|
||||
|
@ -1850,7 +1966,7 @@ class BaseDateFieldType extends FieldType {
|
|||
static getDateFormatsOptionsForValue(field, value) {
|
||||
let formats = [moment.ISO_8601]
|
||||
|
||||
const timeFormats = value.includes(':')
|
||||
const timeFormats = value?.includes(':')
|
||||
? ['', ' H:m', ' H:m A', ' H:m:s', ' H:m:s A']
|
||||
: ['']
|
||||
|
||||
|
@ -1858,7 +1974,7 @@ class BaseDateFieldType extends FieldType {
|
|||
return dateFormats.flatMap((df) => timeFormats.map((tf) => `${df}${tf}`))
|
||||
}
|
||||
|
||||
const containsDash = value.includes('-')
|
||||
const containsDash = value?.includes('-')
|
||||
const s = containsDash ? '-' : '/'
|
||||
|
||||
const usFieldFormats = getDateTimeFormatsFor(
|
||||
|
@ -1904,6 +2020,10 @@ class BaseDateFieldType extends FieldType {
|
|||
return date
|
||||
}
|
||||
|
||||
parseFromLinkedRowItemValue(field, value) {
|
||||
return this.parseInputValue(field, value)
|
||||
}
|
||||
|
||||
formatValue(field, value) {
|
||||
const momentDate = moment.utc(value)
|
||||
if (momentDate.isValid()) {
|
||||
|
@ -2528,6 +2648,10 @@ export class DurationFieldType extends FieldType {
|
|||
return roundDurationValueToFormat(preparedValue, format)
|
||||
}
|
||||
|
||||
parseFromLinkedRowItemValue(field, value) {
|
||||
return this.parseInputValue(field, value)
|
||||
}
|
||||
|
||||
toHumanReadableString(field, value, delimiter = ', ') {
|
||||
return this.formatValue(field, value)
|
||||
}
|
||||
|
@ -3014,6 +3138,10 @@ export class SingleSelectFieldType extends FieldType {
|
|||
}
|
||||
}
|
||||
|
||||
parseFromLinkedRowItemValue(field, value) {
|
||||
return value ? { value } : null
|
||||
}
|
||||
|
||||
prepareValueForUpdate(field, value) {
|
||||
if (value === undefined || value === null) {
|
||||
return null
|
||||
|
@ -3199,6 +3327,11 @@ export class MultipleSelectFieldType extends FieldType {
|
|||
return RowEditFieldMultipleSelect
|
||||
}
|
||||
|
||||
parseFromLinkedRowItemValue(field, value) {
|
||||
// FIXME: what if the option value contains a comma?
|
||||
return value.split(',').map((value) => ({ value: value.trim() }))
|
||||
}
|
||||
|
||||
getFormViewFieldComponents(field) {
|
||||
const { i18n } = this.app
|
||||
const components = super.getFormViewFieldComponents(field)
|
||||
|
@ -3230,9 +3363,9 @@ export class MultipleSelectFieldType extends FieldType {
|
|||
const valuesA = a[name]
|
||||
const valuesB = b[name]
|
||||
const stringA =
|
||||
valuesA.length > 0 ? valuesA.map((obj) => obj.value).join('') : ''
|
||||
valuesA.length > 0 ? valuesA.map((obj) => obj.value).join(', ') : ''
|
||||
const stringB =
|
||||
valuesB.length > 0 ? valuesB.map((obj) => obj.value).join('') : ''
|
||||
valuesB.length > 0 ? valuesB.map((obj) => obj.value).join(', ') : ''
|
||||
|
||||
return collatedStringCompare(stringA, stringB, order)
|
||||
}
|
||||
|
@ -3733,6 +3866,11 @@ export class FormulaFieldType extends mix(
|
|||
return underlyingFieldType.parseInputValue(field, value)
|
||||
}
|
||||
|
||||
parseFromLinkedRowItemValue(field, value) {
|
||||
const underlyingFieldType = this.getFormulaSubtype(field)
|
||||
return underlyingFieldType.parseFromLinkedRowItemValue(field, value)
|
||||
}
|
||||
|
||||
canRepresentFiles(field) {
|
||||
return this.getFormulaSubtype(field)?.canRepresentFiles(field)
|
||||
}
|
||||
|
@ -3860,6 +3998,10 @@ export class MultipleCollaboratorsFieldType extends FieldType {
|
|||
return 'iconoir-community'
|
||||
}
|
||||
|
||||
parseFromLinkedRowItemValue(field, value) {
|
||||
return this.app.store.getters['workspace/getUserByEmail'](value) || null
|
||||
}
|
||||
|
||||
getName() {
|
||||
const { i18n } = this.app
|
||||
return i18n.t('fieldType.multipleCollaborators')
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { getPrimaryOrFirstField } from '@baserow/modules/database/utils/field'
|
||||
import BigNumber from 'bignumber.js'
|
||||
|
||||
export default {
|
||||
computed: {
|
||||
|
@ -64,13 +65,23 @@ export default {
|
|||
|
||||
// Prepare the new value with all the relations and emit that value to the
|
||||
// parent.
|
||||
const newValue = JSON.parse(JSON.stringify(value))
|
||||
const rowValue = this.$registry
|
||||
.get('field', primary.type)
|
||||
.toHumanReadableString(primary, row[`field_${primary.id}`])
|
||||
newValue.push({
|
||||
id: row.id,
|
||||
value: rowValue,
|
||||
// The backend sort by order first and then by id, but we don't have the order
|
||||
// here, so we just sort by id
|
||||
const valueCopy = JSON.parse(JSON.stringify(value))
|
||||
const newValue = [
|
||||
...valueCopy,
|
||||
{ id: row.id, value: rowValue, order: row.order },
|
||||
].toSorted((a, b) => {
|
||||
const orderA = new BigNumber(a.order)
|
||||
const orderB = new BigNumber(b.order)
|
||||
return orderA.isLessThan(orderB)
|
||||
? -1
|
||||
: orderA.isEqualTo(orderB)
|
||||
? a.id - b.id
|
||||
: 1
|
||||
})
|
||||
this.$emit('update', newValue, value)
|
||||
},
|
||||
|
|
|
@ -708,8 +708,6 @@ describe('FieldType tests', () => {
|
|||
fieldRegistry = testApp._app.$registry.registry.field
|
||||
|
||||
// Make sure that we have a mockedField for every field type in the registry
|
||||
console.log(Object.keys(fieldRegistry).sort())
|
||||
console.log(Object.keys(mockedFields).sort())
|
||||
expect(Object.keys(fieldRegistry).sort()).toEqual(
|
||||
Object.keys(mockedFields).sort()
|
||||
)
|
||||
|
|
|
@ -259,3 +259,274 @@ describe('MultipleCollaboratorsFieldType sorting', () => {
|
|||
expect(result).toBe(sortedChars)
|
||||
})
|
||||
})
|
||||
|
||||
describe('LinkRowFieldType sorting text values according to collation', () => {
|
||||
let testApp = null
|
||||
let linkRowFieldType = null
|
||||
|
||||
beforeAll(() => {
|
||||
testApp = new TestApp()
|
||||
linkRowFieldType = testApp._app.$registry.registry.field.link_row
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
testApp.afterEach()
|
||||
})
|
||||
|
||||
test('Test sort matches backend', () => {
|
||||
// This is a naive sorting test running on Node.js and thus not really testing
|
||||
// collation sorting in the browsers where this functionality is mostly used The
|
||||
// Peseta character in particular seems to be sorted differently in our Node.js,
|
||||
// hence it will be ignored for this test
|
||||
const sortedChars = fs
|
||||
.readFileSync(
|
||||
path.join(__dirname, '/../../../../tests/sorted_chars.txt'),
|
||||
'utf8'
|
||||
)
|
||||
.replace(/^\uFEFF/, '') // strip BOM
|
||||
.replace('₧', '') // ignore Peseta
|
||||
const data = fs
|
||||
.readFileSync(
|
||||
path.join(__dirname, '/../../../../tests/all_chars.txt'),
|
||||
'utf8'
|
||||
)
|
||||
.replace(/^\uFEFF/, '') // strip BOM
|
||||
.replace('₧', '') // ignore Peseta
|
||||
const rows = Array.from(data).map((value) => {
|
||||
return { v: [{ value }] }
|
||||
})
|
||||
const relatedField = { type: 'text' }
|
||||
const linkRowField = {
|
||||
link_row_table_primary_field: relatedField,
|
||||
}
|
||||
const sortFunction = linkRowFieldType.getSort('v', 'ASC', linkRowField)
|
||||
rows.sort(sortFunction)
|
||||
const result = rows.map((v) => v.v[0].value).join('')
|
||||
expect(result).toBe(sortedChars)
|
||||
})
|
||||
})
|
||||
|
||||
describe('LinkRowFieldType sorting with other primary fields', () => {
|
||||
let testApp = null
|
||||
let linkRowFieldType = null
|
||||
|
||||
beforeAll(() => {
|
||||
testApp = new TestApp()
|
||||
linkRowFieldType = testApp._app.$registry.registry.field.link_row
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
testApp.afterEach()
|
||||
})
|
||||
|
||||
test('Test ascending and descending order with number primary field', () => {
|
||||
const testData = [
|
||||
{
|
||||
id: 1,
|
||||
order: '1.00000000000000000000',
|
||||
field_link: [
|
||||
{ id: 1, value: '100' },
|
||||
{ id: 2, value: '50' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
order: '2.00000000000000000000',
|
||||
field_link: [{ id: 3, value: '25' }],
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
order: '3.00000000000000000000',
|
||||
field_link: [{ id: 4, value: '' }],
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
order: '4.00000000000000000000',
|
||||
field_link: [],
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
order: '5.00000000000000000000',
|
||||
field_link: [{ id: 5, value: '75' }],
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
order: '6.00000000000000000000',
|
||||
field_link: [{ id: 6, value: '25' }],
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
order: '7.00000000000000000000',
|
||||
field_link: [
|
||||
{ id: 1, value: '100' },
|
||||
{ id: 6, value: '25' },
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
const relatedField = {
|
||||
type: 'number',
|
||||
number_decimal_places: 0,
|
||||
}
|
||||
const linkRowField = {
|
||||
link_row_table_primary_field: relatedField,
|
||||
}
|
||||
const sortASC = linkRowFieldType.getSort('field_link', 'ASC', linkRowField)
|
||||
const sortDESC = linkRowFieldType.getSort(
|
||||
'field_link',
|
||||
'DESC',
|
||||
linkRowField
|
||||
)
|
||||
|
||||
testData.sort(sortASC)
|
||||
let ids = testData.map((obj) => obj.id)
|
||||
expect(ids).toEqual([4, 2, 6, 5, 7, 1, 3])
|
||||
|
||||
testData.sort(sortDESC)
|
||||
ids = testData.map((obj) => obj.id)
|
||||
expect(ids).toEqual([4, 3, 1, 7, 5, 2, 6])
|
||||
})
|
||||
|
||||
test('Test ascending and descending order with duration primary field', () => {
|
||||
const testData = [
|
||||
{
|
||||
id: 1,
|
||||
order: '1.00000000000000000000',
|
||||
field_link: [
|
||||
{ id: 1, value: '0:01:40' },
|
||||
{ id: 2, value: '0:00:50' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
order: '2.00000000000000000000',
|
||||
field_link: [{ id: 3, value: '0:00:25' }],
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
order: '3.00000000000000000000',
|
||||
field_link: [{ id: 4, value: '' }],
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
order: '4.00000000000000000000',
|
||||
field_link: [],
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
order: '5.00000000000000000000',
|
||||
field_link: [{ id: 5, value: '0:01:15' }],
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
order: '6.00000000000000000000',
|
||||
field_link: [{ id: 6, value: '0:00:25' }],
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
order: '7.00000000000000000000',
|
||||
field_link: [
|
||||
{ id: 1, value: '0:01:40' },
|
||||
{ id: 6, value: '0:00:25' },
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
const relatedField = {
|
||||
type: 'duration',
|
||||
duration_format: 'h:mm:ss',
|
||||
}
|
||||
const linkRowField = {
|
||||
link_row_table_primary_field: relatedField,
|
||||
}
|
||||
const sortASC = linkRowFieldType.getSort('field_link', 'ASC', linkRowField)
|
||||
const sortDESC = linkRowFieldType.getSort(
|
||||
'field_link',
|
||||
'DESC',
|
||||
linkRowField
|
||||
)
|
||||
|
||||
testData.sort(sortASC)
|
||||
let ids = testData.map((obj) => obj.id)
|
||||
// Nulls first, then sorted by the lowest number value in the linked rows
|
||||
expect(ids).toEqual([4, 2, 6, 5, 7, 1, 3])
|
||||
|
||||
testData.sort(sortDESC)
|
||||
ids = testData.map((obj) => obj.id)
|
||||
// Nulls first, then sorted by the highest number value in the linked rows
|
||||
expect(ids).toEqual([4, 3, 1, 7, 5, 2, 6])
|
||||
})
|
||||
|
||||
test('Test ascending and descending order with date primary field', () => {
|
||||
const testData = [
|
||||
{
|
||||
id: 1,
|
||||
order: '1.00000000000000000000',
|
||||
field_link: [
|
||||
{ id: 1, value: '06/12/2024 11:30' },
|
||||
{ id: 2, value: '06/12/2024 01:00' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
order: '2.00000000000000000000',
|
||||
field_link: [{ id: 3, value: '05/12/2024 07:00' }],
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
order: '3.00000000000000000000',
|
||||
field_link: [{ id: 4, value: '' }],
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
order: '4.00000000000000000000',
|
||||
field_link: [],
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
order: '5.00000000000000000000',
|
||||
field_link: [{ id: 5, value: '06/12/2024 09:00' }],
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
order: '6.00000000000000000000',
|
||||
field_link: [{ id: 6, value: '05/12/2024 07:00' }],
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
order: '7.00000000000000000000',
|
||||
field_link: [
|
||||
{ id: 1, value: '06/12/2024 11:30' },
|
||||
{ id: 6, value: '05/12/2024 07:00' },
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
const relatedField = {
|
||||
type: 'date',
|
||||
date_format: 'EU',
|
||||
date_time_format: '24',
|
||||
date_include_time: true,
|
||||
date_force_timezone: 'UTC',
|
||||
}
|
||||
const linkRowField = {
|
||||
link_row_table_primary_field: relatedField,
|
||||
}
|
||||
const sortASC = linkRowFieldType.getSort('field_link', 'ASC', linkRowField)
|
||||
const sortDESC = linkRowFieldType.getSort(
|
||||
'field_link',
|
||||
'DESC',
|
||||
linkRowField
|
||||
)
|
||||
|
||||
testData.sort(sortASC)
|
||||
let ids = testData.map((obj) => obj.id)
|
||||
// Nulls first, then sorted by the lowest number value in the linked rows
|
||||
expect(ids).toEqual([4, 2, 6, 5, 7, 1, 3])
|
||||
|
||||
testData.sort(sortDESC)
|
||||
ids = testData.map((obj) => obj.id)
|
||||
// Nulls first, then sorted by the highest number value in the linked rows
|
||||
expect(ids).toEqual([4, 3, 1, 7, 5, 2, 6])
|
||||
})
|
||||
})
|
||||
|
|
Loading…
Add table
Reference in a new issue