2

I have a DRF ViewSet called "QueryCriteriaViewSet" which I'm using in a query builder which allows users to select a field and then select from related criteria. So, for example, a user can select the "reg_status" field and then select from the related criteria of "Active" and "Inactive".

This works totally fine when I select a field from the main "person" model. But I'm running into problems when I select a field from a related model like the "lookup_party" model. The weird thing though, is that when I print the queryset to the console it works perfectly, but when I get call the API it returns a list of empty objects.

As a further example, here's what happens when I make the calls:

api/querycriteria/?fields=reg_status returns:

[
    {"reg_status": "Active"}, 
    {"reg_status": "Inactive"}
]

while api/querycriteria/?fields=party__party_name returns:

[
    {},
    {},
    {},
    {},
    {}
]

even though when I print(queryset) prior to returning the queryset, the following is printed:

<QuerySet [{'party__party_name': None}, {'party__party_name': 'Democratic'}, 
{'party__party_name': 'Non-Partisan'}, {'party__party_name': 'Registered 
Independent'}, {'party__party_name': 'Republican'}]>

Here's the full ViewSet:

class QueryCriteriaViewSet(DefaultsMixin, viewsets.ModelViewSet):
    serializer_class = QueryCriteriaSerializer

    def get_queryset(self):
        fields = self.request.GET.get('fields', None)
        queryset = Person.objects.values(fields).distinct()
        print(queryset)
        return queryset

    def get_fields_to_display(self):
        fields = self.request.GET.get('fields', None)
        return fields.split(',') if fields else None

    def get_serializer(self, instance=None, data=empty, many=False,
                       partial=False):
        """
        Return the serializer instance that should be used for validating and
        deserializing input, and for serializing output.
        """
        serializer_class = self.get_serializer_class()
        context = self.get_serializer_context()
        fields = self.get_fields_to_display()
        return serializer_class(instance, data=data,
                                many=many, partial=partial,
                                context=context, fields=fields)

Let me know if any additional information would be helpful.

Here's my serializer:

from django.contrib.auth import get_user_model
from rest_framework import serializers
from rest_framework.reverse import reverse
from ..models.people import Person
from ..models.people_emails import Email
from ..models.people_addresses import Address
from ..models.people_phones import Phone
from ..models.people_tags import PersonTag
from ..models.people_elections import PersonElection
from ..models.people_districts import PersonDistrict
from ..models.people_attributes import Attribute
from .serializer_dynamic_fields import DynamicFieldsModelSerializer
from .serializer_tag import TagSerializer
from .serializer_email import EmailSerializer
from .serializer_address import AddressSerializer
from .serializer_phone import PhoneSerializer
from .serializer_election import ElectionSerializer
from .serializer_attribute import AttributeSerializer
from .serializer_district import DistrictSerializer

class QueryCriteriaSerializer(DynamicFieldsModelSerializer):

    emails = EmailSerializer(many=True, required=False)
    addresses = AddressSerializer(many=True, required=False)
    phones = PhoneSerializer(many=True, required=False)
    tags = TagSerializer(many=True, required=False)
    elections = ElectionSerializer(many=True, required=False)
    attributes = AttributeSerializer(many=True, required=False)
    districts = DistrictSerializer(many=True, required=False)

    class Meta:
        model = Person
        fields = ('id', 'elected_official', 'title', 'first', 'last', 'middle',    'suffix',
        'full_name', 'birthdate', 'sex', 'website', 'deceased', 'registered',    'party',
        'reg_date', 'reg_status', 'reg_state', 'county', 'match_id',
        'date_added', 'date_updated', 'do_not_call', 'do_not_mail',    'do_not_email', 'do_not_text', 'emails',
        'addresses', 'phones', 'tags', 'attributes', 'elections', 'districts')

And here's the DynamicFieldsModelSerializer:

from django.contrib.auth import get_user_model
from rest_framework import serializers

class DynamicFieldsModelSerializer(serializers.ModelSerializer):
    """
    A ModelSerializer that takes an additional `fields` argument that
    controls which fields should be displayed.
    """

    def __init__(self, *args, **kwargs):
        # Don't pass the 'fields' arg up to the superclass
        fields = kwargs.pop('fields', None)

        # Instantiate the superclass normally
        super(DynamicFieldsModelSerializer, self).__init__(*args, **kwargs)

        if fields is not None:
            # Drop any fields that are not specified in the `fields` argument.
            allowed = set(fields)
            existing = set(self.fields)
            for field_name in existing - allowed:
                self.fields.pop(field_name)
rkm2
  • 53
  • 1
  • 8

1 Answers1

0

ok now found the problem, DynamicFieldsModelSerializer is only working if the fields you are passing to it is a subset of the serializer original fields.

you should use a serializers in a way that accepts extra fields, something like this:

class ExtraDynamicFieldsModelSerializer(serializers.ModelSerializer):


    def __init__(self, *args, **kwargs):
        extra_fields = kwargs.pop('fields', [])
        self.extra_fields = set()
        # Instantiate the superclass normally
        super(ExtraDynamicFieldsModelSerializer, self).__init__(*args, **kwargs)

        allowed = set(extra_fields)
        existing = set(self.fields)
        for field_name in existing - allowed:
            self.fields.pop(field_name)

        for field_name in allowed - existing:
            self.extra_fields.add(field_name)



    def to_representation(self, obj):
        data = super().to_representation(obj)
        for field in self.extra_fields:
            data[field] = obj[field]
        return data
Ehsan Nouri
  • 1,990
  • 11
  • 17
  • Thanks @changak. It's not quite working (but I feel like it's close). Can you explain a little bit more about what `def to_representation(self, obj)` is doing? – rkm2 Aug 14 '18 at 21:08
  • just updated my answer, the problem was in the init function, to_representation it is the function that serializer calls to change the instance to a dict with primitive data types. – Ehsan Nouri Aug 14 '18 at 21:40
  • also in to_represntaion, it will get the value of all the fields that we have put them in the extra from the object and add them to the returning data. – Ehsan Nouri Aug 14 '18 at 21:53
  • thank you! I had to change one line, but now this works. For some reason, `data[field] = getattr(obj, field, '')` returned empty values (it wasn't recognizing 'field' as the attribute. But when I changed that line to `data[field] = obj[field]` it works! If you have any idea why this is the case, I'd love to understand it further, but happy to have the problem solved! – rkm2 Aug 15 '18 at 15:10
  • thanks, I'll update my answer, I think the .values() is causing that, without that the queryset will return a list of instances of the model but now it will return a lis of dicts. – Ehsan Nouri Aug 15 '18 at 15:24