Combining serializer and model functions - python

I have two serializers... MyRegisterSerializer inherits and extends a popular app/package, django-rest-auth, which connects to a fairly standard user table. I also have a Model and serializer for a custom app, TeamSerializer (a one-to-many relationship with users). When a user signs up, I would like them to be able to join a team at the same time, so I somehow need to create a team, return the team ID and then pass that ID to the RegisterSerializer, so that the ID of the team can be stored in the User table. I know I could make two calls, first to create the team and return the value, and then pass it to the register serializer, but is there a way to do this all in one serializer? I am a n00b at python, and cant find a great example of this, considering I have to return the get_cleaned_data() function as it is. Thank you!
class TeamSerializer(serializers.ModelSerializer):
class Meta:
model = Team
fields = ('id', 'name', 'logo', 'user')
class MyRegisterSerializer(RegisterSerializer):
first_name = serializers.CharField()
last_name = serializers.CharField()
def get_cleaned_data(self):
super(MyRegisterSerializer, self).get_cleaned_data()
return {
'team_id': <How do I get this value>
'username': self.validated_data.get('username', ''),
'position': self.validated_data.get('password1', ''),
'email': self.validated_data.get('email', ''),
'first_name': self.validated_data.get('first_name', ''),
'last_name': self.validated_data.get('last_name', '')
}

It depends on how you want to create the team:
1. The team is created by some other information:
You should be able to use this custom field:
from rest_framework.relations import PrimaryKeyRelatedField
class TeamPrimaryKeyRelatedField(PrimaryKeyRelatedField):
def to_internal_value(self, data):
if self.pk_field is not None:
data = self.pk_field.to_internal_value(data)
try:
obj, created = self.get_queryset().get_or_create(
pk=data,
defaults=get_team_data(),
)
return obj
except (TypeError, ValueError):
self.fail('incorrect_type', data_type=type(data).__name__)
And use it in your Serializer:
class MyRegisterSerializer(RegisterSerializer):
team = TeamPrimaryKeyRelatedField()
# ...
2. Use extra user input to create the team:
This looks like a perfect use case for writable nested serializers:
class TeamSerializer(serializers.ModelSerializer):
class Meta:
model = Team
fields = ('id', 'name', 'logo', 'user')
class MyRegisterSerializer(RegisterSerializer):
first_name = serializers.CharField()
last_name = serializers.CharField()
team = TeamSerializer()
def create(self, validated_data):
team_data = validated_data.pop('team')
# You could do this if the user is not necessary in the team object:
team = Team.objects.create(**team_data)
user = super().create(team=team, **validated_data)
# Otherwise:
user = super().create(**validated_data)
# Should this be a many-to-many relationship?
team = Team.objects.create(user=user, **team_data)
# I don't know if this works/you need it:
self.team = team
# Or it should be like this?
self.validated_data['team'] = team
return user
I'm not sure what exactly you need. Let me know if you need further help.

Related

Django Rest Framework, creating a one-to-many field relationship between users and another model

I am trying to create a simple model which holds a number as the primary key (week number) and then a list of users. Thus the model should be something like this,
{
id: 10,
users: [
user1,
user2,
...
]
}
I am pretty sure I should do this with a one-to-many field. Thus I created the following model,
class Schema(models.Model):
week = models.PositiveIntegerField(primary_key=True,
unique=True,
validators=[MinValueValidator(1), MaxValueValidator(53)],
)
users = models.ForeignKey(MyUser, related_name="users", null=True, blank=True, on_delete=models.SET_NULL)
class Meta:
ordering = ('week',)
What I want to happen is that if you do a POST request with an id and a list of users, then it simply creates the model. However if the id already exists, then it should simply clear the users, and add the newly given users instead. This is where I am stuck, I have tried the following (keeping comments in the code),
class SchemaSerializer(serializers.ModelSerializer):
# users = serializers.PrimaryKeyRelatedField(many = True, queryset = MyUser.objects.all())
# user_set = UserSerializer(many = True)
class Meta:
model = Schema
fields = ('week', 'users')
# def create(self, validated_data):
# # users_data = validated_data.pop('users')
# schema = Schema.objects.create(**validated_data)
# # answer, created = Schema.objects.update_or_create(
# # week=validated_data.get('week', 1),
# # defaults={'users', validated_data.get('users', [])}
# # )
# return schema
def update(self, instance, validated_data):
users_data = validated_data.pop('users')
instance.users.clear()
instance.save()
for user in users_data:
instance.users.add(user)
instance.save()
return instance
Another problem I am running into is that, I don't know how the request expects the data, e.g, from Postman, I would think that it would only need the users id and of course the models id (week), however this is one of the things I have tried,
{
"week": 32,
"users": [{
"id": 1,
"first_name": "Test",
"last_name": "test",
"email": "test#test.dk"
}]
}
ForeignKey won't work for you. If one Schema object should have many relations with User model then you have to go with ManyToMany relationship(django docs). So in yours model:
class Schema(models.Model):
week = models.PositiveIntegerField(primary_key=True,
unique=True,
validators=[MinValueValidator(1), MaxValueValidator(53)],
)
users = models.ManyToManyField(MyUser, related_name="users")
class Meta:
ordering = ('week',)
for the part of update i think you need to pass user object not id to add(), so i would try:
def update(self, instance, validated_data):
users_data = validated_data.pop('users')
users = User.object.filter(id__in=user_data)
instance.users.clear()
instance.users.add(*users)
instance.save()
return instance
for api view i recomend to read this thread on stack: REST ManyToMany

