Skip to content
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

API views & urls #36

Closed
Atorich opened this issue Dec 1, 2019 · 8 comments
Closed

API views & urls #36

Atorich opened this issue Dec 1, 2019 · 8 comments
Labels
Point of View For different points of view on different topics Research Required Research is required

Comments

@Atorich
Copy link

Atorich commented Dec 1, 2019

Hi there!

First of all, I would like to say thanks for the great idea of how to organize complex things.

But I want to share some thoughts about the way to organizing views & urls.

I have found the proposed approach to register views not quite RESTful because of
/update & /create aren't resources.

DRF's viewsets introduce a great way to do that but using them out-of-the-box contradicts to Do 1 API per operation rule.

But we still can write separate classes for each API and use viewsets to combine them together & register via DRF router.

Here the draft of viewset_factory function that will help with it.

def viewset_factory(*, name: str, viewset_class=viewsets.GenericViewSet, views_mapping: Mapping, **attrs):
    """Returns ViewSet class with proxy-methods got from views_mapping

    Example:

        Suppose, we have 2 views:
            FooListView(generics.ListAPIView):
                def list(request):
                    # impl.
                    pass

            FooCreateView(generics.CreateAPIView):
                def create(request:
                    # impl.
                    pass

        And we want to combine them into one viewset to make possible
        passing it to the `rest_framework` router

            FooViewSet = viewset_factory(
                name='FooViewSet',
                views_mapping={
                    'list': FooListView,
                    'create': FooCreateView,
                }
            )

        This is roughly equivalent to:

        class FooViewSet(viewsets.GenericViewSet):
            def list(self, request):
                view_class = FooListView(request=request)
                view_class.initial(request)
                return view_class.list(request)

            def create(self, request):
                view_class = FooCreateView(request=request)
                view_class.initial(request)
                return view_class.create(request)

    """

    def _create_viewset_method(view_class, method):
        def _method(cls, request, *args, **kwargs):
            view = view_class(request=request)
            view.args = args
            view.kwargs = kwargs
            view.initial(request)
            m = getattr(view, method)
            return m(request, *args, **kwargs)

        return _method

    viewset_class_name, viewset_class_attrs = name, {}

    for method, view_class in views_mapping.items():
        view_method = _create_viewset_method(view_class, method)
        viewset_class_attrs[method] = view_method

    viewset_class_attrs.update(attrs)

    viewset = type(viewset_class)(viewset_class_name, (viewset_class,), viewset_class_attrs)
    return viewset


And then we can just use DefaultRouter:

router = DefaultRouter()
router.register('foo', views.FooViewSet, basename='foo')

and have RESTful urls with actually separated views.

@RadoRado
Copy link
Member

RadoRado commented Dec 2, 2019

@Atorich That's a smart proposal! Thanks for taking the time 🙇‍♂️

One of the things we try to avoid in our projects is using the router to automatically generate URLs. We have our reasons & one of the main is "greppability" (or ease of searching for something)

That said, for the folks that like to keep their URLs more RESTful, that's a good approach.

I'll give it a go & extend the styleguide with your proposal 👌

@Atorich
Copy link
Author

Atorich commented Dec 2, 2019

Please, don't hurry with it. There are still some not solved issues.

One of them: this approach breaks DRF api-docs because it is expected that every viewset has get_queryset method (or queryset property) and get_serializer method (or serializer_class property) are defined.

The quick solution is to pass something to the factory,
for example:

    name='FooViewSet',
    views_mapping={
        'list': ListFooView,
        'create': CreateFooView,
        'retrieve': RetrieveFooView,
    },

    # `api-docs`
    serializer_class=CreateFooView.Serializer,
    queryset=CreateFooView.queryset,
)

but it's rather ugly

@Atorich
Copy link
Author

Atorich commented Dec 2, 2019

https://gist.github.com/Atorich/8e3ca27808c9f64615663ff2d1ee0bba
the last version of the solution (considers api-docs issues)

@RadoRado RadoRado added Research Required Research is required Point of View For different points of view on different topics labels Mar 15, 2020
@RadoRado
Copy link
Member

👋

I'll close this for now, since some time has passed.

At least for us, we are still sticking with the approach described in the styleguide. The proposed approach also looks good.

Picking an approach and staying consistent is the key thing here.

@stiangrim
Copy link

Hi 👋

I love the styleguide, but do also think that e.g. CREATE on products/create is too verbose ,and looking for a solution to this problem

However, using viewset factories like mentioned by @Atorich does indeed break the drf-spectacular docs.

Has anyone found a better way to tackle this?

@stiangrim
Copy link

Bumping this one ☝️

@RadoRado
Copy link
Member

RadoRado commented Dec 5, 2024

@stiangrim Noted. Will answer Wednesday next week (this is when I do my opensource sweep)

@RadoRado
Copy link
Member

@stiangrim Following-up here:

What you've suggested here - tfranzel/drf-spectacular#1344 - looks like a good approach if you want to have multiple views per a single route.

As an alternative, I'd also explore the routing from DRF - https://www.django-rest-framework.org/api-guide/routers/#custom-routers - and build a custom route doing pretty-much the same.

Now, what I understand is that the main issue is related to drf-spectacular not dealing well with that.

If that's the case, then, perhaps, this is the same issue with the default viewsets.

It looks like you need to use the @extend_schema to do that - https://stackoverflow.com/questions/70941962/how-to-document-individual-actions-of-a-viewset-using-drf-spectacular

image

Let me know if using this would get you the results you need 👍

If that's the case, then, you can build an additional layer of abstraction, to handle your general case.

Cheers!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Point of View For different points of view on different topics Research Required Research is required
Projects
None yet
Development

No branches or pull requests

3 participants