I'm using django-rest-auth for API endpoints for my custom user model. Getting user details, I send a GET request to /rest-auth/user/. This works fine with an authenticated user, but is forbidden for an unauthenticated user marked by a 403 Forbidden error.
However, I want other users to be able to view each other's details. How can I change this?
This test is the one which demonstrates this error:
def test_get_user(self):
# /rest-auth/user/ (GET)
# params: username, first_name, last_name
# returns: pk, username, email, first_name, last_name
client = APIClient()
client.login(username=self.user2.username,
password=self.user2.password)
path = "/rest-auth/user/"
user_data = {
"username": self.username,
}
expected_response = {
"pk": self.id,
"username": self.username,
"email": self.email,
"first_name": '',
"last_name": '',
}
response = client.get(path, user_data)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data, expected_response)
EDIT:
I tried to override the permissions of UserDetailsView, yet I failed to do so properly. How do I do this correctly?
from rest_auth import views
from rest_framework.permissions import IsAuthenticatedOrReadOnly
class CustomUserDetailsView(views.UserDetailsView):
permission_classes = (IsAuthenticatedOrReadOnly, )
django-rest-auth /rest-auth/user/ only allow you to get details of the authenticated user.
class UserDetailsView(RetrieveUpdateAPIView):
"""
Reads and updates UserModel fields
Accepts GET, PUT, PATCH methods.
Default accepted fields: username, first_name, last_name
Default display fields: pk, username, email, first_name, last_name
Read-only fields: pk, email
Returns UserModel fields.
"""
serializer_class = UserDetailsSerializer
permission_classes = (IsAuthenticated,)
def get_object(self):
return self.request.user
def get_queryset(self):
"""
Adding this method since it is sometimes called when using
django-rest-swagger
https://github.com/Tivix/django-rest-auth/issues/275
"""
return get_user_model().objects.all()
if you want unauthenticated users to read all user objects you have write your own view.
serializers.py
User = get_user_model()
class UserSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = "__all__"
views.py
class UserDetailApiView(generics.RetrieveAPIView):
permission_classes = (IsAuthenticatedOrReadOnly,)
queryset = get_user_model().objects.all()
serializer_class = UserSerializer
urls.py
path('api/users/<int:pk>',views.UserDetailApiView.as_view(),name='user')
Related
I have a Django project as following code in model
class Report(models.Model):
created_by_user=models.ForeignKey(User,on_delete=models.CASCADE)
following code in serializer
class ReportSerializer(serializers.ModelSerializer):
class Meta:
model=Report
fields='__all__'
and following code in view
class ReportCreateView(APIView):
def post(self,request, *args, **kwargs):
received_data=ReportSerializer(data=request.data)
if received_data.is_valid():
received_data.save()
return Response(received_data.data, status=status.HTTP_201_CREATED)
return Response(received_data.errors,status.HTTP_400_BAD_REQUEST)
when I send a post request by postman and send username and password in Authorization tab
it error:
{
"created_by_user": [
"This field is required."
]
}
but if I type username or password incorrect it will be
{
"detail": "Invalid username/password."
}
can everybody help me?
Your serializer has no idea about the currently logged-in user.You to pass it as context from request. user or request.
I personally prefer CurrentUserDefault to be used in the serializer. To make it work we need to pass the request as context because CurrentUserDefault picks user from context request. We need to update our views and serializer code as follows
Views file: Add request as context context
class ReportCreateView(APIView):
def post(self,request, *args, **kwargs):
received_data=ReportSerializer(data=request.data, context = {"request": request})
if received_data.is_valid():
received_data.save()
return Response(received_data.data, status=status.HTTP_201_CREATED)
return Response(received_data.errors,status.HTTP_400_BAD_REQUEST)
serializer.py: Update your serializer to auto-populate created_by_user
class ReportSerializer(serializers.ModelSerializer):
created_by_user = serializers.HiddenField(default=serializers.CurrentUserDefault())
class Meta:
model=Report
fields='__all__'
It will solve your user field required issue.
"created_by_user": ["This field is required."]
Now coming to your next part of the issue that is related to incorrect passwords.
By default, APIView picks the default authentication class from settings. Inside projects settings.py, we mostly write these lines while using DRF and they serve as default authentication for APIView:
From settings.py
REST_FRAMEWORK = {
# Use Django's standard `django.contrib.auth` permissions,
# or allow read-only access for unauthenticated users.
"DEFAULT_PERMISSION_CLASSES": [
"rest_framework.permissions.IsAuthenticated",
],
# Authentication settings
"DEFAULT_AUTHENTICATION_CLASSES": [
"rest_framework.authentication.SessionAuthentication",
],
...
}
Inside APIView you can have a look at default permission_classes, and authentication_classes
From inside APIView:
authentication_classes = api_settings.DEFAULT_AUTHENTICATION_CLASSES
throttle_classes = api_settings.DEFAULT_THROTTLE_CLASSES
permission_classes = api_settings.DEFAULT_PERMISSION_CLASSES
that is when you type an invalid password:
"detail": "Invalid username/password."
Provide the correct username and password to your APIView from postman so that it gets the requested logged-in user for auto-populates at DB level.
you don't perform any process on your user data and just need to save the request user, due to that I think you don't need a serializer field for it and it's better to get your current user in view. also if you need more fields to serialize, you can make created_by_user read_only true and set its value on your view.
for example, if you have report name and report desc field in your model:
class Report(models.Model):
created_by_user = models.ForeignKey(User, on_delete=models.CASCADE)
name = models.CharField(max_length=256)
desc = models.TextField()
perform your serializer like this :
class ReportSerializer(serializers.ModelSerializer):
class Meta:
model = Report
fields = '__all__'
extra_kwargs = {
'created_by_user': {'read_only': True},
}
then set created_by_user value in your view:
class ReportCreateView(APIView):
def post(self, request, *args, **kwargs):
request.data['created_by_user'] = request.user # just need add this line
received_data = ReportSerializer(data=request.data)
if received_data.is_valid():
received_data.save()
return Response(received_data.data, status=status.HTTP_201_CREATED)
return Response(received_data.errors, status.HTTP_400_BAD_REQUEST)
I was using Django user model using the ModelViewSet. When I am making a request to update the password for the current user that is logged in. Although I get a 200 OK response but my password never changes to the new one that I changed.
I also tried making the request from my admin user and when I made the PUT request with the password, it got changed to something else and I was logged out from the django admin panel.
Here is my
views.py
class UserViewSet(viewsets.ModelViewSet):
queryset = User.objects.all()
serializer_class = UserSerializer
permission_classes = [IsAuthenticated, IsOwnerOfObject]
authentication_classes = (TokenAuthentication,)
serializers.py
class UserSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = ['id', 'username', 'password']
extra_kwargs = {
'password' : {
'write_only':True,
'required': True
}
}
def create(self, validated_data):
user = User.objects.create_user(**validated_data)
Token.objects.create(user=user) # create token for the user
return user
urls.py
router = DefaultRouter()
router.register('articles', ArticleViewSet, basename='articles')
router.register('users', UserViewSet, basename = 'users')
urlpatterns = [
path('api/', include(router.urls)),
]
permissions.py
class IsOwnerOfObject(permissions.BasePermission):
def has_object_permission(self, request, view, obj):
return obj == request.user
Here is how I am making the request, with Authorisation token in the Headers field
Response :
As #BrianDestura says, you need call set_password to correctly update it.
class UserSerializer(serializers.ModelSerializer):
# --> Add this method
def update(self, instance, validated_data):
# We Can change the username?
instance.username = validated_data['username']
instance.set_password(validated_data['password'])
instance.save()
return instance
I have 2 models, User and UserProfile. UserProfile model has OneToOneField with User model. Here I am trying to update both the models in a single request.
Request Payload:
{'email': ['xxx#gmail.com'], 'first_name': ['Nalin'], 'last_name': ['Dobhal'],}
I have created serializer for both models.
serializers.py
class UserAccountSerializer(serializers.ModelSerializer):
id = serializers.IntegerField(required=False, read_only=True)
mobile = serializers.IntegerField(read_only=True)
email = serializers.EmailField(required=False, read_only=False)
username = serializers.CharField(read_only=True)
class Meta:
model = User
fields = ("id", "mobile", 'email', "username",)
class UserProfileSerializer(serializers.ModelSerializer):
user = UserAccountSerializer(required=False, read_only=False)
# other fields
class Meta:
model = UserProfile
fields = ("user", # other fields)
def update(self, instance, validated_data):
# validated data doesn't have email here, that's why getting value from self.initial_data
if self.initial_data.get("email"):
instance.user.email = self.initial_data.get("email")
instance.user.save()
instance.save()
return instance
views.py
class UserAccountSettingsAPI(generics.RetrieveUpdateAPIView):
http_method_names = ["options", "get", "put", "patch"]
permission_classes = (IsAuthenticated,)
authentication_classes = (TokenAuthentication,)
serializer_class = UserProfileSerializer
def get(self, request, *args, **kwargs):
# some processing
def update(self, request, *args, **kwargs):
profile = UserProfile.objects.select_related("user").get(user_id=request.user.id)
serializer = self.get_serializer(profile, data=request.data)
if serializer.is_valid(raise_exception=False):
serializer.save()
# some other processing only to set key value for context.
return Response(context)
I would like to perform some validation before updating user's email. So my question is where to perform that validation? And is there any better way of doing this? I tried to add def validate_email(self, email): in UserAccountSerializer but it is not getting executed. So I want to make sure that email does not belong to another user and if email exists, I would like to send some custom error message to client.
I have removed unnecessary code.
Try adding this code
create or update in views
if User.objects.filter(email=self.request.data['email']).exists():
return Response({"error": "This email id already exists."})
You could try raising a ValidationError which is part of the serializers library:
from rest_framework import serializers
def update(self, instance, validated_data):
# If `self.request.data["email"]` works
if User.objects.filter(email=self.request.data["email"]).exists():
raise serializers.ValidationError("This email already exists.")
# If `self.request.data["email"]` doesn't work
new_email = self.initial_data.get("email"):
if new_email and User.objects.filter(email=new_email).exists():
raise serializers.ValidationError("This email already exists.")
# Save and return the modified instanced
or if you unique=True to the email field, it will raise an IntegrityError so you don't need to do any further checks. You simply catch the error and handle it yourself.
I assume you want email to be unique. Then you should add unique=True to your user model's email field.
class YourUserModel(AbstractUser):
email = models.EmailField(unique=True)
After you make email a unique field, your database will not allow to add another entry with the same email and it will raise IntegrityError. You can catch this error and return a better error message to your user. Like this:
try:
if serializer.is_valid(raise_exception=False):
serializer.save()
except IntegrityError:
return Response(data={'message':'that email is in use'}, status=HTTP_400_BAD_REQUEST)
As stated in DRF documentation http://www.django-rest-framework.org/api-guide/parsers/#multipartparser, in order to parse multipart/form-data, the MultiPart and form parser must be used. I have a supiscion this is a problem in the Django Rest Framework because I saw a solution on their github issue saying it work using APIView.
from django.contrib.auth.models import User
from rest_framework import viewsets
from api.serializers import UserSerializer,
from rest_framework.parsers import MultiPartParser, FormParser
from rest_framework.response import Response
from rest_framework import status
class UserViewSet(viewsets.ModelViewSet):
"""
API endpoint that allows users to be viewed or edited.
"""
queryset = User.objects.all()
parser_classes = (MultiPartParser, FormParser)
serializer_class = UserSerializer
Picture of me sending request to Postman with result:
Edit: Adding UserSerializer class
class UserSerializer(serializers.HyperlinkedModelSerializer):
snapcapsules = SnapCapsuleSerializer(
many=True,
read_only=True,
allow_null=True,
)
class Meta:
model = User
fields = ('snapcapsules', 'url', 'username', 'email', 'password', )
write_only_fields = ('password',)
def create(self, validated_data):
user = User.objects.create(
username=validated_data['username'],
email=validated_data['email'],
)
user.set_password(validated_data['password'])
user.save()
return user
def update(self, instance, validated_data):
capsules_data = validated_data.pop('snapcapsules')
for capsule in capsules_data:
SnapCapsule.objects.create(user=instance, **capsule)
return instance
This is might not the issue with the ContentType. The error response says that,the UserSerializer excpect a payloadordata which include username and password field
So, Please try to add those fields to the request body and try again
UPDATE
The problem with the Orginal Post was, he added extra headers in POSTMAN (see the pic below) .
Postman will add appropriate Headers aromatically for you. So, you don't have to explicitly mention it
I removed one line and then it worked!
class UserViewSet(viewsets.ModelViewSet):
"""
API endpoint that allows users to be viewed or edited.
"""
queryset = User.objects.all()
serializer_class = UserSerializer
Also, the serializer can be rewrite as follows:
class UserSerializer(serializers.HyperlinkedModelSerializer):
...
def create(self, validated_data):
return User.objects.create_user(
username=validated_data['username'],
email=validated_data['email'],
password=validated_data['password']
)
...
I am creating a an API for registering user based on oauth token. My app has functionality like registration and login, adding restaurant etc. I created user registration part but i get error while login. I want login based on token.
I have used django-oauth-toolkit for this and DRF.
What i have done is
Login based on token
class UserCreateAPI(CreateAPIView):
serializer_class = UserCreateSerializer
queryset = User.objects.all()
permission_classes = [AllowAny]
class UserLoginAPI(APIView):
permission_classes = [AllowAny]
serializer_class = UserLoginSerializer
def post(self, request, *args, **kwargs):
access_token = AccessToken.objects.get(token=request.POST.get('access_token'), expires__gt=timezone.now()) # error is shown here
data = request.data
serializer = UserLoginSerializer(data=data)
if serializer.is_valid(raise_exception=True):
new_data = serializer.data
return Response(new_data, status=status.HTTP_200_OK)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
For creating user with a token
class UserCreateSerializer(ModelSerializer):
class Meta:
model = User
extra_kwargs = {"password": {"write_only": True}}
def create(self, validated_data):
username = validated_data['username']
first_name = validated_data['first_name']
last_name = validated_data['last_name']
email = validated_data['email']
password = validated_data['password']
confirm_password = validated_data['password']
user_obj = User(
username = username,
first_name = first_name,
last_name = last_name,
email = email
)
user_obj.set_password(password)
user_obj.save()
if user_obj:
expire_seconds = oauth2_settings.user_settings['ACCESS_TOKEN_EXPIRE_SECONDS']
scopes = oauth2_settings.user_settings['SCOPES']
application = Application.objects.get(name="Foodie")
expires = datetime.now() + timedelta(seconds=expire_seconds)
access_token = AccessToken.objects.create(user=user_obj,
application=application,
token = generate_token(),
expires=expires,
scope=scopes)
return validated_data
class UserLoginSerializer(ModelSerializer):
# token = CharField(allow_blank=True, read_only=True)
username = CharField()
class Meta:
model = User
fields = [
'username',
'password',
# 'token',
]
extra_kwargs = {"password":
{"write_only": True}
}
I have excluded fields part and validation part for shortening the code
Am i doing the right way?
Try to use request.data instead of request.post in order to get the access_token(http://www.django-rest-framework.org/tutorial/2-requests-and-responses/):
access_token = AccessToken.objects.get(token=request.data.get('access_token'), expires__gt=timezone.now())
use request.DATA if you are using the Version-2 of DRF. request.data is for version-3
============UPDATE========================================
You should implement your own login procedure or modify the existing one; because when user log in, the access_token is not actually sent to the server.
The login procedure should look like this :
When the user enter his login and password, your app should send a post request to 127.0.0.1:8000/o/token asking for the token. The request should contains the username, password, client_id and client_secret.
the server then receives the credentials and if they are valid it returns the access_token.
The rest of the time you should query the server using that token.
But, i'm not sure that it is what you are actually doing.