Django REST specify which fields when viewing a list - python

By default, when asking a list of a model like "/cars", django rest outputs all of the model data. I want it to output only pk's on a list request, and full model data on a detail request. I am using ModelSerializer and ModelViewSet.
PS. Is it supposed to be like this by design? Pulling so much unneeded data seems like such a waste.
Thanks :)

You can simply override the 'list' method of viewset to get desired response like this:
from rest_framework.response import Response
def list(self, request, *args, **kwargs):
pks = []
qs = self.get_queryset()
for obj in qs:
pks.append(obj.pk)
return Response(data=pks)

I customized the code to suit my requirement. I created the following 2 mixins. Make your APIListView extend FieldFilterMixin and your serializer extend SerializerFieldsMixin. Pass fl in GET request with comma separated values to get the required fields in response.
class SerializerFieldsMixin(object):
"""
Return only the fields asked for.
Don't return any extra fields in serializer.
"""
def get_fields(self):
all_fields = super(SerializerFieldsMixin,self).get_fields()
asked_fields = self.context.get('asked_fields')
if not asked_fields:
return all_fields
all_fields = OrderedDict([(k,v) for k,v in all_fields.items() if k in asked_fields])
return all_fields
class FieldFilterMixin(object):
"""
To be used with List/Retrieve views.
Set class attribute fields for the fields you want to display.
Or override get_required_fields to customize.
"""
def get_required_fields(self):
if self.request.GET.has_key("fl"):
return self.request.GET["fl"]
return []
def get_serializer_context(self):
methods_to_act_on = ["GET","HEAD"]
context = super(FieldFilterMixin,self).get_serializer_context()
asked_fields = self.get_required_fields()
if asked_fields and self.request.method in methods_to_act_on:
context["asked_fields"] = asked_fields
return context

Related

Django rest framework: exclude results when nested serializer is empty

I have a nested serializer that works, but I would like to exclude instances where the nested serializer is empty. The filtering I'm using on the nested serializer works, but currently this code returns all Sites, most of which have empty site_observations arrays when filters are applied. I would like to return only Sites that contain site_observations. I have tried a SerializerMethodField for site_observations but have the same issue. Using DRF 3.12
Relevant models are Site, and Observation which has FK to site, with related field=site_observations
serializers.py
class FilteredObsListSerializer(serializers.ListSerializer):
def to_representation(self, data):
projName = self.context["projName"]
# this is my filter which works
data = filter_site_observations(data, self.context["request"],
projName)
return super(FilteredObsListSerializer, self).to_representation(data)
class ObsFilterSerializer(serializers.ModelSerializer):
class Meta:
list_serializer_class = FilteredObsListSerializer
model = Observation
fields = "__all__"
class SiteSerializer(GeoFeatureModelSerializer):
site_observations = ObsFilterSerializer(many=True)
class Meta:
model = Site
geo_field = "geometry"
fields = ("id", "name", "geometry", "site_observations")
views.py
class SiteList(generics.ListAPIView):
queryset = Site.objects.all().order_by("pk")
serializer_class = SiteSerializer
# this is for filtering Observations on segment of an url:
def get_serializer_context(self):
context = super(SiteList, self).get_serializer_context()
context.update({"projName": self.kwargs["name"]})
return context
How can I exclude Sites where site_observations is an empty list? Thanks.
One approach is to tell your view to only work with certain objects that meet some criteria:
class SiteList(generics.ListAPIView):
queryset = Site.objects.filter(
site_observations__isnull=False,
).distinct().order_by('pk')
This will tell SiteList to only work with Site objects that have site_observation relations existing. You need the distinct call here as described by this.
Thanks for the suggestion, which led to an answer. Not running filter_site_observations in the serializer worked. I reworked the filter to run in the view and got the results I needed. (I had tried this before using a SerializerMethodField but couldn't filter it properly; the nested serializer seems a better approach.) Thanks for the suggestions! Here's the filter:
def filter_site_observations(queryset, request, projName):
'''
filters a collection on project, observer status
'''
if request.user.is_authenticated:
authprojects = [
item.project.name
for item in ObserverStatus.objects.filter(observer=request.user.id)
]
if projName in authprojects:
return queryset.filter(site_observations__project__name=projName)
else:
return queryset.filter(
site_observations__project__name=projName).filter(
site_observations__private=False)
else:
return queryset.filter(
site_observations__project__name=projName).filter(
site_observations__private=False)
and the views.py:
class SiteList(generics.ListAPIView):
serializer_class = SiteSerializer
def get_serializer_class(self, *args, **kwargs):
if self.request.method in ("POST", "PUT", "PATCH"):
return SitePostSerializer
return self.serializer_class
def get_queryset(self):
projName = self.kwargs["name"]
queryset = Site.objects.all().order_by('pk')
queryset = filter_site_observations(queryset, self.request, projName)
queryset = queryset.filter(
site_observations__isnull=False, ).distinct()
queryset = self.get_serializer_class().setup_eager_loading(queryset)
return queryset
UPDATE AUG 28
Actually, to get the filtering I needed, I had to run pretty much the same filter in both the Observation serializer AND in the SiteList views.py. This was true regardless of whether I used SerializerMethodField or a simple nested serializer for the child data. Otherwise, I would get either: 1) ALL Sites, including ones that didn't have any Observations, or 2) Sites that had some non-private Observations, but also displayed all the private ones.
filters.py
from users.models import ObserverStatus
def filter_site_observations(queryset, request, projName):
'''
filters a collection on project, observer status; used in views.py.
Both of these filters seem to be required to get proper filtering.
'''
queryset = queryset.filter(site_observations__project__name=projName)
if request.user.is_authenticated:
authprojects = [
item.project.name
for item in ObserverStatus.objects.filter(observer=request.user.id)
]
if projName in authprojects:
return queryset
else:
return queryset.filter(site_observations__private=False)
else:
return queryset.filter(site_observations__private=False)
def filter_observations(queryset, request, projName):
'''
filters observations in ObsSerializer on observer status
'''
if request.user.is_authenticated:
authprojects = [
item.project.name
for item in ObserverStatus.objects.filter(observer=request.user.id)
]
if projName in authprojects:
return queryset
else:
return queryset.filter(private=False)
else:
return queryset.filter(private=False)
So I'm filtering twice and not sure why this needs to happen.

