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
Related
I need to make a search through some different models and than make an endpoint for it with DRF.
So my attempt to make it looks like this
class DataSearch(FlatMultipleModelAPIView):
def get_querylist(self):
q = self.request.query_params.get('query')
if q:
vector_data = SearchVector('research', 'data', 'research__name')
vector_var = SearchVector('label', )
query = SearchQuery(q, search_type='websearch')
querylist = [
{
'queryset': Data.objects.annotate(search=vector_data).filter(search=query),
'serializer_class': DataSerializer
},
{
'queryset': Variable.objects.annotate(search=vector_var).filter(search=query),
'serializer_class': VariableSerializer
}
]
else:
querylist = [Data.objects.none()]
return querylist
Here I'm using DjangoRestMultipleModels for making query goes by two models.
When I'm trying to run thin stuff I have an error
DataSearch cannot use the regular Rest Framework or Django paginators as is. Use one of the included paginators from drf_multiple_models.pagination or subclass a paginator to add the format_response` method.
Any ideas of what paginators doing here? Or how to make all this stuff work correctly?
Swagger documentation says you can do that:
https://swagger.io/docs/specification/grouping-operations-with-tags/
But unfortunately drf-yasg not implementing this feature:
https://github.com/axnsan12/drf-yasg/issues/454
It is said, that I can add custom generator class, but it is a very general answer. Now I see that drf_yasg.openapi.Swagger gets info block and I have thoughts, that this might be right place to put global tags section as an additional init argument, but it deeper, than customizing generator class and I have lack of knowledge of this module
Does anybody have solution to this particular problem, or at least maybe a link to some sort of tutorial, how to properly customize generator class?
Not sure if this is exactly what your are looking for, but I think it might help.
To set tags I use #swagger_auto_schema decorator, which can be applied in a few different ways depending mostly on the type of Views used on your project. Complete details can be found on docs here.
When using Views derived from APIView, you could do something like this:
class ClientView(APIView):
#swagger_auto_schema(tags=['my custom tag'])
def get(self, request, client_id=None):
pass
According to the docs, the limitation is that tags only takes a list of strs as value. So from here on, I believe there is no support for extra attributes over tags as stated at Swagger docs, here.
Anyway, if you only need to define a summary or a description to obtain something like the image below, you could define them using the decorator or a class-level docstring. Here is an example:
class ClientView(APIView):
'''
get:
Client List serialized as JSON.
This is a description from a class level docstring.
'''
def get(self, request, client_id=None):
pass
#swagger_auto_schema(
operation_description="POST description override using
decorator",
operation_summary="this is the summary from decorator",
# request_body is used to specify parameters
request_body=openapi.Schema(
type=openapi.TYPE_OBJECT,
required=['name'],
properties={
'name': openapi.Schema(type=openapi.TYPE_STRING),
},
),
tags=['my custom tag']
)
def post(self, request):
pass
Good luck!
Unfortunately, this is a current issue with drf-yasg.
To actually achieve this, you need to create your own schema generator class:
from drf_yasg.generators import OpenAPISchemaGenerator
class CustomOpenAPISchemaGenerator(OpenAPISchemaGenerator):
def get_schema(self, request=None, public=False):
"""Generate a :class:`.Swagger` object with custom tags"""
swagger = super().get_schema(request, public)
swagger.tags = [
{
"name": "api",
"description": "everything about your API"
},
{
"name": "users",
"description": "everything about your users"
},
]
return swagger
Make sure to also include it in your Schema View
from drf_yasg.views import get_schema_view
from drf_yasg import openapi
schema_view = get_schema_view(
openapi.Info(
title="My API",
default_version='v1',
),
generator_class=CustomOpenAPISchemaGenerator,
)
Hope this works for you!
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
I am quite new to django and recently I have a requirement of a JSON output, for which I use the following django code:
data = serializers.serialize("json", Mymodel.objects.all())
It works great, except that I get a output of:
[{"pk": 8970859016715811, "model": "myapp.mymodel", "fields": {"reviews": "3.5", "title": .....}}]
However, I would like the output to be simply either:
[{"reviews": "3.5", "title": .....}]
or,
[{"id": "8970859016715811", "reviews": "3.5", "title": .....}]
I was wondering if someone could point me to the right direction as to how to achieve this.
You can add 'fields' parameter to the serialize-function, like this:
data = serializers.serialize('xml', SomeModel.objects.all(), fields=('name','size'))
See: https://docs.djangoproject.com/en/dev/topics/serialization/
EDIT 1:
You can customize the serializer to get only the fields you specify.
From Override Django Object Serializer to get rid of specified model:
from django.core.serializers.python import Serializer
class MySerialiser(Serializer):
def end_object( self, obj ):
self._current['id'] = obj._get_pk_val()
self.objects.append( self._current )
# views.py
serializer = MySerialiser()
data = serializer.serialize(some_qs)
You'll need to write a custom Json serializer. Something like this should do the trick:
class FlatJsonSerializer(Serializer):
def get_dump_object(self, obj):
data = self._current
if not self.selected_fields or 'id' in self.selected_fields:
data['id'] = obj.id
return data
def end_object(self, obj):
if not self.first:
self.stream.write(', ')
json.dump(self.get_dump_object(obj), self.stream,
cls=DjangoJSONEncoder)
self._current = None
def start_serialization(self):
self.stream.write("[")
def end_serialization(self):
self.stream.write("]")
def getvalue(self):
return super(Serializer, self).getvalue()
The you can use it like this:
s = FlatJsonSerializer()
s.serialize(MyModel.objects.all())
Or you could register the serializer with django.core.serializers.register_serializer and then use the familiar serializers.serialize shortcut.
Take a look at the django implementation as a reference if you need further customization: https://github.com/django/django/blob/master/django/core/serializers/json.py#L21-62
I just came across this as I was having the same problem. I also solved this with a custom serializer, tried the "EDIT 1" method but it didn't work too well as it stripped away all the goodies that the django JSON encoder already did (decimal, date serialization), which you can rewrite it yourself but why bother. I think a much less intrusive way is to inherit the JSON serializer directly like this.
from django.core.serializers.json import Serializer
from django.utils.encoding import smart_text
class MyModelSerializer(Serializer):
def get_dump_object(self, obj):
self._current['id'] = smart_text(obj._get_pk_val(), strings_only=True)
return self._current
Sso the main culprit that writes the fields and model thing is at the parent level python serializer and this way, you also automatically get the fields filtering that's already built into django's JSON serializer. Call it like this
serializer = MyModelSerializer()
data = serializer.serialize(<queryset>, <optional>fields=('field1', 'field2'))
import json
_all_data = Reporter.objects. all()
json_data = json.dumps([{'name': reporter.full_name} for reporter in _all_data])
return HttpResponse(json_data, content_type='application/json')
Here Reporter is your Model
I am running into a problem trying to include tastypie resources in a larger json response in a regular django view. I would like to have the view return something like this (based on a queryset generated in the view, not from typical tastypie get params):
{
"success": bool,
"message": string,
"error": string,
"objects": [
{
"field_one": bar,
"field_two": foo
}
... more objects ...
]
}
where objects list is a list of serialized tastypie resources, and success, message and error are coming from somewhere else in the view.
Right now, I can't figure out how to avoid turing the serialized resource into strings before the larger dict gets build, so I have something like this currently:
{
"success": bool,
"message": string,
"error": string,
"objects": [
"{"field_one": bar, "field_two": foo..."}",
"{"field_one": baz, "field_two": foobar..."}",
...
]
}
The whole point of this is to keep the model json representations consistent, to minimize friction between using the tastypie api directly, and using the data returned in these views. I'm thinking the solution is to somehow use the full_dehydrate method on each resource without serializing them, and then adding them to the bigger dict, and then serializing that dict, but I'm not sure what serializer to use. Or, maybe there is a better way.
As is often the case, writing this up helped me find a temporary solution. Maybe someone will have some input on how to make this better.
I am using this to prepare a queryset for serialization:
def serialize_queryset(resource_class, queryset):
# hand me a queryset, i give you dehydrated resources
resource = resource_class()
dd = {}
# make meta
dd['meta'] = {}
dd['meta']['limit'] = 1000
dd['meta']['next'] = None
dd['meta']['offset'] = 0
dd['meta']['previous'] = None
dd['meta']['total_count'] = len(queryset)
# objects
dd['objects'] = []
for obj in queryset:
bundle = resource.build_bundle(obj=obj)
dehydrated_obj = resource.full_dehydrate(bundle)
dd['objects'].append(dehydrated_obj)
# return dict
return dd
And I use the Serializer from tastypie.serializer. and in using it in a sample view is goes something like:
from tastypie.serializer import Serializer
serializer = Serializer()
def my_view(request):
#... do some other view stuff ...
# prepare a queryset for serialization
queryset = MyModel.objects.filter(is_cool=True)
data = serialize_queryset(MyModel, queryset)
# add in custom stuff, determined earlier in the view somewhere
data['success'] = success
data['message'] = message
data['error'] = error
# serialize and return response
data = serializer.serialize(data)
return HttpResponse(data, mimetype='application/json')
This seems to work. Maybe you see something bad about this method, or a way to improve it?