3

I am using Django Rest Framework for developing web api for my project. As in my project i need to build nested api's endpoint like this:

   /users/ - to get all users
   /users/<user_pk> - to get details of a particular user
   /users/<user_pk>/mails/ - to get all mails sent by a user
   /users/<user_pk>/mails/<pk> - to get details of a mail sent by a user

So, i am using drf-nested-routers for ease of writing & maintaing these nested resources.

I want output of all my endpoints have hyperlink for getting details of each nested resource alongwith other details like this:

[
    {
        "url" : "http://localhost:8000/users/1",
        "first_name" : "Name1",
        "last_name": "Lastname"
        "email" : "name1@xyz.com",
        "mails": [
            {
                 "url": "http://localhost:8000/users/1/mails/1",
                 "extra_data": "This is a extra data",
                 "mail":{
                     "url": "http://localhost:8000/mails/3"
                     "to" : "abc@xyz.com",
                     "from": "name1@xyz.com",
                     "subject": "This is a subject text",
                     "message": "This is a message text"
                 }
            },
            {
             ..........
            }
           ..........
         ]
    }
    .........
]

To do this, i write my serializers by inherit HyperlinkedModelSerializer as per DRF docs, which automatically adds a url field in response during serialization.

But, by default DRF serializers does not support generation of url for nested resource like above mentioned or we can say more than single lookup field. To handle this situation, they recommended to create custom hyperlinked field.

I followed this doc, and write custom code for handling url generation of nested resource. My code snippets are as follows:

models.py

from django.contrib.auth.models import AbstractUser
from django.db import models

# User model
class User(models.AbstractUser):
    mails = models.ManyToManyField('Mail', through='UserMail', 
                                     through_fields=('user', 'mail'))

# Mail model
class Mail(models.Model):
    to = models.EmailField()
    from = models.EmailField()
    subject = models.CharField()
    message = models.CharField()

# User Mail model
class UserMail(models.Model):
    user = models.ForeignKey('User')
    mail = models.ForeignKey('Mail')
    extra_data = models.CharField()

serializers.py

from rest_framework import serializers
from .models import User, Mail, UserMail
from .serializers_fields import UserMailHyperlink

# Mail Serializer
class MailSerializer(serializers.HyperlinkedModelSerializer):
    class Meta:
        model = Mail
        fields = ('url', 'to', 'from', 'subject', 'message' )

# User Mail Serializer
class UserMailSerializer(serializers.HyperlinkedModelSerializer):
    url = UserMailHyperlink()
    mail = MailSerializer()

    class Meta:
        model = UserMail
        fields = ('url', 'extra_data', 'mail')  


# User Serializer
class UserSerializer(serializers.HyperlinkedModelSerializer):
    mails = UserMailSerializer(source='usermail_set', many=True)

    class Meta:
        model = User
        fields = ('url', 'first_name', 'last_name', 'email', 'mails')

serializers_fields.py

from rest_framework import serializers
from rest_framework.reverse import reverse
from .models import UserMail

class UserMailHyperlink(serializers.HyperlinkedRelatedField):
    view_name = 'user-mail-detail'
    queryset = UserMail.objects.all()

    def get_url(self, obj, view_name, request, format):
        url_kwargs = {
            'user_pk' : obj.user.pk,
            'pk' : obj.pk
        }
        return reverse(view_name, kwargs=url_kwargs, request=request, 
                          format=format)

    def get_object(self, view_name, view_args, view_kwargs):
        lookup_kwargs = {
           'user_pk': view_kwargs['user_pk'],
           'pk': view_kwargs['pk']
        }
        return self.get_queryset().get(**lookup_kwargs)

views.py

from rest_framework import viewsets
from rest_framework.response import Response
from django.shortcuts import get_object_or_404
from .models import User, UserMail
from .serializers import UserSerializer, MailSerializer

class UserViewSet(viewsets.ModelViewSet):
    queryset = User.objects.all()
    serializer_class = UserSerializer