fetch ids with commas in django

view.py
def get(self, *args, **kwargs):
res = Question.objects.all()
# queryset = super().ListAPIView(*args, **kwargs)
if 'tag' in self.request.GET:
# split and directly put in main query
res = res.filter(
Tag_name=self.request.GET['tag']
)
if 'order_by' in self.request.GET:
res = res.order_by(
self.request.GET['order_by'])
serializer = QuestionSerializers(res, many=True)
return Response(serializer.data)
here I am trying to fetch question with some input tag ids and order by some input , how can I use multiple tag ids in url like
http://127.0.0.1:8000/?tag=1,2&order_by=name
so I get all objects with tag ids 1 and 2.
You can pass multiple get parameters with the same name:
http://127.0.0.1:8000/?tag=1&tag=2&order_by=name
In your view you can access the list using the getlist method:
def get(self, *args, **kwargs):
# returns empty list if no tags are provided
tags = self.request.GET.getlist('tag')
# you can set a default field to order_by, if no field is provided
order_by = self.request.GET.get('order_by', 'id')
# convert tags, currently strings, to int
tags = [int(tag) for tag in tags]
# not sure about this part would need to check your models
res = Question.objects.filter(tag__id__in=tags).order_by(order_by)
serializer = QuestionSerializers(res, many=True)
return Response(serializer.data)
You need to split values of tag by , and filter all tags with id__in filter like this
def get(self, *args, **kwargs):
res = Question.objects.all()
# queryset = super().ListAPIView(*args, **kwargs)
if self.request.GET.get('tag'):
# split and directly put in main query
res = res.filter(
tag_id__in=self.request.GET.get('tag').split(',')
)
if self.request.GET.get('order_by'):
res = res.order_by(
self.request.GET['order_by'])
serializer = QuestionSerializers(res, many=True)
return Response(serializer.data)
Well you can have a workaround(or better to say more django rest framework oriented way to achieve this) using ListAPIView of DRF like:
from rest_framework.generics import ListAPIView
class TagListAPIView(ListAPIView):
serializer_class = QuestionSerializers
filter_backends = (OrderingFilter)
ordering_fields = ['name', ...any other fields you wanna put here as an option for ordering]
ordering = 'name' #setting the default ordering
def get_queryset(self):
ids = self.request.query_params.get('tag', None)
if not ids:
return Question.objects.none() #Empty queryset
return Question.objects.filter(id__in=ids.split(','))
Haven't tested the code but yeah it should be somewhere around this and this code does not do all the proper input handling if some garbage input is coming from user(which is the case manytimes).
You could write more strong code for this by incorporating django-filters and putting a id field in that filter. Which would give you all the error validation and handling.
I have provided a links to DRF's doc regarding filter, ordering and all. https://www.django-rest-framework.org/api-guide/filtering/#orderingfilter
Have a look, its great content.
Ping me if anything unclear.

