Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: collection is not always searchable #304

Merged
merged 31 commits into from
Jan 29, 2025
Merged
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
c9fc1fe
fix: collection is not always searchable
jbarreau Dec 27, 2024
2569287
chore: get searchable fields wasn't checkin the operators
jbarreau Dec 30, 2024
6c03fd0
chore: add few tests and fix existents
jbarreau Dec 30, 2024
33b6295
chore: replacer make the collection searchable
jbarreau Dec 30, 2024
b098569
chore: refacto
jbarreau Dec 30, 2024
bf451df
chore: add tests
jbarreau Dec 30, 2024
4618cbb
chore: remove useless variable
jbarreau Jan 3, 2025
77ae2cb
chore: debug CI
jbarreau Jan 6, 2025
a6e6a5e
chore: debug CI
jbarreau Jan 6, 2025
bac1f27
chore: shortest CI to debug
jbarreau Jan 6, 2025
b39331b
chore: shortest CI to debug
jbarreau Jan 6, 2025
1fb814e
chore: shortest CI to debug
jbarreau Jan 6, 2025
daf92ac
chore: debug CI
jbarreau Jan 6, 2025
2c194c7
chore: debug CI
jbarreau Jan 6, 2025
ada3e92
chore: debug CI
jbarreau Jan 6, 2025
5bb08e5
chore: debug CI
jbarreau Jan 6, 2025
cb4e6e5
chore: debug CI
jbarreau Jan 6, 2025
ea6230f
chore: debug CI
jbarreau Jan 6, 2025
5b9c62d
chore: debug CI
jbarreau Jan 6, 2025
8fcb6fe
chore: debug CI
jbarreau Jan 6, 2025
cccbc78
hore: debug CI
jbarreau Jan 6, 2025
5bd9957
chore: debug CI
jbarreau Jan 6, 2025
45737ba
chore: debug CI
jbarreau Jan 6, 2025
56e28b0
chore: debug CI
jbarreau Jan 6, 2025
fe5521e
chore: debug CI
jbarreau Jan 6, 2025
f0271ca
chore: debug CI
jbarreau Jan 6, 2025
38e3518
chore: debug CI
jbarreau Jan 6, 2025
5571c86
chore: debug CI
jbarreau Jan 6, 2025
809ca3d
chore: debug CI
jbarreau Jan 6, 2025
456e942
chore: restore correct values
jbarreau Jan 6, 2025
e071138
chore: add mark schema as dirty
jbarreau Jan 7, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .github/workflows/generic.yml
Original file line number Diff line number Diff line change
@@ -33,7 +33,6 @@ jobs:
- uses: actions/checkout@v4
- id: changes
uses: ./.github/actions/changes

lint:
name: Linting
needs: [ changes ]
Original file line number Diff line number Diff line change
@@ -573,6 +573,22 @@ async def _replace_search():
self.stack.queue_customization(_replace_search)
return self

def disable_search(self) -> Self:
"""Disable the search bar

Documentation:
https://docs.forestadmin.com/developer-guide-agents-python/agent-customization/search

Example:
.disable_search()
"""

async def _disable_search():
cast(SearchCollectionDecorator, self.stack.search.get_collection(self.collection_name)).disable_search()

self.stack.queue_customization(_disable_search)
return self

