Background context
I'm using django rest framework and django-rest-auth to build an API. My authentication scheme doesn't use the default authentication of just the email/username and password and requires other data present on the Profile model.
For this reason, I'm using a custom registration serializer and overriding the REGISTER_SERIALIZER class in the settings.
This is the serializer as follows:
class UserProfileSerializer(serializers.ModelSerializer):
phone_number = PhoneNumberField()
class Meta(object):
model = Profile
fields = ('phone_number', 'birth_date', 'anniversary', 'user_type',)
class UserRegisterSerializer(serializers.Serializer):
username = serializers.CharField(
max_length=get_username_max_length(),
min_length=allauth_settings.USERNAME_MIN_LENGTH,
required=allauth_settings.USERNAME_REQUIRED
)
email = serializers.EmailField(required=allauth_settings.EMAIL_REQUIRED)
serializers.EmailField(required=allauth_settings.EMAIL_REQUIRED)
password1 = serializers.CharField(write_only=True)
password2 = serializers.CharField(write_only=True)
profile = UserProfileSerializer()
..............
Validation methods
..............
def custom_signup(self, request, user):
pass
def get_cleaned_data(self):
return {
'username': self.validated_data.get('username', ''),
'password1': self.validated_data.get('password1', ''),
'email': self.validated_data.get('email', '')
}
def save(self, request):
adapter = get_adapter()
user = adapter.new_user(request)
self.cleaned_data = self.get_cleaned_data()
adapter.save_user(request, user, self)
self.custom_signup(request, user)
setup_user_email(request, user, [])
profile_data = self.validated_data.pop('profile', {})
user_repo.create_object(user=user, **profile_data)
return user
The problem
The serializer and the corresponding endpoint works in that it is creating the user and the associated profile, but in the serializing step when returning the response, I get the following error:
KeyError: "Got KeyError when attempting to get a value for field `profile` on serializer `UserRegisterSerializer`.
The serializer field might be named incorrectly and not match any attribute or key on the `OrderedDict` instance.
Original exception text was: 'profile'."
As you can see, the field exists on the serializer. I tried adding the field to the Meta classe's field property but it didn't work.
I've narrowed it down to the function to_representation in rest_framework/serializers.py at line number 488. The error occurs when the serializer tries call get_attribute on on the nested serializer.
Thanks!
Edit : This is the view in library which uses this serializer
Edit-1 : Stack trace of the error
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 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
I'm trying to implement a user profile in django rest framework.
Users should be able to request the profile of other users; however, since profiles contain sensitive information, I want to limit the information returned to non-owners and non-authenticated users when they request a profile.
I'm looking for a test that I can run inside my view methods that will determine which serializer to use for that request.
How can I do this?
# models.py
class Profile(models.Model):
user = models.OneToOneField(settings.AUTH_USER_MODEL, related_name='profile')
bio = models.CharField(max_length=100)
# dob is sensitive and should be protected...
dob = models.DateTimeField(blank=True, null=True)
My serializers would look like this:
# serializers.py
# Only for the owner...
class ProfileOwnerSerializer(serializers.HyperlinkedModelSerializer):
user = serializers.ReadOnlyField(source='user.id')
first_name = serializers.ReadOnlyField(source='user.first_name')
last_name = serializers.ReadOnlyField(source='user.last_name')
class Meta:
model = Profile
fields = (
'url',
'id',
'dob', #sensitive
'user',
'first_name',
'last_name', #sensitive
)
#For logged in users...
class ProfileSerializer(serializers.HyperlinkedModelSerializer):
user = serializers.ReadOnlyField(source='user.id')
first_name = serializers.ReadOnlyField(source='user.first_name')
class Meta:
model = Profile
fields = (
'url',
'id',
'bio',
'user',
'first_name',
)
#For everyone else...
class NonAuthProfileSerializer:
...
And I would try to distinguish between them here...
# views.py
class ProfileDetail(APIView):
"""
Retrieve a profile instance.
"""
# Can't user permission_classes bc I want to cater to different classes...
def get_object(self, pk):
try:
return Profile.objects.get(pk=pk)
except Profile.DoesNotExist:
raise Http404
def get(self, request, pk, format=None):
profile = self.get_object(pk)
# is_owner = ???
# is_authenticated = ???
# Define the serializer to be ProfileSerializer, ProfileOwnerSerializer, etc.
serializer = CorrectSerializer(
profile,
context={"request": request},
)
return Response(serializer.data)
I don't think it'd be too hard to check if the request was sent by the owner, since I can just cross-reference the profile id.
However, how would I check whether the user was logged in or not? I've tried looking at request.user.auth in the view method, but that seems to be None whether or not the request is logged in.
I think you should be checking with request.user.is_authenticated(). To fill in the blanks:
is_owner = profile.user == request.user
is_authenticated = request.user.is_authenticated()
I am newbie to Tastypie and django and facing some problem related to user update. I have create the UserProfileResource, and defined obj_update method to PATCH user and its profile details. It is working find when I am patching with UserProfile pk i.e.
/api/v1/userprofile/1/ --data {"user":{"username":"mac","first_name":"updated_mac"}, "phone":"09534546789"}
Now I have two questions to modify this code :
1. How to update user details one the basis of username provided and not from pk in the URL i.e.
/api/v1/userprofile/ --data {"user":{"username":"mac","first_name":"updated_mac"}, "phone":"09534546789"}
2. While updating the user profile how can I validate that new phone already does not exists in the system ?
UserProfileResource
class UserProfileResource(ModelResource):
'''Fetch customer details and all coupons'''
user = fields.ForeignKey(UserResource, 'user', null=True, full=True)
class Meta:
authentication = BasicAuthentication()
authorization = DjangoAuthorization()
queryset = UserProfile.objects.filter(user__is_active=True)
resource_name = 'userprofile'
list_allowed_methods = ['get', 'post']
detail_allowed_methods = ['patch']
excludes = ['added_on', 'updated_on']
include_resource_uri = False
def obj_update(self, bundle, **kwargs):
try:
# if raw password is provided then create hash from it
raw_password = bundle.data["user"].get('password')
if raw_password == "":
raise CustomBadRequest(
code="key_error",
message="Blank password is provided.")
bundle.data["user"]["password"] = make_password(raw_password)
except KeyError:
pass
# check if email, phone already not exists - TODO
return super(UserProfileResource, self).obj_update(bundle, **kwargs)
I am trying to program a Django CreateView (CBV), which takes instead of the user id the user email and determines (or creates) the user based on the email.
My model does not contain anything special:
class Project(models.Model):
name = models.CharField(_('Title'), max_length=100,)
user = models.ForeignKey(User, verbose_name=_('user'),)
...
My forms.py adds the additional email field to the form:
class ProjectCreateForm(forms.ModelForm):
email = forms.EmailField(required=True, )
class Meta:
model = Project
fields = ('name', ...,)
In my views.py, I am trying to determine if the user exists or should be created. In both cases, the user id should be saved as part of the Project instance.
class ProjectCreateDetails(CreateView):
form_class = ProjectCreateForm
template_name = '...'
success_url = reverse_lazy('login')
model = Project
def form_valid(self, form):
try:
user = User.objects.get(email=form.email)
except User.DoesNotExist:
user = User.objects.create_user(form.email, form.email, ''.join([random.choice(string.digits + string.letters) for i in range(0, 10)]))
user.save()
form.instance.user = user
return super(ProjectCreateDetails, self).form_valid(form)
However I am facing an error that the 'Solution' object has no attribute 'email'.
Do I need to switch to a FormView instead of a CreateView?
You get the error 'Solution' object has no attribute 'email' because form.email is invalid. Validated data is never available as attributes of a form or model form. When forms (including model forms) are valid, the successfully validated data is available in the form.cleaned_data dictionary.
Note that you don't need to call user.save(). The create_user call has already added the user to the database. You don't have to generate a random password either -- if password is None, then create_user will set an unusable password.
Finally, make sure that you do not include the user field in the ProjectCreateForm. You probably do not, but your code says fields = ('name', ...,) so I can't tell for sure.
Put it together and you get the following (untested) code:
def form_valid(self, form):
try:
user = User.objects.get(email=form.cleaned_data['email'])
except User.DoesNotExist:
user = User.objects.create_user(form.cleaned_data['email'], form.cleaned_data['email'])
form.instance.user = user
return super(ProjectCreateDetails, self).form_valid(form)