How to do content negotiation for binary resource - django-rest-framework

I have an endpoint, X, which spits out json like a charm. The same resource can be generated into a binary variant. X's endpoint is made by a viewset, and the binary version of X has its own endpoint with the help of the action-decorator.
class XViewSet(ReadOnlyModelViewSet):
queryset = X.objects.all()
serializer_class = XSerializer
#action(detail=True, methods=['get'])
def binary(self, request, pk=None):
x = self.get_object()
binx = x.get_binary(FORMAT)
..
Obviously, binary will never spit out json. How do I get a hold of the negotiated FORMAT, and how do I tell django-rest-framework about the binary formats supported by binary?

You shouldn't return the binary data from the ViewSet but have a custom renderer converting it:
from rest_framework.renderers import BaseRenderer, JSONRenderer
class BinaryRenderer(BaseRenderer):
media_type = 'application/octet-stream'
format = 'bin'
render_style = 'binary'
charset = None
def render(self, data, media_type=None, renderer_context=None):
# Either use `data` or access the view via
# the `renderer_context`
view = renderer_context['view']
return view.get_object().get_binary()
class XViewSet(ReadOnlyModelViewSet):
queryset = X.objects.all()
serializer_class = XSerializer
renderer_classes = (JSONRenderer, BinaryRenderer)
Check out the documentation on how the renderer is determined.

Related

Why is my attacchment serializer not saving data?

I have messages and attachments to them (photos, documents, etc.). When I pass data to the serializer, I find that it is empty.
serializer:
class MessageAttachmentSerializer(serializers.ModelSerializer):
file = serializers.FileField()
class Meta:
model = MessageAttachment
fields = ("file", "message", "size")
view:
#api_view(["POST"])
def stuff_message(request):
data = request.data.copy()
data["sender"] = request.user.id
serializer = MessageSerializer(data=data, context={"request": request})
serializer.is_valid(True)
saved_msg = serializer.save()
if request.FILES:
data["message"] = saved_msg
attachment_serializer = MessageAttachmentSerializer(data=data, context={"request": request}, many=True)
attachment_serializer.is_valid(True)
attachment_serializer.save()
try:
bot.send_message(serializer.validated_data["receiver"].telegram_id, text=serializer.validated_data["text"])
except ApiTelegramException:
return Response("Chat undefined.", status=status.HTTP_404_NOT_FOUND)
return Response()
My message serializer works fine. When trying to output validated_data or data from MessageAttachment I get an empty list. Doesn't throw errors.
Corrected to:
#api_view(["POST"])
def stuff_message(request):
request.data._mutable = True
request.data["sender"] = request.user.id
serializer = MessageSerializer(data=request.data)
serializer.is_valid(True)
saved_msg = serializer.save()
request.data["message"] = saved_msg.id
attachments = []
for attachment in request.FILES.getlist("file"):
record = request.data
record["file"] = attachment
attachment_serializer = MessageAttachmentSerializer(data=record)
attachment_serializer.is_valid(True)
saved_attachment = attachment_serializer.save()
attachments.append(saved_attachment.file)
try:
bot.send_message(serializer.validated_data["receiver"].telegram_id, text=serializer.validated_data["text"])
for attachment in attachments:
bot.send_document(serializer.validated_data["receiver"].telegram_id, attachment.file)
except ApiTelegramException:
return Response("Chat undefined.", status=status.HTTP_404_NOT_FOUND)
return Response()
Just a guess but make sure you are returning the .data attribute on your serialized data as in:
output = MessageAttachmentSerializer(data=data).data
Reference: https://www.django-rest-framework.org/api-guide/serializers/
It was the "many" argument. It looks like cases with one file and several need to be handled differently when creating data through a serializer.

How to do 'or' when applying multiple values with the same lookup?