class UserMailViewSet(viewsets.ViewSet):
    queryset = UserMail.objects.all()
    serializer_class = UserMailSerializer

    def list(self, request, user_pk=None):
        mails = self.queryset.filter(user=user_pk)
        serializer = self.serializer_class(mails, many=True,
            context={'request': request}
        )
        return Response(serializer.data)

    def retrieve(self, request, pk=None, user_pk=None):
        queryset = self.queryset.filter(pk=pk, user=user_pk)
        mail = get_object_or_404(queryset, pk=pk)
        serializer = self.serializer_class(mail,
            context={'request': request}
        )
        return Response(serializer.data)

urls.py

from rest_framework.routers import DefaultRouter
from rest_framework_nested import routers
from django.conf.urls import include, url
import views

router = DefaultRouter()
router.register(r'users', views.UserViewSet, base_name='user')

user_router = routers.NestedSimpleRouter(router, r'users',
    lookup='user'
)
user_router.register(r'mails', views.UserMailViewSet,
    base_name='user-mail'
)


urlpatterns = [
    url(r'^', include(router.urls)),
    url(r'^', include(user_router.urls)), 
]

Now, after doing this when i run a project and ping /users/ api endpoint, i got this error:

AttributeError : 'UserMail' object has no attribute 'url'

I couldn't understand why this error came, because in UserMailSerializer i added url field as a attribute of this serializer, so when it has to serialize why it takes url field as a attribute of UserMail model. Please help me out to get away from this problem.

P.S: Please don't suggest any refactoring in models. As, here i just disguised my project real idea with user & mail thing. So, take this as test case and suggest me a solution.

Pranab
  • 2,207
  • 5
  • 30
  • 50
Akshay Pratap Singh
  • 3,197
  • 1
  • 24
  • 33
  • I would suggest trying to use a flat structure with url-query-based filtering instead of a nested structure. – Kevin Brown-Silva Aug 16 '15 at 19:57
  • @KevinBrown, you are right we can handle this situation by flat structure but if there is a constraint that we have to adhere nested structure then how can we handle it ... that's what i am asking in this question. Thnks for ur suggestion. – Akshay Pratap Singh Aug 16 '15 at 20:28

1 Answers1

6

I just needed to do something similar lately. My solution ended up making a custom relations field. To save space, Ill simply (and shamelessly) will point to the source code. The most important part is adding lookup_fields and lookup_url_kwargs class attributes which are used internally to both lookup objects and construct the URIs:

class MultiplePKsHyperlinkedIdentityField(HyperlinkedIdentityField):
    lookup_fields = ['pk']
    def __init__(self, view_name=None, **kwargs):
        self.lookup_fields = kwargs.pop('lookup_fields', self.lookup_fields)
        self.lookup_url_kwargs = kwargs.pop('lookup_url_kwargs', self.lookup_fields)
        ...

That in turn allows the usage like:

class MySerializer(serializers.ModelSerializer):
    url = MultiplePKsHyperlinkedIdentityField(
        view_name='api:my-resource-detail',
        lookup_fields=['form_id', 'pk'],
        lookup_url_kwargs=['form_pk', 'pk']
    )

Here is also how I use it source code.

Hopefully that can get you started.

miki725
  • 27,207
  • 17
  • 105
  • 121
  • 1
    Also didnt want to mention in the answer but at work we recently open-sourced a lib which has a bunch of utilities for working with DRF - https://github.com/dealertrack/django-rest-framework-braces. If you feel something like the `MultiplePKsHyperlinkedIdentityField` might be useful there, feel free to open a request ticket! – miki725 Aug 16 '15 at 22:34
  • Thank you very much, it really works. i really appreciate and love your generic solution. – Akshay Pratap Singh Aug 17 '15 at 09:07
  • Hi @miki725 ! Nice job ;-) ! Why using ` zip` and 2 entries `lookup_fields` + `lookup_url_kwargs` ? I've changed a bit to one property : `lookups` which is a dictionnary (`{lookup_url : lookup_field}`). Inside `get_url` I now have simply `kwargs = {url_key: getattr(obj, key) for url_key, key in self.lookups.items()}, for example. – cedrik Jul 07 '16 at 12:02
  • Wow thank you! Integrating this into my use case was surprisingly painless. I used it for a `//` and changed your code to `lookup_fields = ['pk', 'slug']` , and that was all I had to do to get it to work. [DRF DefaultRouter pk/slug](https://stackoverflow.com/questions/71490389/drf-defaultrouter-pk-slug/) – timotaoh Mar 21 '22 at 19:55