POST new nested object - django

I'm trying to implement this scheme:
http://127.0.0.1:8000/api/get_work/
{
"type": "dns",
"source_alerts": [
{
"source": "alehop.com",
"alerts": [
{
"dns_server": "8.8.4.4",
"ip_addr": "134.211.190.5",
},
{
"dns_server": "7.7.2.2",
"ip_addr": "224.110.70.3",
}
]
}
]
}
And then be able to GET all alerts nested into a source:
** The source will be unique
http://127.0.0.1:8000/api/set_work/dns/alehop.com/
"alerts": [
{
"dns_server": "8.8.4.4",
"ip_addr": "134.211.190.5",
},
{
"dns_server": "7.7.2.2",
"ip_addr": "224.110.70.3",
}
And POST a single alert into that source:
{
"dns_server": "7.7.2.2",
"ip_addr": "224.110.70.3",
}
My question is: is possible to implement a list/create viewset of a route with parameters?
router.register(r'set_work/(?P<type>.+)/(?P<source>.+)', views.SetWorkViewSet)
In that case, how can I use that parameters in the viewset in order to filter the queryset?
Thank you in advance. Any other approaches will be very welcome, I'm very new to python/django.

Sure you can!
Django REST Framework (DRF) for model viewsets (which I assume you are using) implements standard get_object and get_queryset methods. Since you added additional parameters to the url regex, you can reference them via self.kwargs inside the viewset:
def get_queryset(self):
qs = super(...).get_queryset()
return qs.filter(type=self.kwargs['type'], source=self.kwargs['source'])
This will do the filtering which will make list work. As for the create, you will probably need to adjust the serializer in order to use the values from the url kwargs. There are a couple of ways to do that:
add the url kwargs to the data being passed to the serializer when instantiating it
class MyViewSet(ViewSet):
def get_serializer(self, *args, **kwargs):
# add the url kwargs to request data so that serializer will see it
self.request.data.update(self.kwargs)
return super(...).get_serializer(*args, **kwargs)
This technically should work however feels a bit hackish to me so I would not recommend this approach.
adjust the validation logic within the serializer to add the url parameters
class MySerializer(ModelSerializer):
class Meta(object):
model = MyModel
def to_internal_value(self, data):
if hasattr(self, 'internal_data') and 'view' in self.context:
data.update(self.context['view'].kwargs)
return super(...).to_internal_value(data)
I personally feel this approach is cleaner even though it leaks a little of information about the viewset to the serializer.
Please note the code is not tested so you will need to do some testing but it should get you started.

Related

How does one document the returned data for a POST endpoint?

If I have a simple endpoint like so using flask
import random
class MyResource(Resource):
def post(self):
return [i for i in range (random.randrange(100))]
How do I get swagger documentation like so using flask-restplus?
I am looking for some data structure like so
[
type=integer, description='coin flip',
....
]
I can get it to work for the Payload but not for the return/response
From https://flask-restplus.readthedocs.io/en/stable/swagger.html
You can optionally specify a response model as the third argument
model = api.model('Model', {
'name': fields.String,
})
#api.route('/my-resource/')
class MyResource(Resource):
#api.response(200, 'Success', model)
def get(self):
pass

Display possible values (choices) of SlugRelatedField in drf-yasg OpenAPI and Swagger views

I have several models that I use as enums (basically, the model is just a name and a slug), like currencies and countries etc, and I'm trying to show the available choices in drf-yasg without success.
My last attempt was adding this to the serializer's Meta class:
swagger_schema_fields = {
'currency': {'enum': list(Currency.objects.values_list('slug', flat=True))}
}
But of course it failed miserably - not only it didn't show the enum values, it also broke the serializer (because it used the strings instead of the actual model).
Is there any way of doing this?
Eventually I added a new field class that does exactly what I needed. I can't promise it's the most efficient way of solving this (retrieving the swagger page seems to take longer) but it does the job:
# choices_slug_field.py
from drf_yasg.inspectors import RelatedFieldInspector
from rest_framework.metadata import SimpleMetadata
from rest_framework.relations import SlugRelatedField
from rest_framework.serializers import ManyRelatedField, RelatedField
class ShowChoicesMetadata(SimpleMetadata):
def get_field_info(self, field):
field_info = super().get_field_info(field)
if (not field_info.get('read_only') and
isinstance(field, (ManyRelatedField, RelatedField)) and
hasattr(field, 'choices') and
getattr(field, 'show_choices', False)):
field_info['choices'] = [
{
'value': choice_value,
'display_name': str(choice_name)
}
for choice_value, choice_name in field.choices.items()
]
return field_info
class ShowChoicesMixin:
show_choices = True
class ChoicesSlugRelatedField(ShowChoicesMixin, SlugRelatedField):
pass
class ShowChoicesFieldInspector(RelatedFieldInspector):
def field_to_swagger_object(self, field, swagger_object_type, use_references, **kwargs):
dataobj = super().field_to_swagger_object(field, swagger_object_type, use_references, **kwargs)
if (isinstance(field, ChoicesSlugRelatedField) and hasattr(field, 'choices')
and getattr(field, 'show_choices', False) and 'enum' not in dataobj):
dataobj['enum'] = [k for k, v in field.choices.items()]
return dataobj

Bulk insertion in django rest framework

When I am trying to create multiple instances in a single request it shows error if one instance is not correct in the batch. But how can I write my create method to insert correct instances in the batch. That means only correct instances will insert in the db and also show the errors message for the wrong instances.
[
{
"name": "oil",
"unit_price": 200
},
{
"name": "meat",
"unit_type": "raw",
"unit_price": 1000
}
"name": "salt",
"unit_type": "raw",
"unit_price": -100
}
]
I want to insert first two instances will be insert in the db and for last one it will throws an error like this.
"errors":
[
{
"unit_price": [
"Enter positive number."
]
}
]
Here is my serializer
class ProductSerializer(serializers.ModelSerializer):
def validate_unit_price(self, value):
if (value) > 0:
return value
raise serializers.ValidationError("Enter positive number.")
class Meta:
model = Product
fields = [ 'name', 'unit_type', 'unit_price']
Also my views function is
#api_view(['POST'])
def store(request):
serializer = ProductSerializer(data = request.data,many=True)
if serializer.is_valid():
serializer.save()
return Response({'response_code': '500', 'response': status.HTTP_500_INTERNAL_SERVER_ERROR, 'message': 'Something went wrong', 'data': request.data, 'errors': serializer.errors})
Since you're using one ProductSerializer instance, you won't be able to save if is_valid returns False. If you want to create all valid datasets and return the ones that errored, you might want to consider creating a serializer instance per entry, and keeping track of the data returned. You can build out a list of response values for the user that way, so partial failure is allowed.
My suggestion will not make this a "bulk" operation, but it technically isn't with many=True. many=True just wraps your serializer with a ListSerializer, which just calls create using the wrapped serializer per attribute. So you're performing a save() per instance of data as is.

