I just started to learn django last week so please excuse my ignorance if I'm completely approaching this problem the wrong way.
So I've been following a thinkster tutorial on setting up a User model that allows the change of a password in the model. So far I have a url (/api/user) that leads to this view:
class UserRetrieveUpdateAPIView(RetrieveUpdateAPIView):
permission_classes = (IsAuthenticated,)
renderer_classes = (UserJSONRenderer,)
serializer_class = UserSerializer
def retrieve(self, request, *args, **kwargs):
#turns the object recieved into a JSON object
serializer = self.serializer_class(request.user)
return Response(serializer.data, status=status.HTTP_200_OK)
def update(self, request, *args, **kwargs):
serializer_data = request.data
serializer = self.serializer_class(
request.user, data=serializer_data, partial=True
)
serializer.is_valid(raise_exception=True)
serializer.save()
return Response(serializer.data, status=status.HTTP_200_OK)
I understand that this section :
serializer = self.serializer_class(
request.user, data=serializer_data, partial=True
)
serializer.is_valid(raise_exception=True)
serializer.save()
will call upon a serializer class along the lines of:
class UserSerializer(serializers.ModelSerializer):
#This class handles serialization and deserialization of User objects
password = serializers.CharField(
max_length=128,
min_length=8,
write_only=True
)
class Meta:
model = User
fields = ('email', 'username', 'password', 'token',)
read_only_fields=('token',)
def update(self, instance, validated_data):
#performs an update on the user
password = validated_data.pop('password', None)
#have to take out password because setattr does not handle hashing etc
for (key, value) in validated_data.items():
#for the keys after taking out password set them to the updating User instance
setattr(instance, key, value)
if password is not None:
instance.set_password(password) #set_password is handled bydjango
instance.save() #set_password does not save instance
return instance
again I understand this section will essentially take request.data and "update" the model. However I'm stuck on how to test this feature using Postman.
Currently when I send a GET request to the URL using Postman I get this response:
GET Request result
The response is based off of my authenticate class that uses JWT authentication.
My question is, how do I trigger that update function using a Postman HTTP Request.
PATCH(partial_update) or PUT(update) http://127.0.0.1:8000/api/user/user_id/
you can see router table here
Related
So when a user submits a form via frontend (Vue.js), I want to be able to set the created_by attribute in the backend. What is the best way of achieving this?
Views
class ProjectView(generics.RetrieveAPIView):
queryset = Project.objects.order_by('-created_at')
def get(self, request):
queryset = self.get_queryset()
serializer = ProjectsSerializer(queryset, many=True)
return Response(serializer.data)
def post(self, request):
if request.method == 'POST':
serializer = ProjectsSerializer
if serializer.is_valid():
serializer.save()
return JsonResponse(serializer.data, status=201)
return JsonResponse(serializer.errors, status=400)
Serializer
class ProjectsSerializer(serializers.ModelSerializer):
interest_category = serializers.StringRelatedField()
class Meta:
model = Project
fields = (
'project_title',
'project_description',
'interest_category',
'created_by',
'created_at',
'updated_at',
)
The data I am passing from Frontend
project_title: this.project_title,
project_description: this.project_description,
interest_category: this.interest_category,
I do have a token saved in localStorage, but I do not know how to get the ID of the currently logged in user id and pass to backend or set in backend. Any and all help is greatly appreciated!
What is the best way of setting created_by the submitting/requesting user in the backend?
DRF does the work for you, assuming you are using Cookie auth or token auth you can retrieve the user by overriding the get_object :
def get_object(self):
queryset = self.get_queryset()
obj = get_object_or_404(queryset, user=self.request.user)
return obj
https://www.cdrf.co/3.13/rest_framework.generics/RetrieveAPIView.html#get_object
I have a view that calls patch()
class ValidateResetPasswordView(views.APIView):
def patch(self, request, token):
serializer = ValidateResetPasswordRequestSerializer(instance=request.user, data=request.data, partial=True)
serializer.is_valid(raise_exception=True)
serializer.save()
.
.
.
etc
My serializer overrides update() to encrypt the password as
class ValidateResetPasswordRequestSerializer(serializers.Serializer):
password = serializers.CharField(max_length=128, required=True, allow_blank=False, allow_null=False,
write_only=True)
def update(self, instance, validated_data):
instance.set_password(validated_data.get("password"))
instance.save()
return instance
My serializer doesn't catch empty requests. For example, if a client sends out an empty json, my patch() gets processed successfully given no key was provided.
{
}
I expect to get an error that says password is required, or the like. To prevent this issue, I had to manually validate whether a key exists or not within update() function as follows.
class ValidateResetPasswordRequestSerializer(serializers.Serializer):
password = serializers.CharField(max_length=128, required=True, allow_blank=False, allow_null=False,
write_only=True)
def update(self, instance, validated_data):
if len(validated_data) == 0: # force validation
raise serializers.ValidationError(
'Password is required')
validated_data.get("password")
instance.set_password(validated_data.get("password"))
instance.save()
return instance
Am I violating any coding standards in here. Is there a better, correct, way to validate against empty requests?
You are doing it right with writing the Validator Serializer. There is no need for update(), you just need to validate incoming data in your view. So, your validator is going to look like this (though I suggest using min_length for password validation):
class ValidateResetPasswordRequestSerializer(serializers.Serializer):
password = serializers.CharField(max_length=128, required=True, allow_blank=False, allow_null=False,
write_only=True)
The way to integrate it with your view:
class ResetPasswordView(CreateAPIView):
serializer_class = ValidateResetPasswordRequestSerializer
def post(self, request, *args, **kwargs):
serializer = self.serializer_class(data=request.data)
if serializer.is_valid(raise_exception=True)
# update password here and return status code 200
You set your serializer to be ValidateResetPasswordRequestSerializer, you pass request data to it by saying self.serializer_class(data=request.data) and then you check if recieved data is valid with serializer.is_valid(raise_exception=True). If data is not valid Django will raise an exceptions since flag is set to True with relevant message to user (something like 'Password is required', 'Blank is not allowed' etc).
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 using Django v1.7dev with the Django Rest Framework v2.3
I have extended the user Class as follows:
class UserProfile(models.Model):
user = models.OneToOneField(User)
gang = models.ForeignKey('Gang', related_name='gangsters')
def __unicode__(self):
return self.user.username
User.profile = property(lambda u: UserProfile.objects.get_or_create(user=u)[0])
I have also created a User serializer endpoint (which does not show the password when I sent a GET request, but accepts it when I send a post, with the purpose of using it for registration:
class UserSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = ('id', 'username', 'password', 'email')
def restore_object(self, attrs, instance=None):
user = super(UserSerializer, self).restore_object(attrs, instance)
user.set_password(attrs['password'])
return user
def to_native(self, obj):
ret = super(UserSerializer, self).to_native(obj)
del ret['password']
return ret
I'd love to extend the serializer to include also the gang parameter. Something similar to
gang = serializers.Field(source='profile.gang')
but writable instead of ReadOnly, so that when I register a user I can insert also the gang, possibly in an atomic way. What is the best approach in this case? I have tried to play a bit with other fields types, but unsuccessfully till now.
At the moment I am sending two separate post (one for User and one for UserProfile, but I bet there is a better way...)
There are two general approaches you could take: either update the related field in post_save() or use two serializers and require both to be valid before processing the request.
Check out this question for more details on both.
This is the solution I ended up adopting, based on Carlton's linked question (but with some changes since the user needs to be authenticated for the Login, but cannot be authenticated during the Registration:
class UserAuthView(APIView):
#Login. Returns the current user.
def get(self, request, *args, **kwargs):
# Only UserProfileSerializer is required to serialize data
if (request.user.is_authenticated()):
serializer = UserProfileSerializer(
instance=request.user.profile)
return Response(serializer.data, status=status.HTTP_200_OK)
return Response(status=status.HTTP_401_UNAUTHORIZED)
#Register new user.
def post(self, request, format=None):
user_serializer = UserSerializer(data=request.DATA)
errors = dict()
if user_serializer.is_valid():
user =user_serializer.save()
data = request.DATA.copy()
data['user'] = User.objects.latest('id').id
user_profile_serializer = UserProfileSerializer(data=data)
if user_profile_serializer.is_valid():
user_profile_serializer.save()
return Response(user_profile_serializer.data, status=status.HTTP_201_CREATED)
errors.update(user_profile_serializer.errors)
return Response(errors, status=status.HTTP_400_BAD_REQUEST)
errors.update(user_serializer.errors)
return Response(errors, status=status.HTTP_400_BAD_REQUEST)