【问题标题】:Custom Hyperlinked URL field for more than one lookup field in a serializer of DRFDRF 序列化程序中多个查找字段的自定义超链接 URL 字段
【发布时间】:2015-11-09 09:31:27
【问题描述】:

我正在使用 Django Rest Framework 为我的项目开发 web 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 字段作为响应。

但是,默认情况下,DRF 序列化程序不支持像上面提到的那样为嵌套资源生成 url,或者我们可以说不仅仅是单个查找字段。为了处理这种情况,他们建议create custom hyperlinked field

我按照这个文档编写了自定义代码来处理嵌套资源的 url 生成。我的代码sn-ps如下:

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' 对象没有属性 'url'

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

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

【问题讨论】:

  • 我建议尝试使用基于 url-query 过滤的平面结构,而不是嵌套结构。
  • @KevinBrown,你说得对,我们可以通过平面结构来处理这种情况,但是如果存在必须遵守嵌套结构的约束,那么我们该如何处理......这就是我要问的这个问题。谢谢你的建议。

标签: python django serialization django-rest-framework drf-nested-routers


【解决方案1】:

我最近只是需要做一些类似的事情。我的解决方案最终创建了一个自定义关系字段。为了节省空间,我会简单地(并且无耻地)指向source code。最重要的部分是添加 lookup_fieldslookup_url_kwargs 类属性,它们在内部用于查找对象和构造 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

希望这可以帮助您入门。

【讨论】:

  • 也不想在答案中提及,但在工作中,我们最近开源了一个库,其中包含许多用于 DRF 的实用程序 - github.com/dealertrack/django-rest-framework-braces。如果您觉得MultiplePKsHyperlinkedIdentityField 之类的东西在那里可能有用,请随时打开请求票!
  • 非常感谢,它确实有效。我非常感谢并喜欢您的通用解决方案。
  • 嗨@miki725!不错的工作 ;-) !为什么使用 `zip` 和 2 个条目 lookup_fields + lookup_url_kwargs ?我对一个属性进行了一些更改:lookups 这是一个字典 ({lookup_url : lookup_field})。在get_url 内部,我现在只有`kwargs = {url_key: getattr(obj, key) for url_key, key in self.lookups.items()},例如。
猜你喜欢
  • 2019-07-12
  • 1970-01-01
  • 2017-02-04
  • 2022-10-24
  • 1970-01-01
  • 2018-02-20
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多