Renaming DRF serializer fields - python

I'm using DRF serializers to validate incoming data that I retrieve from a JSON API. I'm trying to rename some awkwardly named fields from the response, making it easier to use the serializer.data further on in my code.
Data received from the API looks like this:
{"FunnyNamedField": true, "AnotherWeirdField": false}
And handling code:
resp = requests.get([...])
resp.raise_for_status()
ser = MyFunnyDataSerializer(data=resp.json())
if ser.is_valid():
do_domething_with(ser.data)
I would like the serializer to translate the incoming field names to something more consise. ser.data could look like: {'funny': True, 'weird': False}.
What I tried but doesn't work as I hoped:
class MyFunnyDataSerializer(serializers.Serializer):
funny = serializers.Booleanfield(source='FunnyNamedField')
Is there any way to achieve this without reverting to a SerializerMethodField?

You can override BaseSerializer to achieve this:
from rest_framework import serializers
class CustomSerializer(serializers.BaseSerializer):
def to_representation(self, instance):
return {
<datas>
}
You can do some specific modifications on instance serialization with custom methods.
Another solution could be to write your own validator for one field: Field Validator Method.
So in this documentation example you could modify value before return it.
from rest_framework import serializers
class BlogPostSerializer(serializers.Serializer):
title = serializers.CharField(max_length=100)
content = serializers.CharField()
def validate_title(self, value):
"""
Check that the blog post is about Django.
"""
if 'django' not in value.lower():
raise serializers.ValidationError("Blog post is not about Django")
if value == "something":
value = "something_else"
return value
Hope it helps.

Related

Querying from sql with dynamic model name and fields in Django

