Skip to content

Incompatibility with Django Rest Swagger when using RelationshipView #314

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

Closed
luissalgadofreire opened this issue Jan 9, 2017 · 6 comments

Comments

@luissalgadofreire
Copy link

luissalgadofreire commented Jan 9, 2017

Hi.

Thanks for this library. It works great.

I'm trying to get it to work with Django Rest Swagger but error message Exception Value: ResourceIdentifierObjectsSerializer must be initialized with a model class. keeps coming up. You'll find the traceback below.

This is happening at this piece of code:

class SwaggerSchemaView(APIView):
    """
    Configuration view to generate API documentation using the
    django-rest-swagger library.
    """
    permission_classes = [IsAuthenticated]
    renderer_classes = [
        renderers.OpenAPIRenderer,
        renderers.SwaggerUIRenderer
    ]

    def get(self, request):
        generator = SchemaGenerator(title='Condo API')
        schema = generator.get_schema(request=request)   # <--- Error happens here

        return Response(schema)

Important points:

  • Django Rest Swagger is working fine when not rendering views where DJA is used;
  • I'm using the url trick as pointed out in DJA documentation to show the self URL.
  • I'm also using ResourceRelatedField to render related resources link.

Can anyone help shed some light into this?

EDIT:
After taking out the RelationshipViews from the api url patterns, it worked. So, it would seem the RelationshipView is where incompatibility with Django Rest Swagger resides. Keeping the url field does not result in an exception.

Traceback:  

File "/opt/conda/lib/python3.5/site-packages/django/core/handlers/exception.py" in inner
  39.             response = get_response(request)

File "/opt/conda/lib/python3.5/site-packages/django/core/handlers/base.py" in _legacy_get_response
  249.             response = self._get_response(request)

File "/opt/conda/lib/python3.5/site-packages/django/core/handlers/base.py" in _get_response
  187.                 response = self.process_exception_by_middleware(e, request)

File "/opt/conda/lib/python3.5/site-packages/django/core/handlers/base.py" in _get_response
  185.                 response = wrapped_callback(request, *callback_args, **callback_kwargs)

File "/opt/conda/lib/python3.5/site-packages/django/views/decorators/csrf.py" in wrapped_view
  58.         return view_func(*args, **kwargs)

File "/opt/conda/lib/python3.5/site-packages/django/views/generic/base.py" in view
  68.             return self.dispatch(request, *args, **kwargs)

File "/opt/conda/lib/python3.5/site-packages/rest_framework/views.py" in dispatch
  477.             response = self.handle_exception(exc)

File "/opt/conda/lib/python3.5/site-packages/rest_framework/views.py" in handle_exception
  437.             self.raise_uncaught_exception(exc)

File "/opt/conda/lib/python3.5/site-packages/rest_framework/views.py" in dispatch
  474.             response = handler(request, *args, **kwargs)

File "/usr/src/app/src/config/views.py" in get
  21.         schema = generator.get_schema(request=request)

File "/opt/conda/lib/python3.5/site-packages/rest_framework/schemas.py" in get_schema
  242.         links = self.get_links(request)

File "/opt/conda/lib/python3.5/site-packages/rest_framework/schemas.py" in get_links
  273.             link = self.get_link(path, method, view)

File "/opt/conda/lib/python3.5/site-packages/rest_framework/schemas.py" in get_link
  372.         fields += self.get_serializer_fields(path, method, view)

File "/opt/conda/lib/python3.5/site-packages/rest_framework/schemas.py" in get_serializer_fields
  472.         serializer = view.get_serializer()

File "/opt/conda/lib/python3.5/site-packages/rest_framework/generics.py" in get_serializer
  111.         return serializer_class(*args, **kwargs)

File "/opt/conda/lib/python3.5/site-packages/rest_framework_json_api/serializers.py" in __init__
  24.             raise RuntimeError('ResourceIdentifierObjectsSerializer must be initialized with a model class.')

Exception Type: RuntimeError at /api/
Exception Value: ResourceIdentifierObjectsSerializer must be initialized with a model class.
Request information:
USER: admin@email.com [1]

GET: No GET data

POST: No POST data

FILES: No FILES data

Thanks

@luissalgadofreire luissalgadofreire changed the title ResourceIdentifierObjectsSerializer must be initialized with a model class. Incompatibility with Django Rest Swagger when using RelationshipView. Jan 9, 2017
@luissalgadofreire
Copy link
Author

luissalgadofreire commented Jan 9, 2017

I got it to work.

Basically, the solution to the above was implementing custom content negotiation.

