How to implement ForeignKey with Django serializer - json

I have this models:
class Discipline(models.Model):
name = models.CharField(max_length=200)
class Lesson(models.Model):
discipline = models.ForeignKey(Discipline, on_delete=models.CASCADE, null=True)
date = models.DateField()
regular_slot = models.BooleanField(default=False)
And these serializers:
class DisciplineSerializer(serializers.ModelSerializer):
class Meta:
model = Discipline
fields = ('name')
class LessonSerializer(serializers.ModelSerializer):
discipline = serializers.RelatedField(source='Discipline', read_only=True);
class Meta:
model = Lesson
fields = ('discipline', 'date', 'regular_slot')
I have a view to process a JSON request and to save the data:
def cours_list(request):
if request.method == 'POST':
data = JSONParser().parse(request)
serializer = LessonSerializer(data=data)
if serializer.is_valid():
serializer.save()
return JSONResponse(serializer.data, status=201)
return JSONResponse(serializer.errors, status=400)
My JSON request is as follows:
{"discipline":"Mathematiques","date":"2017-12-03"}
However, I have an error saying that:
'Lesson' object has no attribute 'Discipline'
I believe this is because attribute discipline in Lesson refers only to the id and is not of type Discipline. I could'nt find how to solve this. Is the way I define foreignKey is not correct ?
How to insert a foreignKey reference here while I receive a Discipline object from the JSON request ?

I'm going to help you also with this :)
class DisciplineSerializer(serializers.ModelSerializer):
class Meta:
model = Discipline
fields = ('name')
class LessonSerializer(serializers.ModelSerializer):
discipline = DiscliplineSerializer()
class Meta:
model = Lesson
fields = ('discipline', 'date', 'regular_slot')
Doing this is enough as far as I know.
Edit: Reason of your error is that you write "source="Discipline"" but there is no field named Discipline in your Lesson model. Therefore, it gives an error.

Related

Update multiple model data through one serializer