I'm new in Django and struggling with some dynamic solutions. I am trying to make an application that user can create table from UI and import data into this table from flat file and then user can be able to browse that data by clicking url(for example, if user create a table for employee, then user should be able to have a url like this for employee : localhost/employee). I will work on import part later but for now, i am trying to find a solution to browse data. And below query is almost do this. But i am not able to use alias instead of column names. Is there any way to build something like that dynamic?
def employee(request):
entries = Employee.objects.annotate(First Name=F('FirstName')). only('FirstName','Email')
print(entries)
return render_to_response('employee.html',{'employees': serializers.serialize("json",entries, fields=('First Name','Email'))})
Above query give me below result. I have only email information here. So there is no First Name because annotate is not working.
[{"model": "client.employee", "pk": 1, "fields": {"Email":"employe1#gmail.com"}}]
As per this ticket, django does not allow serializing non-model fields. So, you need to override the serailizer, so you can try like this:
Write the custom serializer(which will override original json serializer) in a separate file i.e custom_serializer.py
# custom_serializer.py
from django.core.serializers.json import Serializer
class CustomSerializer(Serializer):
def end_object(self, obj):
for field in self.selected_fields:
if field == 'pk' or field=='id': # primary key field
continue
elif field in self._current.keys():
continue
else:
try:
self._current[field] = getattr(obj, field)
except AttributeError:
pass
super(CustomSerializer, self).end_object(obj)
inside the views, import that CustomSerializer and use it. for example
# inside views.py
from .custom_serializer import CustomSerializer
def employee(request):
...
json_data = CustomSerializer().serialize(
entries
fields = (
'First_Name',
'email'
)

How to use Django REST Serializers?

So, after reading the Django REST Framework document, and a bunch of tutorials, I am still having trouble understanding how to use the Django serializers to convert incoming POST (JSON) data into a Python object (sorry, I'm new).
Given that I am posting a JSON string to, say, api/foo/bar, how do I write its serializer?
Example JSON:
{ 'name': 'Mr. Foo', address:'Bar Street' }
My controller, Foo contains a bar method as follows:
#detail_route(
methods=['post']
)
def bar(self, request, uuid=None):
serializer = MySampleSerializer(data=request.DATA)
something.clone(serializer.object)
return Response(status=status.HTTP_201_CREATED)
Can somebody explain to me what should my serializer look like? And how do I access the serialized data from the serializer?
As you do not want to use a model, you have to create the serializer from scratch. Something like this should maybe work:
class MySerializer(serializers.Serializer):
name = serializers.CharField(max_length = 100)
adress = serializers.CharField(max_length = 100)
And then you could use it in a request like this:
def bar(self, request, uuid=None):
data = JSONParser().parse(request)
serializer = MySerializer(data = data)
return Response(status=status.HTTP_201_CREATED)
Note however, as you have not created an Django model, you will not be able to save the serialized data (and thus nothing will be saved in the database)
Basically, you pass in the JSON data to the serializer, and then access the data field which will return an ordered dictionary.
def bar(self, request, uuid=None):
serializer = MySampleSerializer(data=request.data)
if serializer.is_valid(raise_exception=True):
my_object = serializer.data # Grab the dict of values
To define a serializer:
class MySampleSerializer(serializers.Serializer):
name = serializers.CharField(max_length=30)
address = serializers.CharField(max_length=30)
You do not have to use the ModelSerializer:
from rest_framework import serializers
class CommentSerializer(serializers.Serializer):
email = serializers.EmailField()
content = serializers.CharField(max_length=200)
created = serializers.DateTimeField()
and access:
serializer = CommentSerializer(data=data)
serializer.is_valid()
# True
serializer.validated_data
by the way, all the above is from the DRF website

django-rest-framework HyperlinkedIdentityField with multiple lookup args

I have the following URL in my urlpatterns:
url(r'^user/(?P<user_pk>[0-9]+)/device/(?P<uid>[0-9a-fA-F\-]+)$', views.UserDeviceDetailView.as_view(), name='user-device-detail'),
notice it has two fields: user_pk, and uid. The URL would look something like: https://example.com/user/410/device/c7bda191-f485-4531-a2a7-37e18c2a252c.
In the detail view for this model, I'm trying to populate a url field that will contain the link back to the model.
In the serializer, I have:
url = serializers.HyperlinkedIdentityField(view_name="user-device-detail", lookup_field='uid', read_only=True)
however, it's failing I think because the URL has two fields:
django.core.exceptions.ImproperlyConfigured: Could not resolve URL for hyperlinked relationship using view name "user-device-detail". You may have failed to include the related model in your API, or incorrectly configured the lookup_field attribute on this field.
How do you use a HyperlinkedIdentityField (or any of the Hyperlink*Field) when the URL has two or more URL template items? (lookup fields)?
I'm not sure if you've solved this problem yet, but this may be useful for anyone else who has this issue. There isn't much you can do apart from overriding HyperlinkedIdentityField and creating a custom serializer field yourself. An example of this issue is in the github link below (along with some source code to get around it):
https://github.com/tomchristie/django-rest-framework/issues/1024
The code that is specified there is this:
from rest_framework.relations import HyperlinkedIdentityField
from rest_framework.reverse import reverse
class ParameterisedHyperlinkedIdentityField(HyperlinkedIdentityField):
"""
Represents the instance, or a property on the instance, using hyperlinking.
lookup_fields is a tuple of tuples of the form:
('model_field', 'url_parameter')
"""
lookup_fields = (('pk', 'pk'),)
def __init__(self, *args, **kwargs):
self.lookup_fields = kwargs.pop('lookup_fields', self.lookup_fields)
super(ParameterisedHyperlinkedIdentityField, self).__init__(*args, **kwargs)
def get_url(self, obj, view_name, request, format):
"""
Given an object, return the URL that hyperlinks to the object.
May raise a `NoReverseMatch` if the `view_name` and `lookup_field`
attributes are not configured to correctly match the URL conf.
"""
kwargs = {}
for model_field, url_param in self.lookup_fields:
attr = obj
for field in model_field.split('.'):
attr = getattr(attr,field)
kwargs[url_param] = attr
return reverse(view_name, kwargs=kwargs, request=request, format=format)
This should work, in your case you would call it like this:
url = ParameterisedHyperlinkedIdentityField(view_name="user-device-detail", lookup_fields=(('<model_field_1>', 'user_pk'), ('<model_field_2>', 'uid')), read_only=True)
Where <model_field_1> and <model_field_2> are the model fields, probably 'id' and 'uid' in your case.
Note the above issue was reported 2 years ago, I have no idea if they've included something like that in newer versions of DRF (I haven't found any) but the above code works for me.

Getting non_field_errors using JSONField()

