Repository: django-json-api/django-rest-framework-json-api

Issue:
Including lists of resources from non-model annotated objects
I have a `Organization` model with a serializer whose instances get some extra data bound to then when using a specific view. this extra data is two lists of `Product` objects, which is not a django model (just a plain old python object) but its representation should look like a regular relation: the attrs for the organization and then the product lists in `included`. I tried the following:

```python

class OrganizationSerializer(serializers.ModelSerializer):
    included_serializers = {
        "current_cycle": ProductSerializer,
        "next_cycle": ProductSerializer,
    }

    current_cycle = ResourceRelatedField(
        many=True, read_only=True, model=Product
    )
    next_cycle = ResourceRelatedField(
        many=True, read_only=True, model=Product
    )

    class Meta:
        model = Organization
        fields = ("pk", "current_cycle", "next_cycle")

    class JSONAPIMeta:
        included_resources = ("current_cycle", "next_cycle")
```
But this results in `AttributeError: type object 'Organization' has no attribute 'current_cycle'`, which happens in `utils.get_related_resource_type`.

Stepping through the code there, it seems like although it's checking the `model` attribute directly on the relation, it's not reaching into the `child_relation` attr when the `many` is set to True. It does a `child` attribute which in my case wasn't there although `child_relation` is.

Doing a quick and dirty fix by including 
```python
    elif (
        hasattr(relation, "child_relation")
        and hasattr(relation.child_relation, "model"
    )):
        relation_model = relation.child_relation.model
```
in the big if/elif/else in `get_related_resource_type` seems to work. I'm however bery new to this library and maybe I'm just missing something. Otherwise I can make a PR with this fix, of course. Any help would be greatly appreciated :) Thanks!