Please go through the description, I tried to describe everything i've encountered while trying to solve this issue.
I have two models, User and DoctorProfile. User model has OneToOne relation with DoctorProfile. I'm trying to update data of both model through one serializer. I've combined two models into one serilzer like below:
class DoctorProfileFields(serializers.ModelSerializer):
"""this will be used as value of profile key in DoctorProfileSerializer"""
class Meta:
model = DoctorProfile
fields = ('doctor_type', 'title', 'date_of_birth', 'registration_number', 'gender', 'city', 'country', )
class DoctorProfileSerializer(serializers.ModelSerializer):
"""retrieve, update and delete profile"""
profile = DoctorProfileFields(source='*')
class Meta:
model = User
fields = ('name', 'avatar', 'profile', )
#transaction.atomic
def update(self, instance, validated_data):
ModelClass = self.Meta.model
profile = validated_data.pop('profile', {})
ModelClass.objects.filter(id=instance.id).update(**validated_data)
if profile:
DoctorProfile.objects.filter(owner=instance).update(**profile)
new_instance = ModelClass.objects.get(id = instance.id)
return new_instance
When I send request with GET method, the DoctorProfileSerializer returns nested data(Combining two models User and DoctorProfile) in the desired fashion.
But when I try to update both models through this serializer, it returns error saying User has no field named 'doctor_type'.
Let's have a look at the JSON i'm trying to send:
{
"name": "Dr. Strange updated twice",
"profile" : {
"doctor_type": "PSYCHIATRIST"
}
}
Let's have a look at how the serializer is receiving the JSON:
{
"name": "Maruf updated trice",
"doctor_type": "PSYCHIATRIST"
}
Models:
class CustomUser(AbstractBaseUser, PermissionsMixin):
class Types(models.TextChoices):
DOCTOR = "DOCTOR", "Doctor"
PATIENT = "PATIENT", "Patient"
#Type of user
type = models.CharField(_("Type"), max_length=50, choices=Types.choices, null=True, blank=False)
avatar = models.ImageField(upload_to="avatars/", null=True, blank=True)
email = models.EmailField(max_length=255, unique=True)
name = models.CharField(max_length=255)
is_active = models.BooleanField(default=True)
is_staff = models.BooleanField(default=False)
objects = CustomBaseUserManager()
USERNAME_FIELD = 'email'
REQUIRED_FIELDS = ['name', 'type'] #email is required by default
def get_full_name(self):
return self.name
def __str__(self):
return self.email
class DoctorProfile(models.Model):
"""Model for Doctors profile"""
class DoctorType(models.TextChoices):
"""Doctor will choose profession category from enum"""
PSYCHIATRIST = "PSYCHIATRIST", "Psychiatrist"
PSYCHOLOGIST = "PSYCHOLOGIST", "Psychologist"
DERMATOLOGIST = "DERMATOLOGIST", "Dermatologist"
SEXUAL_HEALTH = "SEXUAL HEALTH", "Sexual health"
GYNECOLOGIST = "GYNECOLOGIST", "Gynecologist"
INTERNAL_MEDICINE = "INTERNAL MEDICINE", "Internal medicine"
DEVELOPMENTAL_THERAPIST = "DEVELOPMENTAL THERAPIST", "Developmental therapist"
owner = models.OneToOneField(
CustomUser,
on_delete=models.CASCADE,
related_name='doctor_profile'
)
doctor_type = models.CharField(
_("Profession Type"),
max_length=70,
choices=DoctorType.choices,
null=True,
blank=False
)
title = models.IntegerField(_('Title'), default=1, choices=TITLES)
date_of_birth = models.DateField(null=True, blank=False)
gender = models.IntegerField(_('Gender'), default=1, choices=GENDERS)
registration_number = models.IntegerField(_('Registration Number'), null=True, blank=False)
city = models.CharField(_('City'), max_length=255, null=True, blank=True)
country = models.CharField(_('Country'), max_length=255, null=True, blank=True)
def __str__(self):
return f'profile-{self.id}-{self.title} {self.owner.get_full_name()}'
How do I know that the serializer is getting wrong JSON? I debugged the validated_data in the DoctorProfileSerializer and it's showing that it's a flat JSON, there's no key named profile.
I'm assuming the problem is with the source that I've added in the DoctorProfileSerializer. But if I don't use the source the get method returns the following error
Got AttributeError when attempting to get a value for field profile on serializer (DoctorProfileSerializer).
Please let me know if it's solvable also if it's a good approach to do it this way?
Ok, sorry if my answer is too long but let me try to answer step by step,
Models:
class DoctorProfile(models.Model):
# everything as it is
# except I feel comfortable using ForeignKey :D
owner = models.ForeignKey(
CustomUser,
on_delete=models.CASCADE,
related_name='doctor_profile'
)
# everything as it is
class CustomUser(AbstractBaseUser, PermissionsMixin):
# as it is
Serializers:
class DoctorProfileSerializer(serializers.ModelSerializer):
"""Serializer for DoctorProfile."""
class Meta(object):
model = DoctorProfile
fields = [
'id',
'doctor_type',
'title',
'date_of_birth',
'registration_number',
'gender',
'city',
'country',
]
read_only_fields = [
'id',
]
class CustomUserSerializer(serializers.ModelSerializer):
"""Serializer for DoctorProfile."""
# here I'm renaming the related object exactly as the
# related name you've provided on model
doctor_profile = DoctorProfileSerializer(many=False)
class Meta(object):
model = CustomUser
fields = [
'name',
'avatar',
'doctor_profile',
]
read_only_fields = [
'id',
]
def update(self, instance, validated_data):
# instance is the current row of CustomUser
# validated_data is the new incoming data
# use validated_data.pop('doctor_profile') to extract
# doctor_profile data and do whatever is needed on
# DoctorProfile model
# compare them and perform your update method
# as you wish on the DoctorProfile model
# object after updating models, you can query the total
# object again before returning if you want
return updated_object
View:
class CustomUserAPIView(RetrieveUpdateAPIView):
"""CustomUserAPIView."""
permission_classes = [IsAuthenticated]
model = CustomUser
serializer_class = CustomUserSerializer
lookup_field = 'id'
#for returning logged in user info only
def get_queryset(self):
return CustomUser.objects.filter(id=self.request.user.id).first()
def update(self, request, *args, **kwargs):
"""Update override."""
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)
custom_user_obj = CustomUser.objects.filter(
id=instance.id,
).first()
serializer = CustomUserSerializer(custom_user_obj)
return Response(serializer.data)
Run the migration and let me know if you are getting the expected output on GET method. For UPDATE method if you face any problem let me know I will update the answer accordingly right away.
For keeping all the Django Rest Framework related docs handy, use this link
https://www.cdrf.co/