I basically cloned Django Rest Framework's DefaultContentNegotiation to add 'application/vnd.api+json' to method get_accept_list's media type strings so that Django Rest Swagger would accept this format.

Then I included the following in the REST_FRAMEWORK's settings:

# Content Negotiation
# http://www.django-rest-framework.org/api-guide/content-negotiation/
'DEFAULT_CONTENT_NEGOTIATION_CLASS': 'config.negotiation.JsonApiContentNegotiation',

Custom DefaultContentNegotiation:

from django.http import Http404

from rest_framework import HTTP_HEADER_ENCODING, exceptions
from rest_framework.settings import api_settings
from rest_framework.utils.mediatypes import (_MediaType, media_type_matches, order_by_precedence)
from rest_framework.negotiation import BaseContentNegotiation


class JsonApiContentNegotiation(BaseContentNegotiation):
    """
    A clone of Django Rest Framework's :class:`DefaultContentNegotiation` but manually adding
    ``['application/vnd.api+json']`` to method :meth:`get_accept_list`'s media type strings so
    that Django Rest Swagger can read the Django Rest Json Api format.
    """
    settings = api_settings

    def select_parser(self, request, parsers):
        """
        Given a list of parsers and a media type, return the appropriate
        parser to handle the incoming request.
        """
        for parser in parsers:
            if media_type_matches(parser.media_type, request.content_type):
                return parser
        return None

    def select_renderer(self, request, renderers, format_suffix=None):
        """
        Given a request and a list of renderers, return a two-tuple of:
        (renderer, media type).
        """
        # Allow URL style format override.  eg. "?format=json
        format_query_param = self.settings.URL_FORMAT_OVERRIDE
        format = format_suffix or request.query_params.get(format_query_param)

        if format:
            renderers = self.filter_renderers(renderers, format)

        accepts = self.get_accept_list(request)

        # Check the acceptable media types against each renderer,
        # attempting more specific media types first
        # NB. The inner loop here isn't as bad as it first looks :)
        #     Worst case is we're looping over len(accept_list) * len(self.renderers)
        for media_type_set in order_by_precedence(accepts):
            for renderer in renderers:
                for media_type in media_type_set:
                    if media_type_matches(renderer.media_type, media_type):
                        # Return the most specific media type as accepted.
                        media_type_wrapper = _MediaType(media_type)
                        if (
                            _MediaType(renderer.media_type).precedence >
                            media_type_wrapper.precedence
                        ):
                            # Eg client requests '*/*'
                            # Accepted media type is 'application/json'
                            full_media_type = ';'.join(
                                (renderer.media_type,) +
                                tuple('{0}={1}'.format(
                                    key, value.decode(HTTP_HEADER_ENCODING))
                                    for key, value in media_type_wrapper.params.items()))
                            return renderer, full_media_type
                        else:
                            # Eg client requests 'application/json; indent=8'
                            # Accepted media type is 'application/json; indent=8'
                            return renderer, media_type

        raise exceptions.NotAcceptable(available_renderers=renderers)

    def filter_renderers(self, renderers, format):
        """
        If there is a '.json' style format suffix, filter the renderers
        so that we only negotiation against those that accept that format.
        """
        renderers = [renderer for renderer in renderers
                     if renderer.format == format]
        if not renderers:
            raise Http404
        return renderers

    def get_accept_list(self, request):
        """
        Given the incoming request, return a tokenized list of media
        type strings.

        Note:
            This method is customized: it includes ['application/vnd.api+json'] at the end
            to allow django-rest-swagger to render django-rest-json-api style api's.
        """
        header = request.META.get('HTTP_ACCEPT', '*/*')
        return [token.strip() for token in header.split(',')] + ['application/vnd.api+json']

@luissalgadofreire
Copy link
Author

luissalgadofreire commented Jan 10, 2017

Sorry. Though custom content negotiation did allow Django Rest Swagger to properly render the API (with RelationshipViews off), it seems I was too fast to close the issue.

The incompatibility described in the first comment persists.

I could swear it worked yesterday but when I tried again this morning, the same exception occurred. I can't seam to grasp what might have happened. But the fact is, the exception is still there, when I include RelationShipView's in URLConf.

@luissalgadofreire luissalgadofreire changed the title Incompatibility with Django Rest Swagger when using RelationshipView. Incompatibility with Django Rest Swagger when using RelationshipView (ResourceIdentifierObjectsSerializer must be initialized with a model class). Jan 10, 2017
@luissalgadofreire
Copy link
Author

Related to #315.

