Override Django (DRF) Serializer object GET - django

I'm trying to "inject" some raw sql into my DRF nested Serializer:
# SERIALIZERS
class CarSerializer(serializers.ModelSerializer):
class Meta:
model = Car
fields = '__all__'
class DriverSerializer(serializers.ModelSerializer):
car = CarSerializer() # <--- here I don't want to get the Car object but rather inject a raw sql.
class Meta:
model = Driver
fields = '__all__'
The SQL injection is needed to request for a specific version of the data since I'm using MariaDB versioning tables but this is not relevant. How do I override the method that gets the object from CarSerializer? Thank you.

This is untested but I think you want to override the __init__ in DriverSerializer and then load the result of your raw SQL via data, something like this:
class DriverSerializer(serializers.ModelSerializer):
[...]
def __init__(self, *args, **kwargs):
super(DriverSerializer, self).__init__(*args, **kwargs)
name_map = {'column_1': 'obj_attr_1', 'column_2': 'obj_attr_1', 'pk': 'id'}
raw = Car.objects.raw('SELECT ... FROM ...', translations=name_map)
data = {k: getattr(raw[0], k) for k in name_map.keys()}
self.car = CarSerializer(data=data)

You could define method under your model to get related Car
class Car(models.Model):
def current_car(self):
return Car.objects.raw('SELECT ... FROM ...')[0]
Then in serializer you could reuse following method
class DriverSerializer(serializers.ModelSerializer):
car = CarSerializer(source="current_car")
class Meta:
model = Driver
fields = (...)

Thank you everyone for your answers, I managed to make it work although my solution is not as clean as the one suggested from #yvesonline and #iklinak:
I first checked the official DRF documentation on overriding serializers: https://www.django-rest-framework.org/api-guide/serializers/#overriding-serialization-and-deserialization-behavior
In particular I was interested in the overriding of the method: .to_representation(self, instance) that controls the fetching of the object from the database:
from datetime import datetime as dt
from collections import OrderedDict
from rest_framework.relations import PKOnlyObject
from rest_framework.fields import SkipField, empty
def __init__(
self, instance=None, data=empty, asTime=str(dt.now()), **kwargs):
self.asTime = asTime
self.instance = instance
if data is not empty:
self.initial_data = data
self.partial = kwargs.pop('partial', False)
self._context = kwargs.pop('context', {})
kwargs.pop('many', None)
super().__init__(**kwargs)
def to_representation(self, instance):
# substitute instance with my raw query
# WARNING: self.asTime is a custom variable, check the
# __init__ method above!
instance = Car.objects.raw(
'''
select * from db_car
for system_time as of timestamp %s
where id=%s;
''', [self.asTime, instance.id])[0]
ret = OrderedDict()
fields = self._readable_fields
for field in fields:
try:
attribute = field.get_attribute(instance)
except SkipField:
continue
check_for_none = attribute.pk if isinstance(
attribute, PKOnlyObject) else attribute
if check_for_none is None:
ret[field.field_name] = None
else:
ret[field.field_name] = field.to_representation(attribute)
return ret
You can find the original code here: https://github.com/encode/django-rest-framework/blob/19655edbf782aa1fbdd7f8cd56ff9e0b7786ad3c/rest_framework/serializers.py#L335
Then finally in the DriverSerializer class:
class DriverSerializer(serializers.ModelSerializer):
car = CarSerializer(asTime='2021-02-05 14:34:00')
class Meta:
model = Driver
fields = '__all__'

Related

How to obtain the field from the parent serializer while serializing a nested json? [duplicate]