To return custom field in DjangoRestFramework after aggregation

There are lot of answers related to custom fields on stackoverflow but when trying with them, I am getting different error, so posting a separate question.
I wanted to return a JSON Response for the following url
urls.py
path('cards/<int:pk>/smarts', smarts.as_view(), name="smarts"),
I will be using below api.py file to return aggregate fields using Transaction model, the query is working fine, I only have to return appropriate response. Here I have one of the fields as Decimal hence tried with DjangoJSONEncoder but got an error.
api.py
class smarts(generics.ListAPIView):
serializer_class = TransactionSerializer
permission_classes = [permissions.IsAuthenticated, TransactionIsOwnerOrNot]
def get_queryset(self):
card = get_object_or_404(self.request.user.cards, pk=self.kwargs['pk'])
qs=card.transactions.values('vendor').annotate(a=Count('pk'),b=Sum('amount')).order_by('-b')
....CODE REQUIRED
return ....
models.py
class Transactions(models.Model):
amount = models.DecimalField(max_digits=19, decimal_places=2)
vendor = models.CharField(max_length=200)
category = models.CharField(max_length=200)
owner = models.ForeignKey(Cards, on_delete=models.CASCADE, related_name="transactions",null=True)
serializer.py
class TransactionSerializer(serializers.ModelSerializer):
class Meta:
model = Transactions
fields = '__all__'
I found the answer after hit and trial, I used custom serializer to return required field.
serializer.py
class SmartSerializer(serializers.Serializer):
vendor = serializers.CharField(max_length=200)
tot = serializers.IntegerField()
tot_amt = serializers.DecimalField(max_digits=19, decimal_places=2)
api.py
class smartstatements(generics.ListAPIView):
permission_classes = [permissions.IsAuthenticated, TransactionIsOwnerOrNot]
serializer_class = SmartSerializer
def get_queryset(self):
card = get_object_or_404(self.request.user.cards, pk=self.kwargs['pk'])
queryset=card.transactions.values('vendor')
.annotate(tot=Count('pk'),tot_amt=Sum('amount'))
.order_by('-tot_amt')
return queryset

Why is my Django serializer telling me an attribute doesn't exist when I see it defined in my model?

