From 18d89e8eb77d759ac0e501f7598b0a4473b7673a Mon Sep 17 00:00:00 2001 From: Julien Barreau Date: Fri, 22 Dec 2023 11:25:03 +0100 Subject: [PATCH 1/8] fix(projection_parsing): stop parsing projection too deeply into 1 to 1 --- .../resources/collections/filter.py | 51 ++++------- .../tests/resources/collections/test_crud.py | 16 +++- .../resources/collections/test_filter.py | 89 ++++++++++++++++++- 3 files changed, 117 insertions(+), 39 deletions(-) diff --git a/src/agent_toolkit/forestadmin/agent_toolkit/resources/collections/filter.py b/src/agent_toolkit/forestadmin/agent_toolkit/resources/collections/filter.py index 6ae6b24e0..34af39455 100644 --- a/src/agent_toolkit/forestadmin/agent_toolkit/resources/collections/filter.py +++ b/src/agent_toolkit/forestadmin/agent_toolkit/resources/collections/filter.py @@ -11,9 +11,9 @@ from forestadmin.agent_toolkit.exceptions import AgentToolkitException from forestadmin.agent_toolkit.resources.collections.requests import RequestCollection, RequestRelationCollection from forestadmin.agent_toolkit.utils.context import Request -from forestadmin.datasource_toolkit.collections import Collection +from forestadmin.datasource_toolkit.collections import Collection, CollectionException from forestadmin.datasource_toolkit.datasource_customizer.collection_customizer import CollectionCustomizer -from forestadmin.datasource_toolkit.interfaces.fields import PrimitiveType, is_column, is_many_to_one, is_one_to_one +from forestadmin.datasource_toolkit.interfaces.fields import PrimitiveType, is_column from forestadmin.datasource_toolkit.interfaces.query.condition_tree.factory import ( ConditionTreeFactory, ConditionTreeFactoryException, @@ -228,41 +228,26 @@ def _parse_value(jsoned_filters, collection): return jsoned_filters -def _parse_projection_fields( - query: Dict[str, Any], - collection: Union[CollectionCustomizer, Collection], - front_collection_name: str, - is_related: bool = False, -) -> List[str]: - projection_fields: List[str] = [] - try: - fields: str = query[f"fields[{front_collection_name}]"] - except KeyError: - return ProjectionFactory.all(collection) - - if fields == "": +def parse_projection(request: Union[RequestCollection, RequestRelationCollection]) -> Projection: + collection = _get_collection(request) + schema = collection.schema + if not request.query or not request.query.get(f"fields[{collection.name}]"): return ProjectionFactory.all(collection) - for field_name in fields.split(","): - field_schema = collection.get_field(field_name) - if is_column(field_schema): - if is_related: - projection_fields.append(f"{front_collection_name}:{field_name}") - else: - projection_fields.append(field_name) - elif is_one_to_one(field_schema) or is_many_to_one(field_schema): - fk_collection = collection.datasource.get_collection(field_schema["foreign_collection"]) - projection_fields.extend(_parse_projection_fields(query, fk_collection, field_name, True)) - return projection_fields + root_fields = request.query[f"fields[{collection.name}]"].split(",") + explicit_request = [] + for _field in root_fields: + if not schema["fields"].get(_field): + raise CollectionException(f"Field not found '{collection.name}.{_field}'") -def parse_projection(request: Union[RequestCollection, RequestRelationCollection]): - collection = _get_collection(request) - if not request.query: - return ProjectionFactory.all(collection) + if is_column(schema["fields"][_field]): + explicit_request.append(_field) + else: + query_params = f"fields[{_field}]" + explicit_request.append(f"{_field}:{request.query[query_params]}") - projection_fields = _parse_projection_fields(request.query, collection, collection.name) - ProjectionValidator.validate(_get_collection(request), projection_fields) - return Projection(*projection_fields) + ProjectionValidator.validate(_get_collection(request), explicit_request) + return Projection(*explicit_request) def parse_projection_with_pks(request: Union[RequestCollection, RequestRelationCollection]): diff --git a/src/agent_toolkit/tests/resources/collections/test_crud.py b/src/agent_toolkit/tests/resources/collections/test_crud.py index dda4926dc..cb82bfd59 100644 --- a/src/agent_toolkit/tests/resources/collections/test_crud.py +++ b/src/agent_toolkit/tests/resources/collections/test_crud.py @@ -25,6 +25,7 @@ from forestadmin.datasource_toolkit.exceptions import ValidationError from forestadmin.datasource_toolkit.interfaces.fields import FieldType, Operator, PrimitiveType from forestadmin.datasource_toolkit.interfaces.query.condition_tree.nodes.leaf import ConditionTreeLeaf +from forestadmin.datasource_toolkit.interfaces.query.projections import Projection from forestadmin.datasource_toolkit.validations.records import RecordValidatorException @@ -243,7 +244,10 @@ def test_dispatch_error(self, mock_request_collection: Mock): "forestadmin.agent_toolkit.resources.collections.crud.ConditionTreeFactory.match_ids", return_value=ConditionTreeLeaf("id", Operator.EQUAL, 10), ) - @patch("forestadmin.agent_toolkit.resources.collections.crud.ProjectionFactory.all", return_value=["id", "cost"]) + @patch( + "forestadmin.agent_toolkit.resources.collections.crud.ProjectionFactory.all", + return_value=Projection("id", "cost"), + ) @patch( "forestadmin.agent_toolkit.resources.collections.crud.JsonApiSerializer.get", return_value=Mock, @@ -310,7 +314,10 @@ def test_get( "forestadmin.agent_toolkit.resources.collections.crud.ConditionTreeFactory.match_ids", return_value=ConditionTreeLeaf("id", Operator.EQUAL, 10), ) - @patch("forestadmin.agent_toolkit.resources.collections.crud.ProjectionFactory.all", return_value=["id", "cost"]) + @patch( + "forestadmin.agent_toolkit.resources.collections.crud.ProjectionFactory.all", + return_value=Projection("id", "cost"), + ) @patch( "forestadmin.agent_toolkit.resources.collections.crud.JsonApiSerializer.get", return_value=Mock, @@ -342,7 +349,10 @@ def test_get_no_data( "forestadmin.agent_toolkit.resources.collections.crud.ConditionTreeFactory.match_ids", return_value=ConditionTreeLeaf("id", Operator.EQUAL, 10), ) - @patch("forestadmin.agent_toolkit.resources.collections.crud.ProjectionFactory.all", return_value=["id", "cost"]) + @patch( + "forestadmin.agent_toolkit.resources.collections.crud.ProjectionFactory.all", + return_value=Projection("id", "cost"), + ) @patch( "forestadmin.agent_toolkit.resources.collections.crud.JsonApiSerializer.get", return_value=Mock, diff --git a/src/agent_toolkit/tests/resources/collections/test_filter.py b/src/agent_toolkit/tests/resources/collections/test_filter.py index ea57a5e20..250e59bc4 100644 --- a/src/agent_toolkit/tests/resources/collections/test_filter.py +++ b/src/agent_toolkit/tests/resources/collections/test_filter.py @@ -1,11 +1,13 @@ from unittest import TestCase +from unittest.mock import patch -from forestadmin.agent_toolkit.resources.collections.filter import parse_condition_tree +from forestadmin.agent_toolkit.resources.collections.filter import parse_condition_tree, parse_projection from forestadmin.agent_toolkit.resources.collections.requests import RequestCollection from forestadmin.agent_toolkit.utils.context import RequestMethod -from forestadmin.datasource_toolkit.collections import Collection +from forestadmin.datasource_toolkit.collections import Collection, CollectionException from forestadmin.datasource_toolkit.datasources import Datasource -from forestadmin.datasource_toolkit.interfaces.fields import Column, FieldType, Operator, PrimitiveType +from forestadmin.datasource_toolkit.interfaces.fields import Column, FieldType, ManyToOne, Operator, PrimitiveType +from forestadmin.datasource_toolkit.validations.projection import ProjectionValidator class TestFilter(TestCase): @@ -28,10 +30,43 @@ def setUpClass(cls) -> None: filter_operators=[Operator.IN, Operator.EQUAL], type=FieldType.COLUMN, ), + "author": ManyToOne( + type=FieldType.MANY_TO_ONE, + foreign_collection="Person", + foreign_key="auhtor_id", + foreign_key_targe="id", + ), + "author_id": Column( + column_type=PrimitiveType.NUMBER, + type=FieldType.COLUMN, + filter_operators=set([Operator.IN, Operator.EQUAL]), + ), } ) + cls.collection_person = Collection("Person", cls.datasource) + cls.collection_person.add_fields( + { + "id": Column( + column_type=PrimitiveType.NUMBER, + is_primary_key=True, + type=FieldType.COLUMN, + filter_operators=set([Operator.IN, Operator.EQUAL]), + ), + "firstname": Column( + column_type=PrimitiveType.STRING, + filter_operators=[Operator.IN, Operator.EQUAL], + type=FieldType.COLUMN, + ), + "lastname": Column( + column_type=PrimitiveType.STRING, + filter_operators=[Operator.IN, Operator.EQUAL], + type=FieldType.COLUMN, + ), + } + ) cls.datasource.add_collection(cls.collection_book) + cls.datasource.add_collection(cls.collection_person) def test_parse_condition_tree_should_parse_array_when_IN_operator_str(self): request = RequestCollection( @@ -58,3 +93,51 @@ def test_parse_condition_tree_should_parse_array_when_IN_operator_int(self): ) condition_tree = parse_condition_tree(request) self.assertEqual(condition_tree.value, [1, 2]) + + def test_parse_projection_should_parse_in_query_projection(self): + request = RequestCollection( + method=RequestMethod.GET, + body=None, + query={ + "collection_name": "Book", + "fields[Book]": "id,title,author", + "fields[author]": "id", + }, + collection=self.collection_book, + ) + expected_projection = ["id", "title", "author:id"] + + with patch( + "forestadmin.agent_toolkit.resources.collections.filter.ProjectionValidator.validate", + wraps=ProjectionValidator.validate, + ) as spy_validate: + projection = parse_projection(request) + spy_validate.assert_called_once_with(self.collection_book, expected_projection) + self.assertEqual(sorted(projection), sorted(expected_projection)) + + def test_parse_projection_should_return_all_collections_field_as_projection_when_nothing_specified_in_request(self): + request = RequestCollection( + method=RequestMethod.GET, + body=None, + query={ + "collection_name": "Book", + }, + collection=self.collection_book, + ) + expected_projection = ["author:firstname", "author:id", "author:lastname", "author_id", "id", "title"] + + projection = parse_projection(request) + self.assertEqual(sorted(projection), sorted(expected_projection)) + + def test_parse_projection_should_raise_when_query_want_an_unexisting_field(self): + request = RequestCollection( + method=RequestMethod.GET, + body=None, + query={ + "collection_name": "Book", + "fields[Book]": "id,title,blabedoubla", + }, + collection=self.collection_book, + ) + + self.assertRaisesRegex(CollectionException, r"Field not found 'Book.blabedoubla'", parse_projection, request) From 63b2860361c3c6ff7c4bce9a3b14be4383f26383 Mon Sep 17 00:00:00 2001 From: Julien Barreau Date: Fri, 22 Dec 2023 11:33:47 +0100 Subject: [PATCH 2/8] chore: add error description to exception --- src/agent_toolkit/forestadmin/agent_toolkit/utils/id.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/agent_toolkit/forestadmin/agent_toolkit/utils/id.py b/src/agent_toolkit/forestadmin/agent_toolkit/utils/id.py index 864dd01be..16e425a7d 100644 --- a/src/agent_toolkit/forestadmin/agent_toolkit/utils/id.py +++ b/src/agent_toolkit/forestadmin/agent_toolkit/utils/id.py @@ -15,10 +15,10 @@ class IdException(BaseException): def pack_id(schema: CollectionSchema, record: RecordsDataAlias) -> str: schema_pks = SchemaUtils.get_primary_keys(schema) if len(schema_pks) == 0: - raise IdException("") + raise IdException("No primary key found in the collection schema.") pks = [str(record[pk]) for pk in schema_pks if record.get(pk) is not None] if len(pks) == 0: - raise IdException("") + raise IdException(f"Primary key(s) '{'|'.join(schema_pks)}' not found in record.") return "|".join(pks) # type: ignore From e9c14c5f453ff07a5e0cc369f261a9bdb99dc2a2 Mon Sep 17 00:00:00 2001 From: Julien Barreau Date: Fri, 22 Dec 2023 11:35:25 +0100 Subject: [PATCH 3/8] fix(dj_record_serialization): records serializer return None on null relationships --- .../forestadmin/datasource_django/utils/record_serializer.py | 2 +- src/datasource_django/tests/test_django_collection.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/datasource_django/forestadmin/datasource_django/utils/record_serializer.py b/src/datasource_django/forestadmin/datasource_django/utils/record_serializer.py index 929619b55..75552867f 100644 --- a/src/datasource_django/forestadmin/datasource_django/utils/record_serializer.py +++ b/src/datasource_django/forestadmin/datasource_django/utils/record_serializer.py @@ -13,6 +13,6 @@ def instance_to_record_data(instance: Model, projection: Projection) -> RecordsD if relation: record_data[relation_name] = instance_to_record_data(relation, subfields) else: - record_data[relation_name] = {} + record_data[relation_name] = None return record_data diff --git a/src/datasource_django/tests/test_django_collection.py b/src/datasource_django/tests/test_django_collection.py index 3cf24dc93..2ab313f7d 100644 --- a/src/datasource_django/tests/test_django_collection.py +++ b/src/datasource_django/tests/test_django_collection.py @@ -88,7 +88,7 @@ async def test_list_should_work_with_relation(self): [ {"id": 1, "name": "Foundation", "author": {"first_name": "Isaac"}}, {"id": 2, "name": "Harry Potter", "author": {"first_name": "J.K."}}, - {"id": 3, "name": "Unknown Book", "author": {}}, + {"id": 3, "name": "Unknown Book", "author": None}, ], ) @@ -136,7 +136,7 @@ async def test_list_should_work_with_null_relations(self): self.assertEqual( ret, [ - {"id": 3, "name": "Unknown Book", "author": {}}, + {"id": 3, "name": "Unknown Book", "author": None}, ], ) From 5c985755c640d958b11d4cab14c8cefe5a3d4f93 Mon Sep 17 00:00:00 2001 From: Julien Barreau Date: Fri, 22 Dec 2023 11:36:46 +0100 Subject: [PATCH 4/8] chore: various little code improvement --- .../forestadmin/datasource_django/utils/model_introspection.py | 2 -- .../datasource_toolkit/decorators/computed/collections.py | 2 +- .../forestadmin/datasource_toolkit/validations/field.py | 3 +-- 3 files changed, 2 insertions(+), 5 deletions(-) diff --git a/src/datasource_django/forestadmin/datasource_django/utils/model_introspection.py b/src/datasource_django/forestadmin/datasource_django/utils/model_introspection.py index 06dafd34e..9ee725af3 100644 --- a/src/datasource_django/forestadmin/datasource_django/utils/model_introspection.py +++ b/src/datasource_django/forestadmin/datasource_django/utils/model_introspection.py @@ -158,8 +158,6 @@ def build(model: Model) -> CollectionSchema: # get_fields with include hidden doesn't include autogenerated foreign key fields (ending with _id) if isinstance(field, ForeignKey): fields[field.attname] = FieldFactory.build(field) - if isinstance(field, OneToOneField): - fields[field.attname] = FieldFactory.build(field) if field.one_to_one is True: if isinstance(field, OneToOneField): diff --git a/src/datasource_toolkit/forestadmin/datasource_toolkit/decorators/computed/collections.py b/src/datasource_toolkit/forestadmin/datasource_toolkit/decorators/computed/collections.py index 09a361bdb..30716386d 100644 --- a/src/datasource_toolkit/forestadmin/datasource_toolkit/decorators/computed/collections.py +++ b/src/datasource_toolkit/forestadmin/datasource_toolkit/decorators/computed/collections.py @@ -31,7 +31,7 @@ def get_computed(self, path: str) -> Union[ComputedDefinition, None]: except KeyError: return None - related_field, path = path.split(":") + related_field, path = path.split(":", 1) field = cast(RelationAlias, self.get_field(related_field)) foreign_collection: Self = self.datasource.get_collection(field["foreign_collection"]) return foreign_collection.get_computed(path) diff --git a/src/datasource_toolkit/forestadmin/datasource_toolkit/validations/field.py b/src/datasource_toolkit/forestadmin/datasource_toolkit/validations/field.py index da3fc5aa7..43f9c8585 100644 --- a/src/datasource_toolkit/forestadmin/datasource_toolkit/validations/field.py +++ b/src/datasource_toolkit/forestadmin/datasource_toolkit/validations/field.py @@ -24,8 +24,7 @@ class FieldValidator: def validate(cls, collection: Collection, field: str, values: Optional[List[Any]] = None) -> None: nested_field = None if ":" in field: - field, *nested_field = field.split(":") - nested_field = ":".join(nested_field) + field, nested_field = field.split(":", 1) try: schema = collection.schema["fields"][field] except KeyError: From f46d53a110c1dc8d4cfebebadc9dbd579ecb4416 Mon Sep 17 00:00:00 2001 From: Julien Barreau Date: Fri, 22 Dec 2023 11:48:53 +0100 Subject: [PATCH 5/8] chore(dj_agent_initialization): don't create agent when http server not started --- .../forestadmin/django_agent/apps.py | 19 +++------------- src/django_agent/tests/test_agent_creation.py | 22 ++++++++++--------- 2 files changed, 15 insertions(+), 26 deletions(-) diff --git a/src/django_agent/forestadmin/django_agent/apps.py b/src/django_agent/forestadmin/django_agent/apps.py index 3e88c73dd..f67cd6d92 100644 --- a/src/django_agent/forestadmin/django_agent/apps.py +++ b/src/django_agent/forestadmin/django_agent/apps.py @@ -17,6 +17,8 @@ def is_launch_as_server() -> bool: def init_app_agent() -> Optional[DjangoAgent]: + if not is_launch_as_server(): + return None agent = create_agent() if not hasattr(settings, "FOREST_AUTO_ADD_DJANGO_DATASOURCE") or settings.FOREST_AUTO_ADD_DJANGO_DATASOURCE: agent.add_datasource(DjangoDatasource()) @@ -25,7 +27,7 @@ def init_app_agent() -> Optional[DjangoAgent]: if customize_fn: agent = _call_user_customize_function(customize_fn, agent) - if agent and is_launch_as_server(): + if agent: agent.start() return agent @@ -72,18 +74,3 @@ def get_agent(cls) -> DjangoAgent: def ready(self): DjangoAgentApp._DJANGO_AGENT = init_app_agent() - - -# from django.utils.autoreload import DJANGO_AUTORELOAD_ENV -# import os -# no_autoreload = any(arg.casefold() == "noreload" for arg in sys.argv) -# django.utils.autoreload.DJANGO_AUTORELOAD_ENV == "RUN_MAIN" -# print( -# "--run main: ", os.environ.get(DJANGO_AUTORELOAD_ENV) -# ) # to know in which process we are when autoreload is enabled -# print("--launch_as_server: ", launch_as_server) -# print("--no_autoreload: ", no_autoreload) - -# prevent launching for manage command -# prevent launching in reloader parent process -# if not is_manage_py or no_autoreload or os.environ.get(DJANGO_AUTORELOAD_ENV) == "true": diff --git a/src/django_agent/tests/test_agent_creation.py b/src/django_agent/tests/test_agent_creation.py index d7bc4a94d..f94ddc146 100644 --- a/src/django_agent/tests/test_agent_creation.py +++ b/src/django_agent/tests/test_agent_creation.py @@ -183,19 +183,21 @@ def test_should_return_None_when_error_in_customize_fn_import_from_str(self): def test_should_call_agent_start_when_everything_work_well_and_launch_as_server(self): with override_settings(**self.dj_options): - with patch("forestadmin.django_agent.agent.DjangoAgent.start") as mock_start: - with patch("forestadmin.django_agent.apps.is_launch_as_server", return_value=True): - agent = init_app_agent() - mock_start.assert_called_once() - self.assertIsNotNone(agent) - - def test_should_not_call_agent_start_when_everything_work_well_but_not_launch_as_server(self): + with patch("forestadmin.django_agent.apps.is_launch_as_server", return_value=True): + with patch("forestadmin.django_agent.apps.create_agent", wraps=create_agent) as spy_create_agent: + with patch("forestadmin.django_agent.agent.DjangoAgent.start") as mock_start: + agent = init_app_agent() + spy_create_agent.assert_called_once() + mock_start.assert_called_once() + self.assertIsNotNone(agent) + + def test_should_not_initialize_agent_when_not_launch_as_server(self): with override_settings(**self.dj_options): - with patch("forestadmin.django_agent.agent.DjangoAgent.start") as mock_start: + with patch("forestadmin.django_agent.apps.create_agent") as mock_create_agent: with patch("forestadmin.django_agent.apps.is_launch_as_server", return_value=False): agent = init_app_agent() - mock_start.assert_not_called() - self.assertIsNotNone(agent) + mock_create_agent.assert_not_called() + self.assertIsNone(agent) def test_should_not_call_agent_start_when_error_during_customize_fn(self): with override_settings( From 07e28549cfd33bbba2d63112aaed96d6a35c4c53 Mon Sep 17 00:00:00 2001 From: Julien Barreau Date: Fri, 22 Dec 2023 11:50:43 +0100 Subject: [PATCH 6/8] fix(relation_as_pk): PK is now on the foreign_key --- .../utils/forest_schema/generator_collection.py | 8 ++++++-- .../agent_toolkit/utils/forest_schema/generator_field.py | 2 +- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/agent_toolkit/forestadmin/agent_toolkit/utils/forest_schema/generator_collection.py b/src/agent_toolkit/forestadmin/agent_toolkit/utils/forest_schema/generator_collection.py index 241cb6c15..9083fff4f 100644 --- a/src/agent_toolkit/forestadmin/agent_toolkit/utils/forest_schema/generator_collection.py +++ b/src/agent_toolkit/forestadmin/agent_toolkit/utils/forest_schema/generator_collection.py @@ -15,8 +15,12 @@ class SchemaCollectionGenerator: async def build(prefix: str, collection: Union[Collection, CollectionCustomizer]) -> ForestServerCollection: fields: List[ForestServerField] = [] for field_name in collection.schema["fields"].keys(): - if not SchemaUtils.is_foreign_key(collection.schema, field_name): - fields.append(SchemaFieldGenerator.build(collection, field_name)) + if SchemaUtils.is_foreign_key(collection.schema, field_name) and not SchemaUtils.is_primary_key( + collection.schema, field_name + ): + # ignore foreign key because we have relationships, except when the fk is pk + continue + fields.append(SchemaFieldGenerator.build(collection, field_name)) fields = sorted(fields, key=lambda field: field["field"]) return { diff --git a/src/agent_toolkit/forestadmin/agent_toolkit/utils/forest_schema/generator_field.py b/src/agent_toolkit/forestadmin/agent_toolkit/utils/forest_schema/generator_field.py index 8912fce6a..9ffc52008 100644 --- a/src/agent_toolkit/forestadmin/agent_toolkit/utils/forest_schema/generator_field.py +++ b/src/agent_toolkit/forestadmin/agent_toolkit/utils/forest_schema/generator_field.py @@ -174,7 +174,7 @@ def build_many_to_one_schema( "type": cls.build_column_type(key_schema["column_type"]), "defaultValue": key_schema["default_value"], "isFilterable": cls.is_foreign_collection_filterable(foreign_collection), - "isPrimaryKey": bool(key_schema["is_primary_key"]), + "isPrimaryKey": False, "isRequired": any([v["operator"] == Operator.PRESENT for v in validations]), "isSortable": bool(key_schema["is_sortable"]), "validations": FrontendValidationUtils.convert_validation_list(validations), From 49912d04aa409eda3db15bbb5436ca7e0b13bd5a Mon Sep 17 00:00:00 2001 From: Julien Barreau Date: Fri, 22 Dec 2023 11:51:17 +0100 Subject: [PATCH 7/8] fix(one_to_one): the reference of a 1 to 1 is now correct --- .../agent_toolkit/utils/forest_schema/generator_field.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/agent_toolkit/forestadmin/agent_toolkit/utils/forest_schema/generator_field.py b/src/agent_toolkit/forestadmin/agent_toolkit/utils/forest_schema/generator_field.py index 9ffc52008..12f6228f4 100644 --- a/src/agent_toolkit/forestadmin/agent_toolkit/utils/forest_schema/generator_field.py +++ b/src/agent_toolkit/forestadmin/agent_toolkit/utils/forest_schema/generator_field.py @@ -127,7 +127,7 @@ def build_one_to_one_schema( "isReadOnly": bool(key_field["is_read_only"]), "isSortable": bool(target_field["is_sortable"]), "validations": [], - "reference": f"{foreign_collection.name}.{relation['origin_key_target']}", + "reference": f"{foreign_collection.name}.{relation['origin_key']}", } @classmethod From f0355e104e747b2922bd06cc4c7bfd909ec226b8 Mon Sep 17 00:00:00 2001 From: Julien Barreau Date: Fri, 22 Dec 2023 12:04:22 +0100 Subject: [PATCH 8/8] chore(example): update demo project --- .../django_demo/.forestadmin-schema.json | 193 +++++++++++++++++- .../app/migrations/0003_extendedcart.py | 29 +++ ...0004_discountcart_extendedcart_discount.py | 38 ++++ src/_example/django/django_demo/app/models.py | 12 ++ 4 files changed, 269 insertions(+), 3 deletions(-) create mode 100644 src/_example/django/django_demo/app/migrations/0003_extendedcart.py create mode 100644 src/_example/django/django_demo/app/migrations/0004_discountcart_extendedcart_discount.py diff --git a/src/_example/django/django_demo/.forestadmin-schema.json b/src/_example/django/django_demo/.forestadmin-schema.json index e7a2bca1e..b05db0f5f 100644 --- a/src/_example/django/django_demo/.forestadmin-schema.json +++ b/src/_example/django/django_demo/.forestadmin-schema.json @@ -542,6 +542,22 @@ "type": "Number", "validations": [] }, + { + "defaultValue": null, + "enums": null, + "field": "extendedcart", + "inverseOf": "cart", + "isFilterable": true, + "isPrimaryKey": false, + "isReadOnly": false, + "isRequired": false, + "isSortable": true, + "isVirtual": false, + "reference": "app_extendedcart.cart_id", + "relationship": "HasOne", + "type": "Number", + "validations": [] + }, { "defaultValue": null, "enums": null, @@ -1057,6 +1073,177 @@ } ] }, + { + "name": "app_discountcart", + "isVirtual": false, + "icon": null, + "isReadOnly": false, + "integration": null, + "isSearchable": true, + "onlyForRelationships": false, + "paginationType": "page", + "searchField": null, + "actions": [], + "segments": [], + "fields": [ + { + "defaultValue": null, + "enums": null, + "field": "discount", + "integration": null, + "inverseOf": null, + "isFilterable": true, + "isPrimaryKey": false, + "isReadOnly": false, + "isRequired": true, + "isSortable": true, + "isVirtual": false, + "reference": null, + "type": "Number", + "validations": [ + { + "type": "is present", + "value": null, + "message": null + } + ] + }, + { + "defaultValue": null, + "enums": null, + "field": "extendedcart", + "inverseOf": "discount", + "isFilterable": true, + "isPrimaryKey": false, + "isReadOnly": false, + "isRequired": false, + "isSortable": true, + "isVirtual": false, + "reference": "app_extendedcart.discount_id", + "relationship": "HasOne", + "type": "Number", + "validations": [] + }, + { + "defaultValue": null, + "enums": null, + "field": "id", + "integration": null, + "inverseOf": null, + "isFilterable": true, + "isPrimaryKey": true, + "isReadOnly": true, + "isRequired": false, + "isSortable": true, + "isVirtual": false, + "reference": null, + "type": "Number", + "validations": [] + } + ] + }, + { + "name": "app_extendedcart", + "isVirtual": false, + "icon": null, + "isReadOnly": false, + "integration": null, + "isSearchable": true, + "onlyForRelationships": false, + "paginationType": "page", + "searchField": null, + "actions": [], + "segments": [], + "fields": [ + { + "defaultValue": null, + "enums": null, + "field": "cart", + "inverseOf": "extendedcart", + "isFilterable": true, + "isPrimaryKey": false, + "isReadOnly": false, + "isRequired": true, + "isSortable": true, + "isVirtual": false, + "reference": "app_cart.id", + "relationship": "BelongsTo", + "type": "Number", + "validations": [ + { + "type": "is present", + "value": null, + "message": null + } + ] + }, + { + "defaultValue": null, + "enums": null, + "field": "cart_id", + "integration": null, + "inverseOf": null, + "isFilterable": true, + "isPrimaryKey": true, + "isReadOnly": true, + "isRequired": true, + "isSortable": true, + "isVirtual": false, + "reference": null, + "type": "Number", + "validations": [ + { + "type": "is present", + "value": null, + "message": null + } + ] + }, + { + "defaultValue": null, + "enums": null, + "field": "color", + "integration": null, + "inverseOf": null, + "isFilterable": true, + "isPrimaryKey": false, + "isReadOnly": false, + "isRequired": true, + "isSortable": true, + "isVirtual": false, + "reference": null, + "type": "String", + "validations": [ + { + "type": "is present", + "value": null, + "message": null + }, + { + "type": "is shorter than", + "value": 20, + "message": null + } + ] + }, + { + "defaultValue": null, + "enums": null, + "field": "discount", + "inverseOf": "extendedcart", + "isFilterable": true, + "isPrimaryKey": false, + "isReadOnly": false, + "isRequired": false, + "isSortable": true, + "isVirtual": false, + "reference": "app_discountcart.id", + "relationship": "BelongsTo", + "type": "Number", + "validations": [] + } + ] + }, { "name": "app_order", "isVirtual": false, @@ -1204,7 +1391,7 @@ "isRequired": false, "isSortable": true, "isVirtual": false, - "reference": "app_cart.id", + "reference": "app_cart.order_id", "relationship": "HasOne", "type": "Number", "validations": [] @@ -3173,11 +3360,11 @@ ], "meta": { "liana": "agent-python", - "liana_version": "1.2.0-beta.2", + "liana_version": "1.3.0-beta.2", "stack": { "engine": "python", "engine_version": "3.10.11" }, - "schemaFileHash": "cb7254f309cce45b89e83eebc3302e86b1e42ead" + "schemaFileHash": "19bcf807929df4cb8ff109f803dea0789a658df1" } } \ No newline at end of file diff --git a/src/_example/django/django_demo/app/migrations/0003_extendedcart.py b/src/_example/django/django_demo/app/migrations/0003_extendedcart.py new file mode 100644 index 000000000..542e218c3 --- /dev/null +++ b/src/_example/django/django_demo/app/migrations/0003_extendedcart.py @@ -0,0 +1,29 @@ +# Generated by Django 4.2.7 on 2023-12-21 09:47 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ("app", "0002_flaskaddress_flaskcustomer_flaskorder_and_more"), + ] + + operations = [ + migrations.CreateModel( + name="ExtendedCart", + fields=[ + ( + "cart", + models.OneToOneField( + on_delete=django.db.models.deletion.CASCADE, + primary_key=True, + serialize=False, + to="app.cart", + ), + ), + ("color", models.CharField(max_length=20)), + ], + ), + ] diff --git a/src/_example/django/django_demo/app/migrations/0004_discountcart_extendedcart_discount.py b/src/_example/django/django_demo/app/migrations/0004_discountcart_extendedcart_discount.py new file mode 100644 index 000000000..335a3d64c --- /dev/null +++ b/src/_example/django/django_demo/app/migrations/0004_discountcart_extendedcart_discount.py @@ -0,0 +1,38 @@ +# Generated by Django 4.2.7 on 2023-12-21 10:41 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ("app", "0003_extendedcart"), + ] + + operations = [ + migrations.CreateModel( + name="DiscountCart", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("discount", models.FloatField()), + ], + ), + migrations.AddField( + model_name="extendedcart", + name="discount", + field=models.OneToOneField( + null=True, + on_delete=django.db.models.deletion.CASCADE, + to="app.discountcart", + ), + ), + ] diff --git a/src/_example/django/django_demo/app/models.py b/src/_example/django/django_demo/app/models.py index 3f850b646..59f0f4e2a 100644 --- a/src/_example/django/django_demo/app/models.py +++ b/src/_example/django/django_demo/app/models.py @@ -105,3 +105,15 @@ class Cart(models.Model): created_at = models.DateTimeField(auto_now_add=True) order = models.OneToOneField(Order, on_delete=models.CASCADE, null=True) + + +class ExtendedCart(models.Model): + cart = models.OneToOneField(Cart, primary_key=True, on_delete=models.CASCADE) + + color = models.CharField(max_length=20) + + discount = models.OneToOneField("DiscountCart", on_delete=models.CASCADE, null=True) + + +class DiscountCart(models.Model): + discount = models.FloatField()