I'm working with Django-Rest-Framework's serializers. I have two serializers one nested with the other.
class NestedSerializer(serializers.Serializer):
value = AttributeValueField(required=True)
name = serializers.CharField(required=True)
class OuterSerializer(serializers.Serializer):
info = serializers.CharField()
nested = NestedSerializer()
In order to validate the nested serializer's data I need to retrieve input data from the parent serializer, something like this:
class NestedSerializer(serializers.Serializer):
...
def validate(self, data):
# of course, it doesn't work, but thats the idea.
info = self.parent.info
# then validate the NestedSerializer with info.
I can't find any way to get access to those input data from the validate method. Any suggestions? Thanks for your help :).
Before validate() method, DRF serializers call to_internal_value(self, data). You will get all data of parent serializer there. So as you defined validate() method in serializer, define to_internal_value() method and catch parent serializer's data.
You can access initial_data on the parent serializer from the nested serializers validate() method. I've also added some code for using the parent fields run_validation() method, which would validate and return the internal value from to_internal_value(), which might be a better than dealing with the initial data.
class NestedSerializer(serializers.Serializer):
def validate(self, data):
# Retrieve the initial data, perhaps this is all you need.
parent_initial_data = self.parent.initial_data
info = parent_initial_data.get("info", None)
# Get the corresponding field and use `run_validation` or `to_internal_value` if needed
if info:
info_field = self.parent.fields["info"]
info = info_field.run_validation(info)
# info = info_field.to_internal_value(info) # If you don't want validation, but do want the internal value
# Do your thing
return data
Try self.root.instance to get the parent instance in a nested serializer.
It might not be the best idea to do it this way, NestedSerializer should not be aware of the parent object. It would make your code difficult to maintain, also it would make NestedSerializer dependent on OuterSerializer.
Instead, define a validate(self, data) method in the OuterSerializer and run the mutual validation there.
Here's what I'm doing now but I'm interested to see other answers..
Basically I've created a custom field for the field in the parent serializer that needs to be accessed in the child serializer - in this case "customer". Then override to_internal_value() to add the field's validated data as an attribute on the parent serializer.
Once it's been added as an attribute it can be accessed on the child serializer through self.parent.<attribute_name> or on child serializer fields by self.root.<attribute_name>
class CustomerField(serializers.PrimaryKeyRelatedField):
def to_internal_value(self, data):
# Set the parent serializer's `customer` attribute to the validated
# Customer object.
ret = super().to_internal_value(data)
self.parent.customer = ret
return ret
class DebitField(serializers.PrimaryKeyRelatedField):
default_related_name = {
'OnAccount': 'onaccounts',
'Order': 'orders'
}
def get_queryset(self):
# Method must be overridden so the `queryset` argument is not required.
return super().get_queryset()
def set_queryset_from_context(self, model_name):
# Override the queryset depending on the model name.
queryset = self.default_related_name[model_name]
self.queryset = getattr(self.parent.customer, queryset)
def to_internal_value(self, data):
# Get the model from the `debit_type` and the object id from `debit`
# then validate that the object exists in the related queryset.
debit_type = data.pop('debit_type')
self.set_queryset_from_context(debit_type)
super().to_internal_value(data)
class PaymentLineSerializer(serializers.ModelSerializer):
debit = DebitField()
class Meta:
model = PaymentLine
fields = (
'id',
'payment',
'debit_type',
'debit', # GenericForeignKey
'amount',
)
def to_internal_value(self, data, *args):
data['debit'] = {
'debit': data.pop('debit'),
'debit_type': data.pop('debit_type'),
}
ret = super().to_internal_value(data)
return ret
def to_representation(self, instance):
data = super().to_representation(instance)
data['debit'] = instance.debit._meta.object_name
return data
class PaymentSerializer(serializers.ModelSerializer):
customer = CustomerField(queryset=Customer.objects.all())
class Meta:
model = Payment
fields = (
'id',
'customer',
'method',
'type',
'date',
'num_ref',
'comment',
'amount',
)
def __init__(self, *args, **kwargs):
self.customer = None
super().__init__(*args, **kwargs)
self.fields['lines'] = PaymentLineSerializer(
context=self.context,
many=True,
write_only=True,
)
You are almost there!!!
Use self.parent.initial_data to access the data given to the parent serializer.
class NestedSerializer(serializers.Serializer):
value = AttributeValueField(required=True)
name = serializers.CharField(required=True)
def validate(self, attrs):
attrs = super().validate(attrs)
the_input_data = self.parent.initial_data
info = the_input_data['info'] # this will not be the "validated data
# do something with your "info"
return attrs
Do not hardcode the field_name
self.parent.initial_data[self.field_name]

Delete mutation in Django GraphQL

