Modify Django Rest Framework ModelViewSet behavior - django

I basically have the following model in my project:
class ShellMessage(TimeStampedModel):
# There is a hidden created and modified field in this model.
ACTION_TYPE = (
('1' , 'Action 1'),
('2' , 'Action 2')
)
type = models.CharField(max_length=2,choices=ACTION_TYPE,default='1')
action = models.CharField(max_length=100)
result = models.CharField(max_length=300, blank=True)
creator = models.ForeignKey(User)
I created a serializer:
class ShellMessageSerializer(serializers.ModelSerializer):
class Meta:
model = ShellMessage
fields = ('action', 'type', 'result', 'creator')
And a ModelViewSet:
class ShellListViewSet(viewsets.ModelViewSet):
serializer_class = ShellMessageSerializer
queryset = ShellMessage.objects.all()
My issue is the following:
When I create a new ShellMessage with a POST to my API, I don't want to provide the foreignKey of 'creator' but instead just the username of the guy and then process it in my ViewSet to find the user associated with this username and save it in my ShellMessage object.
How can I achieve this using Django rest Framework? I wanted to supercharge create() or pre_save() methods but I'm stuck as all my changes overwrite 'normal' framework behavior and cause unexpected errors.
Thank you.

I finally find my solution just after posting my question :)
So I did the following:
class ShellListViewSet(viewsets.ModelViewSet):
serializer_class = ShellMessageSerializer
queryset = ShellMessage.objects.all()
def pre_save(self, obj):
obj.creator = self.request.user
return super(ShellListViewSet, self).pre_save(obj)
This is working as expected. I hope I did well.

UPDATE: This topic seems to be a duplicate to Editing django-rest-framework serializer object before save
If you intend to intercept and perform some processing before the object gets saved in the model database, then what you're looking for is overriding the method "perform_create" (for POST) or "perform_update" (for PUT/PATCH) which is present within the viewsets.ModelViewSet class.
This reference http://www.cdrf.co/3.1/rest_framework.viewsets/ModelViewSet.html lists all available methods within viewsets.ModelViewSet where you can see that the "create" method calls "perform_create" which in turn performs the actual saving through the serializer object (the object that has access to the model):
def perform_create(self, serializer):
serializer.save()
We can override this functionality that is present in the base class (viewsets.ModelViewSet) through the derived class (the ShellListViewSet in this example) and modify the model attribute(s) that you want to be changed upon saving:
class ShellListViewSet(viewsets.ModelViewSet):
serializer_class = ShellMessageSerializer
queryset = ShellMessage.objects.all()
def findCreator(self):
# You can perform additional processing here to find proper creator
return self.request.user
def perform_create(self, serializer):
# Save with the new value for the target model fields
serializer.save(creator = self.findCreator())
You can also opt to modify the model fields separately and then save (probably not advisable but is possible):
serializer.validated_data['creator'] = self.findCreator()
serializer.save()
Later if the object is already created and you also want to apply the same logic during an update (PUT, PATCH), then within "perform_update" you can either do the same as above through the "serializer.validated_data['creator']" or you could also change it directly through the instance:
serializer.instance.creator = self.findCreator()
serializer.save()
But beware with such updating directly through the instance as from https://www.django-rest-framework.org/api-guide/serializers/ :
class MyModelSerializer(serializers.Serializer):
field_name = serializers.CharField(max_length=200)
def create(self, validated_data):
return MyModel(**validated_data)
def update(self, instance, validated_data):
instance.field_name = validated_data.get('field_name', instance.field_name)
return instance
This means that whatever you assign to the "instance.field_name" object could be overriden if there is a "field_name" data set within the "validated_data" (so in other terms, if the HTTP Body of the PUT/PATCH Request contains that particular "field_name" resulting to it being present in the "validated_data" and thus overriding whatever value you set to the "instance.field_name").

Related

Django Rest Framework modelviewset - updating fields before create

