Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Generate schema for Django rest framework viewset actions

As per the DRF documentation I started using ViewSet and have implemented list, retrieve, create, update and destroyactions. I have another APIView for which I was able to write schema (ManualSchema) and when I navigate to /docs/ I am able to the documentation as well as live endpoint for interaction.

I wish to create separate schema for each of the viewset action. I tried writing one but it doesn't show up so I think I am missing something.

Here is the code:

class Clients(viewsets.ViewSet):

    '''

        Clients is DRF viewset which implements `create`, `update`, `read` actions by implementing create, update, list and retrieve functions respectively.

    '''
    list_schema = schemas.ManualSchema(fields=[
            coreapi.Field(
                'status',
                required=False,
                location='query',
                description='Accepted values are `active`, `inactive`'
            ),          
        ], 
        description='Clients list',
        encoding='application/x-www-form-urlencoded')

    @action(detail=True, schema=list_schema)
    def list(self, request):

        '''Logic for listing'''


    def retrieve(self, request, oid=None):

        '''Logic for retrieval'''


    create_schema = schemas.ManualSchema(fields=[
            coreapi.Field(
                'name',
                required=False,
                location='body',
            ),
            coreapi.Field(
                'location',
                required=False,
                location='body',
            ),              
        ], 
        description='Clients list',
        encoding='application/x-www-form-urlencoded')

    @action(detail=True, schema=create_schema)
    def create(self, request):

        '''Logic for creation'''
like image 576
Rajesh Yogeshwar Avatar asked Nov 05 '18 12:11

Rajesh Yogeshwar


People also ask

What is ViewSet in Django REST framework?

A ViewSet class is simply a type of class-based View, that does not provide any method handlers such as . get() or . post() , and instead provides actions such as . list() and . create() .

Should I use APIView or ViewSet?

APIView and ViewSet all have their right use cases. But most of the time, if you are only doing CRUD on resources, you can directly use ViewSets to respect the DRY principle. But if you are looking for more complex features, you can go low-level because after all, viewsets are also a subclass of APIView .

What is schema in Django REST framework?

API schemas are a useful tool that allow for a range of use cases, including generating reference documentation, or driving dynamic client libraries that can interact with your API. Django REST Framework provides support for automatic generation of OpenAPI schemas.

What is difference between ViewSet and ModelViewSet?

It is more rigid than a generic APIView but it takes away from you some boilerplate/manual config that you would have to repeat again and again in most cases. One step further is the ModelViewSet , which is an extension of the ViewSet for when you are working with Django models.


2 Answers

So I will answer my own question. I took a look at DRF source code for schema generation. I came up with the plan and performed following steps.

I subclassed SchemaGenerator class defined in rest_framework.schemas module. Below is the code.

class CoreAPISchemaGenerator(SchemaGenerator):

    def get_links(self, request=None, **kwargs):

        links = LinkNode()

        paths = list()
        view_endpoints = list()

        for path, method, callback in self.endpoints:
            view = self.create_view(callback, method, request)
            path = self.coerce_path(path, method, view)
            paths.append(path)
            view_endpoints.append((path, method, view))

        if not paths:
            return None

        prefix = self.determine_path_prefix(paths)

        for path, method, view in view_endpoints:

            if not self.has_view_permissions(path, method, view):
                continue

            actions = getattr(view, 'actions', None)
            schemas = getattr(view, 'schemas', None)

            if not schemas:

                link = view.schema.get_link(path, method, base_url=self.url)
                subpath = path[len(prefix):]
                keys = self.get_keys(subpath, method, view, view.schema)
                insert_into(links, keys, link)

            else:

                action_map = getattr(view, 'action_map', None)
                method_name = action_map.get(method.lower())
                schema = schemas.get(method_name)

                link = schema.get_link(path, method, base_url=self.url)
                subpath = path[len(prefix):]
                keys = self.get_keys(subpath, method, view, schema)
                insert_into(links, keys, link)

        return links


    def get_keys(self, subpath, method, view, schema=None):

        if schema and hasattr(schema, 'endpoint_name'):

            return [schema.endpoint_name]

        else:

            if hasattr(view, 'action'):
                action = view.action
            else:

                if is_list_view(subpath, method, view):
                    action = 'list'
                else:
                    action = self.default_mapping[method.lower()]

            named_path_components = [
                component for component
                in subpath.strip('/').split('/')
                if '{' not in component
            ]

            if is_custom_action(action):

                if len(view.action_map) > 1:
                    action = self.default_mapping[method.lower()]
                    if action in self.coerce_method_names:
                        action = self.coerce_method_names[action]
                    return named_path_components + [action]
                else:
                    return named_path_components[:-1] + [action]

            if action in self.coerce_method_names:
                action = self.coerce_method_names[action]

            return named_path_components + [action]

I specifically modified two functions get_links and get_keys as that allow me to achieve what I wanted.

Further, for all the functions in viewsets that I was writing I dedicated an individual schema for it. I simply created a dictionary to keep mappings of function name to schema instance. For better approach I created a separate file to store schemas. For eg. if I had a viewset Clients I created a corresponding ClientsSchema class and within in defined staticmethods which returned schema instances.

Example,

In file where I am defining my schemas,

class ClientsSchema():

    @staticmethod
    def list_schema():

        schema = schemas.ManualSchema(
            fields=[],
            description=''
        )

        schema.endpoint_name = 'Clients Listing'

        return schema

In my apis.py,

class Clients(viewsets.ViewSet):

    schemas = {
        'list': ClientsSchema.list_schema()
    }

    def list(self, request, **kwargs):
        pass

This setup allows me to define schemas for any type of functions that I add to my viewsets. In addition to it, I also wanted that the endpoints have an identifiable name and not the one generated by DRF which is like a > b > update > update. In order to achieve that, I added endpoint_name property to schema object that is returned. That part is handled in get_keys function that is overridden.

Finally in the urls.py where we include urls for documentation we need to use our custom schema generator. Something like this,

urlpatterns.append(url(r'^livedocs/', include_docs_urls(title='My Services', generator_class=CoreAPISchemaGenerator)))

For security purposes I cannot share any snapshots. Apologies for that. Hope this helps.

like image 91
Rajesh Yogeshwar Avatar answered Sep 28 '22 19:09

Rajesh Yogeshwar


I think what you are trying to do is not possible. The ViewSet does not provide any method handlers, hence, you cannot use the @action decorator on the methods create and list, as they are existing routes.

like image 32
y.luis Avatar answered Sep 28 '22 19:09

y.luis