question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Nested routes allow creation of objects for another parent object

See original GitHub issue

So, consider two models as example:

class User(AbstractUser):
    pass

class Account(Model):
    user = ForeignKey(settings.AUTH_USER_MODEL)
    avatar = URLField()

This router:

router.register(r"users", UserViewSet) \
      .register(r"accounts", AccountViewSet, base_name="users-accounts",
                parents_query_lookups=["user"])

Now if you issue a POST to /users/1/accounts/ with {"user": 2, "avatar": "www.example.com"} the account is created for user with ID 2 instead ID 1.

Serializers (which create the stuff in DB) will use request.data and request.data have {"user": "2"}. DRF-Extensions need to force the correct user in this situations.

One workaround to this problem is inject correct user in request.data, something like:

class NestedAccountViewSet(NestedViewSetMixin, AccountViewSet):

    def initialize_request(self, request, *args, **kwargs):
        """
        Inject user from url in request.data
        """
        request = super(NestedAccountViewSet, self).initialize_request(
            request, *args, **kwargs
        )
        if request.data:
            request.data["user"] = kwargs["parent_loookup_user"]

        return request

Change the request.data like code above is easy, but I’m sure that isn’t the best way to do it. Maybe DRF-Extensions can offer some custom serializer that receive extra information from url regex or do some magic in views so we don’t need to do this injection?

Issue Analytics

  • State:open
  • Created 7 years ago
  • Reactions:4
  • Comments:12

github_iconTop GitHub Comments

6reactions
timb07commented, May 11, 2016

I’m facing a similar problem, which is that the parent object shouldn’t have to be specified in the request data, given it’s in the URL already. I’m addressing it by adding to NestedViewSetMixin:

class NestedViewSetCreateMixin(NestedViewSetMixin):
    def perform_create(self, serializer):
        kwargs_id = {key + '_id': value for key, value in self.get_parents_query_dict().items()}
        serializer.save(**kwargs_id)

This probably doesn’t correctly handle all possible configurations, but it’s working for mine.

2reactions
Misairu-Gcommented, Sep 28, 2019
from collections import OrderedDict

from django.utils import six
from django.db.models import Model
from django.db.models.fields.related_descriptors import ForwardManyToOneDescriptor
from rest_framework.exceptions import NotFound
from rest_framework.viewsets import GenericViewSet
from rest_framework_extensions.settings import extensions_api_settings


class NestedGenericViewSet(GenericViewSet):
    """
    This ViewSet is a re-write of the original NestedViewSetMixin from rest_framework_extensions

    The original ViewSet is a little bit buggy regarding the following points:
    - It would return 200 even if the parent lookup does not exist
    - It would allow creation of objects for another parent object
    - FIXME: It does not check whether requester has the permission to access parent object

    The rewrite is based on https://github.com/chibisov/drf-extensions/issues/142,
    credit to @Place1 for the ideas and sample implementation.
    """
    resolved_parents = OrderedDict()

    def initial(self, request, *args, **kwargs) -> None:
        """
        Resolve parent objects.

        Before every request to this nested viewset we want to resolve all the parent
        lookup kwargs into the actual model instances.

        We do this so that if they don't exist a 404 will be raised.

        We also cache the result on `self` so that if the request is a POST, PUT or
        PATCH the parent models can be reused in our perform_create and perform_update
        handlers to avoid accessing the DB twice.
        """
        super(NestedGenericViewSet, self).initial(request, *args, **kwargs)
        self.resolve_parent_lookup_fields()

    def get_queryset(self):
        return self.filter_queryset_by_parents_lookups(
                super(NestedGenericViewSet, self).get_queryset()
        )

    def filter_queryset_by_parents_lookups(self, queryset):
        parents_query_dict = self.get_parents_query_dict()
        if parents_query_dict:
            try:
                return queryset.filter(**parents_query_dict)
            except ValueError:
                raise NotFound()
        else:
            return queryset

    def get_parents_query_dict(self) -> OrderedDict:
        result = OrderedDict()
        for kwarg_name, kwarg_value in six.iteritems(self.kwargs):
            if kwarg_name.startswith(
                    extensions_api_settings.DEFAULT_PARENT_LOOKUP_KWARG_NAME_PREFIX):
                query_lookup = kwarg_name.replace(
                        extensions_api_settings.DEFAULT_PARENT_LOOKUP_KWARG_NAME_PREFIX,
                        '',
                        1
                )
                query_value = kwarg_value
                result[query_lookup] = query_value
        return result

    def resolve_parent_lookup_fields(self) -> None:
        """Update resolved parents to the instance variable"""

        parents_query_dict = self.get_parents_query_dict()

        keys = list(parents_query_dict.keys())

        for i in range(len(keys)):
            # the lookup key can be a django ORM query string like 'project__slug'
            # so we want to split on the first '__' to get the related field's name,
            # followed by the lookup string for the related model. Using the given
            # example the related field will be 'project' and the 'slug' property
            # will be the lookup on that related model

            # TODO: support django ORM query string, like 'project__slug'
            field = keys[i]
            value = parents_query_dict[keys[i]]

            related_descriptor: ForwardManyToOneDescriptor = getattr(self.queryset.model, field)
            related_model: Model = related_descriptor.field.related_model

            # The request must have all previous parents matched, for example
            # /contracts/2/assessment-methods/1/examine/ must satisfies assessment-method=1
            # can be query by contract=2
            previous_parents = {k: self.resolved_parents[k] for k in keys[:i]}

            try:
                self.resolved_parents[field] = related_model.objects.get(
                        **{'pk': value, **previous_parents})
            except related_model.DoesNotExist:
                raise NotFound()

Improved solution based on @Place1 's, work under Python 3.7 with Django 2.2 and DRF 3.10.

The line field, lookup = key.split('__', 1) is deleted as would cause error if no slug is given, since I currently only use primary key for index, it fits my minimal requirement.

Feel free to use it and add your own modification.

At the end, I don’t feel like nested router is a good idea, nested view set make more sense for me. But I don’t really have time to switch to other solution right now.


Edit: If you’re reading this, please keep in mind that nested route/viewset for resources is a bad practice when designing RESTful API, and you should generally avoid doing this. Nesting resources would result unnecessary complication for dependencies, instead, you should use hyperlink for related resources.

Refer here for details about best practice of RESTful API design.

Read more comments on GitHub >

github_iconTop Results From Across the Web

The Guide to Nested Routes with React Router - ui.dev
In this comprehensive, up-to-date guide, you'll learn everything you need to know about creating nested routes with React Router.
Read more >
A Guide to Using Nested Routes in Ruby | Scout APM Blog
A guide detailing how Ruby simplifies the system of creating and managing routes and resources.
Read more >
reactjs - React Router Nested Route renders inside of parent ...
@KonradLinkowski I tried doing it without the nesting but the Link needs a fixed path, ie /products/name-of-product. How do I change the Link...
Read more >
React Router: A simple tutorial on nested routes.
So, we will create an array of objects, where each object will ... Till now, we have created some nested routes but those...
Read more >
How To Create Nested Resources for a Ruby on Rails ...
Step 1 — Scaffolding the Nested Model · Step 2 — Specifying Nested Routes and Associations for the Parent Model · Step 3...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found