Id instead of String when displaying foreign key field in DRF

I'm trying to return the name of the pricing field but all I get is its foreign key id instead. What am I doing wrong here? I looked at some similiar issues on here but I didn't find anything that resembled my situation.
class UserProfileSerializer(serializers.ModelSerializer):
class Meta:
model = UserProfile
fields = (
"assignedteams",
"agent",
"facility",
"organisor",
"avatar",
)
class UserSubscriptionSerializer(serializers.ModelSerializer):
class Meta:
model = Subscription
fields = (
"user",
"pricing",
"status",
)
class UserSerializer(UserDetailsSerializer):
profile = UserProfileSerializer(source="userprofile")
subscription = UserSubscriptionSerializer(source="usersubscription")
class Meta(UserDetailsSerializer.Meta):
fields = UserDetailsSerializer.Meta.fields + ('profile', 'subscription',)
def update(self, instance, validated_data):
userprofile_serializer = self.fields['profile']
userprofile_instance = instance.userprofile
userprofile_data = validated_data.pop('userprofile', {})
usersubscription_serializer = self.fields['subscription']
usersubscription_instance = instance.usersubscription
usersubscription_data = validated_data.pop('usersubscription', {})
# update the userprofile fields
userprofile_serializer.update(userprofile_instance, userprofile_data)
usersubscription_serializer.update(usersubscription_instance, usersubscription_data)
instance = super().update(instance, validated_data)
return instance
You have 2 options to solve this problem.
option1:
If you want to return only the name of your pricing model you can use SlugRelatedField to do it.
Example:
class UserSubscriptionSerializer(serializers.ModelSerializer):
pricing = serializers.SlugRelatedField('name', readonly=True)
class Meta:
model = Subscription
fields = (
"user",
"pricing",
"status",
)
Option2:
If you want to return the Pricing object you can create a new ModelSerializer for your Pricing model and use it.
Example:
class PricingSerializer(serializers.ModelSerializer):
class Meta:
model = Pricing
fields = ["id","name"]
class UserSubscriptionSerializer(serializers.ModelSerializer):
pricing = PricingSerializer(readonly=True)
class Meta:
model = Subscription
fields = (
"user",
"pricing",
"status",
)
There are some other options that can you use but you must explain more about your problem can I will help you with.
you can easily add a new field representation or override the pricing field when want to represent data
so in your serializer add the following code
class UserSubscriptionSerializer(serializers.ModelSerializer):
class Meta:
model = Subscription
fields = (
"user",
"pricing",
"status",
)
def to_representation(self, instance):
data = super().to_representation(instance)
data['pricing_name'] = instance.pricing.name # or replace the name with your pricing name field
return data
As you are saying pricing returned FK id, so i assume pricing column inside Subscription model is a FK to another model, let's assume it Pricing model.
You can create a serializer for Pricing and use it on UserSubscriptionSerializer,
like the way you created UserProfileSerializer and UserSubscriptionSerializer for UserSerializer
But, using directly a nested serializer will give you problem while doing write operation since as far i can understand you are accepting pricing as FK value when creating or updating
To solve this issue you can do some if/else on get_fields() method
class UserSubscriptionSerializer(serializers.ModelSerializer):
class Meta:
model = Subscription
fields = (
"user",
"pricing",
"status",
)
def get_fields(self):
fields = super().get_fields()
# make sure request is passed through context
if self.context['request'] and self.context['request'].method == 'GET':
fields['pricing']=PricingSerializer()
return fields
Now coming back to the question, since you only need the pricing name which i assume name is a column on Pricing model
simply rewrite the previous code as
def get_fields(self):
fields = super().get_fields()
# make sure request is passed through context
if self.context['request'] and self.context['request'].method == 'GET':
fields['pricing'] = serializers.CharField(source='pricing.name', read_only=True)
return fields
P.S: I haven't tested this code on my computer

