Skip to content

Allow users to overwrite get_serializer_class while using related urls #860

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
Nov 11, 2020

Conversation

uliSchuster
Copy link
Contributor

Fixes #859

Description of the Change

Allows users to overwrite the get_serializer() and get_serializer_class() methods in subclassed views that have related or included serializers.

This change seems to interfere with OpenAPI Schema generation. As it stands, the tests test_path_with_id_parameter and test_path_without_parameters are failing, because the result no longer matches the expected snapshot. I suspect that the snapshots were incorrect in the first place, but I am not able to tell for sure.

Checklist

  • PR only contains one change (considered splitting up PR)
  • [o] unit-test added: I did not add a new unit test, but modified the test setup such that the problems became apparent in the existing test cases.
  • [-] documentation updated
  • CHANGELOG.md updated (only for user relevant changes)
  • author name in AUTHORS

An attempt to fix django-json-api#859, which unfortunately breaks two tests in test_views.
Tha basic method was copied over from the GenericAPIView. The comment here does not fit, though.
Ensure that the correct resource is returned during rendering, which in turn requires to use the correct serializer, depending on if it is a related resource or the parent resource.
sliverc pointed out the issue here.
Copy link
Contributor

@n2ygk n2ygk left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please document how someone might use this new functionality.

As pointed out by n2ygk, the schema names here must reflect the Serializer class names, which I changed in the two affected test cases.
Copy link
Contributor Author

@uliSchuster uliSchuster left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for pointing out the issue with the OpenAPI Tests. I fixed the relevant snapshots. And I added a more extensive comment on the reason for the changes.

@codecov
Copy link

codecov bot commented Nov 10, 2020

Codecov Report

Merging #860 (e27b7a4) into master (c392a43) will not change coverage.
The diff coverage is 100.00%.

Impacted file tree graph

@@           Coverage Diff           @@
##           master     #860   +/-   ##
=======================================
  Coverage   97.74%   97.74%           
=======================================
  Files          60       60           
  Lines        3319     3319           
=======================================
  Hits         3244     3244           
  Misses         75       75           
Impacted Files Coverage Δ
example/tests/snapshots/snap_test_openapi.py 100.00% <ø> (ø)
example/tests/test_views.py 100.00% <100.00%> (ø)

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update c392a43...e27b7a4. Read the comment docs.

@uliSchuster uliSchuster requested a review from n2ygk November 10, 2020 19:09
Clarified changelog entry
@sliverc sliverc changed the title 859 fix attempt Allow users to overwrite get_serializer_class while using related urls Nov 11, 2020
Copy link
Member

@sliverc sliverc left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@uliSchuster Thanks for working on this. There are some linting issue which need to be fixed (run flake8 or see CI). I have adjusted the changelog to clarify when this issue is encountered.
I am not too happy about changing an existing test for this but I know tests are currently messy. So I think this is fine for now and can be cleaned up with #468

@n2ygk As you have already started reviewing it I leave you with this to give the final call in terms of approval.

@uliSchuster
Copy link
Contributor Author

