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.
Related
I've stuck with this problem for few days now. Tried different approaches but without success. I have two classes - Poll and PollAnswer. Here they are:
class Poll(Model):
title = CharField(max_length=256)
class PollAnswer(Model):
user_id = CharField(max_length=10)
poll = ForeignKey(Poll, on_delete=CASCADE)
text = CharField(max_length=256)
what is the right way to get list of polls which have answers with used_id equal to the certain string with nested list of that user's answers? like this:
{
'poll_id': 1,
'answers' : {
'user1_answer1: 'answer_text1',
'user1_answer2: 'answer_text2',
'user1_answer3: 'answer_text3',
},
}
and if it's the simple question i probably need some good guides on django orm.
the first thing i tried was to make serializer's method (inherited from drf's ModelSerializer) but got an error that this class can't have such method. after that i tried to use serializer's field with ForeignKey but got polls nested in answers instead. right now i believe i can make Polls.objects.raw('some_sql_query') but that's probably not the best way.
Your problem is described in documentation (also best practise).
You can use nested serializer:
https://www.django-rest-framework.org/api-guide/relations/#writable-nested-serializers
otherwise if u want to keep nested answers as you described:
i would use serializer method field
https://www.django-rest-framework.org/api-guide/fields/#serializermethodfield
then do a little loop over your answers ... and return whatever format you want to.
well, thanks for help the final code is:
serializer:
class UserPollsSerializer(ModelSerializer):
answers = SerializerMethodField()
# drf automatically finds this method by mask get_{field}
def get_answers(self, obj):
answers = self.context.get('answers_qs').filter(poll=obj)
data = QuestionAnswerSerializer(answers, many=True).data
return data
class Meta:
model = Poll
fields = '__all__'
view:
class PollMVS(ModelViewSet):
serializer_class = PollSerializer
permission_classes = [IsAdminUser]
queryset = Poll.objects.all()
# getting polls with answers by user_id
#action(detail=False, methods=['get'])
def user_polls(self, request):
# here i get all polls with user's answers
user_id = request.GET.get('user_id')
polls_ids = QuestionAnswer.objects.values('poll').filter(user_id=user_id).distinct()
u_polls = Poll.objects.filter(pk__in=polls_ids)
# and here i pass them as a queryset and answers' queryset as context in a separate serializer
serializer = UserPollsSerializer(u_polls, many=True, context={'answers_qs': QuestionAnswer.objects.filter(user_id=user_id)})
return Response(serializer.data)
I have two serializers, in which one refers to the other with a many=True relationship.
class AttributeInParentSerializer(ModelSerializer):
masterdata_type = CharField(max_length=256, source='masterdata_type_id')
class Meta:
model = Attribute
fields = ('uuid', 'masterdata_type')
class ArticleInArticleSetSerializer(ModelSerializer):
attributes = AttributeInParentSerializer(many=True)
class Meta:
model = Article
fields = ('uuid', 'attributes')
The ordering of the attributes in the Article are not always the same, but I want to output them in the same order, so in this case ordering on the field masterdata_type. How can I accomplish this? Note that I do not want to change any client of the serializer if possible, and surely not any model.
Old thread, but because it's still popping up on Google I want to share my answer as well. Try overwriting the Serializer.to_representation method. Now you can basically do whatever you want, including customising the sorting of your response. In your case:
class ArticleInArticleSetSerializer(ModelSerializer):
attributes = AttributeInParentSerializer(many=True)
class Meta:
model = Article
fields = ('uuid', 'attributes')
def to_representation(self, instance):
response = super().to_representation(instance)
response["attributes"] = sorted(response["attributes"], key=lambda x: x["masterdata_type"])
return response
you can set the ordering of attributes on ArticleInArticleSetSerializer by writing viewset for ArticleInArticleSetSerializer.
class ArticleInArticleSetViewSet(viewsets.ModelViewSet):
serializer_class = ArticleInArticleSetSerializer
queryset = Article.objects.all().order_by('-attributes_id')
Or you can write a function for listing.
def list(self, request):
self.queryset = self.get_queryset().order_by('-id')
return super(yourViewSet, self).list(self, request)
This code only for reference
I found an answer I prefer over rewriting the to_representation method or doing an inline call to the sorted method (which loads all the instances in memory):
class ArticleInArticleSetSerializer(ModelSerializer):
attributes = serializers.SerializerMethodField(method_name='get_attributes_sorted')
#staticmethod
def get_attributes_sorted(instance):
attributes = instance.attributes.order_by('masterdata_type')
return AttributeInParentSerializer(attributes, many=True).data
# Other stuff ...
This way you use a pure ORM solution (that's translated to SQL). The best performance and no memory consumption during sorting.
You can't set the ordering on ArticleInArticleSetSerializer but you can set the ordering of attributes on AttributeInParentSerializer. This is because you can only set the ordering when you are consuming a serializer rather than when you're defining one.
You could perhaps set it in the __init__ method when the queryset or data is passed in, but then you're making assumptions on what is being passed in. I'd probably end up specifying it in the consumers of ArticleInArticleSetSerializer to avoid any future problems with passing in a list to the serializer.
You can use order_by inside of your serializer like this:
class AttributeInParentSerializer(ModelSerializer):
masterdata_type = CharField(max_length=256, source='masterdata_type_id')
class Meta:
model = Attribute
fields = ('uuid', 'masterdata_type')
order_by = (('masterdata_type',))
Hope it helps!
UPDATE:
Looks like I am mistaken. Could not find it from the documentation and it does not seem to work. Now, I don't think serializers are the place to do ordering. Best way to do is in the model or in the view.
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").
Given a serializer with a reference to a custom serializer:
class IndustryIdeaSerializer(serializers.ModelSerializer):
sub_industry = IndustrySerializer(many=False, read_only=True)
class Meta:
model = myModels.IdeaIndustry
fields = (
'id'
, 'sub_industry'
)
I am unable to save changes to this class when I post JSON like { sub_industry: 12 } or { sub_industry_id: 12 }
It does return the right JSON for displaying the data, and I wouldn't change it from that perspective. However changing it to:
class IndustryIdeaSerializer(serializers.ModelSerializer):
class Meta:
model = myModels.IdeaIndustry
fields = (
'id'
, 'sub_industry'
)
Gives me the save action (can persist with the simple JSON) I want BUT not the read action (doesn't return all the data associated with that foreign key)!
First am I missing something obvious? Is there a pattern to deal with behavior I am after - namely read and return the deep tree, but persist with just the Id's
This is for DRF 3.0. I just whipped this up this afternoon, I will follow up if I encounter any unforeseen problems (likewise, let me know if you spot anything wrong! I am fairly new to DRF)
class EnhancedPrimaryKeyRelatedField(serializers.PrimaryKeyRelatedField):
'''
This custom field extends the PrimaryKeyRelatedField
It overrides to_representation (which generates the data to be
serialized) to use a given serializer.
This allows other serializers to show nested data about a related
field, while still allowing the client to set relations by simply
passing an id.
To initialize, pass the queryset and serializer arguments.
The serializer argument should be a Serializer class.
If the serializer provides Meta.model (such as a ModelSerializer),
and you wish to use the queryset provided by that serializer, you may
omit the queryset argument.
e.g.
# without queryset
child_object = EnhancedPrimaryKeyRelatedField(
serializer=ChildObjectSerializer
)
# with queryset
child_object = EnhancedPrimaryKeyRelatedField(
queryset=models.ChildObject.objects.all(),
serializer=SomeSpecializedSerializer
)
'''
def __init__(self, *args, **kwargs):
assert 'serializer' in kwargs
self.serializer = kwargs['serializer']
del kwargs['serializer']
if 'queryset' not in kwargs:
# Catch any programmer errors
assert 'Meta' in self.serializer.__dict__
assert 'model' in self.serializer.Meta.__dict__
kwargs['queryset'] = self.serializer.Meta.model.objects.all()
super(serializers.PrimaryKeyRelatedField, self).__init__(*args, **kwargs)
def to_representation(self, data):
if hasattr(data.pk, 'all'): # are we dealing with a collection?
return self.serializer(data.pk.all(), many=True).data
elif hasattr(data, 'pk') and data.pk:
return self.serializer(self.queryset.get(pk=data.pk)).data
else:
return data.pk
There's nothing built in that handles this explicitly, but it's now come up a couple of times recently (e.g. here so perhaps we need to make it easier.
The work-around is to subclass PrimaryKeyRelatedField, which will handle setting the relation and override to_native to provide the full serialisation you're after.
I hope that helps.
In my app I have the following models:
class Zone(models.Model):
name = models.SlugField()
class ZonePermission(models.Model):
zone = models.ForeignKey('Zone')
user = models.ForeignKey(User)
is_administrator = models.BooleanField()
is_active = models.BooleanField()
I am using Django REST framework to create a resource that returns zone details plus a nested resource showing the authenticated user's permissions for that zone. The output should be something like this:
{
"name": "test",
"current_user_zone_permission": {
"is_administrator": true,
"is_active": true
}
}
I've created serializers like so:
class ZonePermissionSerializer(serializers.ModelSerializer):
class Meta:
model = ZonePermission
fields = ('is_administrator', 'is_active')
class ZoneSerializer(serializers.HyperlinkedModelSerializer):
current_user_zone_permission = ZonePermissionSerializer(source='zonepermission_set')
class Meta:
model = Zone
fields = ('name', 'current_user_zone_permission')
The problem with this is that when I request a particular zone, the nested resource returns the ZonePermission records for all the users with permissions for that zone. Is there any way of applying a filter on request.user to the nested resource?
BTW I don't want to use a HyperlinkedIdentityField for this (to minimise http requests).
Solution
This is the solution I implemented based on the answer below. I added the following code to my serializer class:
current_user_zone_permission = serializers.SerializerMethodField('get_user_zone_permission')
def get_user_zone_permission(self, obj):
user = self.context['request'].user
zone_permission = ZonePermission.objects.get(zone=obj, user=user)
serializer = ZonePermissionSerializer(zone_permission)
return serializer.data
Thanks very much for the solution!
I'm faced with the same scenario. The best solution that I've found is to use a SerializerMethodField and have that method query and return the desired values. You can have access to request.user in that method through self.context['request'].user.
Still, this seems like a bit of a hack. I'm fairly new to DRF, so maybe someone with more experience can chime in.
You have to use filter instead of get, otherwise if multiple record return you will get Exception.
current_user_zone_permission = serializers.SerializerMethodField('get_user_zone_permission')
def get_user_zone_permission(self, obj):
user = self.context['request'].user
zone_permission = ZonePermission.objects.filter(zone=obj, user=user)
serializer = ZonePermissionSerializer(zone_permission,many=True)
return serializer.data
Now you can subclass the ListSerializer, using the method I described here: https://stackoverflow.com/a/28354281/3246023
You can subclass the ListSerializer and overwrite the to_representation method.
By default the to_representation method calls data.all() on the nested queryset. So you effectively need to make data = data.filter(**your_filters) before the method is called. Then you need to add your subclassed ListSerializer as the list_serializer_class on the meta of the nested serializer.
subclass ListSerializer, overwriting to_representation and then calling super
add subclassed ListSerializer as the meta list_serializer_class on the nested Serializer
If you're using the QuerySet / filter in multiple places, you could use a getter function on your model, and then even drop the 'source' kwarg for the Serializer / Field. DRF automatically calls functions/callables if it finds them when using it's get_attribute function.
class Zone(models.Model):
name = models.SlugField()
def current_user_zone_permission(self):
return ZonePermission.objects.get(zone=self, user=user)
I like this method because it keeps your API consistent under the hood with the api over HTTP.
class ZoneSerializer(serializers.HyperlinkedModelSerializer):
current_user_zone_permission = ZonePermissionSerializer()
class Meta:
model = Zone
fields = ('name', 'current_user_zone_permission')
Hopefully this helps some people!
Note: The names don't need to match, you can still use the source kwarg if you need/want to.
Edit: I just realised that the function on the model doesn't have access to the user or the request. So perhaps a custom model field / ListSerializer would be more suited to this task.
I would do it in one of two ways.
1) Either do it through prefetch in your view:
serializer = ZoneSerializer(Zone.objects.prefetch_related(
Prefetch('zone_permission_set',
queryset=ZonePermission.objects.filter(user=request.user),
to_attr='current_user_zone_permission'))
.get(id=pk))
2) Or do it though the .to_representation:
class ZoneSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = Zone
fields = ('name',)
def to_representation(self, obj):
data = super(ZoneSerializer, self).to_representation(obj)
data['current_user_zone_permission'] = ZonePermissionSerializer(ZonePermission.objects.filter(zone=obj, user=self.context['request'].user)).data
return data