Serialize queryset based on individual field values using Django Rest Framework - python

Goal
If an object has revealed=true it serializes into:
{
"id":1,
"info":"top secret info",
"revealed":true
}
If an object has revealed=false the info field is null:
{
"id":2,
"info":null,
"revealed":false
}
So for a queryset of objects:
[
{
"id":1,
"info":"top secret info 1",
"revealed":true
},
{
"id":2,
"info":null,
"revealed":false
},
{
"id":3,
"info":"top secret info 3",
"revealed":true
}
]
Is it possible to achieve this inside of a Django Rest Framework Model Serializer class?
class InfoSerializer(serializers.ModelSerializer):
class Meta:
model = Info
fields = ('id', 'info', 'revealed')
Background
The DRF docs discuss some advanced serializer usage, and this other post dives into an example. However it doesn't seem to cover this particular issue.
Ideas
A hacky solution would be to iterate over the serialized data afterwards, and remove the info field for every object that has revealed=false. However 1) it involves an extra loop and 2) would need to be implemented everywhere the data is serialized.

I suggest you make the info field appear on all records, but leave it null when revealed is false. If that's acceptable, you should be able to make it happen with a SerializerMethodField.
Alternatively, you could add a revealed_info attribute to the model class, and expose that through the serializer.
#property
def revealed_info(self):
return self.info if self.revealed else None

Related

Make Django REST API accept a list

I'm working on a functionality where I need to be able to post a list consisting of properties to the API. A property has a name, value and unit. Now I have two questions:
How exactly should my list look for the API to accept it as a correct list from the get go? Should I parse the list as Objects? Or is a plain list fine?
I am using the Django REST framework and I have made the API using this tutorial (works perfectly). But how do I make Django accept multiple objects/a list? I have read that it is as simple as adding many = True to where you instantiate the serializer, which I do here:
(for some reason the code won't format unless I put text here)
class PropertyViewSet(viewsets.ModelViewSet):
queryset = Property.objects.all()
serializer_class = PropertySerializer
So I tried doing serializer = PropertySerializer(queryset, many=True), which broke the API view. So I think I have to create a new serializer and view just for this (am I right)? But how do I make sure that my API knows which one to use at the right time?
If anyone could clarify this that would be great, thanks.
If you need to create the object, here is how I did it:
# Mixin that allows to create multiple objects from lists.
class CreateListModelMixin(object):
def get_serializer(self, *args, **kwargs):
""" if an array is passed, set serializer to many """
if isinstance(kwargs.get('data', {}), list):
kwargs['many'] = True
return super(CreateListModelMixin, self).get_serializer(*args, **kwargs)
And then in the view that you would like to use it in just do:
class PropertyCreateView(CreateListModelMixin, generics.CreateAPIView):
serializer_class = PropertySerializer
permission_classes = (IsAuthenticated, )
And that's already it (make sure to put in the mixin as an argument BEFORE the view; like I did it).
Now the body of your postrequest would look like this:
{
[
{
"name": "<some_name>"
"value": "<some_value>"
"unit": "<some_unit>"
},
{
"name": "<some_name>"
"value": "<some_value>"
"unit": "<some_unit>"
},
{
"name": "<some_name>"
"value": "<some_value>"
"unit": "<some_unit>"
},
...
]
}
The cool thing about it, this way you can also just post a single object like this:
{
"name": "<some_name>"
"value": "<some_value>"
"unit": "<some_unit>"
}
I hope this helps! :)
ModelViewSet class provides a create() method which only allows you to create one object at a time. see docs
if you want to POST a list of objects and insert each object to the database, you would have to create a custom view. for e.g.
from rest_framework.decorators import api_view
from django.shortcuts import redirect
from .models import MyModel
#api_view(['POST'])
def insert_list(request):
if request.method == 'POST':
for obj in request.POST['list']: #assuming you are posting a 'list' of objects
MyModel.objects.create(name=obj.name, value=obj.value, unit=obj.unit)
return redirect('url of MyObject List View')
to use your custom APIview alongside the viewset, just add your custom APIview to a different url not used by the viewset url e.g. if your viewset uses r'^myModel/$' then use r'^myModel/insert_list/$' for the createlist custom APIview

ElasticSearch - Filter nested objects without affecting the "parent" object