How to parse a string list query param in DRF serializers?

I'm building a REST API to handle user requests in the following form:
localhost:8000/search/?categories=<category1>,<category2>&parts=<id1>,<id2>
Where , is supposed to be the delimiter for the parser.
My view processes the request and passes the query params to the serializer, but I just cannot get the raw strings parsed to a list of strings.
My attempt so far:
class StringListField(serializers.ListField):
child = serializers.CharField()
class LookupPartsSerializer(serializers.Serializer):
categories = StringListField()
parts = StringListField()
class LookupParts(APIView):
def get(self, request, format=None):
serializer = LookupPartsSerializer(data=request.query_params)
serializer.is_valid()
return Response(serializer.validated_data)
My desired output is like:
{
"categories": [
"<category1>",
"<category2>"
],
"parts": [
"<id1>",
"<id2>"
]
}
But now I'm getting:
{
"categories": [
"<category1>,<category2>"
],
"parts": [
"<id1>,<id2>"
]
}
So basically I'm looking for an option to pass a delimiter argument to the StringListField or add some custom parsing method.
NOTE:
I'm aware, that if I change the query pattern from ?categories=<category1>,<category2>... to ?categories=<category1>&categories=<category2>... then I'd get the desired results, but I'd like to stick to my original approach.
I think the best way is to implement a customer to_representation():
def to_representation(self, instance):
data = super(StringListField, self).to_representation(instance)
data['parts'] = data['parts'].split(',') if isinstance(data['parts'], str) else data['parts']
data['categories'] = data['categories'].split(',') if isinstance(data['categories'], str) else data['categories']
return data
Hope it helps!

Right way to pass Django objects to ExtJS

Django has a built in serialization functionality which allows you to serialize any query result set into JSON:
json_serializer = serializers.get_serializer("json")()
json_serializer.serialize(queryset, ensure_ascii=False)
This produces output such as:
[
{
"pk": 1,
"model": "app_name.model_name",
"fields": {
"field_name": "value",
(...)
}
}
]
If you want to pass this JSON object over to an ExtJS driven application you run into a problem, because ExtJS expects its JSON to be formatted differently:
{
"total": 100,
"success": true,
"objects": [
{
"id": 1,
"field_name": "value",
(...)
}
]
}
There are 2 main differences: the additional meta-data (success, total) and the IDs of the objects which are provided together with other fields in Ext, but not in Django.
There are many possible ways to make one or the other format conform with the second, but what do you consider to be the best way to make this work? Is it a special serializer on the Django side, or maybe a special reader on the ExtJS side...
What do you think is the best way to solve this problem?
Better idea: use a custom serialiser.
settings.py
SERIALIZATION_MODULES = {
'extjson': 'extjs.serialiser.json'
}
extjs\serialiser\json.py
from django.core.serialisers.json import Serialiser
class ExtJSONSerialiser(Serializer)
"""
Serializes a QuerySet to basic Python objects.
"""
def end_object(self, obj):
self._current.update({"id": smart_unicode(obj._get_pk_val(), strings_only=True),})
self.objects.append(self._current)
self._current = None
def getvalue(self):
return {
'total': len(self.objects),
'success': True,
'objects': self.objects,
}
yourcode.py
json_serializer = serializers.get_serializer("extjson")()
json_serializer.serialize(queryset, ensure_ascii=False)
I found a solution which is able to serialize objects, which contain QuerySet as attributes. It's comes from traddicts.org blog, but you can now find it on GitHub:
https://github.com/datamafia/django-query-json-serializer/blob/master/JSONSerializer.py
I further modified the code to work recursively, so in effect I can do the following:
users = User.objects.all()
response = {}
response['success'] = True
response['users'] = users
json_serialize(response)
json_serialize(response, serialize_related=True)
json_serialize(response, serialize_related=True, ignored=['users.groups'])
json_serialize(response, serialize_related=True, ignored=['users.groups.permissions'])
I like your answer Thomas, but I needed something which would be even more flexible.