By referring to this article, I was able to implement the method of and search.
Django-filter with DRF - How to do 'and' when applying multiple values with the same lookup?
I want to know how to do or search for multiple keywords using the same field. How can I implement it?
Here is the code:
from rest_framework import viewsets
from django_filters import rest_framework as filters
from .serializers import BookInfoSerializer
from .models import BookInfo
class MultiValueCharFilter(filters.BaseCSVFilter, filters.CharFilter):
def filter(self, qs, value):
# value is either a list or an 'empty' value
values = value or []
for value in values:
qs = super(MultiValueCharFilter, self).filter(qs, value)
return qs
class BookInfoFilter(filters.FilterSet):
title = MultiValueCharFilter(lookup_expr='contains')
# title = MultiValueCharFilter(lookup_expr='contains', conjoined=False) -> get an error
class Meta:
model = BookInfo
fields = ['title']
class BookInfoAPIView(viewsets.ReadOnlyModelViewSet):
queryset = BookInfo.objects.all()
serializer_class = BookInfoSerializer
filter_class = BookInfoFilter
if I set conjoined=False like this title = MultiValueCharFilter(lookup_expr='contains', conjoined=False) get an error __init__() got an unexpected keyword argument 'conjoined'
Django 3.2.5
django-filter 2.4.0
djangorestframework 3.12.4
Python 3.8.5
You can try to modify the queryset returned from MultiValueCharFilter
and combine values with operator.
example:
import operator
from functools import reduce
class MultiValueCharFilter(BaseCSVFilter, CharFilter):
def filter(self, qs, value):
expr = reduce(
operator.or_,
(Q(**{f'{self.field_name}__{self.lookup_expr}': v}) for v in value)
)
return qs.filter(expr)
class BookInfoFilter(filters.FilterSet):
title = MultiValueCharFilter(lookup_expr='contains')
class Meta:
model = BookInfo
fields = ['title']
You can try to create a new Class called ListFilter like this:
from django_filters.filters import Filter
from django_filters.conf import settings as django_filters_settings
from django.db.models.constants import LOOKUP_SEP
class ListFilter(Filter):
"""
Custom Filter for filtering multiple values.
"""
def __init__(self, query_param: str, *args, **kwargs):
"""
Override default variables.
Args:
query_param (str): Query param in URL
"""
super().__init__(*args, **kwargs)
self.query_param = query_param
self.distinct = True
def filter(self, qs, value):
"""
Override filter method in Filter class.
"""
try:
request = self.parent.request
values = request.query_params.getlist(self.query_param)
# Remove empty value in array
values = list(filter(None, values))
except AttributeError:
values = []
# Filter queryset by using OR expression when lookup_expr is 'in'
# Else filter queryset by using AND expression
if values and self.lookup_expr == 'in':
return super().filter(qs, values)
for value in set(values):
predicate = self.get_filter_predicate(value)
qs = self.get_method(qs)(**predicate)
return qs.distinct() if self.distinct else qs
def get_filter_predicate(self, value):
"""
This function helps to get predicate for filtering
"""
name = self.field_name
if (
name
and self.lookup_expr != django_filters_settings.DEFAULT_LOOKUP_EXPR
):
name = LOOKUP_SEP.join([name, self.lookup_expr])
return {name: value}
My class above supports filter nested fields using both OR and AND expressions. E.g. about filter title field using OR expression:
class BookInfoFilter(filters.FilterSet):
title = ListFilter(query_param='title')
class Meta:
model = BookInfo
fields = ['title']

Updating object using super().update()

I have an object, whose attributes I would like to update. Right now, I am able to update a single attribute eg: name. But the object has several attributes which include name, contact_name, contact_email and contact_phone_number.The following is what I have at the moment.
In views.py
class MerchantViewSet(GetPrefixedIDMixin, viewsets.ModelViewSet):
"""POST support for /merchants/."""
print ("in MerchantViewSet")
queryset = models.Merchant.objects.all()
serializer_class = serializers.CreateMerchantSerializer
lookup_field = "id"
# lookup_value_regex = f"{models.Merchant.id_prefix}_[a-f0-9]{32}"
lookup_value_regex = ".*"
permission_classes = [permissions.MerchantPermission]
def get_queryset(self):
"""Filter the queryset based on the full merchant name or starting with a letter."""
queryset = models.Merchant.objects.all()
search_param = self.request.query_params.get("search", None)
if search_param:
if search_param.startswith("^"):
queryset = queryset.filter(name__istartswith=search_param[1:])
else:
queryset = queryset.filter(name__icontains=search_param)
return queryset
def update(self, request, *args, **kwargs):
request.data["user"] = request.user.id
print (self.get_object().name)
print (request.data)
merchant = self.get_object()
print (response)
print (merchant.name)
for merchants in models.Merchant.objects.all():
print (merchants.id)
if (merchants.id == merchant.id):
merchants.name = request.data["name"]
merchants.save()
I've tried using
response = super(MerchantViewSet, self).update(request, *args, **kwargs)
from what I read in the DRF documentations but using this just returns error 404 when I run my test. Do I simply have to do with I did with name in my code above, with the other attributes? Or is there a more streamlined way to do this?
This is my test:
class MerchantsViewSetTest(tests.BigETestCase): # noqa
#classmethod
def setUpClass(cls): # noqa
super(MerchantsViewSetTest, cls).setUpClass()
cls.application = tests.get_application()
tests.create_group("merchant")
cls.consumer_user = tests.create_consumer()
cls.admin = tests.create_administrator()
cls.merchant_geraldine = models.Merchant.objects.create(
name="Test Account 1",
contact_name="Geraldine Groves",
contact_email="geraldine#example.com",
contact_phone_number="+35310000000",
)
cls. merchant_barbara = models.Merchant.objects.create(
name="Account 2",
contact_name="Barbara",
contact_email="barbara#example.com",
contact_phone_number="+35310000432",
)
def test_edit_merchant(self): # noqa
url = reverse("bige_transactions:merchant-detail", kwargs={'id': self.merchant_geraldine.prefixed_id})
# payload
data = {"name": "Edited"}
# Put data
resp_data = self.put(url, data, user=self.admin, status_code=200)
# Check if data was updated
url = reverse("bige_transactions:merchant-list")
# Try without authenticated user
self.get(url, status_code=401)
# Try with authenticated admin user
resp_data = self.get(url, user=self.admin, status_code=200)
print (resp_data)