Fill Many to Many field through API

I am trying to implement an API to add Agent. The Agent has Many To Many field, Role.
I am using Django and Django Rest Framework.
Here is the models :
class Role(models.Model):
code = models.CharField(max_length=50, primary_key=True)
labe = models.CharField(max_length=50)
def __str__(self):
return '%s %s' % (self.labe, self.code)
class Agent(models.Model):
firstName = models.CharField(max_length=60)
lastName = models.CharField(max_length=60)
role = models.ManyToManyField(Role)
So I created Serializers :
class RegistrationSerializer(serializers.ModelSerializer):
role = serializers.PrimaryKeyRelatedField(
many=True, read_only=True)
class Meta:
model = Agent
fields = ['email', 'firstName', 'lastName', 'role',
'phoneNumber', 'experienceWorkYeares'],
def save(self):
agent = Agent.objects.create(
email=self.validated_data['email'],
firstName=self.validated_data['firstName'],
lastName=self.validated_data['lastName'],
phoneNumber=self.validated_data['phoneNumber'],
experienceWorkYeares=self.validated_data['experienceWorkYeares']
role=self.validated_data['role'] // One of my multiple try but doesn`t work.
)
agent.save()
return agent
How can I retrieve the role I sent via Postman and put it in the agent ? for the role I am POSTing role = "CODE1".
Thank you so much in advance.
Basically what I am trying to do is : For each Agent there one or more role. I trying a lot of thing and I follow documentation but I am not able to do it.
First of all, your role field should not be declared in the Meta class but in the ModelSerializer directly:
class RegistrationSerializer(serializers.ModelSerializer):
role = serializers.PrimaryKeyRelatedField(many=True, read_only=True)
class Meta:
model = Agent
fields = ['email', 'firstName', 'lastName', 'role',
'phoneNumber', 'experienceWorkYeares'],
extra_kwargs = {
'password': {'write_only': True},
}
Then, if you want to write a ManyToManyField you should use something like:
my_role = Role.objects.get(pk=self.validated_data['role'])
agent.role.add(my_role)
Another thing you could do is add a validate_role(self, value) method in your serializer that checks if the primary key provided is correct, and returns Role.objects.get(pk=value) -- then you would get the Role instance in your validated_data. See this part of DRF doc

Django Rest Framework- can I allow pk id or full objects in a serializer's create method?

Lets say I have the following models:
class Author(models.Model):
first_name = models.CharField(max_length=32)
last_name = models.CharField(max_length=32)
class Book(models.Model):
title = models.CharField(max_length=64)
author = models.ForeignKeyField(Author, on_delete=models.CASCADE)
And I have the following serializer:
class BookSerializer(serializers.ModelSerializer):
class Meta:
model = Book
fields = ('id', 'title', 'author')
read_only_fields = ('id')
If I then query my books, A book's data looks like:
{
"id": 1,
"title": "Book Title",
"author": 4
}
Which is what I want, as I return both an array of books, as well as an array of authors, and allow the client to join everything up. This is because I have many authors that are repeated across books.
However, I want to allow the client to either submit an existing author id to create a new book, or all of the data for a new author. E.g.:
Payload for new book with existing author:
{
"title": "New Book!",
"author": 7
}
or, payload for a new book with a new author:
{
"title": "New Book!",
"author": {
"first_name": "New",
"last_name": "Author"
}
}
However the second version, will not pass the data validation step in my serializer. Is there a way to override the validation step, to allow either an author id, or a full object? Then in my serializer's create method, I can check the type, and either create a new author, get its id, and create the new book, or just attach the existing id. Thoughts?
I believe that it is not possible to do it in the way you want ( using one field author).
It just because one serializer cannot handle two different types for one field.
Note: i might be wrong about the previous statement.
However, the following is a potential solution for you. You just need to use different field name to create new author.
class BookSerializer(serializers.ModelSerializer):
author = serializers.PrimaryKeyRelatedField(
required=False,
queryset=Author.objects.all(),
)
author_add = AuthorSerializer(write_only=True, required=False)
class Meta:
model = Book
fields = ('id', 'title', 'author', 'author_add')
read_only_fields = ('id')
def create(self, validated_data):
author_add_data = validated_data.pop('author_add', None)
if author_add is not None:
validated_data['author'] = Author.objects.create(**author_add_data)
return super().create(validated_data)
Note: you need to handle a case where you send both author and author_add. Probably add a check into validation step and raise ValidationError if both are provided.
Offtopic hint: you dont need to explicityl state read_only_fields = ('id',) - primary keys are read-only.
For anyone else trying to do this, here is what I ended up getting working.
For my book serializer I did the following:
class BookSerializer(serializers.ModelSerializer):
# make author a foreign key/id, read-only field so that it isn't
# processed by the validator, and on read returns just the id.
class Meta:
model = Book
fields = ('id', 'title', 'author')
read_only_fields = ('id', 'author',)
# override run_validation to validate our author
def run_validation(self, data):
# run base validation. Since `author` is read_only, it will
# be ignored.
value = super(Book, self).run_validation(data)
# inject our validated author into the validated data
value['author'] = self.validate_author(data['author'])
return value
# Custom author validation
def validate_author(self, author):
errors = OrderedDict()
if isinstance(author, int): # if just a key, retrieve the author
try:
author_instance = Author.objects.get(pk=author)
except Author.DoesNotExist:
errors['author'] = "Author with pk {} does not exist.".format(author)
raise ValidationError(errors)
else: # if passed an author object...
author_serializer = AuthorSerializer(data=author, many=False)
author_serializer.is_valid(raise_exception=True)
author_instance = author_serializer.save()
return author_instance
I need to do a bit more error checking (e.g.- no author passed), but it works quite well- the consumer of the API can submit either an author id, or a serialized author object to create a new author. And the API itself returns just an id as was needed.

