I trying to insert and update a writable nested serializer with Django Rest Framework, following examples like this. But it doesn't work, because somehow after I execute serializer.is_valid() it lose the reference from serializer.validated_data like if it never was sent.
What could I having doing wrong?
My models
class User(AbstractUser):
institution = models.ForeignKey(Institution, on_delete=None, null=True, blank=True)
class Meta:
db_table = 'User'
managed = True
verbose_name = 'Users'
verbose_name_plural = 'Users'
ordering = ['id']
def __str__(self):
return self.email
class Institution(models.Model):
id = models.AutoField(db_column='id', primary_key=True)
name = models.CharField(db_column='name', max_length=255, null=False)
country = models.CharField(db_column='country', max_length=255, null=False)
class Meta:
db_table = 'Institution'
managed = True
verbose_name = 'Institutions'
verbose_name_plural = 'Institutions'
ordering = ['id']
def __str__(self):
return self.name
My serializers
class InstitutionSerializer(serializers.ModelSerializer):
class Meta:
model = Institution
fields = '__all__'
datatables_always_serialize = ('id', 'name', 'country')
class UserSerializer(serializers.HyperlinkedModelSerializer):
institution = InstitutionSerializer()
def create(self, validated_data):
return User.objects.create_user(**validated_data)
def update(self, instance, validated_data):
institution_data = validated_data['institution']
instance.institution = Institution.objects.get(pk=institution_data['id'])
return instance
class Meta:
model = User
fields = (
'id',
'username',
'first_name',
'last_name',
'email',
'password',
'is_active',
'institution',
)
datatables_always_serialize = (
'id',
'username',
'first_name',
'last_name',
'email',
'is_active',
'institution',
)
My view
class UserViewSet(ModelViewSet):
serializer_class = UserSerializer
permission_classes = (IsSuperUserPermission,)
def list(self, request, **kwargs):
params = Q()
if 'search[value]' in request.GET and request.GET['search[value]'] != '':
params = Q(username__icontains=request.GET['search[value]']) |\
Q(first_name__icontains=request.GET['search[value]']) |\
Q(last_name__icontains=request.GET['search[value]']) |\
Q(email__icontains=request.GET['search[value]']) |\
Q(institution__name__icontains=request.GET['search[value]'])
queryset = User.objects.filter(params).select_related().order_by('id')
serializer = self.serializer_class(queryset, many=True)
return Response(serializer.data)
def retrieve(self, request, *args, **kwargs):
queryset = User.objects.filter(pk=request.GET['pk']).select_related()
serializer = UserSerializer(queryset, many=True)
return Response(serializer.data)
def get_permissions(self):
if self.action in ('create',):
self.permission_classes = [AllowAny, ]
return super(self.__class__, self).get_permissions()
def create(self, request, *args, **kwargs):
serializer = UserSerializer(data=request.data)
if serializer.is_valid():
serializer.create(serializer.validated_data)
return Response(serializer.data)
else:
return Response(serializer.errors, status.HTTP_500_INTERNAL_SERVER_ERROR)
def partial_update(self, request, *args, **kwargs):
user = User.objects.get(pk=request.data['id'])
serializer = UserSerializer(instance=user, data=request.data, partial=True)
if serializer.is_valid():
if 'password' in serializer.validated_data:
serializer.validated_data['password'] = make_password(serializer.validated_data['password'])
serializer.update(user, serializer.validated_data)
return Response(serializer.data)
else:
return Response(serializer.errors, status.HTTP_500_INTERNAL_SERVER_ERROR)
Edit.
I submitting data like this:
{
"username": "BLA",
"email": "BLA#BLA.com",
"first_name": "BLA",
"last_name": "BLA",
"institution": 1,
"is_active": true,
"password": "bla12345"
}
Why this problem?
In your update payload, you are providing the institution data as an integer which represents the PK. But you've also defined a nested serializer InstitutionSerializer() inside UserSerializer() class. So, DRF expects a dict like object (DRF probably raise some error by mentioning so. I'm not sure why it's not happened in this situation).
What is the solution?
Since you are passing the institution id, I assume, you need the nested output only on HTTP GET requests. So, override the __init__() method of the UserSerializer() class and restrict the nested serializer usage to only HTTP GET requests
Here is the code
class UserSerializer(serializers.HyperlinkedModelSerializer):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if self.context['request'].method == 'GET':
self.fields['institution'] = InstitutionSerializer()
institution = InstitutionSerializer() # remove this
def create(self, validated_data):
return User.objects.create_user(**validated_data)
def update(self, instance, validated_data):
institution_data = validated_data['institution']
instance.institution = Institution.objects.get(pk=institution_data['id'])
return instance
class Meta:
model = User
fields = (
'id',
'username',
'first_name',
'last_name',
'email',
'password',
'is_active',
'institution',
)
datatables_always_serialize = (
'id',
'username',
'first_name',
'last_name',
'email',
'is_active',
'institution',
)
UPDATE-1
Change your partial_update() method of UserViewSet class as below,
def partial_update(self, request, *args, **kwargs):
user = User.objects.get(pk=request.data['id'])
serializer = UserSerializer(instance=user, data=request.data, partial=True, context={"request": request}) # change is here <<<
if serializer.is_valid():
if 'password' in serializer.validated_data:
serializer.validated_data['password'] = make_password(serializer.validated_data['password'])
serializer.update(user, serializer.validated_data)
return Response(serializer.data)
else:
return Response(serializer.errors, status.HTTP_500_INTERNAL_SERVER_ERROR)
I found a way to solve my problem, using JPG idea. I just add an else using a PrimaryKeyRelatedField to allow serializer to get reference from the model from id.
There might be another solution, bu this one works and looks better than multiple serializers.
class UserSerializer(serializers.HyperlinkedModelSerializer):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if self.context['request'].method == 'GET':
self.fields['institution'] = InstitutionSerializer()
else:
self.fields['institution'] = serializers.PrimaryKeyRelatedField(queryset=Institution.objects.all())
Related
I have a User model that inherits from AbstractUser which has an email field. And a profile model that has an OneToOne relation with the User model
class User(AbstractUser):
email = models.EmailField(unique=True)
class Profile(models.Model):
user = models.OneToOneField(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
phone = models.CharField(max_length=13, validators=[phone_regex], unique=True, null=True, blank=True)
birth_date = models.DateField(blank=True, null=True)
about = models.TextField(max_length=2000, blank=True)
def __str__(self):
return f"{self.user.first_name} {self.user.last_name}"
view.py
class ProfileViewSet(ModelViewSet):
....
#action(detail=False, methods=["GET", "PUT"], permission_classes=[IsAuthenticated])
def me(self, request, *args, **kwargs):
profile = Profile.objects.get(user_id=request.user.id)
if request.method == "GET":
serializer = ProfileSerializer(profile)
return Response(serializer.data)
elif request.method == "PUT":
serializer = ProfileSerializer(profile, request.data)
serializer.is_valid(raise_exception=True)
serializer.save()
return Response(serializer.data)
serializers.py
class ProfileSerializer(serializers.ModelSerializer):
user = CurrentUserSerializer()
def update(self, instance, validated_data):
user_data = validated_data.pop('user')
user_ser = CurrentUserSerializer(instance=instance.user, data=user_data)
if user_ser.is_valid():
user_ser.save()
instance.phone = validated_data.get('phone', instance.phone)
instance.birth_date = validated_data.get('birth_date', instance.birth_date)
instance.about = validated_data.get('about', instance.about)
instance.save()
return instance
class Meta:
model = Profile
fields = [
'id',
'user',
'phone',
'birth_date',
'about',
]
Now when I try to update a user profile I get status: 400 Bad Request error
{
"user": {
"email": [
"user with this email already exists."
]
}
}
using patch instead of put or partial=True doesn't change anything I still get this error. What can I do here?
The thing is you are creating new record on data base and It is not permitted
class ProfileSerializer(serializers.ModelSerializer):
user = CurrentUserSerializer()
def update(self, instance, validated_data):
user_data = validated_data.pop('user')
try:
user=User.objects.get(pk=instance.user.id)
except User.DoesNotExist:
raise ValidationError("User already exist")
#now with user instance you can save the email or do whatever
you want
user.email = "xyaz#gmail.com")
user.save()
instance.phone = validated_data.get('phone', instance.phone)
instance.birth_date = validated_data.get('birth_date', instance.birth_date)
instance.about = validated_data.get('about', instance.about)
instance.save()
return instance
class Meta:
model = Profile
fields = [
'id',
'user',
'phone',
'birth_date',
'about',
]
Instead of a nested serializer I used serializer fields and my problem was solved. I'm not sure if it's a good approach or not.
class ProfileSerializer(serializers.ModelSerializer):
email = serializers.EmailField(source='user.email')
first_name = serializers.CharField(source='user.first_name')
last_name = serializers.CharField(source='user.last_name')
def update(self, instance, validated_data):
user_data = validated_data.pop('user')
user = instance.user
user_ser = CurrentUserSerializer(instance=user, data=user_data)
if user_ser.is_valid():
user_ser.save()
instance.phone = validated_data.get('phone', instance.phone)
instance.birth_date = validated_data.get('birth_date', instance.birth_date)
instance.about = validated_data.get('about', instance.about)
instance.save()
class Meta:
model = Profile
fields = [
'id',
'email',
'first_name',
'last_name',
'phone',
'birth_date',
'about',
]
I have an api that will do a patch on a resource (MyUser). It validates ok and seems to save the object, however when querying the database the changes have not been saved.
class UserSignupView(generics.UpdateAPIView):
serializer_class = MyUserSerializer
def get_object(self, email):
obj = MyUser.objects.get(email=email)
self.check_object_permissions(self.request, obj)
return obj
def patch(self, request):
print(request.user)
user = self.get_object(request.user.email)
print(user.street)
serializer = MyUserSerializer(user, data=request.data, partial=True)
serializer.is_valid(raise_exception=True)
serializer.save()
savedUser = MyUser.objects.get(email=request.user.email)
print(savedUser.street)
print(serializer.data)
return Response(serializer.data)
class MyUserSerializer(serializers.ModelSerializer):
class Meta:
model = MyUser
fields = (
'id', 'first_name', 'last_name', 'email', 'phone_number', 'street', 'locality', 'city',
'county', 'postcode')
Looking at the print statements I get:
user#example.com
None
123 Fake Street
MyUser object
It returns the correct serialised data which contains the changes but the database does not have the changes. The database connection is ok as I can query it and make other reads/writes/etc. It's pretty much the same as the UpdateModelMixin except I've had to override the get_object with a passed in parameter.
Try to override the update method and see what happens:
class MyUserSerializer(serializers.ModelSerializer):
class Meta:
model = MyUser
fields = (
'id', 'first_name', 'last_name', 'email', 'phone_number', 'street', 'locality', 'city',
'county', 'postcode')
def update(self, instance, validated_data):
instance.first_name = validated_data.get('first_name',instance.first_name)
instance.last_name = validated_data.get('last_name',instance.last_name)
instance.email = validated_data.get('email',instance.email)
instance.phone_number = validated_data.get('phone_number',instance.phone_number)
instance.street = validated_data.get('street',instance.street)
instance.locality = validated_data.get('locality',instance.locality)
instance.city = validated_data.get('city',instance.city)
instance.county = validated_data.get('county',instance.county)
instance.postcode = validated_data.get('postcode',instance.postcode)
instance.save()
return instance
i get this error when i am trying to update modified_by field
Tried to update field sales.CustomersTag.modified_by with a model instance, <SimpleLazyObject: <UserProfile: Admin>>. Use a value compatible with CharField.
this is my serializer.py:
class CustomersTagSerializer(serializers.ModelSerializer):
created_by = serializers.CharField(read_only=True, default=serializers.CurrentUserDefault())
modified_by = serializers.CharField(read_only=True, default=serializers.CurrentUserDefault())
def update(self, instance, validated_data):
instance.name = validated_data.get('name', instance.name)
instance.modified_by = validated_data.get('modified_by', instance.modified_by)
instance.save()
return instance
class Meta:
model = models.CustomersTag
fields = (
'id',
'name',
'created_date',
'modified_date',
'created_by',
'modified_by',
)
and this my view.py:
class CustomerTagGetIdPutView(generics.RetrieveAPIView,
mixins.UpdateModelMixin):
permission_classes = (AllowAny,)
queryset = models.CustomersTag.objects.all()
serializer_class = CustomersTagSerializer
def get_object(self):
id = self.kwargs['id']
obj = generics.get_object_or_404(models.CustomersTag, id=id)
return obj
def put(self, request, *args, **kwargs):
return self.update(request, *args, **kwargs)
def patch(self, request, *args, **kwargs):
return self.update(request, *args, **kwargs)
i tried alot to solve this problem but i can't .. any one can help me for this problem
If you're using Django REST Framework generic views and no overriding the behavior of methods like get_serializer or get_serializer_context, your serializer will receive a context object. This context object is a dictionary with the request and the view object.
That said, you can do this by overriding create() and update() in your serializer. For example:
class CustomersTagSerializer(serializers.ModelSerializer):
class Meta:
model = models.CustomersTag
fields = (
'id',
'name',
'created_date',
'modified_date',
'created_by',
'modified_by',
)
def create(self, validated_data):
user = self.context['request'].user
return models.CustomersTag.objects.create(
created_by=user, **validated_data)
def update(self, instance, validated_data):
user = self.context['request'].user
instance.name = validated_data.get('name', instance.name)
instance.modified_by = user
instance.save()
return instance
But maybe if you want to maintain a log history of editions in your models you could use a package like django-auditlog.
You can do this while calling save() in your model.
For example:
class CustomersTagSerializer(serializers.ModelSerializer):
created_by = models.ForeignKey(User, null=True, editable=False)
modified_by = models.ForeignKey(User, null=True, editable=False)
def save(self, *args, **kwargs):
user = get_current_user()
if user and user.is_authenticated():
self.modified_by = user
if not self.id:
self.created_by = user
super(CustomersTagSerializer, self).save(*args, **kwargs)
I come from Vietnam.
I want to update User by Django Rest FrameWork. I can update User by 'pk'. But I can't update User by 'username'. I hope that everyone help me. Thank you so much.
serializers.py
class UserDetailSerializer(serializers.ModelSerializer):
url_update = serializers.HyperlinkedIdentityField(view_name='api:UserUpdateAPIView', lookup_field='username')
class Meta:
model = User
fields = ('url_update', 'username', 'email', 'user_permissions', 'is_staff', 'groups', 'last_login')
class UserUpdateSerialier(serializers.ModelSerializer):
password = serializers.CharField(
style={'input_type': 'password'}
)
class Meta:
model = User
fields = ('pk', 'username', 'password')
def update(self, instance, validated_data):
instance.username = validated_data.get('username', instance.username)
instance.set_password(validated_data.get('password', instance.password))
instance.save()
return instance
views.py
class UserDetailAPIView(generics.RetrieveAPIView):
permission_classes = (permissions.IsAdminUser,)
serializer_class = UserDetailSerializer
#queryset = User.objects.all()
lookup_field = 'username'
def get_object(self):
username = self.kwargs["username"]
return get_object_or_404(User, username=username)
class UserUpdateAPIView(generics.RetrieveUpdateAPIView):
permission_classes = (permissions.IsAdminUser,)
#queryset = User.objects.all()
serializer_class = UserUpdateSerialier
def get_object(self):
username = self.kwargs["username"]
return get_object_or_404(User, username=username)
def put(self, request, *args, **kwargs):
return self.update(request, *args, **kwargs)
urls.py
url(r'^$', views.UserListAPIView.as_view(), name='UserListAPIView'),
url(r'^(?P<username>.*)/$', views.UserDetailAPIView.as_view(), name='UserDetailAPIView'),
url(r'^(?P<username>.*)/update/$', views.UserUpdateAPIView.as_view(), name='UserUpdateAPIView'),
HTTP 404 Not Found
Allow: GET, HEAD, OPTIONS
Content-Type: application/json
Vary: Accept
{
"detail": "Not found."
}
Try editing your view like this,
class UserUpdateAPIView(generics.RetrieveUpdateAPIView):
permission_classes = (permissions.IsAdminUser,)
serializer_class = UserUpdateSerialier
lookup_field = 'username'
def get_object(self):
username = self.kwargs["username"]
return get_object_or_404(User, username=username)
def put(self, request, *args, **kwargs):
return self.update(request, *args, **kwargs)
You need to set lookup_field = 'username' to the UserUpdateAPIView just like you did with the UserDetailAPIView
thee are my serializers for different users
class PlayerSerializer(serializers.ModelSerializer):
class Meta:
model = player
fields = ('contact_name', 'contact_email', 'address',)
class ManagerSerializer(serializers.ModelSerializer):
class Meta:
model = Manager
fields = ('contact_name', 'contact_email', 'address',
'city', 'country',)
class CoachSerializer(serializers.ModelSerializer):
class Meta:
model = Shipper
fields = (' 'contact_name', 'contact_email', 'address', 'city', 'about', 'country',)
these are my user Serializers with relation with each user type
class playerUserSerialzers(serializers.ModelSerializer):
player =PlayerSerializer()
class Meta:
model = MyUser
fields = ('email', 'id', 'password', 'player',)
extra_kwargs = {'password': {'write_only': True}}
def create(self, validated_data):
player_data = validated_data.pop('player')
user = MyUser(
email=validated_data['email'],
)
user.set_password(validated_data['password'])
user = MyUser.objects.create(**validated_data)
Player.objects.create(user=user, **player_data)
user.save()
return user
class managerUserSerialzers(serializers.ModelSerializer):
manager =ManagerSerializer()
class Meta:
model = MyUser
fields = ('email', 'id', 'password', 'manager',)
extra_kwargs = {'password': {'write_only': True}}
def create(self, validated_data):
manager_data = validated_data.pop('manager')
user = MyUser(
email=validated_data['email'],
)
user.set_password(validated_data['password'])
user = MyUser.objects.create(**validated_data)
Manager.objects.create(user=user, **manager_data)
user.save()
return user
class managerUserSerialzers(serializers.ModelSerializer):
coach =CoachSerializer()
class Meta:
model = MyUser
fields = ('email', 'id', 'password', 'coach',)
extra_kwargs = {'password': {'write_only': True}}
def create(self, validated_data):
coach_data = validated_data.pop('coach')
user = MyUser(
email=validated_data['email'],
)
user.set_password(validated_data['password'])
user = MyUser.objects.create(**validated_data)
Coach.objects.create(user=user, **coach_data)
user.save()
return user
this doesn't work but i would like to use different serializers based on the front end request
class MyUserViewSet(mixins.CreateModelMixin,
mixins.RetrieveModelMixin,
mixins.DestroyModelMixin,
mixins.UpdateModelMixin,
mixins.ListModelMixin,
GenericViewSet):
queryset = MyUser.objects.all()
pagination_class = LimitOffsetPagination
permission_classes = (AllowAny,)
how do i switch bases on serializer class here
def get_serializer_class(self,):
if self.request.query_params.get("player", None):
return MyBrokerUserSerialzers
From the documentation you can specify the serializer_class property telling the right serializer you want.
Then, as you are inheriting many mixing classes you can overwrite their methods and get the information from your frontend. For each kind of request method you will have to get the frontend information in a different way.
class MyUserViewSet(mixins.CreateModelMixin,
mixins.RetrieveModelMixin,
mixins.DestroyModelMixin,
mixins.UpdateModelMixin,
mixins.ListModelMixin,
GenericViewSet):
queryset = MyUser.objects.all()
pagination_class = LimitOffsetPagination
permission_classes = (AllowAny,)
def define_serializer_class(self, serializer_class_name):
serializer_class = serializer_class_name
def get(self, request, *args, **kwargs):
#here in GET METHOD you wil user the RetrieveModelMixin
#So get the kind of serializer from you get params in request.GET["param name"]
#and set the right one with a 'if' condition and calling
# self.define_serializer_class(<the name of the right serializer class>)
return self.retrieve(request, *args, **kwargs)
def post(self, request, *args, **kwargs):
#here in POST METHOD you wil user the CreateModelMixin
#So get the kind of serializer from you post playload in request.POST["param name"]
#and set the right one with a 'if' condition and calling
# self.define_serializer_class(<the name of the right serializer class>)
return self.update(request, *args, **kwargs)
def put(self, request, *args, **kwargs):
#Do the same here get the info from the frontend
return self.update(request, *args, **kwargs)
def delete(self, request, *args, **kwargs):
#Do the same here get the info from the frontend
return self.destroy(request, *args, **kwargs)
I'm sorry I cannot tell the right code to do what you want, but
the comments in the code explain step-by-step a way to solve your problem. I never have done it myself because I don't like to use mixings (they make up everying), but it will work for you. Just find the right way to get the information from the frontend for each kind of HTTP method that the mixins use and set the serializer_class as I said above.
I hope it helps you.
In my opinion this is unclear what you are trying to achieve and why are you doing things this way.
Let me put it in the perspective, if you have 3 seaparate objects, create 3 separate backends for them and you do not need anything else.
class UserMixin(object):
def create(self, validated_data):
user = MyUser(email=validated_data.pop('email'))
user.set_password(validated_data.pop('password')
user.save() # now you can use user in relations
validated_data['user'] = user
super(UserMixin, self).create(**validated_data)
return user
class PlayerSerializer(serializers.ModelSerializer, UserMixin):
class Meta:
model = Player
fields = ('id', 'email', 'password', 'contact_name', 'contact_email',
'address')
read_only_fields = ('id',)
extra_kwargs = {'password': {'write_only': True}}
class ManagerSerializer(serializers.ModelSerializer, UserMixin):
class Meta:
model = Manager
fields = ('id', 'email', 'password', 'contact_name', 'contact_email',
'address', 'city', 'country')
read_only_fields = ('id',)
extra_kwargs = {'password': {'write_only': True}}
class CoachSerializer(serializers.ModelSerializer, UserMixin):
class Meta:
model = Shipper
fields = ('id', 'email', 'password', 'contact_name', 'contact_email',
'address', 'city', 'about', 'country',)
read_only_fields = ('id',)
extra_kwargs = {'password': {'write_only': True}}
# now 3 viewsets
class PlayerViewset(viewsets.ModelViewSet):
model = Player
queryset = Player.objects.all()
serializer_class = PlayerSerializer
class ManagerViewset(viewsets.ModelViewSet):
model = Manager
queryset = Manager.objects.all()
serializer_class = ManagerSerializer
class CoachViewset(viewsets.ModelViewSet):
model = Shipper
queryset = Shipper.objects.all()
serializer_class = CoachSerializer
# register api ends
api_router.register('player', PlayerViewset)
api_router.register('manager', ManagerViewset)
api_router.register('coach', CoachViewset)
simple, efective and not much work...
p.s. class names starts with capital letter - please read pep-8