Get post data from CustomField in django rest framework

To make it short:
I have a serializer (django rest framework) that has two custom fields which do not directly correspond to a field of my model and also have a different name. The to_internal_value() method (probably) works, but I don't know how to access the post data of these fields.
And in case you need more details on my case:
I have a django model that looks like this:
class Requirement(models.Model):
job = models.ForeignKey('Job', related_name = 'requirements')
description = models.CharField(max_length = 140)
is_must_have = models.BooleanField() # otherwise is of type b
class Job(models.Model):
...
I want to serialize it in a manner that a job object will look like this:
{ "must_have": [must have requirements], "nice:to_have": [nice to have requirements] }
Therefore, I have custom fields in my serializer for jobs:
class JobSerializer(serializers.Serializer):
nice_to_have = NiceToHaveField(source = 'requirements', allow_null = True)
must_have = MustHaveField(source = 'requirements', allow_null = True)
The NiceToHaveField and the MustHaveField classes simpy override the to_representation() and the to_internal_value() methods, the to_representation also sorts the requirements after type.
But the validated_data in JobSerializer.create never contain these cutom fields. I know the to_internal_value gets called and does its work, but the results are not accessable.
What is the way to solve this?
I found a solution I don't like, there is probably a better way to do this. Anyways, the data is available in the view.request.data. So I used the perform_create hook like this:
def perform_create(self, serializer):
nice_to_have = None
must_have = None
if 'nice_to_have' in self.request.data and self.request.data['nice_to_have'] != None:
field = NiceToHaveField()
nice_to_have = field.to_internal_value(self.request.data['nice_to_have'])
if 'must_have' in self.request.data and self.request.data['must_have'] != None:
field = MustHaveField()
must_have = field.to_internal_value(self.request.data['must_have'])
serializer.save(owner = self.request.user, nice_to_have = nice_to_have, must_have = must_have)

django rest framework writable nested serializer returns empty nested data

