Django REST Framework how to add context to a ViewSet - python

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.

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.

Creating a new viewset with multiple filters for the viewset get method

Hi i have a viewset that I am creating. I want to over ride the the get functino and get all the records that have the filtered parameter which is passed into the get view. I also want to be able to do the rest of the crud functionality - GET POST PUT DELETE - and use the paramater that is passed through the url as a parameter for the POST and UPDATE.
Right now, when i pass in the parameter, rather than filter the data that is returned, it is giving me no details found which is not what i want. i want it to be used as a secondary filter for all the records that i get back from the database.
Here is the code:
viewset
class PreferenceUserViewSet(viewsets.ModelViewSet):
queryset = Preference.objects.all().filter(user_id=1)
serializer_class = PreferenceSerializer
class PreferenceNamespaceViewSet(viewsets.ModelViewSet):
queryset = Preference.objects.all().filter(user_id=1)
serializer_class = PreferenceSerializer
def get_permissions(self):
if self.action == 'create' or self.action == 'destroy':
permission_classes = [IsAuthenticated]
else:
permission_classes = [IsAdminUser]
return [permission() for permission in permission_classes]
#permission_classes((IsAuthenticated))
def list(self, request, namespace=None):
# switch user_id value with logged in users id
queryset = Preference.objects.all().filter(user_id=1, namespace=namespace)
serializer = PreferenceSerializer(queryset, many=True)
return Response(serializer.data)
urls:
path('preferences/<str:namespace>/', PreferenceNamespaceViewSet.as_view({
'get':'list'
})),
path('users/<int:pk>/stacks/', person_stack, name='user-stacks'),
I want to use the namepsace as a secondary filter to all the data that is returned in the GET. I also want to use it as a peice of data that I can enter when creating a new preference.
** I also want to do the samething with a third potential parameter like so... **
potential third parameters:
urlpatterns = [
path('preferences/<str:namespace>/<str:path>', PreferencePathViewSet.as_view({
'get':'list'
})),
path('preferences/<str:namespace>/', PreferenceNamespaceViewSet.as_view({
'get':'list'
})),
path('users/<int:pk>/stacks/', person_stack, name='user-stacks'),
]
I think you shouldn't add namespace as url parameter, instead you can use URL querystring to fetch namespace information(as well as other parameters). for example:
# URL
path('preferences/', PreferencePathViewSet.as_view({
'get':'list'
})
),
# view
class PreferenceNamespaceViewSet(viewsets.ModelViewSet):
queryset = Preference.objects.all().filter(user_id=1)
serializer_class = PreferenceSerializer
def get_permissions(self):
if self.action == 'create' or self.action == 'destroy':
permission_classes = [IsAuthenticated]
else:
permission_classes = [IsAdminUser]
return [permission() for permission in permission_classes]
#permission_classes((IsAuthenticated))
def list(self, request):
queryset = Preference.objects.all()
namespace = request.GET.get('namespace', None)
if namespace:
queryset = queryset.filter(user_id=1, namespace=namespace)
serializer = PreferenceSerializer(queryset, many=True)
return Response(serializer.data)

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 specify which fields when viewing a list

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

Add user specific fields to Django REST Framework serializer

I want to add a field to a serializer that contains information specific to the user making the current request (I don't want to create a separate endpoint for this). Here is the way I did it:
The viewset:
class ArticleViewSet(viewsets.ModelViewSet):
queryset = Article.objects.all()
serializer_class = ArticleSerializer
filter_class = ArticleFilterSet
def prefetch_likes(self, ids):
self.current_user_likes = dict([(like.article_id, like.pk) for like in Like.objects.filter(user=self.request.user, article_id__in=ids)])
def get_object(self, queryset=None):
article = super(ArticleViewSet, self).get_object(queryset)
self.prefetch_likes([article.pk])
return article
def paginate_queryset(self, queryset, page_size=None):
page = super(ArticleViewSet, self).paginate_queryset(queryset, page_size)
if page is None:
return None
ids = [article.pk for article in page.object_list]
self.prefetch_likes(ids)
return page
The serializer:
class ArticleSerializer(serializers.ModelSerializer):
class Meta:
model = Article
def to_native(self, obj):
ret = super(ArticleSerializer, self).to_native(obj)
if obj:
view = self.context['view']
ret['has_liked'] = False
if hasattr(view, 'current_user_liked'):
ret['has_liked'] = obj.pk in view.current_user_liked
return ret
Is there a better place to inject the prefetching of liked articles, or a nicer way to do this in general?
you can do it with SerializerMethodField
Example :
class PostSerializer(serializers.ModelSerializer):
fav = serializers.SerializerMethodField('likedByUser')
def likedByUser(self, obj):
request = self.context.get('request', None)
if request is not None:
try:
liked=Favorite.objects.filter(user=request.user, post=obj.id).count()
return liked == 1
except Favorite.DoesNotExist:
return False
return "error"
class Meta:
model = Post
then you should call serializer from view like this:
class PostView(APIVIEW):
def get(self,request):
serializers = PostSerializer(PostObjects,context={'request':request})
I'd be inclined to try and put as much of this as possible on the Like model object and then bung the rest in a custom serializer field.
In serializer fields you can access the request via the context parameter that they inherit from their parent serializer.
So you might do something like this:
class LikedByUserField(Field):
def to_native(self, article):
request = self.context.get('request', None)
return Like.user_likes_article(request.user, article)
The user_likes_article class method could then encapsulate your prefetching (and caching) logic.
I hope that helps.
According to the Django Documentation - SerializerMethodField, I had to change the code of rapid2share slightly.
class ResourceSerializer(serializers.ModelSerializer):
liked_by_user = serializers.SerializerMethodField()
def get_liked_by_user(self, obj : Resource):
request = self.context.get('request')
return request is not None and obj.likes.filter(user=request.user).exists()

Categories