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

Docs clarifications for "Non-Model ViewSets" #3373

Closed
ncoghlan opened this issue Sep 8, 2015 · 13 comments
Closed

Docs clarifications for "Non-Model ViewSets" #3373

ncoghlan opened this issue Sep 8, 2015 · 13 comments

Comments

@ncoghlan
Copy link

ncoghlan commented Sep 8, 2015

I'm using Django REST Framework for an API integration project (i.e. presenting a unified API that abstracts a subset of the APIs for multiple backend services), and aiming to keep the abstraction layer as stateless as possible.

I think it's excellent for this purpose, but the regular ViewSet examples in the documentation all assume the use of local model objects, and so have required a bit reverse engineering to figure out the key patterns needed to implement my own custom ViewSet classes that work correctly with the rest of the framework (including the browsable API). So far I've figured out that the bare minimum is:

  • define a Serializer subclass, and set that as serializer_class on the custom ViewSet (this is needed to get the submission forms to appear correctly on the browsable API)
  • return Response(self.serializer_class(data, many=True).data) from list methods
  • return Response(self.serializer_class(data).data) from retrieve methods
  • to pass along an unstructured JSON blob retrieved from another API without parsing it locally, use a field definition like raw_data = serializers.DictField(read_only=True)

I'm still figuring out the right implementation patterns for create, update, partial update, and destroy...

As far as where this could go in the documentation, it seems to me that the current "Marking extra extractions for routing" section in http://www.django-rest-framework.org/api-guide/viewsets/ could be broken up to have a preceding section on "Integrating with Routers and the Browsable API" that covers the default methods the routers look for, and how the browsable API generates the default forms.

If this seems reasonable, I'd be happy to work on a PR for it.

@xordoquy
Copy link
Collaborator

xordoquy commented Sep 8, 2015

@ncoghlan thanks for the feedback. The documentation may already have those informations here and there but I think it would benefit having all those grouped together. For example, the routing part is described in the Routers section.

We started discussion whether this use cases should go to the API documentation or be part of some sort of blog or collection of use cases.

@ncoghlan
Copy link
Author

ncoghlan commented Sep 8, 2015

Perhaps a Topic for "Custom ViewSets" would be appropriate?

I since found more of the docs I needed under Serializers (they cover implementing create/update on the serializer in order to enable the save() method). The "set_password" example in the ViewSet docs then led me to a basic "create" implementation that looks like:

def create(self, request):
    serializer = self.serializer_class(data=request.data)
    if not serializer.is_valid():
        return Response(serializer.errors,
                        status=status.HTTP_400_BAD_REQUEST)
    obj = serializer.save()
    return Response(self.serializer_class(obj).data,
                    status=status.HTTP_201_CREATED)

Your mention of the routing API did remind me of an additional bit of reverse engineering I needed to do, which was finding out the names of the generated API endpoints in order to reference them from a template. I ended up using show_urls from django-extensions for that.

@tomchristie
Copy link
Member

@ncoghlan Interesting yup - we do a pretty poor job both of formalizing the browsable API behavior (both in implementation - yuck, and in interface). Could be scope for rounding out this section: http://www.django-rest-framework.org/topics/browsable-api/

@xordoquy
Copy link
Collaborator

xordoquy commented Sep 8, 2015

@ncoghlan create is called after the serializer is validated. We should likely swap the order of the save / validation sections in the serializer documentation.

Edit: forget about this comment, I just realised it's the view's update, not the serializer one.

@ncoghlan
Copy link
Author

ncoghlan commented Sep 8, 2015

Sorry, I wasn't clear - that example create() method is the one on the ViewSet, which only receives the request object and still needs to validate the data (unless I've completely misread the validation documentation).

@ncoghlan
Copy link
Author

ncoghlan commented Sep 8, 2015

For the record, the API prototype I'm working on is at https://github.com/ncoghlan/repofunnel/blob/master/copr2pulp/restapi.py

While I've used DRF before, that was about 4 years ago, so I'm effectively relearning it from scratch as I go :)

@ncoghlan
Copy link
Author

Another topic that's handled by default in HyperlinkedModelSerializer, but needs to be handled manually in custom Serializers: embedding the URL for an entity in its own representation. Reading through the serializer code suggests there's also some particular details related to the Location header when sending HTTP 201 responses.

To get that to work with data dictionaries rather than full objects, I needed the following changes/helpers:

# Adapts HyperlinkedIdentityField to use __getitem__ rather than __getattr__
class DictBasedIdentityField(serializers.HyperlinkedIdentityField):

    def get_url(self, obj, view_name, request, format):
        # Unsaved objects will not yet have a valid URL.
        try:
            lookup_value = obj[self.lookup_field]
        except KeyError:
            return None
        kwargs = {self.lookup_url_kwarg: lookup_value}
        return self.reverse(view_name, kwargs=kwargs, request=request, format=format)

And then setting view_name, lookup_field and lookup_url_kwarg appropriately in the field definition.

I was able to figure that out based on a combination of the Model focused docs at http://www.django-rest-framework.org/api-guide/serializers/#how-hyperlinked-views-are-determined, the HyperlinkedIdentityField docs and reading the source code for HyperlinkedIdentityField, and HyperlinkedRelatedField.

I think my original intuition that ViewSets would be a good fit for this "local proxy for a remote API" use case is being borne out, there just isn't a clear overview of the additional things you need to take into account when you're working with just ViewSets and Serializers, and no local Model objects.

@xordoquy
Copy link
Collaborator

This one should be treated as a different issue. It's a bit more than just documentation to me.

@tomchristie
Copy link
Member

@ncoghlan Noted yup - don't know how we'd want to handle it best, but there's a bunch of coupling there - tends to be soft (ie won't fail if it's not as expected, but will eg degrade the browsable API etc)

@xordoquy
Copy link
Collaborator

BTW, I've worked a bit on a demo of a model less API for DRF.
Will polish it a bit today and write some explanation about.

@ncoghlan
Copy link
Author

I think @xordoquy is right that the hyperlinking question is more than just docs, so I moved that out to a separate RFE.

@xordoquy
Copy link
Collaborator

@ncoghlan 👍 thanks !

@tomchristie
Copy link
Member

I think we ought to close this off as stale.
Perfectly happy to consider any PRs addressing the docs along these lines, but evidently it's not something we're considering a priority otherwise.

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