The docs of Graphene-Django pretty much explains how to create and update an object. But how to delete it? I can imagine the query to look like
mutation mut{
deleteUser(id: 1){
user{
username
email
}
error
}
}
but i doubt that the correct approach is to write the backend code from scratch.
Something like this, where UsersMutations is part of your schema:
class DeleteUser(graphene.Mutation):
ok = graphene.Boolean()
class Arguments:
id = graphene.ID()
#classmethod
def mutate(cls, root, info, **kwargs):
obj = User.objects.get(pk=kwargs["id"])
obj.delete()
return cls(ok=True)
class UserMutations(object):
delete_user = DeleteUser.Field()
Here is a small model mutation you can add to your project based on the relay ClientIDMutation and graphene-django's SerializerMutation. I feel like this or something like this should be part of graphene-django.
import graphene
from graphene import relay
from graphql_relay.node.node import from_global_id
from graphene_django.rest_framework.mutation import SerializerMutationOptions
class RelayClientIdDeleteMutation(relay.ClientIDMutation):
id = graphene.ID()
message = graphene.String()
class Meta:
abstract = True
#classmethod
def __init_subclass_with_meta__(
cls,
model_class=None,
**options
):
_meta = SerializerMutationOptions(cls)
_meta.model_class = model_class
super(RelayClientIdDeleteMutation, cls).__init_subclass_with_meta__(
_meta=_meta, **options
)
#classmethod
def get_queryset(cls, queryset, info):
return queryset
#classmethod
def mutate_and_get_payload(cls, root, info, client_mutation_id):
id = int(from_global_id(client_mutation_id)[1])
cls.get_queryset(cls._meta.model_class.objects.all(),
info).get(id=id).delete()
return cls(id=client_mutation_id, message='deleted')
Use
class DeleteSomethingMutation(RelayClientIdDeleteMutation):
class Meta:
model_class = SomethingModel
You can also override get_queryset.
I made this library for simple model mutations: https://github.com/topletal/django-model-mutations , you can see how to delete user(s) in examples there
class UserDeleteMutation(mutations.DeleteModelMutation):
class Meta:
model = User
Going by the Python + Graphene hackernews tutorial, I derived the following implementation for deleting a Link object:
class DeleteLink(graphene.Mutation):
# Return Values
id = graphene.Int()
url = graphene.String()
description = graphene.String()
class Arguments:
id = graphene.Int()
def mutate(self, info, id):
link = Link.objects.get(id=id)
print("DEBUG: ${link.id}:${link.description}:${link.url}")
link.delete()
return DeleteLink(
id=id, # Strangely using link.id here does yield the correct id
url=link.url,
description=link.description,
)
class Mutation(graphene.ObjectType):
create_link = CreateLink.Field()
delete_link = DeleteLink.Field()

How to have different serializers for read_only & write_only with the same name in DRF?

After you create a Django object in a CreateAPI of DRF, you get a status 201 created and the object is returned with the same serializer you used to create the Django object with.
Wanted: on create: Serializer.comments = Textfield(write_only=True)
and on created (201 status) Serializer.comments = a list of commments
I know it's possible by overriding the CreateAPIView.create function.
However, I'd like to know if it's possible by using write_only=True and read_only=True attributes to serializer fields.
For now I think it's not possible because they both have the same name.
I'd have liked to do something like this using a fake kwarg names actual_name:
class CreateEventSerializer(serializers.ModelSerializer):
comments_readonly = serializers.SerializerMethodField(read_only=True, actual_name='comments')
class Meta:
model = Event
fields = ('id', 'comments', 'comments_readonly')
def __init__(self, *args, **kwargs):
super(CreateEventSerializer, self).__init__(*args, **kwargs)
self.fields['comments'].write_only = True
def get_comments_readonly(self, obj):
comments = obj.comments.replace('\r', '\n')
return [x for x in comments.split('\n') if x != '']
But this way, the JSON that is returned still contains the key "comments_readonly" instead of the wanted key "comments".
Using latest DRF, 3.7.1
In other words:
Is it possible to create a serializer field that behaves differently based on read and write, (using only 1 serializer class)?
This seems to do the trick for the JSON response, but it feels a bit hacky, as DRF HTML forms now shows a python list in the comments textarea field.
class CreateEventSerializer(serializers.ModelSerializer):
class Meta:
model = Event
fields = ('id', 'comments')
def get_comments(self, obj):
comments = obj.comments.replace('\r', '\n')
return [x for x in comments.split('\n') if x != '']
def to_representation(self, instance):
data = super(CreateEventSerializer, self).to_representation(instance)
data['comments'] = self.get_comments(instance)
return data

How do I customize the text of the select options in the api browser?

