I'm confused how to implement methods in serializers and views in DRF:
I have an account model extending AbstractBaseUser. The viewset looks like this:
class AccountViewSet(viewsets.ModelViewSet):
lookup_field = 'username'
queryset = Account.objects.all()
serializer_class = AccountSerializer
def get_permissions(self):
if self.request.method in permissions.SAFE_METHODS:
return (permissions.AllowAny(), TokenHasReadWriteScope())
if self.request.method == 'POST':
return (permissions.AllowAny(), TokenHasReadWriteScope())
return (permissions.IsAuthenticated(), IsAccountOwner(), TokenHasReadWriteScope())
def create(self, request):
serializer = self.serializer_class(data=request.data)
if serializer.is_valid():
Account.objects.create_user(**serializer.validated_data)
return Response(serializer.validated_data, status=status.HTTP_201_CREATED)
return Response({
'status': 'Bad request',
'message': 'Account could not be created with received data.'
}, status=status.HTTP_400_BAD_REQUEST)
The serializer like this:
class AccountSerializer(serializers.ModelSerializer):
password = serializers.CharField(write_only=True, required=False)
confirm_password = serializers.CharField(write_only=True, required=False)
class Meta:
model = Account
fields = ('id', 'email', 'username', 'created_at', 'updated_at',
'first_name', 'last_name', 'tagline', 'password',
'confirm_password',)
read_only_fields = ('created_at', 'updated_at',)
def create(self, validated_data):
return Account.objects.create(**validated_data)
def update(self, instance, validated_data):
instance.username = validated_data.get('username', instance.username)
instance.save()
password = validated_data.get('password', None)
confirm_password = validated_data.get('confirm_password', None)
if password and confirm_password:
instance.set_password(password)
instance.save()
update_session_auth_hash(self.context.get('request'), instance)
return instance
def validate(self, data):
if data['password'] and data['confirm_password'] and data['password'] == data['confirm_password']:
try:
validate_password(data['password'], user=data['username']):
return data
except ValidationError:
raise serializers.ValidationError("Password is not valid.")
raise serializers.ValidationError("Passwords do not match.")
On the create method for the view, it checks if the serializer is valid then saves it and returns responses depending on the outcome. My first question is when is the serializer create() method called? To me it seems that the method is bypassed altogether by calling create_user (a model method) in the view's create() method. Does it get called at all? What is the point of having it?
Second, I'm having trouble returning a status code from the update method, the instance is saved in the serializer. Will the code inside serializer update() work if the validation fails?
Here is what I have so far:
def update(self, request, pk=None):
serializer = self.serializer_class(data=request.data)
if serializer.is_valid():
<< what goes here??? >>
return Response(serializer.validated_data, status=status.HTTP_200_OK)
except serializers.ValidationError as e:
return Response({
'status': 'Bad request',
'message': str(e)
}, status=status.HTTP_400_BAD_REQUEST)
return Response({
'status': 'Bad request',
'message': 'Account could not be updated with received data.'
}, status=status.HTTP_400_BAD_REQUEST)
I desperately need some clarification. I'm unsure how to request flows through the view/serializer methods and I'm not sure how I can save the instance in the serializer and decide which response to return in the view at the same time.
EDIT:
I removed the create and update methods and fixed get_permissions for AccountViewSet and I added username validation to validate as you suggested. I also updated the serializer create and update methods, here are the new versions:
def create(self, validated_data):
instance = super(AccountSerializer, self).create(validated_data)
instance.set_password(validated_data['password'])
instance.save()
return instance
def update(self, instance, validated_data):
instance.username = validated_data.get('username', instance.username)
password = validated_data.get('password', None)
confirm_password = validated_data.get('confirm_password', None)
if password and confirm_password:
instance.set_password(password)
instance.save()
update_session_auth_hash(self.context.get('request'), instance)
else:
instance.save()
return instance
My only questions are is it necessary to call set_password after create? Does't create set the password for the new user? And is it okay to have no code in the view for create and update? Where does serializer.save() get called without the view code and when does the serializer validate run without a call to serializer.is_valid()?
In your create() method in AccountViewSet class, you are creating Account instance when serializer validation passes. Instead, you should be calling serializer.save().
If you have a look at save() method in BaseSerializer class you'll see that it calls either create() or update() method, depending on whether model instance is being created or updated. Since you are not calling serializer.save() in AccountViewSet.create() method, the AccountSerializer.create() method is not being called. Hope this answers your first question.
The answer to your second question too, is a missing serializer.save(). Replace << what goes here??? >> with serializer.save(). This (as I explained above), will call AccountSerializer.update() method.
AccountViewSet:
you do not need .create() and .update() methods, from your example - existing ones should be sufficient
get_permissions() - first "if" is opening your system too wide imho should be removed - you allow anyone to do POST - "aka" create new account, this is ok, but everything else (like GET or PUT) - should be allowed only for the account owner or (if there is a need!) registered users
AccountSerializer:
API will return HTTP400 on failed validation
make sure that id field is readonly, you do not want someone to overwrite existing users
existing create() method can be removed, but I think your's should looks like:
def create(self, validated_data):
instance = super(AccountSerializer, self).create(validated_data)
instance.set_password(validated_data['password'])
instance.save()
return instance
existing update() method... not sure what you wanted, but:
first line is permitting user to change it's username, without validation e.g. username is unique, even more, you do not check at all what is in the username field passed from the request, it may be even empty string or None - fallback on dictionary.get will be called only when key is missing in the dictionary,
if uniqueness of username will be validated on the db level (unique=True in the model's field definition) - you will get strange exception instead of nice error message for the API)
next is validation of the passwords (again) - that was just tested in validate method
after setting user password you save instance.. second time - maybe it is worth to optimize it and have only one save?
if you do not allowing to update all of the fields, maybe it is a good idea to pass update_fields to save() to limit which fields are updated?
validation - just add username validations ;)
Just to quickly understand DRF (very simplified) - Serializers are API's Forms, ViewSets - generic Views, renderers are templates (decide how data is displayed)
--edit--
few more items regarding viewsets, ModelViewSet consist of:
mixins.CreateModelMixin - calling validate & create -> serializer.save -> serializer.create
mixins.RetrieveModelMixin - "get" instance
mixins.UpdateModelMixin - calling validate & update/partial update -> serializer.save -> serializer.update
mixins.DestroyModelMixin - calling instance delete
mixins.ListModelMixin - "get" list of instances (browse)
Related
I am having an issue when using the API to send an update to an existing record.
When I send the API for a new record, it works perfectly. But when I send it for an existing record, I would like it to update the current record, but it just gives me an integrity error instead.
My Serializers.py looks like this:
class PartSerializer(serializers.ModelSerializer):
part = serializers.CharField()
class Meta:
model = DocumentRef
fields = ('part', 'field1', 'field2', 'field3')
def create(self, validated_data):
part = Part.objects.get(part_number=validated_data['part'])
validated_data['part'] = part
return DocumentRef.objects.update_or_create(**validated_data)
I have tried changing update_or_create to just create or just update but it will still only work if the record does not exist yet.
The model it should be referencing is DocumentRef, which looks like this:
class DocumentRef(models.Model):
part = models.OneToOneField(Part, on_delete=models.CASCADE)
field1 = models.FileField(upload_to='mcp/')
field2 = models.FileField(upload_to='qcp/')
field3 = models.FileField(upload_to='cus/')
The API View I am using is this:
class APIDetailTest(APIView):
def get_object(self, pk):
try:
return DocumentRef.objects.get(pk=pk)
except DocumentRef.DoesNotExist:
return HttpResponse(status=status.HTTP_404_NOT_FOUND)
def get(self, request, pk):
part = self.get_object(pk)
serializer = PartSerializer(part)
return Response(serializer.data)
def put(self, request, pk):
part = self.get_object(pk)
serializer = PartSerializer(part, data=request.data)
if serializer.is_valid():
serializer.save()
return Response(serializer.data)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
Edit: Changed create_or_update to update_or_create -- Just made this error in this post, in my code it was correct from the beginning.
Edit2: Have also tried changing the return value to:
return DocumentRef.objects.update_or_create(defaults={'part_id': part.id}, field1=validated_data['field1'], field2=validated_data['field2'], field3=validated_data['field3']) but that still gives the unique constraint failed error.
This is more of a workaround than an answer, but you could try catching the error and treating the request differently.
something like this:
def create(self, validated_data):
...
try:
return DocumentRef.objects.create(**validated_data)
except IntegrityError:
DocumentRef.objects.filter(part=validated_data['part']).delete()
return DocumentRef.objects.create(**validated_data)
obviously, this is not updating the record. Just deleting the existing one and making a new one.
Try using it with defaults and kwargs please read here: django docs
The update_or_create method tries to fetch an object from database
based on the given kwargs. If a match is found, it updates the fields
passed in the defaults dictionary.
You need to update your query like this
def create(self, validated_data):
part = Part.objects.get(part_number=validated_data['part'])
return DocumentRef.objects.update_or_create(defaults={'part': part}, **validated_data)
I have a form that takes "username" from the User model and my own "email" field. I want to change this data for the User model. At first glance everything seems to work fine, the name changes and the mail is the same. But if I only change the mail and I don't touch the username, I get an error: "A user with this name already exists.
file views.py:
form=UserUpdateForm(request.POST)
if form.is_valid():
user=User.objects.get(username=self.request.user)
user.username=form.cleaned_data.get('username')
user.email=form.cleaned_data.get('email')
user.save()
file forms.py:
class UserUpdateForm(forms.ModelForm):
email = forms.EmailField(required=False)
def __init__(self, *args, **kwargs):
super(UserUpdateForm, self).__init__(*args, **kwargs)
if 'label_suffix' not in kwargs:
kwargs['label_suffix'] = '*'
self.fields['username'].widget = forms.TextInput(attrs={'class':'input-text'})
self.fields['email'].widget = forms.EmailInput(attrs={'class':'input-text'})
class Meta:
model = User
fields = ("username","email",)
def clean_email(self):
cleaned_data = super(UserUpdateForm,self).clean()
email=cleaned_data.get('email')
return email
From the doc,
A subclass of ModelForm can accept an existing model instance as the keyword argument instance; if this is supplied, save() will update that instance. If it’s not supplied, save() will create a new instance of the specified model
If you are updating the data, you have to pass the instance to the form as,
# on updationg
form = UserUpdateForm(data= request.POST, instance=your_mode_instance)
Since you are not passing the instance for the second time, Django thinks that the operation is a row insert instead of row update
I have a create user view and here I first register a normal user and then create a player object for that user which has a fk relation with the user.
In my case, I have three different types of users
I created a view to handle register all three different types of users, but my player user has a lot of extra model fields and storing all query params in variables will make it messy.
Is there a better way to handle this, including validation?
TLDR; I created a view to handle register all three different types of users, but my player user has a lot of extra model fields and storing all query params in variables will make it messy. Is there a better way to handle this, including validation?
This is my view.
class CreateUser(APIView):
"""
Creates the User.
"""
def post(self, request):
email = request.data.get('email', None).strip()
password = request.data.get('password', None).strip()
name = request.data.get('name', None).strip()
phone = request.data.get('phone', None)
kind = request.query_params.get('kind', None).strip()
print(kind)
serializer = UserSerializer(data={'email': email, 'password':password})
serializer.is_valid(raise_exception=True)
try:
User.objects.create_user(email=email,
password=password)
user_obj = User.objects.get(email=email)
except:
raise ValidationError('User already exists')
if kind == 'academy':
Academy.objects.create(email=email, name=name, phone=phone, user=user_obj)
if kind == 'coach':
Coach.objects.create(email=email, name=name, phone=phone, user=user_obj)
if kind == 'player':
Player.objects.create(----------)
return Response(status=200)
Use a Model Serializer
In your case, define it in serializers.py like this:
from rest_framework import serializers
class CustomBaseSerializer(serializers.ModelSerializer):
def create(self, validated_data):
validated_data['user'] = self.context['user']
return super(CustomBaseSerializer, self).create(validated_data)
class PlayerSerializer(CustomBaseSerializer):
class Meta:
model = Player
fields = ('count', 'height', 'right_handed', 'location',
'size', 'benchmark_swingspeed',
'benchmark_impactspeed', 'benchmark_stance',
'benchmark_balanceindex',)
class AcademySerializer(CustomBaseSerializer):
class Meta:
model = Academy
fields = '__all__' # Usually better to explicitly list fields
class CoachSerializer(CustomBaseSerializer):
class Meta:
model = Coach
fields = '__all__'
Then in your view
class CreateUser(APIView):
"""
Creates the User.
"""
def post(self, request):
print(kind)
try:
user = User.objects.get(email=request.data.get('email'))
except User.DoesNotExist:
pass
else:
raise ValidationError('User already exists')
user_serializer = UserSerializer(data=request.data)
user_serializer.is_valid(raise_exception=True)
user = user_serializer.save()
if kind == 'academy':
serializer_class = AcademySerializer
if kind == 'coach':
serializer_class = CoachSerializer
if kind == 'player':
serializer_class = PlayerSerializer
serializer = serializer_class(data=request.data, context={'user': user})
serializer.save()
return Response(serializer.data) # Status is 200 by default so you don't need to include it. RESTful API's should return the instance created, this also delivers the newly generated primary key back to the client.
# Oh and if you do serialize the object in the response, write serializers for academy and coach too, so the api response is consistent
Serializers are really powerful and useful. It is well worth thoroughly reading the docs.
First, I'd recommend to POST the parameters in a JSON or a form, instead of using the query params. But regardless the method, the solution is pretty much the same.
First, you could define the fields you're interested in a list. For example:
FIELDS = (
'count',
'height',
'right_handed',
'location',
'size',
'benchmark_swingspeed',
'benchmark_impactspeed',
'benchmark_stance',
'benchmark_balanceindex',
)
And then get all the values from the query params and store them in a dict, like:
player_params = {}
for field in FIELDS:
player_params[field] = request.query_params.get(field)
Now you have all the params required for a player in a dict and you can pass it to the Player model as **kwargs. Of course you'll probably need some validation. But in the end, you'll be able to do the following:
Player.objects.create(user=user_obj, **player_params)
I'm trying to enforce a permission with Django Rest Framework where a specific user cannot post an object containing a user id which is not his.
For example i don't want a user to post a feedback with another id.
My model is something like :
class Feedback(Model):
user = ForeignKey(User)
...
I try to put a permission on my view which would compare the feedback.user.id with the request.user.id, the right work ok on a post on an object and return false, but it's still posting my object... Why?
The View
class FeedbackViewSet(ModelViewSet):
model = Feedback
permission_classes = (IsSelf,)
serializer_class = FeedbackSerializer
def get_queryset(self):
....
The Permission
class IsSelf(permissions.BasePermission):
def has_object_permission(self, request, view, obj):
#return eval(obj.user.id) == request.user.id
return False
I've commented the line to show where the problem lies.
Again the function is correctly called and returns False, but there's just no PermissionDenied raised.
While at it, i'm wondering if this is actually the way to implement this behaviour, and if not, what would be...?
Thanks.
Your problem is that has_object_permission is only called if you're trying to access a certain object. So on creation it is never actually used.
I'd suggest you do the check on validation. Example:
class FeedbackSerializer(HyperlinkedModelSerializer):
def validate(self, attrs):
user = self.context['request'].user
if attrs['user'].id != user.id:
raise ValidationError('Some exception message')
return attrs
If you have some other super serializer class then just change it.
Now that I think of it if the user field must always be the posting user, then you should just make that field read-only and set it on pre_save() in the viewset class.
class FeedbackViewSet(ModelViewSet):
def pre_save(self, obj, *args, **kwargs):
if self.action == 'create':
obj.user = self.request.user
And in the serializer set the user field read-only
class FeedbackSerializer(HyperlinkedModelSerializer):
user = serializers.HyperlinkedRelatedField(view_name='user-detail', read_only=True)
....
I don't know if this is still open...
However, in order to work, you should move that line from "has_object_permission" to "has_permission", something like this:
class IsSelf(permissions.BasePermission):
def has_permission(self, request, view, obj):
if request.method == 'POST':
#your condition
Worked for me.
As it was stated in the selected answer
has_object_permission is only called if you're trying to access a certain object
so you have to place your condition under has_permission instead.
I'm trying to make my User model RESTful via Django Rest Framework API calls, so that I can create users as well as update their profiles.
However, as I go through a particular verification process with my users, I do not want the users to have the ability to update the username after their account is created. I attempted to use read_only_fields, but that seemed to disable that field in POST operations, so I was unable to specify a username when creating the user object.
How can I go about implementing this? Relevant code for the API as it exists now is below.
class UserSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = User
fields = ('url', 'username', 'password', 'email')
write_only_fields = ('password',)
def restore_object(self, attrs, instance=None):
user = super(UserSerializer, self).restore_object(attrs, instance)
user.set_password(attrs['password'])
return user
class UserViewSet(viewsets.ModelViewSet):
"""
API endpoint that allows users to be viewed or edited.
"""
serializer_class = UserSerializer
model = User
def get_permissions(self):
if self.request.method == 'DELETE':
return [IsAdminUser()]
elif self.request.method == 'POST':
return [AllowAny()]
else:
return [IsStaffOrTargetUser()]
Thanks!
It seems that you need different serializers for POST and PUT methods. In the serializer for PUT method you are able to just except the username field (or set the username field as read only).
class UserViewSet(viewsets.ModelViewSet):
"""
API endpoint that allows users to be viewed or edited.
"""
serializer_class = UserSerializer
model = User
def get_serializer_class(self):
serializer_class = self.serializer_class
if self.request.method == 'PUT':
serializer_class = SerializerWithoutUsernameField
return serializer_class
def get_permissions(self):
if self.request.method == 'DELETE':
return [IsAdminUser()]
elif self.request.method == 'POST':
return [AllowAny()]
else:
return [IsStaffOrTargetUser()]
Check this question django-rest-framework: independent GET and PUT in same URL but different generics view
Another option (DRF3 only)
class MySerializer(serializers.ModelSerializer):
...
def get_extra_kwargs(self):
extra_kwargs = super(MySerializer, self).get_extra_kwargs()
action = self.context['view'].action
if action in ['create']:
kwargs = extra_kwargs.get('ro_oncreate_field', {})
kwargs['read_only'] = True
extra_kwargs['ro_oncreate_field'] = kwargs
elif action in ['update', 'partial_update']:
kwargs = extra_kwargs.get('ro_onupdate_field', {})
kwargs['read_only'] = True
extra_kwargs['ro_onupdate_field'] = kwargs
return extra_kwargs
Another method would be to add a validation method, but throw a validation error if the instance already exists and the value has changed:
def validate_foo(self, value):
if self.instance and value != self.instance.foo:
raise serializers.ValidationError("foo is immutable once set.")
return value
In my case, I wanted a foreign key to never be updated:
def validate_foo_id(self, value):
if self.instance and value.id != self.instance.foo_id:
raise serializers.ValidationError("foo_id is immutable once set.")
return value
See also: Level-field validation in django rest framework 3.1 - access to the old value
My approach is to modify the perform_update method when using generics view classes. I remove the field when update is performed.
class UpdateView(generics.UpdateAPIView):
...
def perform_update(self, serializer):
#remove some field
rem_field = serializer.validated_data.pop('some_field', None)
serializer.save()
I used this approach:
def get_serializer_class(self):
if getattr(self, 'object', None) is None:
return super(UserViewSet, self).get_serializer_class()
else:
return SerializerWithoutUsernameField
UPDATE:
Turns out Rest Framework already comes equipped with this functionality. The correct way of having a "create-only" field is by using the CreateOnlyDefault() option.
I guess the only thing left to say is Read the Docs!!!
http://www.django-rest-framework.org/api-guide/validators/#createonlydefault
Old Answer:
Looks I'm quite late to the party but here are my two cents anyway.
To me it doesn't make sense to have two different serializers just because you want to prevent a field from being updated. I had this exact same issue and the approach I used was to implement my own validate method in the Serializer class. In my case, the field I don't want updated is called owner. Here is the relevant code:
class BusinessSerializer(serializers.ModelSerializer):
class Meta:
model = Business
pass
def validate(self, data):
instance = self.instance
# this means it's an update
# see also: http://www.django-rest-framework.org/api-guide/serializers/#accessing-the-initial-data-and-instance
if instance is not None:
originalOwner = instance.owner
# if 'dataOwner' is not None it means they're trying to update the owner field
dataOwner = data.get('owner')
if dataOwner is not None and (originalOwner != dataOwner):
raise ValidationError('Cannot update owner')
return data
pass
pass
And here is a unit test to validate it:
def test_owner_cant_be_updated(self):
harry = User.objects.get(username='harry')
jack = User.objects.get(username='jack')
# create object
serializer = BusinessSerializer(data={'name': 'My Company', 'owner': harry.id})
self.assertTrue(serializer.is_valid())
serializer.save()
# retrieve object
business = Business.objects.get(name='My Company')
self.assertIsNotNone(business)
# update object
serializer = BusinessSerializer(business, data={'owner': jack.id}, partial=True)
# this will be False! owners cannot be updated!
self.assertFalse(serializer.is_valid())
pass
I raise a ValidationError because I don't want to hide the fact that someone tried to perform an invalid operation. If you don't want to do this and you want to allow the operation to be completed without updating the field instead, do the following:
remove the line:
raise ValidationError('Cannot update owner')
and replace it with:
data.update({'owner': originalOwner})
Hope this helps!
More universal way to "Disable field update after object is created"
- adjust read_only_fields per View.action
1) add method to Serializer (better to use your own base cls)
def get_extra_kwargs(self):
extra_kwargs = super(BasePerTeamSerializer, self).get_extra_kwargs()
action = self.context['view'].action
actions_readonly_fields = getattr(self.Meta, 'actions_readonly_fields', None)
if actions_readonly_fields:
for actions, fields in actions_readonly_fields.items():
if action in actions:
for field in fields:
if extra_kwargs.get(field):
extra_kwargs[field]['read_only'] = True
else:
extra_kwargs[field] = {'read_only': True}
return extra_kwargs
2) Add to Meta of serializer dict named actions_readonly_fields
class Meta:
model = YourModel
fields = '__all__'
actions_readonly_fields = {
('update', 'partial_update'): ('client', )
}
In the example above client field will become read-only for actions: 'update', 'partial_update' (ie for PUT, PATCH methods)
This post mentions four different ways to achieve this goal.
This was the cleanest way I think: [collection must not be edited]
class DocumentSerializer(serializers.ModelSerializer):
def update(self, instance, validated_data):
if 'collection' in validated_data:
raise serializers.ValidationError({
'collection': 'You must not change this field.',
})
return super().update(instance, validated_data)
Another solution (apart from creating a separate serializer) would be to pop the username from attrs in the restore_object method if the instance is set (which means it's a PATCH / PUT method):
def restore_object(self, attrs, instance=None):
if instance is not None:
attrs.pop('username', None)
user = super(UserSerializer, self).restore_object(attrs, instance)
user.set_password(attrs['password'])
return user
If you don't want to create another serializer, you may want to try customizing get_serializer_class() inside MyViewSet. This has been useful to me for simple projects.
# Your clean serializer
class MySerializer(serializers.ModelSerializer):
class Meta:
model = MyModel
fields = '__all__'
# Your hardworking viewset
class MyViewSet(MyParentViewSet):
serializer_class = MySerializer
model = MyModel
def get_serializer_class(self):
serializer_class = self.serializer_class
if self.request.method in ['PUT', 'PATCH']:
# setting `exclude` while having `fields` raises an error
# so set `read_only_fields` if request is PUT/PATCH
setattr(serializer_class.Meta, 'read_only_fields', ('non_updatable_field',))
# set serializer_class here instead if you have another serializer for finer control
return serializer_class
setattr(object, name, value)
This is the counterpart of getattr(). The
arguments are an object, a string and an arbitrary value. The string
may name an existing attribute or a new attribute. The function
assigns the value to the attribute, provided the object allows it. For
example, setattr(x, 'foobar', 123) is equivalent to x.foobar = 123.
class UserUpdateSerializer(UserSerializer):
class Meta(UserSerializer.Meta):
fields = ('username', 'email')
class UserViewSet(viewsets.ModelViewSet):
def get_serializer_class(self):
return UserUpdateSerializer if self.action == 'update' else super().get_serializer_class()
djangorestframework==3.8.2
I would suggest also looking at Django pgtrigger
This allows you to install triggers for validation. I started using it and was very pleased with its simplicity:
Here's one of their examples that prevents a published post from being updated:
import pgtrigger
from django.db import models
#pgtrigger.register(
pgtrigger.Protect(
operation=pgtrigger.Update,
condition=pgtrigger.Q(old__status='published')
)
)
class Post(models.Model):
status = models.CharField(default='unpublished')
content = models.TextField()
The advantage of this approach is it also protects you from .update() calls that bypass .save()