carltongibson / django-filter

A generic system for filtering Django QuerySets based on user selections
https://django-filter.readthedocs.io/en/main/
Other
4.39k stars 760 forks source link

Unrecognized field type GeneratedField #1673

Open roniemartinez opened 3 weeks ago

roniemartinez commented 3 weeks ago

I've got this error which is caused by using a GeneratedField. Before migrating to GeneratedField, we used to use a migration script to create the generated field and django-filter has no issue with it.

AssertionError: MyFilter resolved field 'my_field' with 'exact' lookup to an unrecognized field type GeneratedField. Try adding an override to 'Meta.filter_overrides'. See: https://django-filter.readthedocs.io/en/main/ref/filterset.html#customise-filter-generation-with-filter-overrides

I think it should be possible to know what filter to use since GeneratedField has an output_field.

carltongibson commented 2 weeks ago

@roniemartinez Could you put together a proof-of-concept showing what you mean here, perhaps with tests showing different examples? Thanks.

violuke commented 1 day ago

If you have a field in your model like:

code_full = models.GeneratedField(
        expression=If(Q(code_number__isnull=True), Value(None), Concat("code_prefix", "code_number")),
        db_persist=True,
        output_field=models.CharField(blank=True, null=True, max_length=15),
    )

Then an error like @roniemartinez posted above will be raised.

This can be fixed with overriding filter_for_field() (note the # Handle GeneratedFields block):

@classmethod
def filter_for_field(cls, field, field_name, lookup_expr=None):
    if lookup_expr is None:
        lookup_expr = settings.DEFAULT_LOOKUP_EXPR

    # Handle GeneratedFields
    if isinstance(field, GeneratedField):
        new_field = field.output_field
        new_field.model = field.model
        field = new_field

    field, lookup_type = resolve_field(field, lookup_expr)

    default = {
        "field_name": field_name,
        "lookup_expr": lookup_expr,
    }

    filter_class, params = cls.filter_for_lookup(field, lookup_type)
    default.update(params)

    assert filter_class is not None, (
                                         "%s resolved field '%s' with '%s' lookup to an unrecognized field "
                                         "type %s. Try adding an override to 'Meta.filter_overrides'. See: "
                                         "https://django-filter.readthedocs.io/en/main/ref/filterset.html"
                                         "#customise-filter-generation-with-filter-overrides"
                                     ) % (cls.__name__, field_name, lookup_expr, field.__class__.__name__)

    return filter_class(**default)

It might not be the perfect fix, but it's worked well for me in my testing.

carltongibson commented 1 day ago

So there's two parts to this.

The is the error. That should be addressed by #1675, which will enable skipping unknown fields — if you haven't used filter_overrides.

The second is the generated field handling itself:

    # Handle GeneratedFields
    if isinstance(field, GeneratedField):
        new_field = field.output_field
        new_field.model = field.model
        field = new_field

Is it as simple as that? Can we add built-in support?

Disclaimer: I haven't used GeneratedField yet, so haven't looked into what works and doesn't here at all.

violuke commented 1 day ago

Is it as simple as that? Can we add built-in support?

Built-in support would be great! I can't find a problem with this solution so far, and it's been deployed to production today, so 🤞

I'll let you know if we find any issues, but I think this is all that's needed.

carltongibson commented 16 hours ago

OK, but the correct place to add this would be to the FILTER_FOR_DBFIELD_DEFAULTS, probably with a specific Filter subclass.

If someone wants to take that on as an addition (with docs and tests) that would be very welcome.