I have an ElasticSearch mapping for a blog object that contains a nested field for comments. This is so a user can add comments to the blog content shown above. The comments field has a published flag that determines whether or not the comment can be viewed by other users or just by the main user.
"blogs" :[
{
"id":1,
"content":"This is my super cool blog post",
"createTime":"2017-05-31",
"comments" : [
{"published":false, "comment":"You can see this!!","time":"2017-07-11"}
]
},
{
"id":2,
"content":"Hey Guys!",
"createTime":"2013-05-30",
"comments" : [
{"published":true, "comment":"I like this post!","time":"2016-07-01"},
{"published":false, "comment":"You should not be able to see this","time":"2017-10-31"}
]
},
{
"id":3,
"content":"This is a blog without any comments! You can still see me.",
"createTime":"2017-12-21",
"comments" : None
},
]
I want to be able to filter the comments so only True comments will be displayed for each blog object. I want to show every blog, not just those with true comments. All of the other solutions I have found online seem to affect my blog object. Is there a way to filter out the comment object without affecting the querying of all blogs?
So the above example would be returned after the query as such:
"blogs" :[
{
"id":1,
"content":"This is my super cool blog post",
"createTime":"2017-05-31",
"comments" : None # OR EMPTY LIST
},
{
"id":2,
"content":"Hey Guys!",
"createTime":"2013-05-30",
"comments" : [
{"published":true, "comment":"I like this post!","time":"2016-07-01"}
]
},
{
"id":3,
"content":"This is a blog without any comments! You can still see me.",
"createTime":"2017-12-21",
"comments" : None
},
]
The example still shows the blogs that have no comments or false comments.
Is this possible?
I have been using a nested query from this example: ElasticSearch - Get only matching nested objects with All Top level fields in search response
But this example affects the blogs themselves and will not return blogs that have only false comments or no comments.
Please help :) Thank you!
Ok so found out that there is apparently no way to do this using the elasticsearch queries. But I figured out a way to do this on the django/python side (which is what I needed). I'm not sure if anyone will need this information, but if you are in need of this and you are using Django/ES/REST this is what I did.
I followed the elasticsearch-dsl documentation (http://elasticsearch-dsl.readthedocs.io/en/latest/) to connect elasticsearch with my Django app. Then I used the rest_framework_elasticsearch package framework for creating the views.
To create a Mixin that queries only the True nested attributes in the list of elasticsearch items, create a mixin subclass of the rest_framework_elastic.es_mixins ListElasticMixin object. Then overwrite the es_representation definition as follows in our new mixin.
class MyListElasticMixin(ListElasticMixin):
#staticmethod
def es_representation(iterable):
items = ListElasticMixin.es_representation(iterable)
for item in items:
for key in item:
if key == 'comments' and item[key] is not None:
for comment in reversed(item[key]):
if not comment['published']:
item[key].remove(comment)
return items
Make sure that you use the reversed function in the for loop of comments or you will skip over some of your comments in the list.
This I use this new filter in my view.
class MyViewSet(MyListElasticMixin, viewsets.ViewSet):
# Your view code here
def get(self, request, *args, **kwargs):
return self.list(request, *args, **kwargs)
Doing it on the python side is definitely easier and worked.

Finding most popular tag Taggit Tastypie Django

Context
I have been having conflict. Currenly, I am creating a question answer application. Each question has a tag, and I want to show the most popular tags (e.g. tags that have the most questions associated with it).
Specifics
I am using django-taggit's TaggableManager to do so. Here is the model definition of the Question:
class Question(models.Model):
tags = TaggableManager()
date = models.DateTimeField(default=timezone.now)
text = models.TextField(null=True)
So now I have the several tags attached to questions.
Question
How do I make a tastypie resource to display the tags and sort them by which tag has the most questions associated with it?
My attempts
I have really only had one decently successful attempt at making this work. Here is the tastypie resource:
class TagResource_min(ModelResource):
def dehydrate(self, bundle):
bundle.data['total_questions'] len(Question.objects.filter(tags__slug=bundle.obj.slug))
return bundle
class Meta:
queryset=Tag.objects.all()
Returns a nice JSON with a variable called total_questions with the number of questions associated with it. Although, dehydrate is after the sort_by part of the request cycle specified here, so I cannot sort the resource by total_questions. Here is the JSON:
{
"meta": {
"limit": 20,
"next": null,
"offset": 0,
"previous": null,
"total_count": 13
},
"objects": [
{
"id": 1,
"name": "testing tag",
"resource_uri": "/full/tag/1/",
"slug": "testing-tag",
"total_questions": 2
},
...]
}
Another attempt I had went a different way trying to make the queryset be formed from the Question model:
queryset=Question.objects.filter(~Q(tags__slug=None)).values('tags', 'tags__slug').annotate(total_questions=Count(Tag))
The problem with this was that the filter function turns the result to dict type instead of models.Model type. The key difference was that models.Model has the attribute pk which is a type of identification, and the dict does not have that.
Any contribution is much appriciated! Thank you!

Django Swagger Integration

I saw swagger documentation of Flask and Django. In Flask I can design and document my API hand-written.(Include which fields are required, optional etc. under parameters sections).
Here's how we do in Flask
class Todo(Resource):
"Describing elephants"
#swagger.operation(
notes='some really good notes',
responseClass=ModelClass.__name__,
nickname='upload',
parameters=[
{
"name": "body",
"description": "blueprint object that needs to be added. YAML.",
"required": True,
"allowMultiple": False,
"dataType": ModelClass2.__name__,
"paramType": "body"
}
],
responseMessages=[
{
"code": 201,
"message": "Created. The URL of the created blueprint should be in the Location header"
},
{
"code": 405,
"message": "Invalid input"
}
]
)
I can chose which parameters to include, and which not. But how do I implement the same in Django? Django-Swagger Document in
not good at all. My main issue is how do I write my raw-json in Django.
In Django it automates it which does not allows me to customize my json. How do I implement the same kind of thing on Django?
Here is models.py file
class Controller(models.Model):
id = models.IntegerField(primary_key = True)
name = models.CharField(max_length = 255, unique = True)
ip = models.CharField(max_length = 255, unique = True)
installation_id = models.ForeignKey('Installation')
serializers.py
class ActionSerializer(serializers.ModelSerializer):
class Meta:
model = Controller
fields = ('installation',)
urls.py
from django.conf.urls import patterns, url
from rest_framework.urlpatterns import format_suffix_patterns
from modules.actions import views as views
urlpatterns = patterns('',
url(r'(?P<installation>[0-9]+)', views.ApiActions.as_view()),
)
views.py
class ApiActions(APIView):
"""
Returns controllers List
"""
model = Controller
serializer_class = ActionSerializer
def get(self, request, installation,format=None):
controllers = Controller.objects.get(installation_id = installation)
serializer = ActionSerializer(controllers)
return Response(serializer.data)
My questions are
1) If I need to add a field say xyz, which is not in my models how do I add it?
2) Quiet similar to 1st, If i need to add a field which accepts values b/w 3 provided values,ie a dropdown. how do I add it?
3) How I add an optional field? (since in case of PUT request, I might only update 1 field and rest leave it blank, which means optional field).
4) Also how do I add a field that accepts the json string, as this api does?
Thanks
I can do all of these things in Flask by hardcoding my api. But in Django, it automates from my models, which does not(as I believe) gives me the access to customize my api. In Flask, I just need to write my API with hands and then integrate with the Swagger. Does this same thing exist in Django?
Like I just need to add the following json in my Flask code and it will answer all my questions.
# Swagger json:
"models": {
"TodoItemWithArgs": {
"description": "A description...",
"id": "TodoItem",
"properties": {
"arg1": { # I can add any number of arguments I want as per my requirements.
"type": "string"
},
"arg2": {
"type": "string"
},
"arg3": {
"default": "123",
"type": "string"
}
},
"required": [
"arg1",
"arg2" # arg3 is not mentioned and hence 'opional'
]
},
Django-rest-framework does have a lot of useful utility classes such as serializers.ModelSerializer which you are using. However these are optional. You can create totally custom API endpoints.
I suggest that you follow the django rest tutorial here. Part one starts with a custom view like this
from django.forms import widgets
from rest_framework import serializers
from snippets.models import Snippet, LANGUAGE_CHOICES, STYLE_CHOICES
class SnippetSerializer(serializers.Serializer):
pk = serializers.Field() # Note: `Field` is an untyped read-only field.
title = serializers.CharField(required=False,
max_length=100)
code = serializers.CharField(widget=widgets.Textarea,
max_length=100000)
linenos = serializers.BooleanField(required=False)
language = serializers.ChoiceField(choices=LANGUAGE_CHOICES,
default='python')
style = serializers.ChoiceField(choices=STYLE_CHOICES,
default='friendly')
def restore_object(self, attrs, instance=None):
"""
Create or update a new snippet instance, given a dictionary
of deserialized field values.
Note that if we don't define this method, then deserializing
data will simply return a dictionary of items.
"""
if instance:
# Update existing instance
instance.title = attrs.get('title', instance.title)
instance.code = attrs.get('code', instance.code)
instance.linenos = attrs.get('linenos', instance.linenos)
instance.language = attrs.get('language', instance.language)
instance.style = attrs.get('style', instance.style)
return instance
# Create new instance
return Snippet(**attrs)
Note in particular that every API field is specified manually and populated by code here. So they do not have to correspond with model fields.
Your questions
1. Custom field xyz :
As I addressed above, just create a custom serialiser and add a line
class SnippetSerializer(serializers.Serializer):
xyz = serializers.CharField(required=False, max_length=100)
...
2. For options in a list, what you're looking for is a "choice" field.
See the Django documention on choice as Swagger is just the same.
3. How do I make a field optional?
Set the kwarg required=False - note that it's set above for field xyz in my example.
4. Best way to accept a JSON string
Two ways to do this.
Just accept a text string and use a JSON parser in the restore_object code
Define a serialiser that consumes / creates the JSON code and refer to it by name as described here

In django-tastypie, can choices be displayed in schema?

I am trying to figure out whether I can represent model field choices to clients consuming a tastypie API.
I have a django (1.4.1) application for which I am implementing a django-tastypie (0.9.11) API. I have a Model and ModelResource similar to the following:
class SomeModel(models.Model):
QUEUED, IN_PROCESS, COMPLETE = range(3)
STATUS_CHOICES = (
(QUEUED, 'Queued'),
(IN_PROCESS, 'In Process'),
(COMPLETE, 'Complete'),
)
name = models.CharFIeld(max_length=50)
status = models.IntegerField(choices=STATUS_CHOICES, default=QUEUED)
class SomeModelResource(ModelResource):
class Meta:
queryset = SomeModel.objects.all()
resource_name = 'some_model'
When I look at objects in the API, the name and status fields are displayed as follows:
{
...
"objects":[
{
"name": "Some name 1",
"status": 0
},
{
"name": "Some name 2",
"status": 2
}]
}
I know I can alter SomeModelResource with hydrate/dehydrate methods to display the string values for status as follows, which would have more value to clients:
{
...
"objects":[
{
"name": "Some name 1",
"status": "Queued"
},
{
"name": "Some name 2",
"status": "Complete"
}]
}
But how would the client know the available choices for the status field without knowing the inner workings of SomeModel?
Clients creating objects in the system may not provide a status as the default value of QUEUED is desirable. But clients that are editing objects need to know the available options for status to provide a valid option.
I would like for the choices to be listed in the schema description for SomeModelResource, so the client can introspect the available choices when creating/editing objects. But I am just not sure whether this is something available out of the box in tastypie, or if I should fork tastypie to introduce the capability.
Thanks for any feedback!
You can add the choices to the schema by overriding the method in your resource. If you would want to add the choices to any field (maybe to use with many resources), you could create the method as follows:
def build_schema(self):
base_schema = super(SomeModelResource, self).build_schema()
for f in self._meta.object_class._meta.fields:
if f.name in base_schema['fields'] and f.choices:
base_schema['fields'][f.name].update({
'choices': f.choices,
})
return base_schema
I haven't tested the above code but I hope you get the idea. Note that the object_class will be set only if you use the tastypie's ModelResource as it is being get from the provided queryset.
A simpler solution is to hack the choices information into your help_text blurb.
In our example we were able to do:
source = models.CharField(
help_text="the source of the document, one of: %s" % ', '.join(['%s (%s)' % (t[0], t[1]) for t in DOCUMENT_SOURCES]),
choices=DOCUMENT_SOURCES,
)
Easy peasy, automatically stays up to date, and is pretty much side-effect free.

Categories

Resources