Discussion:
codecov[bot]: # [Codecov](https://codecov.io/gh/django-json-api/django-rest-framework-json-api/pull/868?src=pr&el=h1) Report
> Merging [#868](https://codecov.io/gh/django-json-api/django-rest-framework-json-api/pull/868?src=pr&el=desc) (bce1f95) into [master](https://codecov.io/gh/django-json-api/django-rest-framework-json-api/commit/c356806d9f8ad15669a049b6b4bb041a1c9ea482?el=desc) (c356806) will **decrease** coverage by `0.02%`.
> The diff coverage is `n/a`.

[![Impacted file tree graph](https://codecov.io/gh/django-json-api/django-rest-framework-json-api/pull/868/graphs/tree.svg?width=650&height=150&src=pr&token=kJ4T4hcVh8)](https://codecov.io/gh/django-json-api/django-rest-framework-json-api/pull/868?src=pr&el=tree)

```diff
@@            Coverage Diff             @@
##           master     #868      +/-   ##
==========================================
- Coverage   97.74%   97.71%   -0.03%     
==========================================
  Files          60       60              
  Lines        3319     3276      -43     
==========================================
- Hits         3244     3201      -43     
  Misses         75       75              
```


| [Impacted Files](https://codecov.io/gh/django-json-api/django-rest-framework-json-api/pull/868?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [rest\_framework\_json\_api/renderers.py](https://codecov.io/gh/django-json-api/django-rest-framework-json-api/pull/868/diff?src=pr&el=tree#diff-cmVzdF9mcmFtZXdvcmtfanNvbl9hcGkvcmVuZGVyZXJzLnB5) | `90.00% <0.00%> (-0.19%)` | :arrow_down: |
| [rest\_framework\_json\_api/relations.py](https://codecov.io/gh/django-json-api/django-rest-framework-json-api/pull/868/diff?src=pr&el=tree#diff-cmVzdF9mcmFtZXdvcmtfanNvbl9hcGkvcmVsYXRpb25zLnB5) | `88.29% <0.00%> (-0.12%)` | :arrow_down: |
| [rest\_framework\_json\_api/parsers.py](https://codecov.io/gh/django-json-api/django-rest-framework-json-api/pull/868/diff?src=pr&el=tree#diff-cmVzdF9mcmFtZXdvcmtfanNvbl9hcGkvcGFyc2Vycy5weQ==) | `97.33% <0.00%> (-0.11%)` | :arrow_down: |
| [tests/conftest.py](https://codecov.io/gh/django-json-api/django-rest-framework-json-api/pull/868/diff?src=pr&el=tree#diff-dGVzdHMvY29uZnRlc3QucHk=) | `100.00% <0.00%> (ø)` | |
| [example/factories.py](https://codecov.io/gh/django-json-api/django-rest-framework-json-api/pull/868/diff?src=pr&el=tree#diff-ZXhhbXBsZS9mYWN0b3JpZXMucHk=) | `100.00% <0.00%> (ø)` | |
| [example/tests/test\_views.py](https://codecov.io/gh/django-json-api/django-rest-framework-json-api/pull/868/diff?src=pr&el=tree#diff-ZXhhbXBsZS90ZXN0cy90ZXN0X3ZpZXdzLnB5) | `100.00% <0.00%> (ø)` | |
| [example/tests/test\_errors.py](https://codecov.io/gh/django-json-api/django-rest-framework-json-api/pull/868/diff?src=pr&el=tree#diff-ZXhhbXBsZS90ZXN0cy90ZXN0X2Vycm9ycy5weQ==) | `100.00% <0.00%> (ø)` | |
| [example/tests/test\_openapi.py](https://codecov.io/gh/django-json-api/django-rest-framework-json-api/pull/868/diff?src=pr&el=tree#diff-ZXhhbXBsZS90ZXN0cy90ZXN0X29wZW5hcGkucHk=) | `100.00% <0.00%> (ø)` | |
| [example/tests/test\_parsers.py](https://codecov.io/gh/django-json-api/django-rest-framework-json-api/pull/868/diff?src=pr&el=tree#diff-ZXhhbXBsZS90ZXN0cy90ZXN0X3BhcnNlcnMucHk=) | `100.00% <0.00%> (ø)` | |
| [example/api/resources/identity.py](https://codecov.io/gh/django-json-api/django-rest-framework-json-api/pull/868/diff?src=pr&el=tree#diff-ZXhhbXBsZS9hcGkvcmVzb3VyY2VzL2lkZW50aXR5LnB5) | `100.00% <0.00%> (ø)` | |
| ... and [8 more](https://codecov.io/gh/django-json-api/django-rest-framework-json-api/pull/868/diff?src=pr&el=tree-more) | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/django-json-api/django-rest-framework-json-api/pull/868?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/django-json-api/django-rest-framework-json-api/pull/868?src=pr&el=footer). Last update [c356806...bce1f95](https://codecov.io/gh/django-json-api/django-rest-framework-json-api/pull/868?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).

sliverc: LGTM. Thanks for your work.

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4613aa1..17a6335 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -8,7 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 Note that in line with [Django REST Framework policy](http://www.django-rest-framework.org/topics/release-notes/),
 any parts of the framework not mentioned in the documentation should generally be considered private API, and may be subject to change.
 
-## [Unreleased] - TBD
+## [Unreleased]
 
 ### Added
 
@@ -17,6 +17,7 @@ any parts of the framework not mentioned in the documentation should generally b
 ### Fixed
 
 * Allow users to overwrite a view's `get_serializer_class()` method when using [related urls](https://django-rest-framework-json-api.readthedocs.io/en/stable/usage.html#related-urls)
+* Correctly resolve the resource type of `ResourceRelatedField(many=True)` fields on plain serializers
 
 
 ## [4.0.0] - 2020-10-31
diff --git a/rest_framework_json_api/utils.py b/rest_framework_json_api/utils.py
index 4168330..f7ddac7 100644
--- a/rest_framework_json_api/utils.py
+++ b/rest_framework_json_api/utils.py
@@ -215,6 +215,11 @@ def get_related_resource_type(relation):
                 return get_related_resource_type(parent_model_relation)
 
     if relation_model is None:
+        # For ManyRelatedFields on plain Serializers the resource_type
+        # cannot be determined from a model, so we must get it from the
+        # child_relation
+        if hasattr(relation, "child_relation"):
+            return get_related_resource_type(relation.child_relation)
         raise APIException(
             _("Could not resolve resource type for relation %s" % relation)
         )


Test patch:
diff --git a/tests/test_utils.py b/tests/test_utils.py
index 657f816..a0e4773 100644
--- a/tests/test_utils.py
+++ b/tests/test_utils.py
@@ -243,6 +243,27 @@ def test_get_related_resource_type(model_class, field, output):
     assert get_related_resource_type(field) == output
 
 
+@pytest.mark.parametrize(
+    "related_field_kwargs,output",
+    [
+        ({"queryset": BasicModel.objects}, "BasicModel"),
+        ({"queryset": BasicModel.objects, "model": BasicModel}, "BasicModel"),
+        ({"model": BasicModel, "read_only": True}, "BasicModel"),
+    ],
+)
+def test_get_related_resource_type_from_plain_serializer_class(
+    related_field_kwargs, output
+):
+    class PlainRelatedResourceTypeSerializer(serializers.Serializer):
+        basic_models = serializers.ResourceRelatedField(
+            many=True, **related_field_kwargs
+        )
+
+    serializer = PlainRelatedResourceTypeSerializer()
+    field = serializer.fields["basic_models"]
+    assert get_related_resource_type(field) == output
+
+
 class ManyToManyTargetSerializer(serializers.ModelSerializer):
     class Meta:
         model = ManyToManyTarget