Multiple lookup_fields for django rest framework

2020-02-10 21:16发布

问题:

I have multiple API which historically work using id as the lookup field:

/api/organization/10

I have a frontend consuming those api.

I'm building a new interface and for some reasons, I would like to use a slug instead an id:

/api/organization/my-orga

The API is built with Django Rest Framework. Except the change of lookup field, the api behavior should stay the same.

Is there a solution to allow my API to work with both a slug and a pk ? Those two path should give them same results:

/api/organization/10
/api/organization/my-orga

Here is my API definition:

# urls.py
router = DefaultRouter()
router.register(r'organization', Organization)
urlpatterns = router.urls

#view.py
class Organization(viewsets.ModelViewSet):
    queryset = OrganisationGroup.objects.all()
    serializer_class = OrganizationSerializer

# serializer.py
class OrganizationSerializer(PermissionsSerializer):
    class Meta:
        model = Organization

Thanks for your help.

回答1:

Try this

from django.db.models import Q
import operator
class MultipleFieldLookupMixin(object):
    def get_object(self):
        queryset = self.get_queryset()             # Get the base queryset
        queryset = self.filter_queryset(queryset)  # Apply any filter backends
        filter = {}
        for field in self.lookup_fields:
            filter[field] = self.kwargs[field]
        q = reduce(operator.or_, (Q(x) for x in filter.items()))
        return get_object_or_404(queryset, q)

Then in View

class Organization(MultipleFieldLookupMixin, viewsets.ModelViewSet):
    queryset = OrganisationGroup.objects.all()
    serializer_class = OrganizationSerializer
    lookup_fields = ('pk', 'another field')

Hope this helps.



回答2:

class MultipleFieldLookupMixin(object):
    """
    Apply this mixin to any view or viewset to get multiple field filtering
    based on a `lookup_fields` attribute, instead of the default single field filtering.
    """

    def get_object(self):
        queryset = self.get_queryset()  # Get the base queryset
        queryset = self.filter_queryset(queryset)
        filter = {}
        for field in self.lookup_fields:
            if self.kwargs[field]:  # Ignore empty fields.
                filter[field] = self.kwargs[field]
        return get_object_or_404(queryset, **filter)  # Lookup the object


class RetrieveUserView(MultipleFieldLookupMixin, generics.RetrieveAPIView):
    queryset = User.objects.all()
    serializer_class = UserSerializer
    lookup_fields = ('account', 'username')



回答3:

I solved the similar problem by overriding retrieve method and check pk field's value against any pattern. For example if it consists of only numbers.

def retrieve(self, request, *args, **kwargs):
    if kwargs['pk'].isdigit():
        return super(Organization, self).retrieve(request, *args, **kwargs)
    else:
        # get and return object however you want here.


回答4:

I think the fundamental answer is that this would not be good REST/API design and just isn't something DRF would enable.



回答5:

The official docs have an example for this at https://www.django-rest-framework.org/api-guide/generic-views/#creating-custom-mixins

Also, you need to modify the urls.py adding a new route for the same view, but with the new field name.



回答6:

I think best way is to override the get_object(self) method

class Organization(generics.RetrieveAPIView):
    serializer_class = serializer_class = OrganizationSerializer
    queryset = Organization.objects.all()
    multiple_lookup_fields = ['pk', 'slug']

    def get_object(self):
        queryset = self.get_queryset()
        filter = {}
        for field in self.multiple_lookup_fields:
            filter[field] = self.kwargs[field]

        obj = get_object_or_404(queryset, **filter)
        self.check_object_permissions(self.request, obj)
        return obj