DRF 序列化程序中多个查找字段的自定义超链接 URL 字段

Custom Hyperlinked URL field for more than one lookup field in a serializer of DRF

我正在使用 Django Rest Framework 为我的项目开发网络 api。在我的项目中,我需要像这样构建嵌套的 api 端点:

   /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

所以,我使用 drf-nested-routers 是为了便于编写和维护这些嵌套资源。

我希望我所有端点的输出都有超链接,用于获取每个嵌套资源的详细信息以及其他详细信息,如下所示:

[
    {
        "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"
                 }
            },
            {
             ..........
            }
           ..........
         ]
    }
    .........
]

为此,我根据 DRF 文档通过继承 HyperlinkedModelSerializer 来编写我的序列化程序,它会在序列化期间自动添加一个 url 字段作为响应。

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.

我遵循了这个文档,并编写了自定义代码来处理 url 嵌套资源的生成。我的代码片段如下:

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)), 
]

现在,当我 运行 一个项目并 ping /users/ api 端点时,我收到了这个错误:

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

我不明白为什么会出现这个错误,因为在 UserMailSerializer 中我添加了 url 字段作为这个序列化程序的属性,所以当它必须序列化时为什么需要 url 字段作为 UserMail 模型的属性。 请帮助我摆脱这个问题。

P.S: 请不要在模型中提出任何重构建议。因为,在这里我只是用 user & mail 伪装了我的项目真实想法。因此,将此作为测试用例并建议我一个解决方案。

我最近只是需要做一些类似的事情。我的解决方案最终创建了一个自定义关系字段。为了节省space,我会干脆(并且不要脸)指向source code。最重要的部分是添加 lookup_fieldslookup_url_kwargs class 属性,这些属性在内部用于查找对象和构造 URI:

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)
        ...

这又允许这样的用法:

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

这也是我的使用方法source code

希望这可以帮助您入门。