I am using rest_framework v3.1.3 in django 1.8. I am pretty new to django.
Here are the relevant model definitions
#python_2_unicode_compatible
class UserFitbit(models.Model):
user = models.OneToOneField(User, related_name='fituser')
fitbit_user = models.CharField(max_length=32)
auth_token = models.TextField()
auth_secret = models.TextField()
#this is a hack so that I can use this as a lookup field in the serializers
#property
def user__userid(self):
return self.user.id
def __str__(self):
return self.user.first_name + ' ' + self.user.last_name
def get_user_data(self):
return {
'user_key': self.auth_token,
'user_secret': self.auth_secret,
'user_id': self.fitbit_user,
'resource_owner_key': self.auth_token,
'resource_owner_secret': self.auth_secret,
'user_id': self.fitbit_user,
}
def to_JSON(self):
return json.dumps(self, default=lambda o: o.__dict__,
sort_keys=True, indent=4)
class Challenge(models.Model):
name=models.TextField()
status=models.TextField() #active, pending, ended, deleted
start_date=models.DateField()
end_date=models.DateField()
#members=models.ManyToManyField(UserFitbit)
members=models.ManyToManyField(User)
admin=models.ForeignKey(UserFitbit,related_name='admin')
#for each member get stats between the start and end dates
def memberstats(self):
stats = []
for member in self.members.all():
fbu = UserFitbit.objects.filter(user__id=member.id)
fu = UserData.objects.filter(userfitbit=fbu)
fu = fu.filter(activity_date__range=[self.start_date,self.end_date])
fu = fu.annotate(first_name=F('userfitbit__user__first_name'))
fu = fu.annotate(user_id=F('userfitbit__user__id'))
fu = fu.annotate(last_name=F('userfitbit__user__last_name'))
fu = fu.values('first_name','last_name','user_id')
fu = fu.annotate(total_distance=Sum('distance'),total_steps=Sum('steps'))
if fu:
stats.append(fu[0])
return stats
def __str__(self):
return 'Challenge:' + str(self.name)
class Meta:
ordering = ('-start_date','name')
And here is the serializer for the challenge
class ChallengeSerializer(serializers.ModelSerializer):
links = serializers.SerializerMethodField(read_only=True)
memberstats = MemberStatSerializer(read_only=True,many=True)
#these are user objects
#this should provide a hyperlink to each member
members = serializers.HyperlinkedRelatedField(
#queryset defines the valid selectable values
queryset=User.objects.all(),
view_name='user-detail',
lookup_field='pk',
many=True,
)
class Meta:
model=Challenge
fields = ('id','name','admin','status','start_date','end_date','members','links','memberstats',)
read_only_fields = ('memberstats','links',)
def get_links(self, obj) :
request = self.context['request']
return {
'self': reverse('challenge-detail',
kwargs={'pk':obj.pk},request=request),
}
As you can see the Challenge has a many to many relationship with User. This is the built in User model from django not UserFitBit defined here.
With these definitions when I go to the api browser for a challenge I need to be able to select the users based on their name, but the select only shows their User id property and the hyperlink url. I would like the members to be User objects, but I don't know how to change the text for the select options since I don't think I can change the built in User object. What is the best way to change the select box options to show the users name from the User object rather than the username field and hyperlink?
Here is an image:
I'm not sure if this is the best way but after reading DRF's source code, I would try this.
Subclass the HyperlinkedRelatedField and override the choices property.
import six
from collections import OrderedDict
class UserHyperLinkedRelatedField(serializers.HyperLinkedRelatedField):
#property
def choices(self):
queryset = self.get_queryset()
if queryset is None:
return {}
return OrderedDict([
(
six.text_type(self.to_representation(item)),
six.text_type(item.get_full_name())
)
for item in queryset
])
then would simply replace the field in the serializer.
members = UserHyperlinkedRelatedField(
queryset=User.objects.all(),
view_name='user-detail',
lookup_field='pk',
many=True,
)
The DRF docs also mentioned that there's a plan to add a public API to support customising HTML form generation in future releases.
Update
For DRF 3.2.2 or higher, there will be an available display_value method.
You can do
class UserHyperLinkedRelatedField(serializers.HyperLinkedRelatedField):
def display_value(self, instance):
return instance.get_full_name()
Because this is a many related field I also had to extend the ManyRelatedField and override the many_init method of the RelatedField to use that class. Can't say I understand all of this just yet, but it is working.
class UserManyRelatedField(serializers.ManyRelatedField):
#property
def choices(self):
queryset = self.child_relation.queryset
iterable = queryset.all() if (hasattr(queryset, 'all')) else queryset
items_and_representations = [
(item, self.child_relation.to_representation(item))
for item in iterable
]
return OrderedDict([
(
six.text_type(item_representation),
item.get_full_name() ,
)
for item, item_representation in items_and_representations
])
class UserHyperlinkedRelatedField(serializers.HyperlinkedRelatedField):
#classmethod
def many_init(cls, *args, **kwargs):
list_kwargs = {'child_relation': cls(*args, **kwargs)}
for key in kwargs.keys():
if key in MANY_RELATION_KWARGS:
list_kwargs[key] = kwargs[key]
return UserManyRelatedField(**list_kwargs)
members = UserHyperlinkedRelatedField(
queryset=User.objects.all(),
view_name='user-detail',
lookup_field='pk',
many=True,
)