I'm using Python 3.7 and the Django rest framework to serialize some models into JSOn data. I have this
data = {
'articlestats': ArticleStatSerializer(articlestats, many=True).data,
and then I have defined the following serializers ...
class LabelSerializer(serializers.ModelSerializer):
class Meta:
model = Label
fields = ['name']
...
class ArticleSerializer(serializers.ModelSerializer):
label = LabelSerializer()
class Meta:
model = Article
fields = ['id', 'title', 'path', 'url', 'label']
class ArticleStatSerializer(serializers.ModelSerializer):
article = ArticleSerializer()
class Meta:
model = ArticleStat
fields = ['id', 'article', 'score']
I have defined my Label model like so ...
class Label(models.Model):
name = models.CharField(max_length=200)
def __str__(self):
return self.name
class Meta:
unique_together = ("name",)
but I'm getting this error when Django processes my serialize line ...
AttributeError: Got AttributeError when attempting to get a value for field `name` on serializer `LabelSerializer`.
The serializer field might be named incorrectly and not match any attribute or key on the `str` instance.
Original exception text was: 'str' object has no attribute 'name'.
Not sure why it's complaining. The "name" attribute is right there. What else should I be doing?
Edit: Models asked for ...
class ArticleStat(models.Model):
objects = ArticleStatManager()
article = models.ForeignKey(Article, on_delete=models.CASCADE, related_name='articlestats')
score = models.FloatField(default=0, null=False)
class Article(models.Model):
objects = ArticleManager()
title = models.TextField(default='', null=False)
path = models.TextField(default='', null=False)
url = models.TextField(default='', null=False)
label = models.TextField(default='', null=True)
created_on = models.DateTimeField(db_index=True, default=datetime.now)
In Article model the label field is TextField, not any kind of related fields. But in your serializer, it expects a related field.
class ArticleSerializer(serializers.ModelSerializer):
label = LabelSerializer()
class Meta:
model = Article
fields = ['id', 'title', 'path', 'url', 'label']

FilterSet for ManyToMany relationship existence

Basic models:
class ModelA(Model):
name = CharField(...)
class ModelB(Model):
model_a = ManyToManyField(ModelA, blank=True)
class ModelBFilter(filters.FilterSet):
unassigned = BooleanFilter(field_name='model_a', lookup_expr='isnull')
class Meta:
model = ModelB
fields = ['unassigned']
How do I filter (with django-filter) to find the ModelB's that do not have a corresponding related model?
It looks to me that what you have should work. Perhaps the fields = ['unassigned'] is unnecessary? According to the documentation you can also negate the filter thusly:
class ModelBFilter(filters.FilterSet):
assigned = BooleanFilter(field_name='model_a', lookup_expr='isnull', exclude=True)
class Meta:
model = ModelB

depth = 1 doesn't work properly and it's saves Null in ManyToManyField and ForeignKey fields in Django Rest Framework

after adding depth = 1 doesn't work properly
=> models.py file
class State(models.Model):
state_name = models.CharField(max_length = 30, unique=True)
def __unicode__(self):
return str(self.state_name)
class City(models.Model):
state = models.ForeignKey(State, related_name='state_city')
city_name = models.CharField(max_length = 30)
def __unicode__(self):
return str(self.city_name)
class Meta:
ordering = ('city_name',)
unique_together = ('state', 'city_name',)
class Snippet(models.Model):
created = models.DateTimeField(auto_now_add=True)
title = models.CharField(max_length=100, blank=True, default='')
code = models.TextField()
linenos = models.BooleanField(default=False)
owner = models.ForeignKey('auth.User', related_name='snippets')
state = models.ForeignKey(State,blank=True,null=True)
city = models.ManyToManyField(City)
=> serializers.py file
class StateSerializer(serializers.ModelSerializer):
class Meta:
model = State
class CitySerializer(serializers.ModelSerializer):
state_name = serializers.CharField(source='state.state_name', read_only=True)
class Meta:
model = City
class SnippetSerializer(serializers.ModelSerializer):
owner = serializers.ReadOnlyField(source='owner.username', read_only=True)
class Meta:
model = Snippet
fields = ('id', 'title', 'code', 'linenos', 'owner', 'state', 'city')
depth = 1
I have added ForeignKey and ManyToManyField fields in state and city respectively. It doesn't save values in SnippetSerializer while added depth = 1 in Meta Class (it saves Null value in state and city fields). When I add depth = 1 JSON showing related fields as it should be but it doesn't work properly while add new Snippet. Without depth = 1 it works fine.
I have complex database where tables has many ManyToMany and ForeignKey related fields. Please give me suggestion so I can get related data in JSON.
I have djangorestframework-3.1.2 version. I have used latest version too but same problem. please give me solution and thanks in advance.
I faced the same problem and managed to solve it. Since the problem is with the depth, I just change the depth value in the init method.
class SnippetSerializer(serializers.ModelSerializer):
owner = serializers.ReadOnlyField(source='owner.username', read_only=True)
class Meta:
model = Snippet
fields = ('id', 'title', 'code', 'linenos', 'owner', 'state', 'city')
depth = 1
def __init__(self, *args, **kwargs):
super(SnippetSerializer, self).__init__(*args, **kwargs)
request = self.context.get('request')
if request and request.method=='POST':
self.Meta.depth = 0
else:
self.Meta.depth = 1
In the code above, I changed the depth dynamically according to what type of request that I made.
But, this is the workaround that I found myself, I'm not sure if this is the best practice but it solve the problem with just little modification.
depth is only for representation (http://www.django-rest-framework.org/api-guide/serializers/#specifying-nested-serialization). If you want to create/update the related fields too you have to overwrite the create/update methods in the serializer (http://www.django-rest-framework.org/api-guide/relations/#writable-nested-serializers).
Greetings.