How to incorporate data from two distinct sources (that don't have a RDBMS relationship) in a single serializer?

I'm trying to serialize some objects whose data is stored in 2 databases, linked by common UUIDs. The second database DB2 stores personal data, so it is run as a segregated microservice to comply with various privacy laws. I receive the data as a decoded list of dicts (rather than an actual queryset of model instances). How can I adapt the ModelSerializer to serialize this data?
Here's a minimal example of interacting with DB2 to get the personal data:
# returns a list of dict objects, approx representing PersonalData.__dict__
# `custom_filter` is a wrapper for the Microservice API using `requests`
personal_data = Microservice.objects.custom_filter(uuid__in=uuids)
And here's a minimal way of serializing it, including the date of birth:
class PersonalDataSerializer(serializers.Serializer):
uuid = serializers.UUIDField() # common UUID in DB1 and DB2
dob = serializers.DateField() # personal, so can't be stored in DB1
In my application, I need to serialize the Person queryset, and related personal_data, into one JSON array.
class PersonSerializer(serializers.ModelSerializer):
dob = serializers.SerializerMethodField()
# can't use RelatedField for `dob` because the relationship isn't
# codified in the RDBMS, due to it being a separate Microservice.
class Meta:
model = Person
# A Person object has `uuid` and `date_joined` fields.
# The `dob` comes from the personal_data, fetched from the Microservice
fields = ('uuid', 'date_joined', 'dob',)
def get_dob(self):
raise NotImplementedError # for the moment
I don't know if there's a nice DRF way to link the two. I definitely don't want to be sending (potentially thousands of) individual requests to the microservice by including a single request in get_dob. The actual view just looks like this:
class PersonList(generics.ListAPIView):
model = Person
serializer_class = PersonSerializer
def get_queryset(self):
self.kwargs.get('some_filter_criteria')
return Person.objects.filter(some_filter_criteria)
Where should the logic go to link the microservice data into the serializer, and what should it look like?
I suggest you to override the serializer and your list method.
Serializer:
class PersonSerializer(models.Serializer):
personal_data = serializers.DictField()
class Meta:
model = Person
make a function to add personal_data dictionary to persons object. Use this method before giving the list of person objects to the serializer.
def prepare_persons(persons):
person_ids = [p.uuid for p in persons]
personal_data_list = Microservice.objects.custom_filter(uuid__in=person_ids)
personal_data_dict = {pd['uuid']: pd for pd in personal_data_list}
for p in persons:
p.personal_data = personal_data_dict[p.id]
return persons
def list(self, request, *args, **kwargs):
queryset = self.filter_queryset(self.get_queryset())
page = self.paginate_queryset(queryset)
if page is not None:
page = prepare_persons(page)
serializer = self.get_serializer(page, many=True)
return self.get_paginated_response(serializer.data)
else:
persons = prepare_persons(queryset)
serializer = self.get_serializer(persons, many=True)
return Response(serializer.data)
Because you want to only hit your database one time, a good way to add your extra data to your queryset is by adding a custom version of ListModelMixin to your ViewSet that includes extra context:
class PersonList(generics.ListAPIView):
...
def list(self, request, *args, **kwargs):
queryset = self.filter_queryset(self.get_queryset())
# Pseudo-code for filtering, adjust to work for your use case
filter_criteria = self.kwargs.get('some_filter_criteria')
personal_data = Microservice.objects.custom_filter(filter_criteria)
page = self.paginate_queryset(queryset)
if page is not None:
serializer = self.get_serializer(
page,
many=True,
context={'personal_data': personal_data}
)
return self.get_paginated_response(serializer.data)
serializer = self.get_serializer(
queryset,
many=True,
context={'personal_data': personal_data}
)
return Response(serializer.data)
Then, access the extra context in your serializer by overriding the to_representation method:
def to_representation(self, instance):
"""Add `personal_data` to the object from the Microservice"""
ret = super().to_representation(instance)
personal_data = self.context['personal_data']
ret['personal_data'] = personal_data[instance.uuid]
return ret

Complex aggregation methods as single param in query string

I’m trying to design a flexible API with django REST. What I meant by this is to have basically any field filterable through a query string and in addition to that have a param in the query string that can denote some complex method to perform. Ok, here are the details:
views.py
class StarsModelList(generics.ListAPIView):
queryset = StarsModel.objects.all()
serializer_class = StarsModelSerializer
filter_class = StarsModelFilter
serializers.py
class StarsModelSerializer(DynamicFieldsMixin, serializers.ModelSerializer):
class Meta:
model = StarsModel
fields = '__all__'
mixins.py
class DynamicFieldsMixin(object):
def __init__(self, *args, **kwargs):
super(DynamicFieldsMixin, self).__init__(*args, **kwargs)
fields = self.context['request'].query_params.get('fields')
if fields:
fields = fields.split(',')
# Drop any fields that are not specified in the `fields` argument.
allowed = set(fields)
existing = set(self.fields.keys())
for field_name in existing - allowed:
self.fields.pop(field_name)
filters.py
class CSVFilter(django_filters.Filter):
def filter(self, qs, value):
return super(CSVFilter, self).filter(qs, django_filters.fields.Lookup(value.split(u","), "in"))
class StarsModelFilter(django_filters.FilterSet):
id = CSVFilter(name='id')
class Meta:
model = StarsModel
fields = ['id',]
urls.py
url(r’^/stars/$’, StarsModelList.as_view())
this give me the ability to construct query strings like so:
/api/stars/?id=1,2,3&fields=type,age,magnetic_field,mass
this is great I like this functionality, but there are also many custom aggregation/transformation methods that need to be applied to this data. What I would like to do is have an agg= param like so:
/api/stars/?id=1,2,3&fields=type,age,magnetic_field,mass,&agg=complex_method
or just:
/api/stars/?agg=complex_method
where defining the complex_method grabs the correct fields for the job.
I’m not exactly sure where to start and where to add the complex methods so I would really appreciate some guidance. I should also note the api is only for private use supporting a django application, its not exposed to the public.
Definitely would be good to see your MyModelList class but anyway my example as per https://docs.djangoproject.com/en/1.10/ref/class-based-views/base/
from django.http import HttpResponse
from django.views import View
class StarsModelList(generics.ListAPIView):
queryset = StarsModel.objects.all()
serializer_class = StarsModelSerializer
filter_class = StarsModelFilter
def complex_method(request):
# do smth to input parameters if any
return HttpResponse('Hello, World!')
def get(self, request, *args, **kwargs):
if request.GET.get('agg', None) == 'complex_method':
return self.complex_method(request)
return HttpResponse('Hi, World!')

Django REST Framework how to add context to a ViewSet

The ViewSets do everything that I want, but I am finding that if I want to pass extra context to a template (with TemplateHTMLRenderer) then I will have to get at the functions that give responses.. (like list(), create(), etc)
The only way I can see to get into these is to completely redefine them in my ViewSet, but it seems that there should be an easy way to add a bit of context to the Template without having to redefine a whole set of methods...
class LanguageViewSet(viewsets.ModelViewSet):
"""Viewset for Language objects, use the proper HTTP methods to modify them"""
# TODO: add permissions for this view?
queryset = Language.objects.all()
serializer_class = LanguageSerializer
filter_backends = (filters.DjangoFilterBackend, )
filter_fields = ('name', 'active')
Right now my code is looking like this but I will be wanting to add different context to the responses and I am trying to avoid redefining an entire method for such a small change. like this...
class LanguageViewSet(viewsets.ModelViewSet):
"""Viewset for Language objects, use the proper HTTP methods to modify them"""
# TODO: add permissions for this view?
queryset = Language.objects.all()
serializer_class = LanguageSerializer
filter_backends = (filters.DjangoFilterBackend, )
filter_fields = ('name', 'active')
def list(self, **kwargs):
"""Redefinition of list"""
..blah blah everything that list does
return Response({"foo": "bar"}, template_name="index.html")
I faced the same issue and resolved in a slightly different way in Django Rest Framework (DRF) 3.x. I believe it is not necessary to override the relatively complex render method on the TemplateHTMLRenderer class, but only the much simpler method get_template_context (or: resolve_context in earlier versions of DRF).
The procedure is as follows:
Override the get_renderer_context method on your ViewSet (as already suggested):
def get_renderer_context(self):
context = super().get_renderer_context()
context['foo'] = 'bar'
return context
Subclass TemplateHTMLRenderer, but only override the get_template_context method instead of the entire render method (render calls self.get_template_context to retrieve the final context to pass to the template):
class ModifiedTemplateHTMLRenderer(TemplateHTMLRenderer):
def get_template_context(self, data, renderer_context):
"""
Override of TemplateHTMLRenderer class method to display
extra context in the template, which is otherwise omitted.
"""
response = renderer_context['response']
if response.exception:
data['status_code'] = response.status_code
return data
else:
context = data
# pop keys which we do not need in the template
keys_to_delete = ['request', 'response', 'args', 'kwargs']
for item in keys_to_delete:
renderer_context.pop(item)
for key, value in renderer_context.items():
if key not in context:
context[key] = value
return context
{{ foo }} is now available as a template variable - as are all other variables added in get_renderer_context.
Although I disagree with 'pleasedontbelong' in principle, I agree with him on the fact that the extra contextual data ought to be emitted from the serializer. That seems to be the cleanest way since the serializer would be returning a native python data type which all renderers would know how to render.
Heres how it would look like:
ViewSet:
class LanguageViewSet(viewsets.ModelViewSet):
queryset = Language.objects.all()
serializer_class = LanguageSerializer
filter_backends = (filters.DjangoFilterBackend, )
filter_fields = ('name', 'active')
def get_serializer_context(self):
context = super().get_serializer_context()
context['foo'] = 'bar'
return context
Serializer:
class YourSerializer(serializers.Serializer):
field = serializers.CharField()
def to_representation(self, instance):
ret = super().to_representation(instance)
# Access self.context here to add contextual data into ret
ret['foo'] = self.context['foo']
return ret
Now, foo should be available inside your template.
Another way to achieve this, in case you don't wish to mess with your serializers, would be to create a custom TemplateHTMLRenderer.
class TemplateHTMLRendererWithContext(TemplateHTMLRenderer):
def render(self, data, accepted_media_type=None, renderer_context=None):
# We can't really call super in this case, since we need to modify the inner working a bit
renderer_context = renderer_context or {}
view = renderer_context.pop('view')
request = renderer_context.pop('request')
response = renderer_context.pop('response')
view_kwargs = renderer_context.pop('kwargs')
view_args = renderer_context.pop('args')
if response.exception:
template = self.get_exception_template(response)
else:
template_names = self.get_template_names(response, view)
template = self.resolve_template(template_names)
context = self.resolve_context(data, request, response, render_context)
return template_render(template, context, request=request)
def resolve_context(self, data, request, response, render_context):
if response.exception:
data['status_code'] = response.status_code
data.update(render_context)
return data
To add data into the context, ViewSets provide a get_renderer_context method.
class LanguageViewSet(viewsets.ModelViewSet):
queryset = Language.objects.all()
serializer_class = LanguageSerializer
filter_backends = (filters.DjangoFilterBackend, )
filter_fields = ('name', 'active')
def get_renderer_context(self):
context = super().get_renderer_context()
context['foo'] = 'bar'
return context
{'foo': 'bar'} should now be available in your template.

Categories