I have a modelviewset:
class ExpenseViewSet(ModelViewSet):
permission_classes = [permissions.IsAuthenticated, HasMetis]
serializer_class = ExpenseSerializer
def get_queryset(self):
return Expense.objects.filter(recorded_by=self.request.user)
And a serializer:
class ExpenseSerializer(serializers.ModelSerializer):
class Meta:
model = Expense
fields = ["flat_id", "flat_group_id", "description", "notes", "amount"]
These are the fields that are POSTed to the viewset, but they are not sufficient to populate the object completely, so I need to add some more fields.
I've tried overriding the serializer, like so:
class ExpenseSerializer(serializers.ModelSerializer):
class Meta:
model = Expense
fields = ["flat_id", "flat_group_id", "description", "notes", "amount"]
def create(self, validated_data):
expense = Expense.objects.create(
flat_id=validated_data["operations_flat"],
flat_group_id=validated_data["operations_building"],
description=validated_data["description"],
notes=validated_data["notes"],
amount=validated_data["amount"],
recorded_by=self.request.user,
)
return expense
This, however, is never called (tested by sticking a print statement in the create function - it never runs). Apparently this is because of this question: Django Rest Framework serializer create() doesn't get triggered This explains the issue, but not how to solve my problem.
I'm not sure whether I need to override the is_valid function of the serializer, the create function of the serializer, or the create function of the viewset, or something else.
You should override the perform_create() on viewset something like this and pass in your extra data for object creation:
def perform_create(self, serializer):
serializer.save(user=self.request.user)
Reference : Save and deletion hooks: https://www.django-rest-framework.org/api-guide/generic-views/#genericapiview (search perform_create in this page for faster lookup)

is_valid() returns false on nested serializers

The code
Serializers.py:
class ActivitySerializer(serializers.ModelSerializer):
class Meta:
model = Activity
class RouteOrderingSerializer(serializers.ModelSerializer):
activity = ActivitySerializer()
class Meta:
model = RouteOrdering
fields = ('id','activity','day','order','route','activity')
def create(self, validated_data):
routeordering = RouteOrdering.objects.create(**validated_data)
return routeordering
a create func:
def make(data):
serializer = RouteOrderingSerializer(data=data)
serializer.is_valid(raise_exception=True)
serializer.save()
The problem
For some reason, because of the writable nested serializer, the serializer.is_valid() function finds the passed data as not valid and does not let me save the instance.
I have tried also to skip the is_valid() function but of course it is not allowed by the rest framework.
The data is on the currect format, and the ORM should accept it.
Any help will be appriciated :)
Probably worth to note
When im passing data to create for Routeorderingserializer, I want to pass only an activity_id and not the whole activity object. iv'e tried it with a full activity object too but it didn't help.

How do you pass parameters which are not associated with a serializer field in Django Rest Framework?

