Ejemplo n.º 1
0
    def create_view(self, callback, method, request=None):
        """
        customized create_view which is called when all routes are traversed. part of this
        is instantiating views with default params. in case of custom routes (@action) the
        custom AutoSchema is injected properly through 'initkwargs' on view. However, when
        decorating plain views like retrieve, this initialization logic is not running.
        Therefore forcefully set the schema if @extend_schema decorator was used.
        """
        override_view = OpenApiViewExtension.get_match(callback.cls)
        if override_view:
            callback.cls = override_view.view_replacement()

        view = super().create_view(callback, method, request)

        if isinstance(view, viewsets.GenericViewSet) or isinstance(
                view, viewsets.ViewSet):
            action = getattr(view, view.action)
        elif isinstance(view, views.APIView):
            action = getattr(view, method.lower())
        else:
            error(
                'Using not supported View class. Class must be derived from APIView '
                'or any of its subclasses like GenericApiView, GenericViewSet.'
            )
            return view

        # in case of @extend_schema, manually init custom schema class here due to
        # weakref reverse schema.view bug for multi annotations.
        schema = getattr(action, 'kwargs', {}).get('schema', None)
        if schema and inspect.isclass(schema):
            view.schema = schema()

        return view
Ejemplo n.º 2
0
    def _map_model_field(self, model_field, direction):
        assert isinstance(model_field, models.Field)
        # to get a fully initialized serializer field we use DRF's own init logic
        try:
            field_cls, field_kwargs = serializers.ModelSerializer().build_field(
                field_name=model_field.name,
                info=get_field_info(model_field.model),
                model_class=model_field.model,
                nested_depth=0,
            )
            field = field_cls(**field_kwargs)
        except:  # noqa
            field = None

        # For some cases, the DRF init logic either breaks (custom field with internal type) or
        # the resulting field is underspecified with regards to the schema (ReadOnlyField).
        if field and not anyisinstance(field, [serializers.ReadOnlyField, serializers.ModelField]):
            return self._map_serializer_field(field, direction)
        elif isinstance(model_field, models.ForeignKey):
            return self._map_model_field(model_field.target_field, direction)
        elif hasattr(models, model_field.get_internal_type()):
            # be graceful when the model field is not explicitly mapped to a serializer
            internal_type = getattr(models, model_field.get_internal_type())
            field_cls = serializers.ModelSerializer.serializer_field_mapping[internal_type]
            return self._map_serializer_field(field_cls(), direction)
        else:
            error(
                f'could not resolve model field "{model_field}". failed to resolve through '
                f'serializer_field_mapping, get_internal_type(), or any override mechanism. '
                f'defaulting to "string"'
            )
            return build_basic_type(OpenApiTypes.STR)
Ejemplo n.º 3
0
    def create_view(self, callback, method, request=None):
        """
        customized create_view which is called when all routes are traversed. part of this
        is instantiating views with default params. in case of custom routes (@action) the
        custom AutoSchema is injected properly through 'initkwargs' on view. However, when
        decorating plain views like retrieve, this initialization logic is not running.
        Therefore forcefully set the schema if @extend_schema decorator was used.
        """
        override_view = OpenApiViewExtension.get_match(callback.cls)
        if override_view:
            original_cls = callback.cls
            callback.cls = override_view.view_replacement()

        view = super().create_view(callback, method, request)

        # callback.cls is hosted in urlpatterns and is therefore not an ephemeral modification.
        # restore after view creation so potential revisits have a clean state as basis.
        if override_view:
            callback.cls = original_cls

        if isinstance(view, viewsets.ViewSetMixin):
            action = getattr(view, view.action)
        elif isinstance(view, views.APIView):
            action = getattr(view, method.lower())
        else:
            error(
                'Using not supported View class. Class must be derived from APIView '
                'or any of its subclasses like GenericApiView, GenericViewSet.'
            )
            return view

        action_schema = getattr(action, 'kwargs', {}).get('schema', None)
        if not action_schema:
            # there is no method/action customized schema so we are done here.
            return view

        # action_schema is either a class or instance. when @extend_schema is used, it
        # is always a class to prevent the weakref reverse "schema.view" bug for multi
        # annotations. The bug is prevented by delaying the instantiation of the schema
        # class until create_view (here) and not doing it immediately in @extend_schema.
        action_schema_class = get_class(action_schema)
        view_schema_class = get_class(callback.cls.schema)

        if not issubclass(action_schema_class, view_schema_class):
            # this handles the case of having a manually set custom AutoSchema on the
            # view together with extend_schema. In most cases, the decorator mechanics
            # prevent extend_schema from having access to the view's schema class. So
            # extend_schema is forced to use DEFAULT_SCHEMA_CLASS as fallback base class
            # instead of the correct base class set in view. We remedy this chicken-egg
            # problem here by rearranging the class hierarchy.
            mro = tuple(
                cls for cls in action_schema_class.__mro__
                if cls not in api_settings.DEFAULT_SCHEMA_CLASS.__mro__
            ) + view_schema_class.__mro__
            action_schema_class = type('ExtendedRearrangedSchema', mro, {})

        view.schema = action_schema_class()
        return view