I'm following this link (http://www.django-rest-framework.org/api-guide/relations/#writable-nested-serializers) to write nested serializer. But when I pop the 'vars' from validated_data in the create method of HostSerializer, I found it's empty.
I'm using django 1.9.2 and django restframework 3.3.2.
My model:
class Host(models.Model):
name = CharField(max_length=20, primary_key=True)
vm_cpu = IntegerField(default=2)
vm_mem = IntegerField(default=2048)
create_vm = BooleanField(default=True)
def __unicode__(self):
return('%s' % (self.name))
class Variable(models.Model):
name = CharField(max_length=10)
value = CharField(max_length=20)
host = models.ForeignKey(Host, related_name='vars')
def __unicode__(self):
return('%s=%s' % (self.name, self.value))
Serializer
class VariableSerializer(ModelSerializer):
class Meta:
model = Variable
class HostSerializer(ModelSerializer):
vars = VariableSerializer(many=True)
class Meta:
model = Host
def create(self, validated_data):
# i set a break point here and found vars_data is empty
vars_data = validated_data.pop('vars')
host = Host.objects.create(**validated_data)
for v in vars_data:
Variable.objects.create(host = host, **v)
return host
This is the problem I found vars_data is an empty list:
def create(self, validated_data):
# i set a break point here and found vars_data is empty
vars_data = validated_data.pop('vars')
Here's the rest of the code
admin.py
class VariableAdmin(admin.ModelAdmin):
list_display = ['name', 'value']
class HostAdmin(admin.ModelAdmin):
list_display = ['name']
admin.site.register(Variable, VariableAdmin)
admin.site.register(Host, HostAdmin)
urls.py
router = DefaultRouter()
router.register(r'variables', VariableViewSet, base_name='variables')
router.register(r'hosts', HostViewSet, base_name='hosts')
urlpatterns = [
url(r'^', include(router.urls)),
]
views.py
class VariableViewSet(ModelViewSet):
queryset = Variable.objects.all()
serializer_class = VariableSerializer
class HostViewSet(ModelViewSet):
queryset = Host.objects.all()
serializer_class = HostSerializer
My test program
post.py
import json
import requests
file = 'host.json'
url = 'http://localhost:8001/test_nest/hosts/'
with open(file, 'r') as f:
j = f.read()
data = json.loads(j)
r = requests.post(url, data = data)
print r.text
And here's the test data
host.json
{
"name": "host4",
"vars": [
{
"name": "var2-a",
"value": "a1"
},
{
"name": "var2-b",
"value": "a2"
}
],
"vm_cpu": 2,
"vm_mem": 2048,
"create_vm": true
}
I'm new to django. So I'm wondering if it's something simple and obvious. Did I use the wrong viewset? Did I post to the wrong URL? Or I setup the URL structure wrong?
I your serializers try using...
def update(self, instance,validated_data):
instance.vars_data = validated_data.get('vars',instance.vars)
instance.host = Host.objects.create(**validated_data)
for v in vars_data:
v,created=Variable.objects.create(host = host, **v)
instance.v.add(v)
return host
The following code works for me. Maybe you can try it:
models.py
class UserProfile(AbstractUser):
pass
class TobaccoCert(models.Model):
license = models.CharField(primary_key=True, max_length=15)
license_image = models.ImageField(upload_to="certs", blank=True, null=True, verbose_name='cert image')
views.py
class UserViewSet(mixins.CreateModelMixin, viewsets.GenericViewSet):
serializer_class = UserRegSerializer
...
def create(self, request, *args, **kwargs):
data = request.data
# here i make my nested data `certs` and update it to data
image_data = request.FILES.get('images')
_license = data.get('username', None)
certs = {'license': _license, 'license_image': image_data}
data['certs'] = certs
serializer = self.get_serializer(data=data)
serializer.is_valid(raise_exception=True)
user = self.perform_create(serializer) # save it
ret_dict = serializer.data
payload = jwt_payload_handler(user)
ret_dict["token"] = jwt_encode_handler(payload) # 获取token
ret_dict["name"] = user.name if user.name else user.username
headers = self.get_success_headers(serializer.data)
return Response(ret_dict, status=status.HTTP_201_CREATED, headers=headers)
serializer.py
class CertSerializer(serializers.ModelSerializer):
pass
class UserRegSerializer(serializers.ModelSerializer):
# [Serializer relations - Django REST framework](https://www.django-rest-framework.org/api-guide/relations/#writable-nested-serializers)
# this line is vars of your code
certs = CertSerializer(many=True,write_only=True)
...
password = serializers.CharField(
style={'input_type': 'password'}, help_text="passowrd", label="passowrd", write_only=True,
)
class Meta:
model = User
fields = ("username", "mobile", "password", 'province', 'city', 'dist', 'code', 'certs')
def create(self, validated_data):
"""
"""
# here we get certs
certs_data = self.initial_data.get('certs')
# just pop certs because it is useless for user create
certs_empty = validated_data.pop('certs')
user = super().create(validated_data=validated_data)
user.set_password(validated_data["password"])
user.save()
# use certs data to create cert
image_url = self.save_image(certs_data)
_license = validated_data.get('username', None)
TobaccoCert.objects.create(license=_license, license_image=image_url)
return user
In short, you need to add your nested parameters directly in the request parameters. In my example code, it is certs which is vars in your example and then use certs_data = self.initial_data.get('certs') get the parameters you passed in create method.
Another possible way is post your data before your requests:
In postman:
enter image description here
in request:
python - Django Rest Framework writable nested serializer with multiple nested objects - Stack Overflow
In addition, you can try to modify queryDict directly. You can refer to this link and here
Some useful links
django - Need help understanding many and source fields in a serializer - Stack Overflow
python - Django rest framework writeable nested serializer data missing from validated_data - Stack Overflow

Resources