Get data in multiple level nested serializer using Django Rest - django-rest-framework

Need to serialize three models nested in three levels.
There are users assigned areas and these contains point. The users contains multiple areas. Areas have multiple points associated.
Users links areas using many to many relationship.
Areas Links with point using Foreign in the points.
Users can be assigned to multiple areas. Areas can have multiple points.
User Profile Model
class UserProfile(AbstractBaseUser,PermissionsMixin):
phone_number= PhoneNumberField( unique=True)
name=models.CharField(max_length=255)
organisation=models.CharField(max_length=255)
is_active=models.BooleanField(default=True)
is_staff=models.BooleanField(default=False)
added_by=models.ForeignKey(settings.AUTH_USER_MODEL,default=1)
group = models.ForeignKey('auth.Group', null=True)
areas=models.ManyToManyField('area.Area',blank=True)
objects=UserProfileManager()
Areas Model
from django.db import models
from django.conf import settings
# Create your models here.
class Area(models.Model):
areaName =models.TextField()
timestamp = models.DateTimeField(auto_now=False, auto_now_add=True)
updated = models.DateTimeField(auto_now=True, auto_now_add=False)
user = models.ForeignKey(settings.AUTH_USER_MODEL )
def __str__(self):
return self.areaName
Point Model
from django.db import models
from django.conf import settings
# Create your models here.
class Point(models.Model):
name =models.TextField()
area = models.ForeignKey('area.Area', on_delete=models.CASCADE)
latitude=models.CharField(max_length=200)
longitude=models.CharField(max_length=200)
timestamp=models.DateTimeField(auto_now=False,auto_now_add=True)
updated=models.DateTimeField(auto_now=True,auto_now_add=False)
user = models.ForeignKey(settings.AUTH_USER_MODEL )
def __str__(self):
return self.name
I want a result like following:
{
"id": 3,
"phone_number": "+919999999999",
"name": "Ak",
"organisation": "sp",
"group": 1,
"areas": [
{
"id": 1,
"areaName": "Area 51",
"user": 1
points:[{
}]
},
{
"id": 2,
"areaName": "Rrea 343",
"user": 1
point:[{}]
}
]
},
{
"id": 4,
"phone_number": "+918888888888",
"name": "Chitra Sahu",
"organisation": "sd",
"group": 2,
"areas": [
{
"id": 1,
"areaName": "Area 51",
"user": 1
point:[{
latitude:'23.2323',
longitude:'23.2323'
},
{
latitude:'21.1223',
longitude:'32.34345'
}
]
},
{
"id": 2,
"areaName": "Rrea 343",
"user": 1
point:[{
latitude:'23.2323',
longitude:'23.2323'
},
{
latitude:'21.1223',
longitude:'32.34345'
}]
}
]
},
So Far I have tried the following
class AreasSerializer(serializers.ModelSerializer):
class Meta:
model=Area
fields=('id','areaName','user')
class AreasUserSerializer(serializers.ModelSerializer):
areas = AreasSerializer(many=True, read_only=True)
class Meta:
model = UserProfile
fields = ('id','phone_number','name','organisation','group','areas')
class AreasUserPointSerializer(serializers.ModelSerializer):
areasUsers=AreasUserSerializer()
class Meta:
model=Point
fields =('id','areasUsers' )
Views
'''Fetch list all question '''
class AreasPointsUsersListApiView(ListAPIView):
serializer_class=serializers.AreasUserPointSerializer
def get_queryset(self):
queryset=UserProfile.objects.all()
user=self.request.query_params.get('user_id',None)
if user is not None:
queryset = queryset.filter(id=user)
#if areas is not None:
# queryset = queryset.filter(areas=areas)
return queryset
.py
This code is not working properly.
I need to serialize it so that the Users consists Areas based on Many to Many relationship. These areas are linked to point using the foreign key in Point.
EDIT
Edit:
Areas serializer