Ejemplo n.º 4
0
    def _map_model_field(self, model_field, direction):
        assert isinstance(model_field, models.Field)
        # to get a fully initialized serializer field we use DRF's own init logic
        try:
            field_cls, field_kwargs = serializers.ModelSerializer(
            ).build_field(
                field_name=model_field.name,
                info=get_field_info(model_field.model),
                model_class=model_field.model,
                nested_depth=0,
            )
            field = field_cls(**field_kwargs)
        except:  # noqa
            field = None

        # For some cases, the DRF init logic either breaks (custom field with internal type) or
        # the resulting field is underspecified with regards to the schema (ReadOnlyField).
        if field and isinstance(field, serializers.PrimaryKeyRelatedField):
            # special case handling only for _resolve_path_parameters() where neither queryset nor
            # parent is set by build_field. patch in queryset as _map_serializer_field requires it
            if not field.queryset:
                field.queryset = model_field.related_model.objects.none()
            return self._map_serializer_field(field, direction)
        elif field and not anyisinstance(
                field, [serializers.ReadOnlyField, serializers.ModelField]):
            return self._map_serializer_field(field, direction)
        elif isinstance(model_field, models.ForeignKey):
            return self._map_model_field(model_field.target_field, direction)
        elif hasattr(models, 'JSONField') and isinstance(
                model_field, models.JSONField):
            # fix for DRF==3.11 with django>=3.1 as it is not yet represented in the field_mapping
            return build_basic_type(OpenApiTypes.OBJECT)
        elif hasattr(models, model_field.get_internal_type()):
            # be graceful when the model field is not explicitly mapped to a serializer
            internal_type = getattr(models, model_field.get_internal_type())
            field_cls = serializers.ModelSerializer.serializer_field_mapping.get(
                internal_type)
            if not field_cls:
                warn(
                    f'model field "{model_field.get_internal_type()}" has no mapping in '
                    f'ModelSerializer. it may be a deprecated field. defaulting to "string"'
                )
                return build_basic_type(OpenApiTypes.STR)
            return self._map_serializer_field(field_cls(), direction)
        else:
            error(
                f'could not resolve model field "{model_field}". failed to resolve through '
                f'serializer_field_mapping, get_internal_type(), or any override mechanism. '
                f'defaulting to "string"')
            return build_basic_type(OpenApiTypes.STR)
Ejemplo n.º 5
0
    def _map_model_field(self, field):
        assert isinstance(field, models.Field)
        drf_mapping = serializers.ModelSerializer.serializer_field_mapping

        if field.__class__ in drf_mapping:
            # use DRF native field resolution - taken from ModelSerializer.get_fields()
            return self._map_serializer_field(drf_mapping[field.__class__]())
        elif isinstance(field, models.ForeignKey):
            return self._map_model_field(field.target_field)
        else:
            error(
                f'could not resolve model field "{field}" due to missing mapping.'
                'either your field is custom and not based on a known subclasses '
                'or we missed something. let us know.'
            )
            return build_basic_type(OpenApiTypes.STR)
Ejemplo n.º 6
0
 def _get_serializer(self):
     view = self.view
     try:
         if isinstance(view, GenericAPIView):
             # try to circumvent queryset issues with calling get_serializer. if view has NOT
             # overridden get_serializer, its safe to use get_serializer_class.
             if view.__class__.get_serializer == GenericAPIView.get_serializer:
                 return view.get_serializer_class()()
             return view.get_serializer()
         elif isinstance(view, APIView):
             # APIView does not implement the required interface, but be lenient and make
             # good guesses before giving up and emitting a warning.
             if callable(getattr(view, 'get_serializer', None)):
                 return view.get_serializer()
             elif callable(getattr(view, 'get_serializer_class', None)):
                 return view.get_serializer_class()()
             elif hasattr(view, 'serializer_class'):
                 return view.serializer_class
             else:
                 error(
                     f'Unable to guess serializer for {view.__class__.__name__}. This is graceful '
                     f'fallback handling for APIViews. Consider using GenericAPIView as view base '
                     f'class, if view is under your control. ignoring view for now. '
                 )
         else:
             error(
                 'Encountered unknown view base class. please report this issue. ignoring for now'
             )
     except Exception as exc:
         error(
             f'Exception raised while getting serializer from {view.__class__.__name__}. Hint: '
             f'Is get_serializer_class() returning None or is get_queryset() not working without '
             f'a request? Ignoring the view for now. (Exception: {exc})')