@sliverc sliverc changed the title Incompatibility with Django Rest Swagger when using RelationshipView (ResourceIdentifierObjectsSerializer must be initialized with a model class). Incompatibility with Django Rest Swagger when using RelationshipView Jan 14, 2019
@n2ygk
Copy link
Contributor

n2ygk commented May 20, 2019

@sliverc, @leifurhauks: Does this check for instance actually break anything if instance is missing here, given the parent DRF BaseSerializer class defaults instance=None? Worst case, a later invocation of to_representation() might throw an exception. None of the tests care if this code is removed;-) and then it can be used to generate a Swagger/OAS schema for a RelationshipView.

def __init__(self, *args, **kwargs):
self.model_class = kwargs.pop('model_class', self.model_class)
if 'instance' not in kwargs and not self.model_class:
raise RuntimeError(
'ResourceIdentifierObjectsSerializer must be initialized with a model class.'
)
super(ResourceIdentifierObjectSerializer, self).__init__(*args, **kwargs)

In testing with forthcoming DRF 3.10's generateschema, this throws this error which looks basically the same as the issue with older django-rest-swagger (not surprisingly):

/Users/alan/src/django-training/env/bin/python /Users/alan/src/django-training/manage.py generateschema
Traceback (most recent call last):
  File "/Users/alan/src/django-training/manage.py", line 22, in <module>
    execute_from_command_line(sys.argv)
  File "/Users/alan/src/django-training/env/lib/python3.6/site-packages/django/core/management/__init__.py", line 381, in execute_from_command_line
    utility.execute()
  File "/Users/alan/src/django-training/env/lib/python3.6/site-packages/django/core/management/__init__.py", line 375, in execute
    self.fetch_command(subcommand).run_from_argv(self.argv)
  File "/Users/alan/src/django-training/env/lib/python3.6/site-packages/django/core/management/base.py", line 323, in run_from_argv
    self.execute(*args, **cmd_options)
  File "/Users/alan/src/django-training/env/lib/python3.6/site-packages/django/core/management/base.py", line 364, in execute
    output = self.handle(*args, **options)
  File "/Users/alan/src/django-training/env/lib/python3.6/site-packages/rest_framework/management/commands/generateschema.py", line 33, in handle
    schema = generator.get_schema(request=None, public=True)
  File "/Users/alan/src/django-training/env/lib/python3.6/site-packages/rest_framework/schemas/openapi.py", line 67, in get_schema
    paths = self.get_paths(None if public else request)
  File "/Users/alan/src/django-training/env/lib/python3.6/site-packages/rest_framework/schemas/openapi.py", line 51, in get_paths
    operation = view.schema.get_operation(path, method)
  File "/Users/alan/src/django-training/env/lib/python3.6/site-packages/rest_framework/schemas/openapi.py", line 113, in get_operation
    operation['responses'] = self._get_responses(path, method)
  File "/Users/alan/src/django-training/env/lib/python3.6/site-packages/rest_framework/schemas/openapi.py", line 468, in _get_responses
    serializer = view.get_serializer()
  File "/Users/alan/src/django-training/env/lib/python3.6/site-packages/rest_framework/generics.py", line 110, in get_serializer
    return serializer_class(*args, **kwargs)
  File "/Users/alan/src/django-training/env/lib/python3.6/site-packages/rest_framework_json_api/serializers.py", line 34, in __init__
    'ResourceIdentifierObjectsSerializer must be initialized with a model class.'
RuntimeError: ResourceIdentifierObjectsSerializer must be initialized with a model class.

Here's the stack frame when breakpoint on L33:

args = {tuple} <class 'tuple'>: ()
self = {ResourceIdentifierObjectSerializer} ResourceIdentifierObjectSerializer(context={'request': None, 'format': None, 'view': <myapp.views.CourseRelationshipView object>})
self.view.permission_classes = {AttributeError}'ResourceIdentifierObjectSerializer' object has no attribute 'view'
kwargs = {dict} <class 'dict'>: {'context': {'request': None, 'format': None, 'view': <myapp.views.CourseRelationshipView object at 0x10b2419b0>}}

@n2ygk
Copy link
Contributor

n2ygk commented May 24, 2019

Even if though OPTIONS error is fixed by #633, Django REST Swagger will not currently generate a correct JSONAPI schema as it is just calling DRF's schema generator which returns a coreapi schema. See #604 which addresses this using DRF's forthcoming switch over to OpenAPI 3.0 schema support and JSONAPI extension of that being added to DJA.

@n2ygk
Copy link
Contributor

n2ygk commented May 24, 2019

Closing in favor of #604

@n2ygk n2ygk closed this as completed May 24, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

3 participants