Recently started working with Django REST framework. I have a user table and another table that stores some relevant data for each user. I've set up POST/GET/DELETE methods fine, but I can't get the method for perform_update working - I keep getting a KeyError at /api/sdgdata/1
'user_id' error in Postman when I attempt a put request for a user. Plz see code below:
Models:
class TestDataTwo(models.Model):
user = models.ForeignKey("auth.User", related_name="testdatatwo", on_delete=models.CASCADE)
test_name = models.CharField(max_length=1024, null=True, default="N/A")
Serializers:
class TestDataTwoSerializer(serializers.ModelSerializer):
class Meta:
model = TestDataTwo
fields = (
"id",
"test_name",
)
def update(self, instance, validated_data):
# get user id from validated data:
user_id = validated_data.pop('user_id')
# get user:
user = User.objects.get(id=user_id)
# set user on instance:
instance.user = user
instance.save()
# continue with update method:
super().update(instance, validated_data)
Views:
class TestDataTwoViewSet(ModelViewSet):
queryset = TestDataTwo.objects.all().order_by('id')
serializer_class = TestDataTwoSerializer
paginator = None
# CREATE NEW TESTDATATWO ROW FOR USER
def perform_create(self, serializer):
serializer.save(user=self.request.user)
# GET ALL ENTRIES OF TESTDATATWO FOR SPECIFIC USER, EXCEPT IF SUPERUSER, THEN RETURN ALL
def get_queryset(self):
# if self.request.user.is_superuser:
# return self.queryset
# else:
return self.queryset.filter(user=self.request.user)
def perform_update(self, serializer):
instance = self.get_object()
serializer.save(user=self.request.user)
return Response(serializer.data, status=status.HTTP_200_OK)
# DELETE TESTDATATWO ID
def destroy(self, request, *args, **kwargs):
instance = self.get_object()
self.perform_destroy(instance)
return Response(status=status.HTTP_204_NO_CONTENT)
Postman GET:
Postman PUT:
I've tried a number of variations on the perform_update method, but I am guessing I am missing a reference to the user's user_id somehow...Appreciate any help.
This error is caused by validated_data.pop('user_id'), since user_id was not defined as a field on the serializer nor was it passed in the request data. It seems you are aiming to save the current user as the user field of the TestDataTwo instance being updated.
In that case you don't need to override update, since saving the user based on self.request.user on a TestDataTwo instance is already done by the serializer's save here:
def perform_update(self, serializer):
serializer.save(user=self.request.user)
This means that you don't have to do any processing in the serializer anymore, so it can be just:
class TestDataTwoSerializer(serializers.ModelSerializer):
class Meta:
model = TestDataTwo
fields = ("id", "test_name",)
# No need to override update for saving self.request.user to `TestDataTwo`'s user
For more information on this functionality, you can have a read here..[DRF-docs-Passing-additional-attributes-to-save].
Related
So I have a model like this.
class Member(BaseModel):
objects = models.Manager()
user = models.ForeignKey('api_backend.User', db_index=True, on_delete=models.CASCADE)
cluster = models.ForeignKey('api_backend.Cluster', on_delete=models.CASCADE)
And a generic api view for the same.
lass MemberPutRetrieveUpdateDeleteView(PutAsCreateMixin, MultipleFieldLookupMixin, generics.RetrieveUpdateDestroyAPIView):
queryset = api_models.Member.objects.all()
permission_classes = [permissions.IsAuthenticated, IsMemberOrKickMembers]
lookup_fields = ['user', 'cluster']
def get_serializer_class(self):
if self.request.method in ['PUT']:
return api_serializers.PartialMemberSerializer
return api_serializers.MemberSerializer
def destroy(self, request, *args, **kwargs):
member = self.get_object()
if member.cluster.owner == member.user:
raise exceptions.ValidationError("cannot delete membership with this cluster as you own it.")
return super(MemberPutRetrieveUpdateDeleteView, self).destroy(request, *args, **kwargs)
I am currently using these mixins.
class PutAsCreateMixin(object):
"""
The following mixin class may be used in order to support PUT-as-create
behavior for incoming requests.
"""
def update(self, request, **kwargs):
partial = kwargs.pop('partial', False)
instance = self.get_object_or_none()
serializer = self.get_serializer(instance, data=request.data, partial=partial)
serializer.is_valid(raise_exception=True)
if instance is None:
if not self.lookup_fields:
lookup_url_kwarg = self.lookup_url_kwarg or self.lookup_field
lookup_value = self.kwargs[lookup_url_kwarg]
extra_kwargs = {self.lookup_field: lookup_value}
else:
# add kwargs for additional fields
extra_kwargs = {field: self.kwargs[field] for field in self.lookup_fields if self.kwargs[field]}
serializer.save(**extra_kwargs)
return Response(serializer.data, status=201)
serializer.save()
return Response(serializer.data)
def partial_update(self, request, *args, **kwargs):
kwargs['partial'] = True
return self.update(request, *args, **kwargs)
def get_object_or_none(self):
try:
return self.get_object()
except Http404:
if self.request.method == 'PUT':
# For PUT-as-create operation, we need to ensure that we have
# relevant permissions, as if this was a POST request. This
# will either raise a PermissionDenied exception, or simply
# return None.
self.check_permissions(clone_request(self.request, 'POST'))
else:
# PATCH requests where the object does not exist should still
# return a 404 response.
raise
class MultipleFieldLookupMixin(object):
"""
Apply this mixin to any view or viewset to get multiple field filtering
based on a `lookup_fields` attribute, instead of the default single field filtering.
"""
def get_object(self):
queryset = self.get_queryset() # Get the base queryset
queryset = self.filter_queryset(queryset) # Apply any filter backends
filter = {}
for field in self.lookup_fields:
if self.kwargs[field]: # Ignore empty fields.
filter[field] = self.kwargs[field]
obj = get_object_or_404(queryset, **filter) # Lookup the object
self.check_object_permissions(self.request, obj)
return obj
So, in my serializer, I have multiple lookup fields -user and cluster. Both of these are foreign keys and have their own kwargs in the url.
So my api url is like this.
path('clusters/<int:cluster>/members/<int:user>/', views.MemberPutRetrieveUpdateDeleteView.as_view())
and I would expect a sample url to be like this:
'clusters/3/members/2/'
where 1 is the id of the cluster and 2 is the id of the member.
So basically a put request to this url must create a member which has:
an user foreign key of id 2
a cluster foreign key of id 3
But when trying to create the same with the mixin, I get the following error.
in __set__
self.field.remote_field.model._meta.object_name,
ValueError: Cannot assign "2": "Member.user" must be a "User" instance.
How can I fix this error? Can someone please help me?
thanks a lot!
The answer is actually very simple,
I hope this will save a lot of someone's time.
I had to change the lookup fields to the following
lookup_fields = ['user_id', 'cluster_id']
and the url kwargs to the following
'clusters/<int:cluster_id>/members/<int:user_id>/'
this way, django knows that only the id of the foreign key is present in the url, and not the foreign key object itself. When perform-creating with the lookup fields, everything works just as expected!
I have a Viewset where a user can create reviews for a specific product and list their all the reviews they have created for all products:
class ProductReviewViewset(CreateModelMixin, ListModelMixin, GenericViewSet):
serializer_class = ProductReviewSerializer
def get_queryset(self):
# Return all reviews for user that are not marked as deleted
user = self.request.user
return ProductReview.objects.filter(user=user, deleted=False).order_by('review__name')
def perform_create(self, serializer):
# Add user to review object before saving
user = self.request.user
serializer.save(user=self.request.user)
The problem is that this allows the user to create multiple reviews for the same object. I wanted to prevent this, and only allow to create a review if there is no other review already created for that product (reviews marked as "deleted" will not be counted).
So far I was able to override the .create() method and do the serialization, validation and checking for duplicates in there.
class ProductReviewViewset(CreateModelMixin, ListModelMixin, GenericViewSet):
serializer_class = ProductReviewSerializer
def get_queryset(self):
# Return all reviews for user that are not marked as deleted
user = self.request.user
return ProductReview.objects.filter(user=user, deleted=False).order_by('review__name')
def create(self, request, *args, **kwargs):
# Override create method to prevent duplicate object creation
serializer = ProductReviewSerializer(data=self.request.data)
serializer.is_valid(raise_exception=True)
user = self.request.user
product = serializer.validated_data['product']
exists = ProductReview.objects.filter(user=user, product=product, deleted=False).exists()
if not exists:
serializer.save(user=user)
return Response(status=status.HTTP_201_CREATED)
else:
return Response(status=status.HTTP_409_CONFLICT)
Is there an easier or better way of doing this? Or is this the way to do it?
Thanks!
An alternative would be using get_or_create queryset method : get_or_create
def create(self, request, *args, **kwargs):
# Override create method to prevent duplicate object creation
serializer = ProductReviewSerializer(data=self.request.data)
serializer.is_valid(raise_exception=True)
user = self.request.user
product = serializer.validated_data['product']
obj, created = ProductReview.objects.get_or_create(user=user, product=product, deleted=False, defaults=seriaizer.validated_data)
if not created:
serializer.save(user=user)
return Response(status=status.HTTP_201_CREATED)
else:
return Response(status=status.HTTP_409_CONFLICT)
the get_or_create will prevent race conditions, when request made in parallel
I have a project that autenticate by oauth2_provider.ext.rest_framework.OAuth2Authentication. There is a token in the request headers for authenticating and identifying. I think I should not include user id in the request data explicitly when process a create action. But serializer need user info to create a new instance. So I include a user_id field in serializer, and put the value into the request.data dict after authenticating.
Is it good? Is there any better way?
serializers.py
class serializer(serializers.Serializer):
user = UserSerializer(read_only=True)
user_id = serializers.UUIDField(write_only=True)
content = serializers.CharField()
views.py
class CommentList(generics.ListCreateAPIView):
def create(self, request, *args, **kwargs):
request.data['user_id'] = request.user.id
return super(CommentList, self).create(request)
It is cleaner to override create(validated_data) in your serializer:
class CommentSerializer(serializers.Serializer):
...
def create(self, validated_data):
user = self.context['request'].user
comment = Comment.objects.create(
user=user,
**validated_data
)
return comment
See http://www.django-rest-framework.org/api-guide/serializers/#saving-instances
Then you do not need to customise your view, you can just use a generic view.
if user is authenticated then grab the value from request object and pass it to serializers by using get_serializer_context method
class CommentList(generics.ListCreateAPIView):
permission_classes = [IsAuthenticated]
def get_serializer_context(self):
return {'user_id':self.request.user.id}
class serializer(serializers.Serializer):
#your field, model
pass
def create(self, validated_data):
user_id = self.context['user_id']
return Comment.objects.create(user_id=user_id, **validated_data)
I'm working on a project using django-rest-framework. In my API view, an authenticated user can create other users. But, only five. Then if there are five users registered by one user, I want to send him in the response that hit the limit. Then, I need to get on my serializer the authenticated user but, I can't find a way to pass it from my ModelViewSet to my serializer.
This is my code:
View:
class ChildUserViewSet(viewsets.ModelViewSet):
serializer_class = ChildUserSerializer
queryset = User.objects.all()
authentication_classes = (
TokenAuthentication,
)
permission_classes = (
IsAuthenticated,
)
def perform_create(self, serializer):
account_group = self.request.user.userprofile.get_account_group
mobile_number = serializer.data.get('mobile_number')
password = serializer.data.get('password')
user = serializer.save()
user.set_password(password)
user.save()
# Generate user profile
UserProfile.objects.create(
user=user,
mobile_number=mobile_number,
user_type=CHILD,
related_account_group=account_group,
)
Serializer:
class ChildUserSerializer(serializers.ModelSerializer):
mobile_number = serializers.CharField()
class Meta:
model = User
fields = (
'first_name',
'last_name',
'email',
'password',
'mobile_number',
)
def validate(self, data):
"""
Check that the start is before the stop.
"""
# Get authenticated user for raise hit limit validation
def validate_email(self, value):
if User.objects.filter(email=value):
raise serializers.ValidationError("This field must be unique.")
return value
def create(self, validated_data):
username = generate_unique_username(
u'{0}{1}'.format(
validated_data['first_name'],
validated_data['last_name'],
)
)
user = User(
username=username,
first_name=validated_data['first_name'],
last_name=validated_data['last_name'],
email=validated_data['email'],
)
user.set_password(validated_data['password'])
user.save()
return user
Then, in the def validate(self, data) function of my serializer, I want to get the currently authenticated user.
How can I pass the request.user from my APIView to my serializer?
I found an even easier way of accomplishing this! It turns out that Rest Framework's GenericAPIView base class (from which all of Rest Framework's generic View classes descend) includes a function called get_serializer_context():
def get_serializer_context(self):
"""
Extra context provided to the serializer class.
"""
return {
'request': self.request,
'format': self.format_kwarg,
'view': self
}
As you can see, the returned context object contains the same request object that the View receives. This object then gets set when the serializer is initialized:
def get_serializer(self, *args, **kwargs):
"""
Return the serializer instance that should be used for validating and
deserializing input, and for serializing output.
"""
serializer_class = self.get_serializer_class()
kwargs['context'] = self.get_serializer_context()
return serializer_class(*args, **kwargs)
Thus to access the user who made the request, you just need to call self.context['request'].user from within your Serializer's validate_ function:
class TemplateSerializer(serializers.ModelSerializer):
def validate_parent(self, value):
print(self.context['request'].user)
return value
class Meta:
model = Template
And the best part is that you don't have to override anything in your ModelViewSet, they can stay as simple as you want them to:
class TemplateViewSet(viewsets.ModelViewSet):
serializer_class = TemplateSerializer
permission_classes = [IsAdmin]
In your views when you initialize serializer like
serializer = ChildUserSerializer(data=request.DATA,context={'request':request})
,send a context which contains request.Then in Serializers inside function call
request=self.context['request']
Then you can access request.user.
You can pass additional context to your serializer with serializer = ChildUserSerializer(data, context={'request': request}). You can then access the authenticated user via request.user within your serializer validation method.
In djangorestframework > 3.2.4 the rest_framework.generic.GenericAPIView class includes the http request by default in the serializer context.
So inside your serializer you can access it by: self.context['request'] and the user self.context['request'].user
So your ChildUserSerializer will look like:
class ChildUserSerializer(serializers.ModelSerializer):
mobile_number = serializers.CharField()
....
def validate(self, data):
"""
Check that the start is before the stop.
"""
# Get authenticated user for raise hit limit validation
user = self.context['request'].user
# do something with the user here
def validate_email(self, value):
if User.objects.filter(email=value):
raise serializers.ValidationError("This field must be unique.")
return value
...
I am not very experience with Django REST framework and have been trying out many things but can not make my PATCH request work.
I have a Model serializer. This is the same one I use to add a new entry and ideally I Would want to re-use when I update an entry.
class TimeSerializer(serializers.ModelSerializer):
class Meta:
model = TimeEntry
fields = ('id', 'project', 'amount', 'description', 'date')
def __init__(self, user, *args, **kwargs):
# Don't pass the 'fields' arg up to the superclass
super(TimeSerializer, self).__init__(*args, **kwargs)
self.user = user
def validate_project(self, attrs, source):
"""
Check that the project is correct
"""
.....
def validate_amount(self, attrs, source):
"""
Check the amount in valid
"""
.....
I tried to use a class based view :
class UserViewSet(generics.UpdateAPIView):
"""
API endpoint that allows timeentries to be edited.
"""
queryset = TimeEntry.objects.all()
serializer_class = TimeSerializer
My urls are:
url(r'^api/edit/(?P<pk>\d+)/$', UserViewSet.as_view(), name='timeentry_api_edit'),
My JS call is:
var putData = { 'id': '51', 'description': "new desc" }
$.ajax({
url: '/en/hours/api/edit/' + id + '/',
type: "PATCH",
data: putData,
success: function(data, textStatus, jqXHR) {
// ....
}
}
In this case I would have wanted my description to be updated, but I get errors that the fields are required(for 'project'and all the rest). The validation fails. If add to the AJAX call all the fields it still fails when it haves to retrieve the 'project'.
I tried also to make my own view:
#api_view(['PATCH'])
#permission_classes([permissions.IsAuthenticated])
def edit_time(request):
if request.method == 'PATCH':
serializer = TimeSerializer(request.user, data=request.DATA, partial=True)
if serializer.is_valid():
time_entry = serializer.save()
return Response(status=status.HTTP_201_CREATED)
return Response(status=status.HTTP_400_BAD_REQUEST)
This did not work for partial update for the same reason(the validation for the fields were failing) and it did not work even if I've sent all the fields. It creates a new entry instead of editing the existing one.
I would like to re-use the same serializer and validations, but I am open to any other suggestions.
Also, if someone has a piece of working code (ajax code-> api view-> serializer) would be great.
class DetailView(APIView):
def get_object(self, pk):
return TestModel.objects.get(pk=pk)
def patch(self, request, pk):
testmodel_object = self.get_object(pk)
serializer = TestModelSerializer(testmodel_object, data=request.data, partial=True) # set partial=True to update a data partially
if serializer.is_valid():
serializer.save()
return JsonResponse(code=201, data=serializer.data)
return JsonResponse(code=400, data="wrong parameters")
Documentation
You do not need to write the partial_update or overwrite the update method. Just use the patch method.
Make sure that you have "PATCH" in http_method_names. Alternatively you can write it like this:
#property
def allowed_methods(self):
"""
Return the list of allowed HTTP methods, uppercased.
"""
self.http_method_names.append("patch")
return [method.upper() for method in self.http_method_names
if hasattr(self, method)]
As stated in documentation:
By default, serializers must be passed values for all required fields or they will raise validation errors. You can use the partial argument in order to allow partial updates.
Override update method in your view:
def update(self, request, *args, **kwargs):
instance = self.get_object()
serializer = TimeSerializer(instance, data=request.data, partial=True)
serializer.is_valid(raise_exception=True)
serializer.save(customer_id=customer, **serializer.validated_data)
return Response(serializer.validated_data)
Or just override method partial_update in your view:
def partial_update(self, request, *args, **kwargs):
kwargs['partial'] = True
return self.update(request, *args, **kwargs)
Serializer calls update method of ModelSerializer(see sources):
def update(self, instance, validated_data):
raise_errors_on_nested_writes('update', self, validated_data)
# Simply set each attribute on the instance, and then save it.
# Note that unlike `.create()` we don't need to treat many-to-many
# relationships as being a special case. During updates we already
# have an instance pk for the relationships to be associated with.
for attr, value in validated_data.items():
setattr(instance, attr, value)
instance.save()
return instance
Update pushes the validated_data values to the given instance. Note that update should not assume all the fields are available. This helps to deal with partial updates (PATCH requests).
The patch method is worked for me using viewset in DRF. I'm changing you code:
class UserViewSet(viewsets.ModelViewSet):
queryset = TimeEntry.objects.all()
serializer_class = TimeSerializer
def perform_update(self, serializer):
user_instance = serializer.instance
request = self.request
serializer.save(**modified_attrs)
return Response(status=status.HTTP_200_OK)
Use ModelViewSet instead and override perform_update method from UpdateModelMixin
class UserViewSet(viewsets.ModelViewSet):
queryset = TimeEntry.objects.all()
serializer_class = TimeSerializer
def perform_update(self, serializer):
serializer.save()
# you may also do additional things here
# e.g.: signal other components about this update
That's it. Don't return anything in this method. UpdateModelMixin has implemented update method to return updated data as response for you and also clears out _prefetched_objects_cache. See the source code here.
I ran into this issues as well, I solved it redefining the get_serializer_method and adding custom logic to handle the partial update. Python 3
class ViewSet(viewsets.ModelViewSet):
def get_serializer_class(self):
if self.action == "partial_update":
return PartialUpdateSerializer
Note: you may have to override the partial_update function on the serializer. Like so:
class PartialUpdateSerializer(serializers.Serializer):
def partial_update(self, instance, validated_data):
*custom logic*
return super().update(instance, validated_data)
Another posiblity is to make the request by URL. For example, I have a model like this
class Author(models.Model):
FirstName = models.CharField(max_length=70)
MiddleName = models.CharField(max_length=70)
LastName = models.CharField(max_length=70)
Gender = models.CharField(max_length=1, choices = GENDERS)
user = models.ForeignKey(User, default = 1, on_delete = models.CASCADE, related_name='author_user')
IsActive = models.BooleanField(default=True)
class Meta:
ordering = ['LastName']
And a view like this
class Author(viewsets.ModelViewSet):
queryset = Author.objects.all()
serializer_class = AuthorSerializer
So can enter http://127.0.0.1:8000/author/ to get or post authors. If I want to make a PATCH request you can point to http://127.0.0.1:8000/author/ID_AUTHOR from your client. For example in angular2, you can have something like this
patchRequest(item: any): Observable<Author> {
return this.http.patch('http://127.0.0.1:8000/author/1', item);
}
It suppose you have configured your CORS and you have the same model in back and front.
Hope it can be usefull.