I'm trying to make a PATCH request using to the Django Rest Framework but get the following error:
{"image_data": [{"non_field_errors": ["Invalid data"]}]
I understand that JSONField() could give some issues so I have taken care of that by adding to_native and from_native, But, I'm still running into this issue. I don't think JSONField() is the problem here at all, but still worth mentioning.
I believe I'm doing something fundamentally wrong in how I am trying to update the related field.
Code below...
Models:
class Photo(models.Model):
user = models.ForeignKey(AppUser, help_text="Item belongs to.")
image_data = models.ForeignKey("PhotoData", null=True, blank=True)
class PhotoData(models.Model):
thisdata = JSONField()
Serializers:
class ExternalJSONField(serializers.WritableField):
def to_native(self, obj):
return json.dumps(obj)
def from_native(self, value):
try:
val = json.loads(value)
except TypeError:
raise serializers.ValidationError(
"Could not load json <{}>".format(value)
)
return val
class PhotoDataSerializer(serializers.ModelSerializer):
thisdata = ExternalJSONField()
class Meta:
model = PhotoData
fields = ("id", "thisdata")
class PhotoSerializer(serializers.ModelSerializer):
image_data = PhotoDataSerializer()
class Meta:
model = Photo
fields = ("id","user", "image_data")
PATCH:
> payload = {"image_data": {"thisdata": "{}"}}
> requests.patch("/photo/123/",payload )
I have also tried:
> payload = {"image_data": [{"thisdata": "{}"}]}
> requests.patch("/photo/123/",payload )
But again giving the same error:
[{"non_field_errors": ["Invalid data"]}]
The original idea of Django Rest Framework's serialization of relations is to not change values of related fields.
It means that your payload should contain a pk of PhotoData object, not a dataset for it.
It's like in models you can't assign a dict to a foreign key field.
Good (works only with serializers.PrimaryKeyRelatedField which contains problems itself):
payload = {"image_data": 2}
Bad (not works in DRF by default):
payload = {"image_data": {'thisdata': '{}'}}
Actually the data model that you provided doesn't need PhotoData at all (you can move thisdata field to Photo), but let's assume you have a special case even when Zen of Python says Special cases aren't special enough to break the rules..
So, here is some possible ways:
Using fields serializers (your original way)
What you want to do now is possible but is very ugly solution.
You may create a PhotoDataField (works for me, but not ready to use code, only for demonstration)
class PhotoDataField(serializers.PrimaryKeyRelatedField):
def field_to_native(self, *args):
"""
Use field_to_native from RelatedField for correct `to_native` result
"""
return super(serializers.RelatedField, self).field_to_native(*args)
# Prepare value to output
def to_native(self, obj):
if isinstance(obj, PhotoData):
return obj.thisdata
return super(PhotoDataField, self).to_native(obj)
# Handle input value
def field_from_native(self, data, files, field_name, into):
try:
int(data['image_data'])
except ValueError:
# Looks like we have a data for `thisdata` field here.
# So let's do write this to PhotoData model right now.
# Why? Because you can't do anything with `image_data` in further.
if not self.root.object.image_data:
# Create a new `PhotoData` instance and use it.
self.root.object.image_data = PhotoData.objects.create()
self.root.object.image_data.thisdata = data['image_data']
self.root.object.image_data.save()
return data['image_data']
except KeyError:
pass
# So native behaviour works (e.g. via web GUI)
return super(PhotoDataField, self).field_from_native(data, files, field_name, into)
and use it in PhotoSerializer
class PhotoSerializer(serializers.ModelSerializer):
image_data = PhotoDataField(read_only=False, source='image_data')
class Meta:
model = Photo
fields = ("id", "user", "image_data")
so the request will works well
payload = {"image_data": '{}'}
resp = requests.patch(request.build_absolute_uri("/api/photo/1/"), payload)
and the "good" request also
photodata = PhotoData.objects.get(pk=1)
payload = {"image_data": photodata.pk}
resp = requests.patch(request.build_absolute_uri("/api/photo/1/"), payload)
and in result you will see in GET request "image_data": <photodata's thisdata value>,.
But, even if you will fix the validation problems with this approach it still be a huge pain in ass as you can see from my code (this is only thing DRF can offers you when you want to "break a normal workflow", Tastypie offers more).
Normalize your code and use #action (recommended)
class PhotoDataSerializer(serializers.ModelSerializer):
class Meta:
model = PhotoData
fields = ("id", "thisdata")
class PhotoSerializer(serializers.ModelSerializer):
image_data = PhotoDataSerializer() # or serializers.RelatedField
class Meta:
model = Photo
fields = ("id", "user", "image_data", "test")
and now define a specific method in your api's view that you will be able to use to set the data for any photo
from rest_framework import viewsets, routers, generics
from rest_framework.decorators import action
from rest_framework.response import Response
from rest_framework import status
# ViewSets define the view behavior.
class PhotoViewSet(viewsets.ModelViewSet):
model = Photo
serializer_class = PhotoSerializer
#action(methods=['PATCH'])
def set_photodata(self, request, pk=None):
photo = self.get_object()
serializer = PhotoDataSerializer(data=request.DATA)
if serializer.is_valid():
if not photo.image_data:
photo.image_data = PhotoData.objects.create()
photo.save()
photo.image_data.thisdata = serializer.data
photo.image_data.save()
return Response({'status': 'ok'})
else:
return Response(serializer.errors,
status=status.HTTP_400_BAD_REQUEST)
Now you can do almost the same request as you doing now, but you have much more extensibility and division of responsibilities in code.
See the URL, it's appended when you have #action's wrapped method.
payload = {"thisdata": '{"test": "ok"}'}
resp = requests.patch(request.build_absolute_uri("/api/photo/1/set_photodata/"), payload)
Hope this helps.

Django REST Framework PATCH fails on required fields

We are creating an API that needs to allow a user to update a record. In many cases, like a status update or name change, only one field will change. This seems an appropriate use-case scenario for a PATCH request. As I understand it this is a 'partial' update.
We've implemented Django's REST Framework and run into this issue. For a record such as a "AccountUser" I want to change a name field only so I send the following request:
PATCH /api/users/1/ HTTP/1.1
Host: localhost
X-CSRFToken: 111122223333444455556666
Content-Type: application/json;charset=UTF-8
Cache-Control: no-cache
{ "fullname": "John Doe" }
The record obviously has other attributes including a couple of 'related' fields such as 'account' which is required for a new record. When submit the request, the response is a 400 error with the following body:
{ "account": [ "This field is required." ] }
The serializer for the user looks like this:
class AccountUserSerializer(serializers.ModelSerializer):
account = serializers.PrimaryKeyRelatedField()
class Meta:
model = AccountUser
fields = ('id', 'account', 'fullname', ... )
depth = 1
And the model looks like this:
class AccountUser(models.Model):
''' Account User'''
fullname = models.CharField(max_length=200,
null=True,blank=True)
account = models.ForeignKey(Account,
on_delete=models.PROTECT
)
objects = AccountUserManager()
def __unicode__(self):
return self.email
class Meta:
db_table = 'accounts_account_user'
Am I doing something wrong here or is it wrong to expect to be able to update a single field on a record this way. Thanks! This community rocks!
EDIT:
Requested - AccountUserManager:
class AccountUserManager(BaseUserManager):
def create_user(self, email, account_name):
username = hash_email_into_username(email)
...
account = Account.objects.get(name=account_name)
account_user = AccountUser(email=email,user=user,account=account)
account_user.save()
return account_user
It doesn't look like your manager is filtering the user. I'd encourage you to use pdb and set a breakpoint in your view code and step through to see why its attempting to create a new record. I can vouch that we use PATCH to complete partial updates all the time and only send a few fields to update without issue.
Only other thought is that you're some how sending a value for account (like null) that's triggering the validation error even though you're listed example only shows sending the fullname field.
See my answer about partial updates. Also you can see the drf docs and this one docs
In my case, all I had to do was add required=False to my serializer field's arguments.
A novice mistake to be sure, but I thought I'd mention it here in case it helps anyone.
I came here because I got into a similar problem. CREATE is fine but PATCH isn't.
Since OP doesn't post the ViewSets and I suspend if he is using a custom ViewSets, which I got a mistake when override the update function:
class MyViewSet(ModelViewSet):
def update(self, request, *args, **kwargs):
// Do some checking
return super().update(request, args, kwargs)
My mistake is in the call to super(). Missing the asterisks. This fix my problem
return super().update(request, *args, **kwargs)
you can simply add this line after the fields variable in serializers.py
class AccountUserSerializer(serializers.ModelSerializer):
account = serializers.PrimaryKeyRelatedField()
class Meta:
model = AccountUser
fields = ('id', 'account', 'fullname', ... )
depth = 1
extra_kwargs = {
'account' : {'required' : False}
}

Categories