Django rest framework nested serialization not working properly

I have these serializers in my app:
class ScheduleSerializer(serializers.ModelSerializer):
class Meta:
model = Schedule
fields = ('id',)
class DisciplineSerializer(serializers.ModelSerializer):
class Meta:
model = Discipline
fields = ('id',)
class WriteTeacherSerializer(serializers.ModelSerializer):
disciplines = DisciplineSerializer(many=True)
schedules = ScheduleSerializer(many=True)
class Meta:
model = Teacher
fields = ('phone_number', 'bio', 'price', 'disciplines', 'schedules')
depth = 1
def update(self, instance, validated_data):
print "\n"
#Debugging here
print validated_data
print "\n"
print instance.__dict__
print "\n"
instance.phone_number = validated_data['phone_number']
instance.bio = validated_data['bio']
instance.price = validated_data['price']
disciplines = validated_data.pop('disciplines')
schedules = validated_data.pop('schedules')
for discipline in disciplines:
try:
stored_discipline = Discipline.objects.get(id=discipline['id'])
instance.disciplines.add(stored_discipline)
except Discipline.DoesNotExist:
raise Http404
for schedule in schedules:
try:
stored_schedule = Schedule.objects.get(id=schedule['id'])
instance.schedules.add(stored_discipline)
except Discipline.DoesNotExist:
raise Http404
instance.save()
return instance
As you can see I am trying a nested serialization with the fields schedules and disciplines. I think I followed the documentation, but the nested serialization is not working when I test it. I printed the instance and validated_data objects and tested it on the shell.
I start the data in this format:
data = {u'phone_number': u'+99999999999', u'bio': u'BIO', u'price': 40, u'disciplines': [{'id': 1}], u'schedules': [{'id': 2}]}
I got a teacher instance and started the serializer like this:
serializer = WriteTeacherSerializer(teacher, data=data)
It shows True on a serializer.is_valid() call.
However when I try to save it the validated_data and the instance.__dict__ are like that:
#validated_data
{u'phone_number': u'+5584998727770', u'bio': u'BIO', u'price': 40, u'disciplines': [OrderedDict()], u'schedules': [OrderedDict()]}
#instance.__dict__
{'phone_number': u'', 'bio': u'', 'price': 50, 'profile_id': 2, '_state': <django.db.models.base.ModelState object at 0xb64a6bec>, 'id': 6}
They don't seem to notice the nested fields wich makes the update() method not work.
Am I doing something wrong?
Here is my Teacher Model as well:
class Teacher(models.Model):
price = models.IntegerField(default=50)
phone_regex = RegexValidator(regex=r'^\+?1?\d{9,15}$', message="Wrong phone number format.")
phone_number = models.CharField(validators=[phone_regex], max_length=15, blank=True)
profile = models.OneToOneField(Profile, on_delete=models.CASCADE)
schedules = models.ManyToManyField(Schedule, related_name='schedules')
disciplines = models.ManyToManyField(Discipline, related_name='disciplines')
bio = models.CharField(max_length=200, blank=True)
If you are just sending IDs then you don't need to add the nested serializer, just specify the field name of the ForeignKey or ManyToManyField.
class WriteTeacherSerializer(serializers.ModelSerializer):
class Meta:
model = Teacher
fields = ('phone_number', 'bio', 'price', 'disciplines', 'schedules')
I am also wondering if it is because you have a depth=1 flag?
DRF doesn't support nested updates out of the box. You have to override the Serializer's update method, and write your own update logic, so you'd be seeing an error for this warning if you were sending nested data.

Categories