I am trying to implement authentication by combining Django Rest Framework and Angular, but I am suffering from user information update.
Angular sends it to Django with the PUT method, Django accepts the request with View "AuthInfoUpdateView".
class AuthInfoUpdateView(generics.GenericAPIView):
permission_classes = (permissions.IsAuthenticated,)
serializer_class = AccountSerializer
lookup_field = 'email'
queryset = Account.objects.all()
def put(self, request, *args, **kwargs):
serializer = AccountSerializer(data=request.data)
if serializer.is_valid():
serializer.save()
return Response(serializer.data)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
At this time, Django accepts the request as below.
request.data = {'email': 'test3#example.com', 'username': 'test3', 'profile': 'i am test3'}
request.user = test3#example.com
And the serializer is implementing as below.
from django.contrib.auth import update_session_auth_hash
from rest_framework import serializers
from .models import Account, AccountManager
class AccountSerializer(serializers.ModelSerializer):
password = serializers.CharField(write_only=True, required=False)
class Meta:
model = Account
fields = ('id', 'username', 'email', 'profile', 'password')
def create(self, validated_data):
return Account.objects.create_user(request_data=validated_data)
def update(self, instance, validated_data):
insntance.username = validated_data.get('username', instance.username)
insntance.email = validated_data.get('email', instance.email)
insntance.profile = validated_data.get('profile', instance.profile)
instance = super().update(instance, validated_data)
return instance
I tried to update the user from Angular in such an implementation, and the following response is returned.
"{"username":["account with this username already exists."],"email":["account with this email address already exists."]}"
It is thought that it is because you did not specify the record to update, but is there a way to solve it smartly without changing the current configuration so much?
I need your help.
use
class AuthInfoUpdateView(generics.UpdateAPIView):
use http method patch can partial_update your instance.
method PATCH -> partial update instance
method PUT -> update instance
Related
I don't know how to pass user_id from requests.user.id and use it in the CustomerSerializer to save the object to the database. The error stems from the fact that user_id exists in the customer table in the database but it does not show up as a field to be passed in the rest_framework API frontend (only phone and profile_image do).
Here is the Customer model:
class Customer(models.Model):
phone = models.CharField(max_length=14)
profile_image = models.ImageField(blank=True, null=True)
user = models.OneToOneField(
settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
Here is the ViewSet:
class CustomerViewSet(ModelViewSet):
queryset = Customer.objects.all()
permission_classes = [permissions.IsAdminUser]
serializer_class = CustomerSerializer
# Only admin users can make requests other than 'GET'
def get_permissions(self):
if self.request.method == 'GET':
return [permissions.AllowAny()]
return [permissions.IsAdminUser()]
#action(detail=False, methods=['GET', 'PUT'])
def me(self, request):
customer, created = Customer.objects.get_or_create(user_id=request.user.id)
if request.method == 'GET':
serializer = CustomerSerializer(customer)
return Response(serializer.data)
elif request.method == 'PUT':
serializer = CustomerSerializer(customer, data=request.data)
serializer.is_valid(raise_exception=True)
serializer.save()
return Response(serializer.data)
... and here is the Serializer:
class CustomerSerializer(serializers.ModelSerializer):
class Meta:
model = Customer
fields = ['id', 'user_id', 'profile_image', 'phone']
```python
... and when I try to save a new customer by sending a POST request to the endpoint with the following data:
```json
{
"profile_image": "Images/image.png",
"phone": "009293930"
}
I get the following error:
IntegrityError at /api/customers/
(1048, "Column 'user_id' cannot be null")
Request Method: POST
Request URL: http://127.0.0.1:8000/api/customers/
Django Version: 4.0.6
Exception Type: IntegrityError
Exception Value:
(1048, "Column 'user_id' cannot be null")
Exception Location: /home/caleb/.local/share/virtualenvs/Cribr-svgsjjVF/lib/python3.8/site-packages/pymysql/err.py, line 143, in raise_mysql_exception
Python Executable: /home/caleb/.local/share/virtualenvs/Cribr-svgsjjVF/bin/python
Python Version: 3.8.10
Python Path:
['/home/caleb/Desktop/Cribr',
'/home/caleb/Desktop/Cribr',
'/snap/pycharm-professional/290/plugins/python/helpers/pycharm_display',
'/usr/lib/python38.zip',
'/usr/lib/python3.8',
'/usr/lib/python3.8/lib-dynload',
'/home/caleb/.local/share/virtualenvs/Cribr-svgsjjVF/lib/python3.8/site-packages',
'/snap/pycharm-professional/290/plugins/python/helpers/pycharm_matplotlib_backend']
Server time: Thu, 28 Jul 2022 23:38:53 +0000
I figured the issue here is that the serializer class is not getting the user_id value from the POST request. I tried passing request.user.id to the serializer from the viewset through a context object (i.e., context={'user_id': request.user.id}) but I couldn't figure out how to then add it to the validated data which the serializer passes to the save method.
Any help on this issue will be much appreciated. Thanks in advance.
The benefit of using DRF and viewsets is that most of the work has already been done for you. In instances such as this, you usually just need to tweak a few things to get it working the way you want. I've re-written your solution for you below:
class CustomerViewSet(ModelViewSet):
queryset = Customer.objects.all()
permission_classes = [permissions.IsAdminUser]
serializer_class = CustomerSerializer
# Only admin users can make requests other than 'GET'
def get_permissions(self):
if self.request.method == 'GET':
return [permissions.AllowAny()]
return [permissions.IsAdminUser()]
def get_object(self):
customer, created = Customer.objects.get_or_create(user_id=self.request.user.id)
return customer
#action(detail=False, methods=['GET'])
def me(self, request):
return self.retrieve(request)
def create(self, request, *args, **kwargs):
customer_exists = Customer.objects.filter(user=request.user).exists()
if customer_exists:
return self.update(request, *args, **kwargs)
else:
return super().create(request, *args, **kwargs)
def perform_create(self, serializer):
serializer.save(user=self.request.user)
From the DRF docs:
The ModelViewSet class inherits from GenericAPIView and includes implementations for various actions, by mixing in the behavior of the various mixin classes.
The actions provided by the ModelViewSet class are .list(), .retrieve(), .create(), .update(), .partial_update(), and .destroy().
The ModelViewSet will also set up a urlconf for you, which, excluding list will expect an object pk (primary key) to be provided in the url to allow the view to know what resource in the database you are trying to access. In your case, you want to determine that resource based on the authentication credentials provided in the request. To do this, we can override get_object to get or create the customer based on the authenticated user's id.
The next change we make is to define our action for the GET method. We want to be able to retrieve a resource, without specifying the pk in the url conf, hence detail=False. We can then simply call the builtin retrieve function from this action, which in turn will use get_object to get and return the customer object.
Thirdly, your PUT request will be directed to update, which is inherited from ModelViewSet, so you don't need to do anything here as you've already overwritten get_object.
def update(self, request, *args, **kwargs):
partial = kwargs.pop('partial', False)
instance = self.get_object()
serializer = self.get_serializer(instance, data=request.data, partial=partial)
serializer.is_valid(raise_exception=True)
self.perform_update(serializer)
if getattr(instance, '_prefetched_objects_cache', None):
# If 'prefetch_related' has been applied to a queryset, we need to
# forcibly invalidate the prefetch cache on the instance.
instance._prefetched_objects_cache = {}
return Response(serializer.data)
Lastly, you want to set user on your serializer to read only (or just remove it entirely), as this should always be set based on the credentials passed in the request.
class CustomerSerializer(serializers.ModelSerializer):
class Meta:
model = Customer
fields = ['id', 'user_id', 'profile_image', 'phone']
read_only_fields = ['user_id']
A great resource for looking at all the functions that you inherit from DRF classes is https://www.cdrf.co/.
Good luck, hope this helps!
Okay, I managed to solve it by overriding the create method in the serializer. I added the following:
class CustomerSerializer(serializers.ModelSerializer):
class Meta:
model = Customer
fields = ['id', 'user_id', 'profile_image', 'phone']
read_only_fields = ['user_id']
# NEW ---------------------------------
def create(self, validated_data):
user = self.context['request'].user
customer = Customer.objects.filter(user_id=user)
if customer.exists():
raise serializers.ValidationError(
'Customer already exists')
else:
customer = Customer.objects.create(
user=user, **validated_data)
return customer
The object saves fine now.
I have a model like this:
class Professional(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE)
dummy_text = models.CharField(max_length=300)
a serializer like this:
class ProfessionalSerializer(serializers.ModelSerializer):
class Meta:
model = Professional
fields = '__all__'
and a view like this one:
class CreateProfessional(generics.CreateAPIView):
serializer_class = ProfessionalSerializer
The thing is, I need to pass the current authenticated user for a given request as the user for my serializer, I'm getting an error because obviously the user field is required as stated in my model, but I can't find an elegant way to do so, how could I go about it?
Set the user as a read_only_fields in the serializer meta. This will prevent accepting the user data from the payload.
class ProfessionalSerializer(serializers.ModelSerializer):
class Meta:
model = Professional
fields = '__all__'
read_only_fields = ["user"]
Then, override the perform_create(...) method of the view class
class CreateProfessional(generics.CreateAPIView):
serializer_class = ProfessionalSerializer
def perform_create(self, serializer):
serializer.save(user=self.request.user)
You can make validate method with like validate_user and do following code inside.
self.context['view'].request.user()
class CreateProfessional(generics.CreateAPIView):
serializer_class = ProfessionalSerializer
def create(self, request, *args, **kwargs):
serializer = self.serializer_class(data=request.data, context={'request': request})
serializer.is_valid(raise_exception=True)
serializer.save()
class ProfessionalSerializer(serializers.ModelSerializer):
class Meta:
model = Professional
fields = '__all__'
def create(self, validated_data):
user = self.context['request'].user
like this you can get your current user
I found a very nice way to do it, I dont wan't to return custom responses, I will let that to the framework, what I did was the following in my view:
def get_serializer(self, *args, **kwargs):
# redefine method to parameterize the serializer
# while leaving the response handling to the
# framework
serializer_class = self.get_serializer_class()
kwargs['context'] = self.get_serializer_context()
kwargs['context']['user'] = self.request.user
return serializer_class(*args, **kwargs)
and in my serializer:
class EntitySerializer(serializers.ModelSerializer):
class Meta:
model = Entity
fields = '__all__'
extra_kwargs = {'user': {'required': False}}
def create(self, validated_data):
user = self.context['request'].user
entity = Entity.objects.create(user=user, **validated_data)
return entity
You can use ModelViewSet instead of single API Views. ModelViewSet handles all the crud operations automatically.
Let's say you have the model
class Professional(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE)
dummy_text = models.CharField(max_length=300)
and the serializer
class ProfessionalSerializer(serializers.ModelSerializer):
class Meta:
model = Professional
fields = '__all__'
So you can add a ModelViewSet
class ProfessionalViewSet(ModelViewSet):
queryset = Professional.objects.all()
permission_classes = [IsAuthenticated]
serializer_class = ProfessionalSerializer
this will handle all the crud operations for your model. Following example class will show you the methods that a viewset has.
class UserViewSet(viewsets.ViewSet):
"""
Example empty viewset demonstrating the standard
actions that will be handled by a router class.
If you're using format suffixes, make sure to also include
the `format=None` keyword argument for each action.
"""
def list(self, request):
pass
def create(self, request):
pass
def retrieve(self, request, pk=None):
pass
def update(self, request, pk=None):
pass
def partial_update(self, request, pk=None):
pass
def destroy(self, request, pk=None):
pass
these will handle all the actions. checkout the documentation for more
you'll also have to add the router to handle all the urls automatically which is also covered in the documentation (above link). Following is an example(urls.py).
from myapp.views import UserViewSet
from rest_framework.routers import DefaultRouter
router = DefaultRouter()
router.register(r'users', UserViewSet, basename='user')
urlpatterns = router.urls
Here's a serializer for registering a user.
class RegisterSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = ('id', 'username', 'email', 'password')
extra_kwargs = {'password': {'write_only': True}}
def create(self, validated_data):
user = User.objects.create_user(validated_data['username'], validated_data['email'], validated_data['password'])
return user
Here's the api view:
class RegisterView(generics.GenericAPIView):
serializer_class = RegisterSerializer
def post(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
user = serializer.save()
return Response({
"user": UserSerializer(user, context=self.get_serializer_context()).data,
# "token": AuthToken.objects.create(user)[1]
})
On the api view, if I try to pass in a name that is exactly the same as an existing name, it will say that it already exists. However, I can still make the emails the same which I don't want. Is there a way to get it so that I can also tell DRF that I would like the email to have to be unique as well?
There are 2 options:
Enforcing the unique criterion on the serializer level:
from rest_framework import serializers
from rest_framework.validators import UniqueValidator
class RegisterSerializer(serializers.ModelSerializer):
email = serializers.EmailField(
validators=[UniqueValidator(queryset=User.objects.all())]
) # recommend using `get_user_model`
class Meta:
model = User # recommend using `get_user_model`
...
...
Using a custom User model that enforces the unique criterion on the model level. More details here:
from django.contrib.auth.models import AbstractUser
from django.db import models
class User(AbstractUser):
email = models.EmailField(unique=True)
Note: The 2nd option also requires making changes to the settings and potentially other areas of the code if you reference the User model directly in your other models. If you are directly using User in other areas of your code, please take a look at using get_user_model.
Since you are using the ModelSerializer I think you can achieve it by having emails as unique field in the model itself and the serializer will handle the validation part for you.
I have a UserUpdateAPIView, in it I can edit the user information:
class UserUpdateAPIView(RetrieveUpdateAPIView):
queryset = User.objects.filter(is_admin=False, is_staff=False, is_superuser=False).exclude(status=4)
serializer_class = UserDetailSerializer
lookup_field = "username"
def perform_update(self, serializer):
serializer.save()
The UserDetailSerializer:
class UserDetailSerializer(ModelSerializer):
"""
user detail
"""
class Meta:
model = User
exclude = [
'password',
]
depth = 1
Now, every user can access the UserUpdateAPIView, so its a bad design. I just want the super admin and the user itself can access the APIView, how to implement it?
I know I can use permissions = [IsAdminUser] to allow the admin users to access this API, but I just want to let the super admin user and the user itself to access.
from rest_framework import permissions
from rest_framework.compat import is_authenticated
class IsAdminUserOrSelf(permissions.BasePermission):
def has_object_permission(self, request, view, obj):
# this methid is called in get_object method.
# obj mean the object you retrieve.Here you retrieved is User instance.
# It's can be any model instance,depend on the Molde you Retrieve in views.
# if you want everyone can see user info
if request.method in permissions.SAFE_METHODS:
return True
# if you use Django2.0 is_authenticated(request.user) should be changed to request.user.is_authenticated
if request.user and is_authenticated(request.user):
# is self or is superuser
return obj == request.user or request.user.is_superuser
else:
return False
class UserUpdateAPIView(RetrieveUpdateAPIView):
permissions = [IsAdminUserOrSelf,]
queryset = User.objects.filter(is_admin=False, is_staff=False, is_superuser=False).exclude(status=4)
serializer_class = UserDetailSerializer
lookup_field = "username"
def perform_update(self, serializer):
serializer.save()
As per the DRF documentation, the serializer_class attribute should be set when using GenericAPIView. But why does the serializer_class attribute even works with APIView?
Here is my API code:
class UserView(APIView):
serializer_class = SignupSerializer
#transaction.atomic
def post(self, request):
email = get_data_param(request, 'email', None)
password = get_data_param(request, 'password', None)
params = request.POST.copy()
params["username"] = email
serializer = UserSerializer(data=params)
if serializer.is_valid(raise_exception=True):
user = serializer.save()
user.set_password(password)
user.save()
params["user"] = user.id
serializer = CustomProfileSerializer(data=params)
if serializer.is_valid(raise_exception=True):
profile = serializer.save()
return Response(response_json(True, profile.to_dict(), None))
class SignupSerializer(serializers.Serializer):
email = serializers.EmailField(max_length=100)
password = serializers.CharField(max_length=50)
When I browse this API in the browser it does show the email and password fields as input but if I don't set this serializer_class attribute, no input fields are shown.
Ideally, this serializer_class attribute should not work with APIView. I have searched a lot but there is nothing available related to this.
Can anyone please provide an explanation for this behavior? Thanks.
I think this can help you.
create serializer.py and write:
from rest_framework import serializers
from .models import User
class UserSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = ('email', 'password')
and views.py:
from rest_framework.response import Response
from rest_framework.views import APIView
from .serializers import UserSerializer
from .models import User
class AddUserAPIView(APIView):
def post(self, request):
user_serializer = UserSerializer(data=request.data)
if user_serializer.is_valid():
user = user_serializer.save()
user.set_password(user_serializer.data["password"])
return Response({'message': 'user added successfully!'})
return Response({'message': user_serializer.errors})
You are absolutely right!!:
APIView doesn't utilize a serializer_class (by default) because it is not meant to handle any request processing logic!
What happens though is that the BrowsableAPIRenderer that is used to render the API in the browser checks for a serializer_class attribute and set's it as the View serializer if it exists. We can see this in the BrowsableAPIRenderer code:
The _get_serializer class of the renderer:
def _get_serializer(self, serializer_class, view_instance, request, *args, **kwargs):
kwargs['context'] = {
'request': request,
'format': self.format,
'view': view_instance
}
return serializer_class(*args, **kwargs)
And the way it is used to set the renderer serializer if it exists, inside the get_rendered_html_form:
Line 483: has_serializer_class = getattr(view, 'serializer_class', None)
Lines 497 to 509:
if has_serializer:
if method in ('PUT', 'PATCH'):
serializer = view.get_serializer(instance=instance, **kwargs)
else:
serializer = view.get_serializer(**kwargs)
else:
# at this point we must have a serializer_class
if method in ('PUT', 'PATCH'):
serializer = self._get_serializer(view.serializer_class, view,
request, instance=instance, **kwargs)
else:
serializer = self._get_serializer(view.serializer_class, view,
request, **kwargs)
In essence, you accidentally override the BrowserAPIRenderer's default behavior regarding the APIView by providing the serializer_class attribute. For what is worth, my opinion on the matter is that this should not be possible!
I use the django rest framework default get_schema_view() to provide auto-generated openapi schema from which I auto generate a javascript client for.
This works for ViewSets, but the payload wasn't being provided for views defined by APIView.
Where I have defined serializers, I found that adding get_serializer() method to my APIView classes allowed the schema to be generated with the serializer defined payload.
from rest_framework.response import Response
from rest_framework.views import APIView
from .serializers import UserSerializer
from .models import User
class AddUserAPIView(APIView):
def get_serializer(self, *args, **kwargs):
return UserSerializer(*args, **kwargs)
def post(self, request):
user_serializer = UserSerializer(data=request.data)
if user_serializer.is_valid():
user = user_serializer.save()
user.set_password(user_serializer.data["password"])
return Response({'message': 'user added successfully!'})
return Response({'message': user_serializer.errors})