I wasn't quite sure which linter you are using and didn't want to break other things (I'm running black and prospector on my project. But I'm happy to fix all remaining linting issues.

About the tests: As I understood from the source, the tests rely on a single example application. The data model, views and serializers of this application effectively are the entities under test. The test cases present already do cover the methods where the issue arises if the entities under test would have included the special case at hand. Writing additional tests alone would not have shown the issue without additional serializers, so I added these serializers.

The alternative would be to add a model with its own views and serializers, and then write tests that use these new classes. I think this would habe been lots of additional code without that much additional benefit.

@n2ygk
Copy link
Contributor

n2ygk commented Nov 11, 2020

@uliSchuster:

I wasn't quite sure which linter you are using and didn't want to break other things (I'm running black and prospector on my project. But I'm happy to fix all remaining linting issues.

Prior to pushing your commits, you can use tox -e lint locally to perform a lint test. See also #857 regarding converting the codebase to use black, adding pre-commit hooks, etc.

Also use tox -e docs to locally generate the documentation as html in docs/_build/html/index.html.

About the tests: As I understood from the source, the tests rely on a single example application. The data model, views and serializers of this application effectively are the entities under test. The test cases present already do cover the methods where the issue arises if the entities under test would have included the special case at hand. Writing additional tests alone would not have shown the issue without additional serializers, so I added these serializers.

Per #855 and #856 @sliverc has recently begun converting tests away from using the example app and to consistently use pytest. So, for now, doing them in the example is the right way -- which is why it is so ugly.

tox will run all the tests against the matrix of python, Django, DRF and DJA versions.

(Hmm, this should all get added to CONTRIBUTING.md I suppose: #861)

Copy link
Contributor

@n2ygk n2ygk left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@sliverc please review my review comments as I like the depth of understanding that you have.

return Response(serializer.data)

def get_serializer_class(self):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

removing this exposes the upstream DRF get_serializer_class(). It seems this needs to remain for compatibility. @sliverc Do you agree?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My whole point in making the change here is to do exactly this: expose the upstream method, so that it can be overwritten. get_serializer_class() is a public method on the base class. As such, it is part of DRF's public API - the DRF documentation explicitly mentions that the method can be overwritten. In my opinion, an extension library should preserve the upstream API.

The current version of DJA does break the path to the upstream method. There might be cases where DJA users rely on this bug in some way. The change here would break their code. I'm just a first-time user of DJA without any idea about how it is being used in other projects, what the effects might be, and who might be affected.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Forgive me for being dense but how is get_serializer_class() in DJA not able to be overridden by you? You've completely removed the function from DJA; not just added get_related_serializer().

This extension library routinely overrides the upstream DRF because it is extending it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do Full understand that you don't want to run the risk of breaking someone's application. And given that I am fairly new to DJA, I might not fully understand its inner workings, either.

I came across the issue the present PR is trying to solve, when I had to overwrite get_serializer_class() on a custom viewset, in exactly the same way I now added as part of the AuthorViewSet in the test example. I investigated the issue and discovered that when processing a GET request for a related instance, RelatedMixin.get_related_instance() directly accessed the self.serializer_class field instead of getting the class via its accessor method. Consequently, the upstream get_serializer_class() would not be called, either, which would be my overwritten version. Hence my remark that DJA effectively prevents get_serializer_class() to be overwritten on a custom viewset, which I consider to be a bug.

My first attempt to fix the issue was to have RelatedMixin.get_related_instance call the upstream method instead of accessing the field directly. But this breaks things badly. During two exchanges with @sliverc (see the comments on the issue here) showed that the functionality of RelatedMixin.get_serializer_class is actually not about getting the serializer class, but getting the class of related serializers. I followed the hint of @sliverc to rename the method from get_serializer_class() to get_related_serializer_class(), which better captures its functionality. This way, RelatedMixin.get_related_instance can call get_serializer_class() of the parent class (potentially overwritten), while all functionality that has to do with getting serializers for related views is now handled by aptly named get_related_* methods.

To make this work, I need to do some other adjustments along the call chain. In particular, retrieve_related(), which is the entry point for a GET request on a related resource, now calls the renamed get_related_serializer_class(). One more change, also pointed out by @sliverc, was in utils.get_resource_name(): Here, I had to make the distinction if the serializer of the present viewset is needed, or if the serializer of a related viewset should be returned. The distinction between the two cases depends on the related_field kwarg, similar to the condition in get_serializer_class()(not get_related_serializer_class().

I hope this helps to provide some understanding of what I am trying to accomplish. I was very much focussed on the functionality of getting related resources; because of my limited understanding of DJA, I am not able to tell if other parts of its functionality are affected by the changes introduced in RelatedMixin. Here, I relied on the tests, which are all passing. If there is something else I can do to clear things up or to provide better explanation, please let me know. I am also available for a quick online call, if that can help to better assess the changes I introduced.

Copy link
Member

@sliverc sliverc Nov 11, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let me try to shed some light on this... RelatedMixin mistakenly shadowed get_serializer_class to get related serializer. Shadowing happened as RelatedMixin only works when used with a DRF generic view which provides a get_serializer_class method
For RelatedMixin to be able to get parent serializer it then simply used self.serializer_class which is not DRF conform. However this only happened when related urls have been configured as otherwise this if is always false and there is no difference to the method not being overwritten in RelatedMixin.

If someone overwrote get_serializer_class in its view without using related urls it would just work as RelatedMixin did not do anything anyway in such a case then passing it on to super. If someone tried to overwrite get_serializer_class with related urls this user would run into exactly the same problem as @uliSchuster has.

Therefore I don't see that this change affects existing users.

Copy link
Member

@sliverc sliverc left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks good now and is in my opinion safe to be merged.

It actually follows our second goal to be compatible with DRF as much as possible.

@n2ygk I hope my inline explanations make sense and I suggest we merge this. Otherwise let me know.

return Response(serializer.data)

def get_serializer_class(self):
Copy link
Member

@sliverc sliverc Nov 11, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let me try to shed some light on this... RelatedMixin mistakenly shadowed get_serializer_class to get related serializer. Shadowing happened as RelatedMixin only works when used with a DRF generic view which provides a get_serializer_class method
For RelatedMixin to be able to get parent serializer it then simply used self.serializer_class which is not DRF conform. However this only happened when related urls have been configured as otherwise this if is always false and there is no difference to the method not being overwritten in RelatedMixin.

If someone overwrote get_serializer_class in its view without using related urls it would just work as RelatedMixin did not do anything anyway in such a case then passing it on to super. If someone tried to overwrite get_serializer_class with related urls this user would run into exactly the same problem as @uliSchuster has.

Therefore I don't see that this change affects existing users.

@n2ygk
Copy link
Contributor

n2ygk commented Nov 11, 2020

Thanks for your contribution @uliSchuster and apologies for not understanding it. Thanks as usual @sliverc for explaining.

@n2ygk n2ygk merged commit 1e2594f into django-json-api:master Nov 11, 2020
@uliSchuster
Copy link
Contributor Author

Thanks very much! Are you planning on a release that includes this change anytime soon?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Allow overwriting of get_serializer_class with related urls
3 participants