I'm using Django 1.6.8, and Django Rest 2.4.4. I have a Person model with separate models for Address and PhoneNumbers.
class Person(models.Model):
address = models.OneToOneField(Address, blank=True, null=True)
phoneNumbers = models.ManyToManyField(PhoneNumber, blank=True)
class Address(models.Model):
address = models.CharField(max_length=50)
city = models.CharField(max_length=50)
state = models.CharField(max_length=2)
class PhoneNumber(models.Model):
number = models.CharField(max_length=15)
numberType = models.CharField(default='Mobile', max_length=15)
I'm using nested serializers in Django REST framework.
class PersonSerializer(serializers.HyperlinkedModelSerializer):
id = serializers.Field('id')
address = AddressSerializer(source='address')
phoneNumbers = PhoneNumberSerializer(many=True)
This works for GET (I get address and phone numbers as nested json fields), but I need the same for PUT / POST / PATCH. Specifically, for each phone number, I want it to be updated if the id is specified, or created if there is no id in json. And the same for address, all in the same API call.
You need to implement your own create() and / or update() methods to support this.
It's explained in the Django Rest doc and it's available since Django Rest 3.0:
The following example demonstrates how you might handle creating a user with a nested profile object.
class UserSerializer(serializers.ModelSerializer):
profile = ProfileSerializer()
class Meta:
model = User
fields = ('username', 'email', 'profile')
def create(self, validated_data):
profile_data = validated_data.pop('profile')
user = User.objects.create(**validated_data)
Profile.objects.create(user=user, **profile_data)
return user
(...)
Because the behavior of nested creates and updates can be ambiguous, and may require complex dependancies between related models, REST framework 3 requires you to always write these methods explicitly. The default ModelSerializer .create() and .update() methods do not include support for writable nested representations.
Related
I am new with Django Rest Framework and wanted to understand what's the accepted practice for writing Serializers that work with nested relationships.
Say, I have a models called Client and Invoice (this is just an illustrative example):
class Client(models.Model)
name = models.CharField(max_length=256)
class Invoice(models.Model)
client = models.ForeignKey(Client)
date = models.DateTimeField()
amount = models.DecimalField(max_digits=10, decimal_places=3)
I want to create a Serializer for Client that supports the following use cases:
Create a Client
When I create an Invoice, refer to the Client using its id.
Let's say I use this implementation:
class ClientSerializer(serializers.ModelSerializer):
class Meta:
model = Client
fields = ['id', 'name']
class InvoiceSerializer(serializers.ModelSerializer):
client = ClientSerializer()
class Meta:
model = Invoice
fields = ['id', 'client', 'date', 'amount']
def create(self, data):
client = Client.objects.get(pk=data['client']['id'])
invoice = Invoice(client=client,
date=datetime.fromisoformat(data['date']),
amount=Decimal(data['amount']))
invoice.save()
With this code, if I try to create an Invoice, I need the client object in the POST data to contain name as well. There is no config of the name field (read_only=True, write_only=True, required=False) that allows me to create and read Client as well as not be required when creating the Invoice.
How should this be solved?
Is the accepted practice that the request include the name field anyways?
Can we somehow created nested models like this? /api/Client/<id:client_id>/Invoice
Do we create multiple Serializer classes for each model - one for it's own viewset, and another for use in other models' viewsets?
Thanks!
This is an accepted pratice, but it has its advantages and disadvantages. Actual good practice depends on your actual needs. Here, as you suggested, while creating an Invoice, you also need to send a client name in the request, which should not be necessary. To overcome that need, one possible practive can be as follows:
class ClientSerializer(serializers.ModelSerializer):
class Meta:
model = Client
fields = ['id', 'name']
class InvoiceSerializer(serializers.ModelSerializer):
client = serializers.PrimaryKeyRelatedField(queryset=Client.objects.all())
class Meta:
model = Invoice
fields = ['id', 'client', 'date', 'amount']
With this approach, you only include client's id in the serializer. You'll only need to send a client id in the requset with this approach, and don't need to write a custom create method on the serializer. Disadvantage of this approach is; you do not have the client name when listing invoices. so if you need to display client name when displaying an invoice, we'd need to improve this solution a bit:
class InvoiceSerializer(serializers.ModelSerializer):
client = serializers.PrimaryKeyRelatedField(queryset=Client.objects.all())
client_details = ClientSerializer(source='client', read_only=True)
class Meta:
model = Invoice
fields = ['id', 'client', 'client_details', 'date', 'amount']
With this approach, we have added a read-only field, client_details, that keeps the data in client serilaizer. So for write operations we use client field, which is only an id, and to read details about a client, we use client_details field.
Another approach could be defining a separate client serializer to be used as a child serializer in InvoiceSerializer only:
class ClientSerializer(serializers.ModelSerializer):
class Meta:
model = Client
fields = ['id', 'name']
class InvoiceClientSerializer(serializers.ModelSerializer):
name = serializers.CharField(read_only=True)
class Meta:
model = Client
fields = ['id', 'name']
class InvoiceSerializer(serializers.ModelSerializer):
client = InvoiceClientSerializer()
class Meta:
model = Invoice
fields = ['id', 'client', 'date', 'amount']
def create(self, data):
client = Client.objects.get(pk=data['client']['id'])
invoice = Invoice(client=client,
date=datetime.fromisoformat(data['date']),
amount=Decimal(data['amount']))
invoice.save()
In this approach, we have defined a specia client serializer for use in InvoiceSerializer only, that has name field as read only. So while creating / updating an Invoice, you won't need to send client name, but while listing invoices, you get the client name. Advantage of this approach to the one before use, we do not need to use two separate fields for client field for writing and reading details.
For your second question, it is not supported out of the box by DRF, but you can take a look at this package, which provides that functionality and is listed on DRF's own documentation: https://github.com/alanjds/drf-nested-routers
for an app with three objects - User, Event, and Action where users take actions on events creating an object like:
class Action(models.Model):
event = models.ForeignKey('CauseActivity', on_delete=models.CASCADE, related_name='actions')
user = models.ForeignKey('users.User', on_delete=models.CASCADE, related_name='actions')
type = models.CharField(max_length=100, choices=ACTION_TYPES)
date_created = models.DateTimeField(auto_now_add=True)
date_updated = models.DateTimeField(auto_now=True)
I would like our RESTful API to be able to return the actions for one specific user, either within the user request (which requires no extra API calls) or with one additional request (get actions by user)
a simple User serializer with 'actions' as a field will return only the obj ids (response to GET ex: "actions":[108,109])
from rest_framework import serializers
class UserSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = (
..., 'actions', )
I am aware of nesting to serialize custom fields like username = serializers.ReadOnlyField(source='user.name') but is there a way to use serializers.ListSerializer or .ModelSerializer to serialize the full set of Action objects' fields within my UserSerializer? I read through https://www.django-rest-framework.org/api-guide/serializers/ and best approach isn't really clear to me. thanks
create a serializer for your Action model (let's call it ActionSerializer) then make your code like
class UserSerializer(serializers.ModelSerializer):
actions = ActionSerializer(many=True)
class Meta:
model = User
fields = ('actions',...)
I have written basic model serializers in Django where the api mimics the data model. I now have a requirement to store User Preference in database. The api contains an array.
My User Models :
class User(models.Model):
email_id = models.EmailField(max_length=80, blank=True, primary_key=True)
class UserPreference(models.Model)
email_id = models.ForeignKey('User')
preference = models.CharField(maxlength=20)
An ideal json post request would look something like this
{
email:"abhishek#gmail.com"
preference : [ 'books', 'food', 'lifestyle', 'travel']
}
I wish to save this json schema to the UserPreference model. This requires multiple inserts for preference. What will be a good serializer design for it ?
I tried
class UserPreferenceSerializer(serializers.ModelSerializer):
class Meta:
model = UserPreference
fields = ('email_id', 'preference')
you could use StringRelatedField of Django Rest Framework.
Make below changes and you will get the response in the way you want.
models.py (put related_name there)
class UserPreference(models.Model):
email_id = models.ForeignKey(User, related_name='preference')
preference = models.CharField(maxlength=20)
serializers.py
class UserSerializer(serializers.ModelSerializer):
preference = serializers.StringRelatedField(many=True)
class Meta:
model = User
fields = ('email_id', 'preference')
You could make your model like this:
class UserPreference(models.Model)
email_id = models.ForeignKey('User')
preference = models.ManyToManyField('Preference') #create model for preferences
Then add custom create method to your serializer:
def create(self, validated_data):
user = self.context.get('user') #you can pass context={'user': self.request.user} in your view to the serializer
up = UserPreference.objects.create(email_id=user)
up.save()
preference = validated_data.get('preference', [])
up.preference.add(*preference)
up.save()
return up
I'm trying to allow a field that belongs to a related object to be readable and writable from a ModelSerializer. I have a model Group:
class Group(models.Model):
...
name = models.CharField(max_length=128)
def get_language(self):
line = self.line_set.all()[0]
return line.language
...
and corresponding serializer:
class GroupSerializer(serializers.ModelSerializer):
language = serializers.CharField(source='get_language')
class Meta:
model = Group
fields = ('id', 'name', 'language')
lookup_field= 'pk'
The Group model is related to the Line model, where the language field lives:
class Line(models.Model):
...
language = models.CharField(max_length=24)
groups = models.ManyToManyField(Group, blank=True, null=True)
...
I'm trying to expose Line.language in the Group API for reading and writing. I've tried using CharField as above, but when I go to post a new model, I get this error:
TypeError at /AO/s/v2/group/
'get_language' is an invalid keyword argument for this function
I also thought of trying a custom related field and implementing from_native, but that requires a queryset which isn't relevant here.
Thanks in advance.
Let's say I'm using the default auth.models.User plus my custom Profile and Address models which look like this:
class Profile(models.Model):
user = models.OneToOneField(User)
primary_phone = models.CharField(max_length=20)
address = models.ForeignKey("Address")
class Address(models.Model):
country = CountryField(default='CA')
province = CAProvinceField(default='BC')
city = models.CharField(max_length=80)
postal_code = models.CharField(max_length=6)
street1 = models.CharField(max_length=80)
street2 = models.CharField(max_length=80, blank=True, null=True)
street3 = models.CharField(max_length=80, blank=True, null=True)
Now I want to create a registration form. I could create a ModelForm based on User but that won't include fields for the Profile and Address (which are required). So what's the best way to go about building this form? Should I even use ModelForm at all?
Furthermore, how would I use the same form for editing the complex object? I could easily pass an instance of Profile back to it, which holds references to the necessary Address and Profile objects, but how do I get it to fill in the fields for me?
What about using 3 separate ModelForm. One for Address, one for User, and one for Profile but with :
class ProfileForm(ModelForm):
class Meta:
model = Profile
exclude = ('user', 'address',)
Then, process these 3 forms separately in your views. Specifically, for the ProfileForm use save with commit=False to update user and address field on the instance :
# ...
profile_form = ProfileForm(request.POST)
if profile_form.is_valid():
profile = profile_form.save(commit=False)
# `user` and `address` have been created previously
# by saving the other forms
profile.user = user
profile.address = address
Don't hesitate to use transactions here to be sure rows get inserted only when the 3 forms are valid.
You should look into the officially recommended way to extend the User model first, as seen in the docs, which I believe comes directly from the project manager's personal blog about the subject. (The actual blog article is rather old, now)
As for your actual issue with forms, have a look at the project manager's own reusable django-profiles app and see if perusing the code solves your issue. Specifically these functions and the views in which they are utilized.
Edited to Add:
I've looked into it a bit (as I needed to do so myself). It seems something like so would be sufficient:
# apps.profiles.models
from django.db import models
from django.contrib.auth.models import User
class UserProfile(models.Model):
user = models.ForeignKey(User, unique=True)
...
birth_date = models.DateField(blank=True, null=True)
joined = models.DateTimeField(auto_now_add=True)
modified = models.DateTimeField(auto_now=True)
class Meta:
verbose_name = 'user profile'
verbose_name_plural = 'user profiles'
db_table = 'user_profiles'
class Address(models.Model):
user = models.ForeignKey(UserProfile)
...
# apps.profiles.forms
from django import forms
from django.forms import ModelForm
from django.forms.models import inlineformset_factory
from django.contrib.auth.models import User
from apps.profiles.models import UserProfile, Address
class UserForm(ModelForm):
class Meta:
model = User
...
class UserProfileForm(ModelForm):
class Meta:
model = UserProfile
...
AddressFormSet = inlineformset_factory(UserProfile, Address)
I was using "..." to snip content in the code above. I have not yet tested this out but from looking through examples and the documentation on forms I believe this to be correct.
Note I put the FK from the Address model to the UserProfile and not the other way around, as in your question. I believe the inline formsets need this to work correctly.
Then of course in your views and templates you will end up treating UserForm, UserProfileForm, and AddressFormSet separately but they can all be inserted into the same form.
I think your are looking for inline formsets with model forms. This helps you to deal with multiple forms on one page and also takes care of foreign key relations.
Update:
Maybe this question helps you too: Django: multiple models in one template using forms