I have resolved this using LocationSerializer which invoked by AreasSerializer.
I am sharing the code snippet. It was pretty easy.
class PointSerializer(serializers.ModelSerializer):
class Meta:
model = Point
fields=('id','latitude','longitude')
class AreasLocationSerializer(serializers.ModelSerializer):
points = PointSerializer(many =True, read_only=True)
class Meta:
model=Area
fields=('id','areaName','points','user')
class AreasUserLocationSerializer(serializers.ModelSerializer):
areas =AreasLocationSerializer(many=True, read_only=True)
class Meta:
model=UserProfile
fields =('id','phone_number','name','areas')

Related

How can I add a field to a model serializer that has a reverse relationship with another model

I have two models. Fiction and Review model. They are the following:
class Fiction(models.Model):
"""
Model that encopasses a Movie, TV Series, book or similar
"""
MOVIE = 1
TV_SERIES = 2
BOOK = 3
PODCAST = 4
TYPE = (
(MOVIE, 'Movie'),
(TV_SERIES, 'TV-Series'),
(BOOK, 'Book'),
(PODCAST, 'Podcast')
)
title = models.CharField(max_length=50)
description = models.CharField(max_length=200)
active = models.BooleanField(default=True)
created = models.DateTimeField(auto_now_add=True)
platform = models.ForeignKey(
StreamPlatform,
on_delete=models.SET_NULL,
related_name='fictions',
null = True
)
type = models.PositiveSmallIntegerField(
choices = TYPE,
default = MOVIE
)
def __str__(self):
return self.title
and
class Review(models.Model):
"""
model for fiction reviews from users
"""
rating = models.PositiveSmallIntegerField(validators=[MinValueValidator(1), MaxValueValidator(5)])
fiction = models.ForeignKey(Fiction, on_delete=models.CASCADE, related_name="reviews")
description = models.CharField(max_length=200, null = True, blank =True)
created = models.DateTimeField(auto_now_add=True)
updated = models.DateTimeField(auto_now=True)
def __str__(self):
return str(self.rating) + " | " + str(self.fiction)
class Meta:
ordering = ['-created']
and also two serializers
for fiction
class FictionSerializer(serializers.ModelSerializer):
"""
serializer for Movie model
"""
class Meta:
model = Fiction
fields = "__all__"
and for review
class ReviewSerializer(serializers.ModelSerializer):
class Meta:
model = Review
fields = ['rating', 'fiction', 'description']
I want to be able to display the rating of the review inside the fiction serializers. I tried something like:
rating = serializers.ReadOnlyField(source='reviews.rating')
but it didnt work. Anyone has an idea?
Since you added reviews as a related name, you can use that.
Here is a working example for you. (I've created a small project for this, so this definitely works)
class ReviewRatingSerializer(serializers.ModelSerializer):
class Meta:
model = Review
fields = ('rating', )
class FictionSerializer(serializers.ModelSerializer):
"""
serializer for Movie model
"""
reviews = ReviewRatingSerializer(many=True)
class Meta:
model = Fiction
fields = "__all__"
This might cause lots of database queries if you want to return lots of Fiction items at once.
To fix that, you should use prefetch_related in your views.py
Here is a simple example for a list view.
class GetFictionMovies(ListAPIView):
pagination_class = None
serializer_class = FictionSerializer
def get_queryset(self):
queryset = Fiction.objects.all().prefetch_related('reviews')
return queryset
Output will be similar to this.
[
{
"id": 1,
"reviews": [
{
"rating": 3
},
{
"rating": 4
}
],
"title": "Starwars",
"description": "asdasd",
"active": true,
"created": "2021-06-27T16:28:55.521748Z",
"type": 1
},
{
"id": 2,
"reviews": [
{
"rating": 5
},
{
"rating": 2
}
],
"title": "LOTR",
"description": "asdasd",
"active": true,
"created": "2021-06-27T16:29:03.227639Z",
"type": 1
},
{
"id": 3,
"reviews": [
{
"rating": 4
},
{
"rating": 3
}
],
"title": "GODFATHER",
"description": "asdasd",
"active": true,
"created": "2021-06-27T16:34:45.171444Z",
"type": 1
}
]
My advice for you is to always check for number of queries made to the db and try to avoid duplicate calls to the db.

Alter Queryset before returning in DRF

I have a model structure similar to the one below:
Store -> some store fields
Books -> some book fields, FK to Store
BookProperty -> name, value, FK to Books (a one to many relationship), FK to store
The book property can store any info for the book eg. no_of_pages, publisher etc added by the store.
I need to make an API endpoint where I can get all BookProperty for a store.
I used the url:
/stores/:store_id/bookproperty
Used a ModelSerializer for BookProperty with fields = [publisher, no_of_pages]
Used a genericViewSet with a ListModelMixin.
The endpoint turned out like this below:
{
"count": 4,
"next": null,
"previous": null,
"results": [
{
"name": "publisher",
"value": "somePublisher"
},
{
"name": "pages",
"value": "1000"
},
{
"name": "publisher",
"value": "someOtherPublisher"
},
{
"name": "publisher",
"value": "somePublisher"
}
]
}
The problem with this is that multiple objects can have the same name, value pairs. I need this information in a way where all the objects are unique and grouped kind of like this:
{
{"name":"publisher", "value":["somePublisher", "someOtherPublisher"]},
{"name":"pages", "value":["1000"]},
}
I'm trying to override the get_queryset(self) but it's not working.
Any help would be appreciated. Thanks in advance!
EDIT:
models.py
class BookProperty(models.Model):
books = models.ForeignKey(
Books,
on_delete=models.CASCADE,
)
name = models.CharField(max_length=100)
value = models.CharField(max_length=100)
store = models.ForeignKey(
"Store",
on_delete=models.CASCADE,
)
serializers.py
class BookPropertySerializer(serializers.ModelSerializer):
class Meta:
model = models.BookProperty
fields = ["name", "value"]
views.py
class BookPropertyViewSet(mixins.ListModelMixin, viewsets.GenericViewSet):
serializer_class = serializers.BookPropertySerializer
I think that instead of overriding the get_queryset(self) I should try changing the def list(self, request, *args, **kwargs) :
def list(self, request, *args, **kwargs):
queryset = self.filter_queryset(self.get_queryset())
tag_dictionary = defaultdict()
things = list(queryset)
for key, group in itertools.groupby(things, lambda x: x.name):
for thing in group:
if key not in tag_dictionary.keys():
tag_dictionary[key] = [thing.value]
else:
tag_dictionary[key].append(thing.value)
for key in tag_dictionary.keys():
tag_dictionary[key] = list(set(tag_dictionary[key]))
return Response(json.dumps(tag_dictionary))
The above solution is working but might not be the best one.

How do you access nested object information with Django Rest Framework?

I've got two models connected through a ManyToManyField that links projects together with users, as such:
class Project(Model):
STATUS_CHOICES = (
('active', 'Active'),
('archived','Archived'),
)
name = CharField(max_length=50)
members = ManyToManyField("accounts.User", through='ProjectUser')
organization = ForeignKey(Organization, related_name="organizations", on_delete=CASCADE, verbose_name="Team")
status = CharField(max_length=10, choices=STATUS_CHOICES, default='active')
def __str__(self):
return self.name
class Meta:
db_table = 'project'
ordering = ('organization', 'name')
unique_together = ('name', 'organization',)
class ProjectUser(Model):
ROLE_CHOICES = (
('member', 'Member'),
('admin','Admin'),
)
user = ForeignKey("accounts.User", on_delete=CASCADE)
project = ForeignKey(Project, on_delete=CASCADE)
user_hour_cost = DecimalField(max_digits=6, decimal_places=2, default=0)
role = CharField(max_length=10, choices=ROLE_CHOICES, default='member')
class Meta:
db_table = 'projectuser'
ordering = ('user',)
unique_together = ('project', 'user',)
and a ProjectSerializer that looks like this:
class ProjectSerializer(serializers.ModelSerializer):
class Meta:
model = Project
fields = ["name", "organization", "members"]
I wish to extract data about the users when using the ProjectSerializer (e.g get the username, email, first name, last name from the User model). All I get back with this serializer is
{
"name": "Project X/Y",
"organization": 1,
"members": [
2,
1
]
}
Is there a way for me to traverse the information on the members so my template can use it? E.g members[0].username?
I can't just use depth = 1 because that returns data directly from User model, but ignores the fields on the ProjectUser model
I'm looking for something along the lines of
{
"name": "Project X/Y AB",
"organization": 1,
"projectusers": [
{
"user": ["id": 1, "username": "foo", "first_name": "joey"],
"project": 1,
"user_hour_cost": "550.00",
"role": "admin"
},
{
"user": ["id": 2, "username": "hellboy", "first_name": "erik"],
"project": 1,
"user_hour_cost": "190.00",
"role": "member"
}
]
}
Doesn't necessarily have to look just like this - but I need for my frontend to receive information about the user that sits on the User table in my db
Maybe you could try to specify your own serializer for the (project)users. This is covered more in depth in the official DRF docs.
class ProjectSerializer(serializers.ModelSerializer):
members = MemberSerializer(many=True)
class Meta:
model = Project
fields = ["name", "organization", "members"]
and define your Member and User Serializer:
class MemberSerializer(serializers.ModelSerializer):
user = UserSerializer()
class Meta:
model = ProjectMember
fields = ["user ", "...", "role "]
class UserSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = ["id", "username", "first_name", "..."]
But beware that making such a construct writeable is tricky. You would probably have to overwrite your Serializers create() methods to implement this. See here for more details.
I actually solved it by just nesting another serialized object inside ProjectUser
User Serializer
from accounts.models import User
from rest_framework import serializers
class UserSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = ("username", "first_name", "email")
and then
from .models import Project
from .models import ProjectUser
from accounts.serializers import UserSerializer
from rest_framework import serializers
class ProjectUserSerializer(serializers.ModelSerializer):
user = UserSerializer()
class Meta:
model = ProjectUser
fields = ("user", "user_hour_cost", "role")
class ProjectSerializer(serializers.ModelSerializer):
projectusers = ProjectUserSerializer(many=True, read_only=True)
class Meta:
model = Project
fields = ["name", "organization", "projectusers"]
Which returned
{
"name": "Project XXAA",
"organization": 1,
"projectusers": [
{
"user": {
"username": "Google",
"first_name": "Chrome",
"email": "google#chrome.com"
},
"user_hour_cost": "550.00",
"role": "admin"
},
{
"user": {
"username": "Mozilla",
"first_name": "Joey",
"email": "mozilla#firefox.com"
},
"user_hour_cost": "190.00",
"role": "member"
}
]
}
Good enough to work with!

Getting rest history from Django simple History

I am using django-simple-history (1.8.1) and DRF (3.5.3). I want to get a rest service containing the history of each element. Let's take an example !
models.py
class Product(models.Model):
name = models.CharField(max_length=50)
price = models.IntegerField()
history = HistoricalRecords()
def __str__(self):
return self.name
So, what must be serializers.py ? I'd like to GET something like :
[
{
"id": 1,
"name": "Apple",
"price": 8,
"history": [
{
"history_id": 1,
"id": 1,
"name": "Apple",
"price": 0,
"history_date": "2016-11-22T08:02:08.739134Z",
"history_type": "+",
"history_user": 1
},
{
"history_id": 2,
"id": 1,
"name": "Apple",
"price": 10,
"history_date": "2016-11-22T08:03:50.845634Z",
"history_type": "~",
"history_user": 1
},
{
"history_id": 3,
"id": 1,
"name": "Apple",
"price": 8,
"history_date": "2016-11-22T08:03:58.243843Z",
"history_type": "~",
"history_user": 1
}
]
}
]
After searching whitout finding the solution, I finally found it by myself. But if someone have a better solution...
I know it's been a year, but anyway, maybe someone finds it useful. Here is my solution (it seems far easier to me):
A new serializer field:
class HistoricalRecordField(serializers.ListField):
child = serializers.DictField()
def to_representation(self, data):
return super().to_representation(data.values())
Now simply use it as a a field in your serializer:
history = HistoricalRecordField(read_only=True)
This makes use of DRF's built in list and dict serializers, only trick is to pass it the correct iterable, which is being done by calling .values() on the simple-history model manager class.
Here's my solution.
In serializers.py :
from rest_framework import serializers
from .models import Product
class sHistory(serializers.ModelSerializer):
def __init__(self, model, *args, fields='__all__', **kwargs):
self.Meta.model = model
self.Meta.fields = fields
super().__init__()
class Meta:
pass
class sProduct(serializers.ModelSerializer):
class Meta:
model = Product
fields = '__all__'
history = serializers.SerializerMethodField()
def get_history(self, obj):
model = obj.history.__dict__['model']
fields = ['history_id', ]
serializer = sHistory(model, obj.history.all().order_by('history_date'), fields=fields, many=True)
serializer.is_valid()
return serializer.data
It works ! I'm quite proud about it ! any suggestions ?
There seems to be an even clearer and simpler way
class AnySerializer(serializers.ModelSerializer):
history = serializers.SerializerMethodField()
class Meta:
model = MyModel
fields = (....
....
'history',
)
read_only_fields = ('history',)
def get_history(self, obj):
# using slicing to exclude current field values
h = obj.history.all().values('field_name')[1:]
return h
You can create a serializer like this:
class ProductHistorySerializer(serializers.ModelSerializer):
class Meta:
model = Product.history.model
fields = '__all__'
Then in view, You can have the code below:
#...
logs = ProductHistorySerializer(Product.history.filter(price__gt=100), many=True)
return Response({'isSuccess': True, 'data': logs.data})

Django Rest Framework: How to Append to array

I have a user object where I want to add a new friend onto their existing friends list. If below is the existing friends list for user1 , how would I POST/PUT a new friend onto that existing list without reposting the whole friends list.
{
"friends": [
{
"first_name": "Bob"
},
{
"first_name": "Dave"
},
{
"first_name": "Jon"
}
],
"first_name": "User1",
}
What would my JSON look like in the POST and what do I have to do on the DRF end to allow me to update this array without reposting the existing array.
I tried to POST the following but it just overwrote the entire friend list
PATCH /api/profile/55522221111/ HTTP/1.1
{"friends":[{"first_name":"new friend"}]}
An alternative to your strategy could be redefining your models so as to create a one-to-many reference of the inner objects in the array, which in your example has {"first_name": "Bob"} in it to the main object.
This would allow you to post into the friends array just by mentioning a foreign key in model.
Something like :
models.py
class User(models.Model):
first_name = models.CharField(max_length=64)
class Friend(models.Model):
first_name = models.CharField(max_length=64)
user = models.ForeignKey(User, related_name='friends')
serializers.py
class FriendSerializer(serializers.ModelSerializer):
class Meta:
model = Friend
fields = ('id', 'first_name', )
class UserSerializer(serializers.ModelSerializer):
friends = FriendSerializer(many=True)
class Meta:
model = User
fields = ('id', 'first_name', 'friends', )
views.py
class UserViewSet(viewsets.ModelViewSet):
queryset = User.objects.all()
serializer_class = UserSerializer
class FriendViewSet(viewsets.ModelViewSet):
queryset = User.objects.all()
serializer_class = FriendSerializer
urls.py
router = routers.DefaultRouter()
router.register(r'users', UserViewSet)
router.register(r'friends', FriendViewSet)
urlpatterns = [
url(r'^api/', include(router.urls)),
]
This should result in 2 urls like (1) /api/users/ and (2) /api/friends/ and you can post into (2) and if references are correct it will show as you wish in (1).
{
"friends": [
{
"first_name": "Bob"
},
{
"first_name": "Dave"
},
{
"first_name": "Jon"
}
],
"first_name": "User1",
}
You'd be better off creating a new endpoint, something like:
POST /api/profile/555222211111/friends/add/
{
"first_name": "new_friend"
}

Resources