ModelChoiceField in forms.Form won't validate if queryset is overridden

I have a django ModelChoiceField that won't validate if I override the queryset.
class PersonalNote(forms.Form):
tile = ModelChoiceField(queryset=Tile.objects.none())
note = forms.CharField()
form = PersonalNote()
form.fields['tile'].queryset = Tile.objects.filter(section__xxx=yyy)
The form.is_valid() error is: "Select a valid choice. That choice is not one of the available choices".
If Tile.objects.none() is replaced with Tile.objects.all() it validates, but loads far too much data from the database. I've also tried:
class PersonalNote(forms.Form):
tile = ModelChoiceField(queryset=Tile.objects.none())
note = forms.CharField()
def __init__(self, *args, **kwargs):
yyy = kwargs.pop('yyy', None)
super(PersonalNote, self).__init__(*args, **kwargs)
if yyy:
self.fields['tile'].queryset = Tile.objects.filter(section__xxx=yyy)
What might be wrong here? Note the real application also overrides the label, but that does not seem to be a factor here:
class ModelChoiceField2(forms.ModelChoiceField):
def label_from_instance(self, obj):
assert isinstance(obj,Tile)
return obj.child_title()
After 2 hours I found the solution. Because you specified a queryset of none in the class definition, when you instantiate that PersonalNote(request.POST) to be validated it is referenceing a null query set
class PersonalNote(forms.Form):
tile = ModelChoiceField(queryset=Tile.objects.none())
note = forms.CharField()
To fix this, when you create your form based on a POST request be sure to overwrite your queryset AGAIN before you check is_valid()
def some_view_def(request):
form = PersonalNote(request.POST)
**form.fields['tile'].queryset = Tile.objects.filter(section__xxx=yyy)**
if form.is_valid():
#Do whatever it is
When you pass an empty queryset to ModelChoiceField you're saying that nothing will be valid for that field. Perhaps you could filter the queryset so there aren't too many options.
I also had this problem. The idea is to dynamically change the queryset of a ModelChoiceField based on a condition (in my case it was a filter made by another ModelChoiceField).
So, having the next model as example:
class FilterModel(models.Model):
name = models.CharField()
class FooModel(models.Model):
filter_field = models.ForeignKey(FilterModel)
name = models.CharField()
class MyModel(models.Model):
foo_field = models.ForeignKey(FooModel)
As you can see, MyModel has a foreign key with FooModel, but not with FilterModel. So, in order to filter the FooModel options, I added a new ModelChoiceField on my form:
class MyForm(forms.ModelForm):
class Meta:
model = MyModel
def __init__(self, *args, **kwargs):
# your code here
self.fields['my_filter_field'] = forms.ModelChoiceField(FilterModel, initial=my_filter_field_selected)
self.fields['my_filter_field'].queryset = FilterModel.objects.all()
Then, on your Front-End you can use Ajax to load the options of foo_field, based on the selected value of my_filter_field. At this point everyting should be working. But, when the form is loaded, it will bring all the posible options from FooModel. To avoid this, you need to dynamically change the queryset of foo_field.
On my form view, I passed a new argument to MyForm:
id_filter_field = request.POST.get('my_filter_field', None)
form = MyForm(data=request.POST, id_filter_field=id_filter_field)
Now, you can use that argument on MyForm to change the queryset:
class MyForm(forms.ModelForm):
# your code here
def __init__(self, *args, **kwargs):
self.id_filter_field = kwargs.pop('id_filter_field', None)
# your code here
if self.id_filter_field:
self.fields['foo_field'].queryset = FooModel.objects.filter(filter_field_id=self.id_filter_field)
else:
self.fields['foo_field'].queryset = FooModel.objects.none()