1
votes

I'm pretty new to Django restframework, what i'm trying now is to return object with foreignkey.

class User(models.Model):
    name = models.CharField(max_length=255,blank=True)
    date_created = models.DateTimeField(auto_now_add=True)
    date_modiefied = models.DateTimeField(auto_now=True)
    area = models.CharField(max_length=255,blank=True)
    uuid = models.CharField(max_length=255)
    home = models.CharField(max_length=255,blank=True)
    work = models.CharField(max_length=255,blank=True)
    mobileNo = models.CharField(max_length=255,blank=True)
    email = models.CharField(max_length=255,blank=True)
    appVersionCode = models.CharField(max_length=255,blank=True)
    photoUrl = models.CharField(max_length=255,blank=True)
    serverTime = models.CharField(max_length=255,blank=True)
    fcmTokenId = models.CharField(max_length=255,blank=True)
   def __str__(self):
    return self.name

class LocationData(models.Model):
   user = models.ForeignKey(
     User, related_name='user', on_delete=models.DO_NOTHING)
    source_id = models.CharField(max_length=255)
    latitude = models.CharField(max_length=255)
    longitude = models.CharField(max_length=255)
    speed = models.CharField(max_length=255)
    kms = models.CharField(max_length=255)
    date_created = models.DateTimeField(auto_now=True)
    date_modiefied = models.DateTimeField(auto



class UserSerializer(serializers.ModelSerializer):
  class Meta:
    model = User
    fields = '__all__'

class LocationDataSerializer(serializers.ModelSerializer): 

class Meta:
    model = LocationData
    fields = '__all__'
    depth = 1

I'm using def get_queryset(self):

class SyncIndexLastDataViewSet(viewsets.ModelViewSet):
serializer_class = LocationDataSerializer

def get_queryset(self):

    userid = self.request.query_params.get('user_id', None)
    userExist = User.objects.filter(id=userid)
    if userExist.exists():
        # call the original 'list' to get the original response
        queryset = LocationData.objects.values('source_id').filter(user__id=userid).order_by('-source_id')[:1]
        lastSourceId = queryset[0]['source_id']
        response = {"collection": {"data": lastSourceId,"statusCode": status.HTTP_200_OK,"version":"1.0"}}
        json = JSONRenderer().render(response)
        # customize the response data
        if response is not None:
            return json
    else:
        # return response with this custom representation
        response = {"collection": {"data": "","statusCode":status.HTTP_404_NOT_FOUND,"version":"1.0","error":"Not found"}}
        return response

Right now the result is inside the response is below and immediately it throws this error

But i want that queryset to return as below one, Hence i can read those key-pair values in android

{ "collection": {
  "data": {
    "id": 31,
    "source_id": "55",
    "latitude": "24654",
    "longitude": "454654",     
    "date_created": "2019-02-08T17:10:09.318644Z",
    "date_modiefied": "2019-02-08T17:10:09.318714Z",
    "area": "54546",
    "user": {
        "id": 1,
        "name": "Dormy",
        "date_created": "1992-01-18T03:29:53.388000Z",
        "date_modiefied": "2018-02-19T05:17:00.164000Z",
        "serverTime": "",
        "fcmTokenId": ""
      }
  },
    "statusCode": 200,
    "version": "1.0"
 }

Now the error throws

AttributeError: Got AttributeError when attempting to get a value for field source_id on serializer LocationDataSerializer. The serializer field might be named incorrectly and not match any attribute or key on the int instance. Original exception text was: 'int' object has no attribute 'source_id'.

Thanks!

4
Do you want all of your response in this way or just special type of request - response ? - Shakil
By the above way I mentioned! - EngineSense
you can add annotate in your query. Using that you can add custom field to your query - Sarang
@Sarang do you have sample! Please post it - EngineSense
docs.djangoproject.com/en/2.1/ref/models/querysets/#annotate check this link. There are many functionalities which you can use with annotate. - Sarang

4 Answers

5
votes

The answer to this depends on what type of view you are using but the bottom line is you don't do this in get_queryset you do this in the method for the type of reguest.

For example if you are using a RetrieveAPIView you should override the retrieve method from the RetrieveModelMixin like so:

class MyAPIView(RetrieveAPIView):
    queryset = MyModel.objects.all()
    serializer_class = MySerializer

    def retrieve(self, request, *args, **kwargs):
        instance = self.get_object()
        serializer = self.get_serializer(instance)
        data = {
            "collection": {
                "data": serializer.data
            },
            "statusCode": 200,
            "version": "1.0"
        }
        return Response(data)

If you are using something else like a ListAPIView then you want to see what is used by that in the relevant method and override that to wrap your data.

The main thing to realise here is that it has nothing to do with getting the queryset - which is just about getting data from the database. This is about transforming the data into the correct format when sending back a response. As a result the work should be done at the point the response is made.

2
votes

There are couple of solution possible for this problem. NDevox already mention how we can overwrite our retrive function and get our expected response. But If we want this will be done with every response for every api end-point and if we go this way we need to overwrite every function then its quite burden and its DRY we should avoid this as possible. One of the possible way to introduce a middleware or overwrite Response so that we can get our generic response for every-api end-point without explicitly overwrite every functionality.

Possible Solution One

As we are using DRF here we can add our own return responses with various media types, say for application/json.

First We need to add in our settings.py

REST_FRAMEWORK = {
    ...
    'DEFAULT_RENDERER_CLASSES': (
        'app_name.renderers.ApiRenderer',  # our own render middleware
    ),
    ...
}

And in our custom render middleware

from rest_framework.renderers import BaseRenderer
from rest_framework.utils import json


class ApiRenderer(BaseRenderer):

    def render(self, data, accepted_media_type=None, renderer_context=None):
        our_response_dict = {
            'version': '1.0'
            'data': {},
            'message': '',
        }
        if data.get('data'):
            our_response_dict['data'] = data.get('data')
        if data.get('status'):
            our_response_dict['statusCode'] = data.get('status')
        if data.get('message'):
            our_response_dict['message'] = data.get('message')
        data = our_response_dict
        return json.dumps(data)

Reference Link

Possible Solution Two

If we are using ModelViewset then there is another way we can achievement that. Say Our Views.py are like following

class A(serializer.ModelSerializer):
    ........

class B(serializer.ModelSerializer):
    ........


class C(serializer.ModelSerializer):
    ........

Our goal is to overwrite ModelViewset's to_representation function and return our custom result. This will like as following

from collections import OrderedDict

class OurParentViewset(serializer.ModelSerializer):

    ......
    def to_representation(self, instance):
        data = super(serializers.ModelSerializer, self).to_representation(instance)
        result = OrderedDict()
        result['data'] = data
        result['version'] = '1.0'
        result['statusCode'] = '2xx' # i am not fully sure how to customize this
        return result

    class A(OurParentViewset):
        ........

    class B(OurParentViewset):
        ........


    class C(OurParentViewset):
        ........
2
votes

Implementing a custom renderer here seems to be a ways to go.

You can have requests from your android client include in the Accept header a way to identify the client to the renderer. 1e.g.

Accept: application/json; android=true

Then compose a renderer using the JSONRenderer class to provide the format for your Android client.

# ./formatters/android_format.py

from rest_framework.renderers import JSONRenderer, BaseRenderer
from django.http.multipartparser import parse_header

class AndroidV1FormatRenderer(BaseRenderer):
    media_type = 'application/json'
    format = 'json'

    json_renderer = JSONRenderer()

    def android(self, accepted_media_type):
        base_media_type, params = parse_header(accepted_media_type.encode('ascii'))
        return 'android' in params

    def render(self, data, accepted_media_type=None, renderer_context=None):
        response = renderer_context['response']
        android = self.android(accepted_media_type)
        if android:
            data = {
                "collection": {"data": data},
                "statusCode": response.status_code,
                "version": "1.0"
            }

        return json_renderer.render(
            wrapped_data, accepted_media_type, renderer_context)

This can then be used where you require response formatted that way using renderer_classes attribute of your APIView. 2

1
votes

Since get_queryset won't allow you to customize the response data. I decide to take the query value that's important to me.

http://localhost/api/users/?user_id=1 --> changed into ...api/users/1

 def retrieve(self, request, *args, **kwargs):
    """ userid = self.request.query_params.get('user_id', None) """

    userid = kwargs.get('pk')
    userExist = User.objects.filter(id=userid)
    if userExist.exists():
        # call the original 'list' to get the original response
        queryset =  LocationData.objects.values('source_id').filter(user__id=userid).order_by('-source_id')[:1]
        lastSourceId = queryset[0]['source_id']
        response = {"collection": {"data": lastSourceId,"statusCode": status.HTTP_200_OK,"version":"1.0"}}
        # customize the response data
        if response is not None:
            return Response(response)
    else:
        # return response with this custom representation
        response = {"collection": {"data": "","statusCode":status.HTTP_404_NOT_FOUND,"version":"1.0","error":"Not found"}}
        return response