1
0
Fork 0
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:
Davide Silvestri 2024-12-13 13:27:52 +00:00
parent a364f72c4a
commit d78f6b6906
38 changed files with 1425 additions and 325 deletions

View file

@ -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

View file

@ -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 = [

View file

@ -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)

View file

@ -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):
"""

View file

@ -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:

View file

@ -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:

View file

@ -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

View file

@ -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")

View file

@ -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"

View file

@ -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

View file

@ -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))

View file

@ -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
)

View file

@ -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",
},

View file

@ -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()},
],
},
]

View file

@ -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",
}

View file

@ -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,
},
]

View file

@ -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

View file

@ -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()

View file

@ -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

View file

@ -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}",
)

View file

@ -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}",
)

View file

@ -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",
}
]

View file

@ -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,

View file

@ -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]

View file

@ -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",

View file

@ -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",
}
]

View file

@ -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()}
],
}
],
}

View file

@ -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(

View file

@ -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):

View file

@ -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()}
],
}

View file

@ -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": {},

View file

@ -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"
}

View file

@ -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)

View file

@ -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()

View file

@ -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')

View file

@ -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)
},

View file

@ -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()
)

View file

@ -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])
})
})