I'm trying to implement two serializer classes which will allow me to create both user and profile objects from a flattened POST request.
I tried the implementation described here and it works perfectly fine for updating (and only updating) existing objects.
Here is my current implementation:
# serializers.py
class UserSerializer(serializers.ModelSerializer):
password = serializers.CharField(
write_only=True, required=True, style={"input_type": "password"}
)
class Meta:
model = User
fields = (
"username",
"password",
# ...
"date_joined",
)
read_only_fields = ("date_joined")
class UserProfileSerializer(serializers.ModelSerializer):
user = UserSerializer()
def to_representation(self, instance):
representation = super().to_representation(instance)
representation.update(representation.pop("user"))
return representation
def to_internal_value(self, data):
user_internal = {}
for key in UserSerializer.Meta.fields:
if key in data:
user_internal[key] = data.pop(key)
internal = super().to_internal_value(data)
internal["user"] = user_internal
return internal
def create(self, validated_data):
user_data = validated_data.pop("user")
user = User.objects.create(**user_data)
user.set_password(user_data["password"])
user.save()
profile = UserProfile.objects.create(user=user, **validated_data)
return profile
class Meta:
model = UserProfile
fields = (
"user",
"date_updated",
# ...
"phone_number",
)
# views.py
class Register(generics.CreateAPIView):
serializer_class = UserProfileSerializer
name = "userprofile-create"
I expect the app to take the flattened JSON and create both user and profile objects.
Example POST body:
{
"username": "test_user",
"password": "P#$$w0rd",
"first_name": "Foo",
"last_name": "Boo",
"email": "foo#example.com",
"street": "Random Street",
"street_number": "11",
"flat_number": "11",
"zip_code": "11-111",
"city": "Some City",
"province": 1,
"phone_number": "111222333"
}
When I'm browsing through the API, the view still expects JSON with nested User object:
{
"user": {
"username": "",
"password": "",
"first_name": "",
"last_name": "",
"email": ""
},
"street": "",
"street_number": "",
"flat_number": "",
"zip_code": "",
"city": "",
"province": null,
"phone_number": ""
}
Overriding to_internal_value() and to_representation() wasn't correct approach.
I've created only one serializer with all fields and overriden create() method:
class UserProfileSerializer(serializers.ModelSerializer):
username = serializers.CharField(source="user.username")
password = serializers.CharField(
source="user.password",
write_only=True,
required=True,
style={"input_type": "password"},
)
first_name = serializers.CharField(source="user.first_name")
last_name = serializers.CharField(source="user.last_name")
email = serializers.EmailField(source="user.email")
def create(self, validated_data):
user_data = validated_data.pop("user")
user = User.objects.create(**user_data)
user.set_password(user_data["password"])
user.save()
profile = UserProfile.objects.create(user=user, **validated_data)
return profile
class Meta:
model = UserProfile
fields = (
"username",
"password",
"first_name",
"last_name",
"email",
"date_updated",
"street",
"street_number",
"flat_number",
"zip_code",
"city",
"province",
"phone_number",
)
Related
How to update a list in a nested serializer by id
I have a user who has multiple contacts
Example:
contact serializer
class ContactSerializer(serializers.ModelSerializer):
class Meta:
model = Contact
fields = [
'id',
'name',
'last_name'
]
user serializer
class UserSerializer(serializers.ModelSerializer):
email = serializers.EmailField(
required=True,
validators=[
UniqueValidator(queryset=User.objects.all())
]
)
contacts = ContactSerializer(many=True)
class Meta:
model = User
fields = [
"email",
"contacts"
]
def update(self, instance, validated_data):
contacts_data = validated_data.pop('contacts')
contacts = (instance.contacts).all()
contacts = list(contacts)
instance.name = validated_data.get('name', instance.name)
instance.save()
# many contacts
for contact_data in contacts_data:
contact = contacts.pop(0)
contact.name = contact_data.get('name', contact.name)
contact.last_name = contact_data.get('last_name', contact.last_name)
contact.save()
return instance
I want to update contacts by ID, for now it works, but it only updates the first contact in the list
Example I want to update the contact with ID 4 and 6
Payload request
{
"name": "string",
"contacts": [
{
"id": 4,
"name": "string",
"last_name": "string"
},
{
"id": 6,
"name": "string",
"last_name": "string"
}
]
}
Any ideas or recommendations?
Try it like this
def update(self, instance, validated_data):
contacts_data = validated_data.pop('contacts')
instance.name = validated_data.get('name', instance.name)
instance.save()
# many contacts
for contact_data in contacts_data:
contact = Contact.objects.get(pk=contact_data['id']) # this will crash if the id is invalid though
contact.name = contact_data.get('name', contact.name)
contact.last_name = contact_data.get('last_name', contact.last_name)
contact.save()
return instance
Here in my model I have multiple roles for users like HR, IT, ADMIN, FINANCE, EMPLOYEE and PROJECT_MANAGER. Purpose is to assign roles to user more than one based on role names. But I'm unable to get there.
Here are the models:
class UserRole(HRMBaseModel):
HR = 'HR'
IT = 'IT'
ADMIN = 'ADMIN'
FINANCE = 'FINANCE'
EMPLOYEE = 'EMPLOYEE'
PROJECT_MANAGER = 'PROJECT MANAGER'
ROLE_CHOICES = (
(HR, 'hr'),
(IT, 'it'),
(ADMIN, 'admin'),
(FINANCE, 'finance'),
(EMPLOYEE, 'employee'),
(SUPER_ADMIN, 'super admin'),
(PROJECT_MANAGER, 'project manager'),
)
role_name = models.CharField(max_length=255, choices=ROLE_CHOICES)
def __str__(self):
return self.role_name
class User(AbstractUser, HRMBaseModel):
username = models.CharField(max_length=50, unique=True)
email = models.EmailField(_('email address'))
role = models.ManyToManyField(UserRole)
Here is serializers.py to create user
class UserSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = ('username', 'email', 'first_name', 'last_name', 'role', 'password')
extra_kwargs = {'password': {'write_only': True}}
def create(self, validated_data):
role_data = validated_data.pop('role')
password = validated_data.pop('password')
user = User(**validated_data)
user.set_password(password)
user.save()
profile_data = {
"display_name": validated_data['first_name'],
"fullname": f"{validated_data['first_name']} {validated_data['last_name']}",
"dob": None,
"address": None,
"country": None,
"city": None,
"state": None,
"zip_code": None,
"profile_avatar": None
}
UserProfile.objects.create(user=user, **profile_data)
user.role.add(UserRole.objects.get(role_name=role_data[0]))
return user
Here is how I'm creating user
{
"email": "test_99#admin.com",
"first_name": "test_99",
"last_name": "test_99",
"username": "test_99",
"password": "test_99",
"role": [3]
}
But this is how I want to create user
{
"email": "test_99#admin.com",
"first_name": "test_99",
"last_name": "test_99",
"username": "test_99",
"password": "test_99",
"role": ["IT", "ADMIN", "HR"]
}
Almost there - let's modify your create method a bit:
def create(self, validated_data):
# pop role, should be a list ["IT", "ADMIN", "HR"]:
role_names = validated_data.pop('role')
# filter for role objects based on data in list:
roles = UserRole.objects.filter(role_name__in=role_names)
# other logic in create:
user = ...
...
# finally, add roles to user:
user.role.add(*roles)
user.save()
return user
I have created an Author model in which it has a OneToOne relationship with default Django User, as below:
from django.contrib.auth.models import User
from django.db import models
class Author(models.Model):
user = models.OneToOneField(
User,
related_name='author',
on_delete=models.CASCADE,
default="",
)
is_author = models.BooleanField(
default=True
)
And here I have created the following viewset:
class AuthorViewSet(viewsets.ModelViewSet):
serializer_class = AuthorSerializer
queryset = Author.objects.all()
def get_permissions(self):
if self.action == "list" or self.action == "retrieve" or self.action == "update":
self.permission_classes = [IsCurrentOwner, permissions.IsAdminUser]
elif self.action == "create":
self.permission_classes = [permissions.AllowAny]
return super(AuthorViewSet, self).get_permissions()
Question Is there any way that I can create both user and author in one step (request)? How?
serializer code:
class AuthorSerializer(serializers.ModelSerializer):
class Meta:
model = Author
fields = "__all__"
extra_kwargs = {
'password': {'write_only': True},
'id': {'read_only': True}
}
I have tried the following request but it doesn't work.
#url: localhost:8000/users/
#method: POST
{
"user": {
"username": "mostafa",
"password": "1"
}
}
Error:
{
"user": [
"Incorrect type. Expected pk value, received dict."
]
}
For this, you will need to create another serializer for user:
class UserSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = "__all__"
extra_kwargs = {
'password': {'write_only': True},
}
After that, make the user field in your AuthorSerializer an instance of UserSerializer and override the create method in this manner:
class AuthorSerializer(serializers.ModelSerializer):
user = UserSerializer()
class Meta:
model = Author
fields = "__all__"
extra_kwargs = {
'id': {'read_only': True}
}
def create(self, validated_data):
user = None
if "user" in validated_data:
user_data = validated_data.pop("user") or {}
user = User.objects.create_user(**user_data) # Assuming that it is default django user model
author = Author.objects.create(user=user, **validated_data)
author.save()
return author
curl script for your request payload:
curl --location --request POST 'http://127.0.0.1:8000/users/' \
--header 'Content-Type: application/json' \
--data-raw '{
"user": {
"username": "mostafa",
"password": "abcd"
},
"is_author": true
}'
And it responses as below:
{
"id": 1,
"user": {
"id": 1,
"last_login": null,
"is_superuser": false,
"username": "mostafa",
"first_name": "",
"last_name": "",
"email": "",
"is_staff": false,
"is_active": true,
"date_joined": "2020-12-05T09:54:37.674749Z",
"groups": [],
"user_permissions": []
},
"is_author": true
}
views.py
def districts_list(request):
obj_districts_list = Districts.objects.all()
data = serializers.serialize(
'json', obj_districts_list, fields=('district_name'))
return HttpResponse(data, content_type="application/json")
models.py
class Districts(models.Model):
id = models.AutoField(primary_key=True)
district_name = models.CharField(max_length=40)
country = models.ForeignKey('Country', on_delete=models.CASCADE)
state = models.ForeignKey('States', on_delete=models.CASCADE)
def __str__(self):
return '{}'.format(self.district_name)
OUTPUT:
But i just want the fields inside the Accounts.districts as output in JSON format.
Use QuerySet.values() to retrieve the fields you're interested in as dicts, then pass this to json.dumps(), ie:
>>> qs = User.objects.values("username", "first_name", "last_name", "email", "userprofile__nickname")
>>> print(json.dumps(list(qs), indent=2))
[
{
"username": "toto",
"first_name": "",
"last_name": "",
"email": "toto#example.com",
"userprofile__nickname": "Toto le toto"
},
{
"username": "root",
"first_name": "Root",
"last_name": "Root",
"email": "root#example.com",
"userprofile__nickname": "Root"
},
]
Depending on your models fields, you may have to provide a custom JSONDecoder subclass for datetimes and other types not directly handled by the default JSONEncoder.
I'm trying to make custom create()method to save my user profile when a User is created, the serializer works perfect for the GET method, but for POST it only shows one field instead of all fields.
I specified the fields = '__all__' property, but still don't work
GET:
JSON Response:
[
{
"job_title": {
"id": 1,
"name": "Desarrollador"
},
"birthdate": "2019-11-06",
"roles": [
{
"id": 1,
"name": "Administrador",
"description": "Administrador del sistema",
"key": "ADMIN"
}
]
}
]
POST:
Expected JSON:
[
{
"birthdate": "2019-11-06",
}
]
Serializers:
class UserProfileSerializer(serializers.ModelSerializer):
class Meta:
model = UserProfile
fields = '__all__'
depth = 1
class UserSerializer(serializers.ModelSerializer):
user_profile = UserProfileSerializer(read_only=False, many=False)
class Meta:
model = User
fields = '__all__'
depth = 1
def create(self, validated_data):
profile_data = validated_data.pop('user_profile')
user = User.objects.create(**validated_data)
UserProfile.objects.create(user=user, **profile_data)
return user
views.py
class UserDetail(generics.RetrieveUpdateDestroyAPIView):
profile = serializers.PrimaryKeyRelatedField(many=False, queryset=UserProfile.objects.all())
queryset = User.objects.all()
serializer_class = UserSerializer
class UserProfileList(generics.ListCreateAPIView):
queryset = UserProfile.objects.all()
serializer_class = UserProfileSerializer
models.py
class UserProfile(models.Model):
user = models.OneToOneField(User, related_name='user_profile', on_delete=models.CASCADE)
birthdate = models.DateField(default=datetime.date.today)
job_title = models.ForeignKey(JobTitle, related_name='job_title', on_delete=models.CASCADE)
roles = models.ManyToManyField(Role)
def createProfile(sender, **kwargs):
if kwargs['created']:
user_profile = UserProfile.objects.created(user=kwargs['instance'])
post_save.connect(createProfile, sender=User)
def __str__(self):
return self.user.username
I need that POST method also requires the full JSON, the same as the GET JSON response, in order to complete the create method. Need this for POST method:
{
"id": 1,
"user_profile": {
"job_title": {
"id": 1,
"name": "Desarrollador"
},
"birthdate": "2019-11-06",
"roles": [
{
"id": 1,
"name": "Administrador",
"description": "Administrador del sistema",
"key": "ADMIN"
}
]
},
"password": "pbkdf2_sha256$150000$0XdmQpeiPtVl$rQh2MEYV+IO5Y4gm2o1md2cVzgn/mL95r6m1TvRmG3g=",
"last_login": "2019-11-06T00:34:25-06:00",
"is_superuser": true,
"username": "aruiz",
"first_name": "",
"last_name": "",
"email": "correo#correo.com",
"is_staff": true,
"is_active": true,
"date_joined": "2019-11-05T11:37:49-06:00",
"groups": [],
"user_permissions": []
}