Django Rest Framework modelviewset - updating fields before create - django

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)

Related

Django REST framework is changing object values when request method is GET

i have problem with (not wanted) objects values setting. I have viewset where serialiser is chosen by request.method. I want to update DateTimeField only WITH POST/PUT method, and then can check this value with GET method but.. after posting new object I got json like this
{
"datetimefield": "2021-10-26 23:01:53.272194"
}
and after GET method I got this (for the same object):
{
"datetimefield": ""
}
my code:
class SomeViewSet(viewsets.ModelViewSet):
queryset = SomeModel.objects.all()
def get_serializer_class(self):
if (self.request.method == 'POST'):
return FirstSerializer
else:
return SecondAppSerializer
class FirstSerializer(serializers.HyperlinkedModelSerializer):
datetimefield = serializers.SerializerMethodField()
def get_datetimefield(self, obj):
return str(datetime.today())
class Meta:
model = SomeModel
fields = ('datetimefield')
class SecondSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = SomeObject
fields = ('datetimefield')
class SomeModel(models.Model):
datetimefield = models.CharField(max_length=100)
Anyone know how to solve this?
You can think of SerializerMethodField as a calculated read-only field, so the output that you got from POST is just generated by get_datetimefield and never really saved to the database. That's why you get an empty data on GET.
To solve this, you can remove the datetimefield field from the serializer, and let save handle it by overriding perform_create:
class FirstSerializer(serializers.HyperlinkedModelSerializer):
# remove datetimefield
class Meta:
model = SomeModel
fields = ('some_other_fields...')
class SomeViewSet(viewsets.ModelViewSet):
...
def perform_create(self, serializer):
serializer.save(datetimefield=str(datetime.today()))
Or if possible, I highly recommend to just change the model field to DateTimeField with auto_now_add set to True.
In this approach, there is no need to manage datetimefield on the view or serializer. The model will automatically handle it for you when a new object is created. So:
class SomeModel(models.Model):
datetimefield = models.DateTimeField(auto_now_add=True)

Why does a queryset applied in a ModelForm not inherit a queryset from a ModelManager?

I have a custom queryset on a model manager:
class TenantManager(models.Manager):
def get_queryset(self):
return super().get_queryset().filter(myfield=myvalue)
class TenantModel(TenantModelMixin, models.Model):
objects = TenantManager()
class Meta:
abstract = True
I use the abstract TenantModel as a mixin with another model to apply the TenantManager. E.g.
class MyModel(TenantModel):
This works as expected, applying the TenantManager filter every time MyModel.objects.all() is called when inside a view.
However, when I create a ModelForm with the model, the filter is not applied and all results (without the filter are returned. For example:
class AddPersonForm(forms.ModelForm):
class Meta:
model = MyModel
fields = ('person', )
Why is this and how to I ensure the ModelManager is applied to the queryset in ModelForm?
Edit
#Willem suggests the reason is forms use ._base_manager and not .objects (although I can not find this in the Django source code), however the docs say not to filter this kind of manager, so how does one filter form queries?
Don’t filter away any results in this type of manager subclass
This
manager is used to access objects that are related to from some other
model. In those situations, Django has to be able to see all the
objects for the model it is fetching, so that anything which is
referred to can be retrieved.
If you override the get_queryset() method and filter out any rows,
Django will return incorrect results. Don’t do that. A manager that
filters results in get_queryset() is not appropriate for use as a base
manager.
You can do it in two ways:
First: When creating the form instance, add the queryset for the desired field.
person_form = AddPersonForm()
person_form.fields["myfield"].queryset = TenantModel.objects.filter(myfield="myvalue")
Second: Override the field's queryset in the AddPersonForm itself.
class AddPersonForm(forms.ModelForm):
class Meta:
model = MyModel
fields = ('person', )
def __init__(self, *args, **kwargs):
super(AddPersonForm, self).__init__(*args, **kwargs)
self.fields['myfield'].queryset = TenantModel.objects.filter(myfield="myvalue")
I'm not sure why your code doesn't properly works. Probably you haven't reload django app. You could load queryset in __init__ of your form class
class AddPersonForm(forms.ModelForm):
person = forms.ModelMultipleChoiceField(queryset=None)
class Meta:
model = MyOtherModel
fields = ('person', )
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['person'].queryset = MyModel.objects.all()

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

Modify Django Rest Framework ModelViewSet behavior

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").

How can I apply a filter to a nested resource in Django REST framework?

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