Repository: graphql-python/graphene-django

Issue:
Change in get_filtering_args_from_filterset on 2.4.0 breaks some filters
### Context

In the latest release there is a bugfix (#682) that intends to _Ensure correct filter types for DjangoFilterConnectionFields_, which is related to issue #678. The problem is that the change in the function `get_filtering_args_from_filterset()` has two possible issues (one minor and one major):

1. `form_field` not defined (_minor_):
This is a simple one. Following the logic in this new implementation if the `name` of the filter is not in the `filterset_class.declared_filters` and `field_name` is not an attribute of the model, it will never get form_field defined, so when it gets to:

```python
if not form_field:
    form_field = filter_field.field
```

We get a `local variable 'form_field' referenced before assignment`.

2. Filters that are not applied to fields of the model will crash (_major_):
After fixing the previous issue, we still have the problem that the new implementation will only look for filters either in the `declared_filters` or for filter names that could match a field from a model. In our case, we have a mixin that overrides the `get_filters` method and updates the filters with some common ones that are not field filters. So we get a `django.core.exceptions.FieldDoesNotExist: xxx has no field named 'xxxxx'`

### Steps to reproduce it

1. Create a filterset based on a mixin:
```python
class FooMixin:

    @classmethod
    def get_filters(cls):
        filters = super().get_filters()
        filters.update({
                'partner__status': django_filters.ChoiceFilter(
                    method='filter_status',
                    field_name='users__status',
                    choices=users.field.model.STATUS,
                ),
            })

        return filters

class FooFilter(FooMixin, django_filters.FilterSet):
    . . .
```
2. Try any query that uses this filter.

### What can we do?

I do have a proposal on how to fix this issue, but I wanted to hear your opinion first about this issue, as if this is an _actual_ issue or if we are doing something wrong. So please, if you have any suggestions, let me know.

### Proposal

My proposal to fix this issue would be changing the current implementation of the function mentioned before `get_filtering_args_from_filterset`:

```python
def get_filtering_args_from_filterset(filterset_class, type):
    """ Inspect a FilterSet and produce the arguments to pass to
        a Graphene Field. These arguments will be available to
        filter against in the GraphQL
    """
    from ..forms.converter import convert_form_field

    args = {}
    model = filterset_class._meta.model
    for name, filter_field in six.iteritems(filterset_class.base_filters):
        form_field = None

        if name in filterset_class.declared_filters:
            form_field = filter_field.field
        else:
            field_name = name.split("__", 1)[0]

            if hasattr(model, field_name):
                model_field = model._meta.get_field(field_name)

                if hasattr(model_field, "formfield"):
                    form_field = model_field.formfield(
                        required=filter_field.extra.get("required", False)
                    )

        # Fallback to field defined on filter if we can't get it from the
        # model field
        if not form_field:
            form_field = filter_field.field

        field_type = convert_form_field(form_field).Argument()
        field_type.description = filter_field.label
        args[name] = field_type

    return args
```

Here we are defining `form_field` as `None`, in case it doesn't match any of the following conditions, and checking if the filter is related to a field of the model before calling `get_field`.


Discussion:
Thanks for the detailed write up @kikeh ! You're proposal looks great, could you raise a PR with the changes and the necessary regression tests? 

I confess that I haven't had much experience with django_filters so it's not surprising that I missed something when implementing that bug fix.
@jkimbo sure, I am going to create the PR.

And don't worry, this one was a quite _specific_ issue but it was not so hard to spot :)

Resolving patch:
diff --git a/graphene_django/filter/utils.py b/graphene_django/filter/utils.py
--- a/graphene_django/filter/utils.py
+++ b/graphene_django/filter/utils.py
@@ -13,21 +13,25 @@ def get_filtering_args_from_filterset(filterset_class, type):
     args = {}
     model = filterset_class._meta.model
     for name, filter_field in six.iteritems(filterset_class.base_filters):
+        form_field = None
+
         if name in filterset_class.declared_filters:
             form_field = filter_field.field
         else:
             field_name = name.split("__", 1)[0]
-            model_field = model._meta.get_field(field_name)
 
-            if hasattr(model_field, "formfield"):
-                form_field = model_field.formfield(
-                    required=filter_field.extra.get("required", False)
-                )
+            if hasattr(model, field_name):
+                model_field = model._meta.get_field(field_name)
+
+                if hasattr(model_field, "formfield"):
+                    form_field = model_field.formfield(
+                        required=filter_field.extra.get("required", False)
+                    )
 
-            # Fallback to field defined on filter if we can't get it from the
-            # model field
-            if not form_field:
-                form_field = filter_field.field
+        # Fallback to field defined on filter if we can't get it from the
+        # model field
+        if not form_field:
+            form_field = filter_field.field
 
         field_type = convert_form_field(form_field).Argument()
         field_type.description = filter_field.label


Test patch:
diff --git a/graphene_django/filter/tests/test_fields.py b/graphene_django/filter/tests/test_fields.py
--- a/graphene_django/filter/tests/test_fields.py
+++ b/graphene_django/filter/tests/test_fields.py
@@ -818,3 +818,106 @@ class Query(ObjectType):
         }
     """
     )
+
+
+def test_filter_filterset_based_on_mixin():
+    class ArticleFilterMixin(FilterSet):
+        @classmethod
+        def get_filters(cls):
+            filters = super(FilterSet, cls).get_filters()
+            filters.update(
+                {
+                    "viewer__email__in": django_filters.CharFilter(
+                        method="filter_email_in", field_name="reporter__email__in"
+                    )
+                }
+            )
+
+            return filters
+
+        def filter_email_in(cls, queryset, name, value):
+            return queryset.filter(**{name: [value]})
+
+    class NewArticleFilter(ArticleFilterMixin, ArticleFilter):
+        pass
+
+    class NewReporterNode(DjangoObjectType):
+        class Meta:
+            model = Reporter
+            interfaces = (Node,)
+
+    class NewArticleFilterNode(DjangoObjectType):
+        viewer = Field(NewReporterNode)
+
+        class Meta:
+            model = Article
+            interfaces = (Node,)
+            filterset_class = NewArticleFilter
+
+        def resolve_viewer(self, info):
+            return self.reporter
+
+    class Query(ObjectType):
+        all_articles = DjangoFilterConnectionField(NewArticleFilterNode)
+
+    reporter_1 = Reporter.objects.create(
+        first_name="John", last_name="Doe", email="john@doe.com"
+    )
+
+    article_1 = Article.objects.create(
+        headline="Hello",
+        reporter=reporter_1,
+        editor=reporter_1,
+        pub_date=datetime.now(),
+        pub_date_time=datetime.now(),
+    )
+
+    reporter_2 = Reporter.objects.create(
+        first_name="Adam", last_name="Doe", email="adam@doe.com"
+    )
+
+    article_2 = Article.objects.create(
+        headline="Good Bye",
+        reporter=reporter_2,
+        editor=reporter_2,
+        pub_date=datetime.now(),
+        pub_date_time=datetime.now(),
+    )
+
+    schema = Schema(query=Query)
+
+    query = (
+        """
+        query NodeFilteringQuery {
+            allArticles(viewer_Email_In: "%s") {
+                edges {
+                    node {
+                        headline
+                        viewer {
+                            email
+                        }
+                    }
+                }
+            }
+        }
+    """
+        % reporter_1.email
+    )
+
+    expected = {
+        "allArticles": {
+            "edges": [
+                {
+                    "node": {
+                        "headline": article_1.headline,
+                        "viewer": {"email": reporter_1.email},
+                    }
+                }
+            ]
+        }
+    }
+
+    result = schema.execute(query)
+
+    assert not result.errors
+    assert result.data == expected