RenaudLN / dash-pydantic-form

MIT License
31 stars 6 forks source link

Add support for List[BaseModel] #9

Closed CY-ipercept closed 5 months ago

CY-ipercept commented 5 months ago

Hi again, @RenaudLN

Could support for List[BaseModel] be added when creating a prefilled form? I have a model where one of the parameters takes in a list of another model, and this other model contains parameters of floats, str, int, List[str], List[float] and List[int]. Hence, I would like to have these values populate the pre-filled form.

Example model structure

class Worker(BaseModel):
    name: str
    associates: List[str]

class Track(BaseModel):
    workers: List[Worker]
    count: int

The prefilled form shows workers:[{name:"Ben", 'associates': [None]}]

even though the json_obj has a list of strings

Thanks!

RenaudLN commented 5 months ago

Hey @CY-ipercept list of BaseModel is already covered by dash-pydantic-form. Are you having issues with it? https://pydf-docs.onrender.com/api/list_field

CY-ipercept commented 5 months ago

yep, i've updated the issue @RenaudLN

CY-ipercept commented 5 months ago

Also, another question about the pre-filled form feature.

If i have a model with an optional field such as info: Optional[SomeModel] = None

class SomeModel 
    compulsory_field: str
    default_field: Literal["test", "test1", "test2"] = "test"

The item that i passed in does not have any info field filled in, but the form_data suddenly contains a value for default_field when I access the form_data via a dash callback, and throws validation errors for other missing fields in SomeModel like compulsory_field

CY-ipercept commented 5 months ago
class Branch(BaseModel):
    name: Literal["West", "North", "East", "South"] = "West"

class Department(BaseModel):
    name: str
    sub_departments: List[str]

class Colleagues(BaseModel):
    department: List[Department]
    branch: Branch = Branch()

class Employee(BaseModel):
    """Employee model."""

    name: str = Field(title="Name", description="Name of the employee", min_length=2)
    age: int = Field(title="Age", description="Age of the employee, starting from their birth", ge=18)
    mini_bio: str | None = Field(title="Mini bio", description="Short bio of the employee", default=None)
    joined: date = Field(title="Joined", description="Date when the employee joined the company")
    office: Office = Field(title="Office", description="Office of the employee")
    metadata: Metadata | None = Field(title="Employee metadata", default=None)
    location: HomeOffice | WorkOffice | None = Field(title="Work location", default=None, discriminator="type")
    pets: list[Pet] = Field(title="Pets", description="Employee pets", default_factory=list)
    jobs: list[str] = Field(
        title="Past jobs", description="List of previous jobs the employee has held", default_factory=list
    )
    colleagues: Optional[Colleagues] = None

bob = Employee(
    name="Bob",
    age=30,
    joined="2020-01-01",
    mini_bio="### Birth\nSomething something\n\n### Education\nCollege",
    office="au",
    metadata={"languages": ["fr", "en"], "siblings": 2},
    pets=[{"name": "Rex", "species": "cat"}],
    location={
        "type": "home_office",
        "has_workstation": True,
    },
    jobs=["Engineer", "Lawyer"],
    colleagues={"department": [{"name": "Engineering", "sub_departments": ["Software", "Hardware"]}]},
)

ModelForm(
    # Employee,
    bob,
    AIO_ID,
    FORM_ID,
)

image

Hope this helps! @RenaudLN

RenaudLN commented 5 months ago

The prefilled form shows workers:[{name:"Ben", 'associates': [None]}]

Hey @CY-ipercept, 0.2.2 fixes the issue with the pre-filled scalar list, thanks for bringing it up.

I'll have a look at the optional fields issue but this one is a bit trickier. Will let you know.

Update: on the optional field issue, it's not something that can be fixed clientside as it depends on the pydantic validation which can only happen serverside. The best I could do is a utility python function that creates a pydantic model with None if the field is optional and the sub-field fails to validate.

anchengyang commented 5 months ago

ah thanks @RenaudLN , I actually pulled the repo and made some changes, but was not able to push those changes cos of permission rights. But since you made the change already, I guess there is no need for mine. Thanks for making the changes!

This was the change I made, if you want to see:

def get_subitem(item: BaseModel, parent: str) -> BaseModel:
    """Get the subitem of a model at a given parent.

    e.g., get_subitem(person, "au_metadata") = AUMetadata(param1=True, param2=False)
    """
    if parent == "":
        return item

    path = parent.split(SEP)

    first_part = path[0]
    if isinstance(first_part, str) and first_part.isdigit():
        first_part = int(first_part)

    if len(path) == 1:
        if isinstance(item, BaseModel):
            return getattr(item, first_part)
        if isinstance(item, dict) and isinstance(first_part, int):
            return list(item.values())[first_part]
        return item[first_part]

    if isinstance(item, list) and isinstance(first_part, int):
        return get_subitem(item[first_part], SEP.join(path[1:]))

    return get_subitem(getattr(item, first_part), SEP.join(path[1:]))
RenaudLN commented 5 months ago

My fix is similar. If you want to contribute to the project, you'll need to fork the repo, create a branch then create a pull request from there 🙂

RenaudLN commented 5 months ago

@CY-ipercept I added a from_form_data function to use a default value if it cannot validate a field. Note that it will go up the tree of fields until it finds a default value. An example of how to use it is in the usage.py

Let me know if this works for your case and I'll close this issue.

Edit: available in 0.3.0

CY-ipercept commented 5 months ago

Hi @RenaudLN , unfortunately, I encountered an error when trying to add a department. Below is the screenshot of the error and the model that i used.

image

class Branch(BaseModel):
    name: Literal["West", "North", "East", "South"] = "West"

class Department(BaseModel):
    name: str
    sub_departments: List[str]

class Colleagues(BaseModel):
    department: List[Department]
    branch: Branch = Branch()

class Employee(BaseModel):
    """Employee model."""

    name: str = Field(title="Name", description="Name of the employee", min_length=2)
    age: int = Field(title="Age", description="Age of the employee, starting from their birth", ge=18)
    mini_bio: str | None = Field(title="Mini bio", description="Short bio of the employee", default=None)
    joined: date = Field(title="Joined", description="Date when the employee joined the company")
    office: Office = Field(title="Office", description="Office of the employee")
    metadata: Metadata | None = Field(title="Employee metadata", default=None)
    location: HomeOffice | WorkOffice | None = Field(title="Work location", default=None, discriminator="type")
    pets: list[Pet] = Field(title="Pets", description="Employee pets", default_factory=list)
    jobs: list[str] = Field(
        title="Past jobs", description="List of previous jobs the employee has held", default_factory=list
    )
    colleagues: Optional[Colleagues] = None

bob = Employee(
    name="Bob",
    age=30,
    joined="2020-01-01",
    mini_bio="### Birth\nSomething something\n\n### Education\nCollege",
    office="au",
    metadata={"languages": ["fr", "en"], "siblings": 2},
    pets=[{"name": "Rex", "species": "cat"}],
    location={
        "type": "home_office",
        "has_workstation": True,
    },
    jobs=["Engineer", "Lawyer"],
)

Update: The updating of the form data seems a little buggy, as I have 2 strings in my list of sub-departments but the form_data shows 1 image

CY-ipercept commented 5 months ago

@RenaudLN I managed to come up with a fix for the from_form_data function, will create a PR when it is ready