I have a form with fields which are not associated with a model. I assume to implement the equivalent using a REST API (django-rest-framework), I would have to pass those additional fields, which are not associated with a Serializer? How do I do that?
Let's say the additional field is number_of_pages. I use that for some calculation. How do I allow that to be passed in my REST call?
if you are using ModelSerializer from DjangoRestFramework, just add a field.
by default only model fields are added, but nothing limits you to add more, the only thing that may be problematic (but I've not tested it) - you may have too many fields while creating or updating model - in such a case, you will need to remove those fields in create() and update() methods before calling save().
class MyModelSerializer(serializers.ModelSerializer):
number_of_pages = fields.IntegerField()
# this I'm not sure if needed
def create(self, validated_data):
validated_data.pop('number_of_pages')
return super(MyModelSerializer, self).create(validated_data)
def update(self,instance, validated_data):
validated_data.pop('number_of_pages')
return super(MyModelSerializer, self).update(instance, validated_data)
# end
class Meta:
fields = ('mymodelfield_1', 'mymodelfield_2', 'number_of_pages')
model = MyModel
If, you are using django-rest-framework, then you can use SerializerMethodField() to pass additional fields in Api. Below I give an example.
class UserGroupSerializer(serializers.ModelSerializer):
"""
This Serializer pass additionl field count. by using SerializerMethodField()
"""
count = serializers.SerializerMethodField()
class Meta:
model = UserGroup
def get_count(self,obj):
"""return length of group"""
return Groupmember.objects.filter(user_group=obj.id).count()
and use
serializer_class = UserGroupSerializer
in view.
I think it can help you.
Thanks

Advance serialization in DRF

I'm trying to achieve something with Django Rest Framework.
The idea is for a model to have several fields of several types in read-only, and have the same fields writable for the user that would take precedence when serving the data.
Since this should not be very clear, an example :
The model would be :
class Site(models.Model):
title = models.CharField(_('Title'),max_length=300)
title_modified = models.CharField(_('Title'),max_length=300)
The viewset to be defined :
class SiteViewSet(viewsets.ModelViewSet):
serializer_class = SiteSerializer
queryset = Site.objects.all()
The serializer :
class SiteSerializer(serializers.ModelSerializer):
class Meta:
model = Site
depth = 1
What i want to do is be able to only serve the "title" data to the client, but the title would have either the title field if title_modified is empty or the title_modified field if it's not empty.
On the same idea when the client writes to the title i would like my server to write the data to title_modified instead and always leave the title info untouched.
I don't know how to achieve this a way that's generic enough to be applicable to all types of fields.
I thought it would simply require some magic on the serialization/unserialization but i can't seem to find it.
Any idea would be appreciated.
Thanks.
Since you are using ModelViewSets, you can override the default actions like .list(), .retrieve(), .create(), etc to do what you want or create your custom actions. Relevant info for ModelViewSets can be found here and here.
Actually, there are plenty of ways to go about this, and you do not even need to use ModelViewSet. You can actually use the generic views for this one. The real trick is to leverage the power of the CBVs and OOP in general. Here is a sample code wherein you provide a custom retrieval process of a single instance, while retaining all the rest's out-of-the-box behavior that a ModelViewSet provides.
class SiteViewSet(viewsets.ModelViewSet):
serializer_class = SiteSerializer
queryset = Site.objects.all()
def retrieve(self, request, *args, **kwargs):
instance = self.get_object()
# You can use the serializer_class specified above (SiteSerializer)
serializer = self.get_serializer(instance)
# Or perform some other manipulation on your instance first,
# then use a totally different serializer for your needs
instance = data_manipulation(instance)
serializer = AnotherSiteSerializer(instance)
# Finally return serialized data
return Response(serializer.data)
# Or if you want, return random gibberish.
return Response({'hello': 'world'})
I think you can override the to_representation() method of serializer to solve your problem:
class SiteSerializer(serializers.ModelSerializer):
class Meta:
model = Site
depth = 1
exclude = ('title')
def to_representation(self, instance):
rep = super(SiteSerializer, self).to_representation(instance)
if not rep.get('title_modified', ''):
rep['title_modified'] = instance.title
return rep
This will return title as title_modified if title_modified is empty. User will always work on title_modified as required.
For more details please read modelserializer and Advanced serializer usage.

Django Rest Framework: How to save a field as an object of a different model, then return the key to the original model

Sorry for the weird title.
I have 2 models:
class TranslationWord(models.Model):
translation = models.TextField(unique=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class TranslationByUser(models.Model):
synset = models.ForeignKey(Synset)
user = models.ForeignKey(User)
translation = models.ForeignKey(TranslationWord)
The first one is supposed to basically just save words. The second is supposed to get a user's input. If the word exists in the first class, the foreign key value is simply stored. If it doesn't exist, I want to first create an instance of the TranslationWord, and then add the foreign key to the second class.
I'm doing all this with the Django Rest Framework, so I'm pretty stumped.
Currently, I've got these 2 models, 2 serializers (both just instances of ModelSerializer), and a view to save it (ListCreateAPIView).
How should I go about doing this?
These are basically the steps for creating a successfully validated object in a ModelViewSet create method (it's defined in CreateModelMixin):
if serializer.is_valid():
self.pre_save(serializer.object)
self.object = serializer.save(force_insert=True)
self.post_save(self.object, created=True)
headers = self.get_success_headers(serializer.data)
return Response(serializer.data, status=status.HTTP_201_CREATED,
headers=headers)
It means you can override pre_save for your action in your ViewSet for TranslationByUser, setting attributes as side-effects in the object:
def pre_save(self, obj):
#get your_translation_word from obj or self.kwargs
your_translation_word = get_translation_word()
translation = TranslationWord(translation=your_translation_word)
translation.save()
setattr(obj, 'translation', translation)
#if you also want to support Update, call super method
super(TranslationByUserViewSet, self).pre_save(obj)
Another thing you can try is to define TranslationWordSerializer as a nested field in TranslationByUserSerializer. This topic is explained in the docs. Not sure if DRF handles everything about the creation though. I've only tested this behaviour with Multi-table Inheritance (and it works).
Anyway, for anyone who is curious, I created a write-only field in the Serializer, and used it to create the instance in the restore_object method.
class MySerializer(serializers.ModelSerializer):
user = UserSerializer(required=False)
translation = TranslationLemmaSerializer(required=False)
translation_text = serializers.WritableField(required=False, write_only=True)
class Meta:
model = TranslationByUser
fields = ('id','user','synset', 'translation', 'translation_text',)
read_only_fields = ('id', 'created_at', 'updated_at',)
def restore_object(self, attrs, instance=None):
print attrs
if instance is not None:
instance.synset = attrs.get('synset', instance.synset)
return instance
translation_text = attrs.get('translation_text')
del attrs['translation_text'] #delete non-model attribute before actually creating it
translationHIT = TranslationByUser(**attrs) #create model here
translation_id = None
try:
translation_instance = TranslationWord.objects.get(translation=translation_text) #check if translationWord is already present
except:
translation_instance = TranslationWord(translation=translation_text)
translation_instance.save() #otherwise, create it
TranslationByUser.translation = translation_instance
print attrs
return TranslationByUser
def get_validation_exclusions(self,instance=None):
exclusions = super(MySerializer, self).get_validation_exclusions()
return exclusions + ['user', 'translation']