def add_chart(self, name: str, definition: CollectionChartDefinition) -> Self:
"""Create a new API chart

Original file line number Diff line number Diff line change
@@ -32,12 +32,22 @@ class SearchCollectionDecorator(CollectionDecorator):
def __init__(self, collection: Collection, datasource: Datasource[BoundCollection]):
super().__init__(collection, datasource)
self._replacer: SearchDefinition = None
self._searchable = len(self._get_searchable_fields(self.child_collection, False)) > 0

def disable_search(self):
self._searchable = False
self.mark_schema_as_dirty()

def replace_search(self, replacer: SearchDefinition):
self._replacer = replacer
self._searchable = True
self.mark_schema_as_dirty()

def _refine_schema(self, sub_schema: CollectionSchema) -> CollectionSchema:
return {**sub_schema, "searchable": True}
return {
**sub_schema,
"searchable": self._searchable,
}

def _default_replacer(self, search: str, extended: bool) -> ConditionTree:
searchable_fields = self._get_searchable_fields(self.child_collection, extended)
@@ -77,18 +87,14 @@ async def _refine_filter(
return _filter

def _build_condition(self, field: str, schema: Column, search: str) -> Union[ConditionTree, None]:
if (
schema["column_type"] == PrimitiveType.NUMBER
and search.isnumeric()
and Operator.EQUAL in schema.get("filter_operators", [])
):
if schema["column_type"] == PrimitiveType.NUMBER and search.isnumeric():
try:
value = int(search)
except ValueError:
value = float(search)
return ConditionTreeLeaf(field, Operator.EQUAL, value)

if schema["column_type"] == PrimitiveType.ENUM and Operator.EQUAL in schema.get("filter_operators", []):
if schema["column_type"] == PrimitiveType.ENUM:
search_value = self.lenient_find(schema["enum_values"], search)
if search_value is not None:
return ConditionTreeLeaf(field, Operator.EQUAL, search_value)
@@ -103,17 +109,11 @@ def _build_condition(self, field: str, schema: Column, search: str) -> Union[Con
operator = Operator.CONTAINS
elif support_equal:
operator = Operator.EQUAL
else:
operator = None

if operator:
return ConditionTreeLeaf(field, operator, search)

if (
schema["column_type"] == PrimitiveType.UUID
and is_valid_uuid(search)
and Operator.EQUAL in schema.get("filter_operators", [])
):
if schema["column_type"] == PrimitiveType.UUID and is_valid_uuid(search):
return ConditionTreeLeaf(field, Operator.EQUAL, search)

def lenient_find(self, haystack: List[str], needle: str) -> Union[str, None]:
@@ -126,14 +126,14 @@ def _get_searchable_fields(self, collection: Collection, extended: bool) -> List
fields: List[Tuple[str, ColumnAlias]] = []

for name, field in collection.schema["fields"].items():
if is_column(field):
if is_column(field) and self._is_searchable_field(field):
fields.append((name, field))

if extended and (is_many_to_one(field) or is_one_to_one(field) or is_polymorphic_one_to_one(field)):
related = collection.datasource.get_collection(field["foreign_collection"])

for sub_name, sub_field in related.schema["fields"].items():
if is_column(sub_field):
if is_column(sub_field) and self._is_searchable_field(sub_field):
fields.append((f"{name}:{sub_name}", sub_field))

if extended and is_polymorphic_many_to_one(field):
@@ -145,3 +145,18 @@ def _get_searchable_fields(self, collection: Collection, extended: bool) -> List
)

return fields

def _is_searchable_field(self, field: Column) -> bool:
operators = field.get("filter_operators", [])

if field["column_type"] == PrimitiveType.STRING and (
Operator.CONTAINS in operators or Operator.EQUAL in operators
):
return True

if field["column_type"] in [PrimitiveType.NUMBER, PrimitiveType.UUID, PrimitiveType.ENUM] and (
Operator.EQUAL in operators
):
return True

return False
Original file line number Diff line number Diff line change
@@ -52,10 +52,39 @@ def setUpClass(cls) -> None:
def setUp(self) -> None:
self.datasource: Datasource = Datasource()
self.collection_person = Collection("Person", self.datasource)
self.collection_person.add_fields(
{
"id": {
"is_primary_key": True,
"type": FieldType.COLUMN,
"column_type": PrimitiveType.NUMBER,
"filter_operators": set([Operator.EQUAL, Operator.IN]),
}
}
)

self.no_searchable_fields_collection = Collection("NotSearchable", self.datasource)
self.no_searchable_fields_collection.add_fields(
{
"id": {
"is_primary_key": True,
"type": FieldType.COLUMN,
"column_type": PrimitiveType.NUMBER,
"filter_operators": set(),
}
}
)
self.datasource.add_collection(self.collection_person)
self.datasource.add_collection(self.no_searchable_fields_collection)

self.datasource_decorator = DatasourceDecorator(self.datasource, SearchCollectionDecorator)
self.decorated_collection_person = self.datasource_decorator.get_collection("Person")
self.decorated_collection_person: SearchCollectionDecorator = self.datasource_decorator.get_collection(
"Person"
) # type:ignore

self.decorated_not_searchable_collection: SearchCollectionDecorator = self.datasource_decorator.get_collection(
"NotSearchable"
) # type:ignore

def test_replace_search_should_work(self):
def replacer(search: Any, search_extended: bool, context: CollectionCustomizationContext):
@@ -64,9 +93,28 @@ def replacer(search: Any, search_extended: bool, context: CollectionCustomizatio
self.decorated_collection_person.replace_search(replacer)
assert self.decorated_collection_person._replacer == replacer

def test_schema_is_searchable_should_be_true(self):
def test_schema_is_searchable_should_be_true_by_default_when_fields_can_be_searched(self):
assert self.decorated_collection_person.schema["searchable"] is True

def test_schema_is_searchable_should_be_false_when_no_fields_can_be_searched(self):
assert self.decorated_not_searchable_collection.schema["searchable"] is False

def test_schema_conflict_on_replace_and_disable_apply_the_latest_one(self):
self.decorated_collection_person.mark_schema_as_dirty()
assert self.decorated_collection_person.schema["searchable"] is True

self.decorated_collection_person.disable_search()
self.decorated_collection_person.mark_schema_as_dirty()
assert self.decorated_collection_person.schema["searchable"] is False

self.decorated_collection_person.replace_search(None)
self.decorated_collection_person.mark_schema_as_dirty()
assert self.decorated_collection_person.schema["searchable"] is True

def test_schema_is_searchable_should_be_false_when_disabling_search(self):
self.decorated_collection_person.disable_search()
assert self.decorated_collection_person.schema["searchable"] is False

def test_refine_filter_should_return_the_given_filter_for_empty_filter(self):
filter_ = Filter({"search": None})

@@ -169,6 +217,7 @@ def test_search_must_be_applied_on_all_fields(self):
"condition_tree": ConditionTreeBranch(
Aggregator.OR,
conditions=[
ConditionTreeLeaf("id", Operator.EQUAL, 1584),
ConditionTreeLeaf("number", Operator.EQUAL, 1584),
ConditionTreeLeaf("label", Operator.CONTAINS, "1584"),
],
@@ -206,11 +255,11 @@ def test_for_enum_value(self):
def test_search_number_in_all_field(self):
self.collection_person.add_field(
"field1",
Column(column_type=PrimitiveType.NUMBER, filter_operators=[Operator.EQUAL], type=FieldType.COLUMN),
Column(column_type=PrimitiveType.NUMBER, filter_operators=set([Operator.EQUAL]), type=FieldType.COLUMN),
)
self.collection_person.add_field(
"field2",
Column(column_type=PrimitiveType.NUMBER, filter_operators=[Operator.EQUAL], type=FieldType.COLUMN),
Column(column_type=PrimitiveType.NUMBER, filter_operators=set([Operator.EQUAL]), type=FieldType.COLUMN),
)

self.collection_person.add_field(
@@ -230,6 +279,7 @@ def test_search_number_in_all_field(self):
"condition_tree": ConditionTreeBranch(
Aggregator.OR,
conditions=[
ConditionTreeLeaf("id", Operator.EQUAL, 1584),
ConditionTreeLeaf("field1", Operator.EQUAL, 1584),
ConditionTreeLeaf("field2", Operator.EQUAL, 1584),
],
@@ -572,3 +622,13 @@ async def replacer_fn(value, extended, context):
filter_ = Filter({"search": "something", "search_extended": True})
self.loop.run_until_complete(self.decorated_collection_person._refine_filter(self.mocked_caller, filter_))
spy_replacer_fn.assert_awaited_with("something", True, ANY)

def test_disable_search_should_mark_schema_as_dirty(self):
with patch.object(self.decorated_collection_person, "mark_schema_as_dirty") as mark_schema_as_dirty:
self.decorated_collection_person.disable_search()
mark_schema_as_dirty.assert_called_once()

def test_replace_search_should_mark_schema_as_dirty(self):
with patch.object(self.decorated_collection_person, "mark_schema_as_dirty") as mark_schema_as_dirty:
self.decorated_collection_person.replace_search(None)
mark_schema_as_dirty.assert_called_once()