Skip to content
Merged
193 changes: 190 additions & 3 deletions src/_example/django/django_demo/.forestadmin-schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -1204,7 +1391,7 @@
"isRequired": false,
"isSortable": true,
"isVirtual": false,
"reference": "app_cart.id",
"reference": "app_cart.order_id",
"relationship": "HasOne",
"type": "Number",
"validations": []
Expand Down Expand Up @@ -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"
}
}
Original file line number Diff line number Diff line change
@@ -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)),
],
),
]
Original file line number Diff line number Diff line change
@@ -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",
),
),
]
12 changes: 12 additions & 0 deletions src/_example/django/django_demo/app/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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]):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Loading