Django nested serializer with serializermethodfield - python

Context
I have an API endpoint api/v1/checkin/ that returns the current DeviceGroup and the AppVersions for an App that have to be active.
Problem
The endpoint currently returns data along with the correctly filtered AppVersions like this:
{
"customer_device_uuid": "8d252b78-6785-42ea-9aee-b6f9e0f870b5",
"device_group": {
"group_uuid": "869b409d-f281-492e-bb62-d3168aea4394",
"device_group_name": "Default",
"color": "#0a2f45",
"is_default": true,
"app_versions": [
"2.0",
"1.1"
]
}
}
Goal
I want the app_versions in the response to contain more data like this:
{
"customer_device_uuid": "8d252b78-6785-42ea-9aee-b6f9e0f870b5",
"device_group": {
"group_uuid": "869b409d-f281-492e-bb62-d3168aea4394",
"device_group_name": "Default",
"color": "#0a2f45",
"is_default": true,
"app_versions": [
{
"app_version_uuid": "UUID here",
"app_version_name": "1.1",
"package_id": "package name here",
"auto_start": false,
"version_code": 1,
"version_name": "0.1",
"source": "link to file here"
}, ...
]
}
}
Serializers
# serializers.py
class AppVersionSerializer(serializers.ModelSerializer):
auto_start = serializers.BooleanField(source='app_uuid.auto_start')
class Meta:
model = AppVersion
fields = ('app_version_uuid', 'app_version_name', 'package_id', 'auto_start', 'version_code', 'version_name',
'source')
class DeviceGroupSerializer(serializers.ModelSerializer):
app_versions = serializers.SerializerMethodField(read_only=True)
# filters the app versions per app
def get_app_versions(self, model):
qs = model.get_current_app_versions()
return [o.app_version_name for o in qs]
class Meta:
model = DeviceGroup
fields = ('group_uuid', 'device_group_name', 'color', 'is_default', 'app_versions')
class CheckinSerializer(serializers.ModelSerializer):
device_group = DeviceGroupSerializer(many=False, read_only=True, source='group_uuid')
class Meta:
model = CustomerDevice
fields = ('customer_device_uuid', 'customer_uuid', 'device_id_android', 'device_group')
extra_kwargs = {
'customer_uuid': {'write_only': True},
'device_id_android': {'write_only': True}
}
I am guessing that I have to change the get_app_versions() in order to achieve my goal but I have no idea how.
What should I change in order to get the response I want?
EDIT
The get_current_app_versions method that does the filtering
# models.py
def get_current_app_versions(self):
return (
AppVersion.objects
.filter(appversiondevicegrouplink__release_date__lt=timezone.now())
.filter(appversiondevicegrouplink__device_group=self)
.order_by('app_uuid__app_uuid', '-appversiondevicegrouplink__release_date')
.distinct('app_uuid__app_uuid')
)

You are correct in assuming that you will have to change get_app_versions and instead of returning a list in the line return [o.app_version_name for o in qs] you will need to return a list of dictionaries.
You will need to create a full serializer for the AppVersions model. and then in your get_app_versions properly serialize you return values by passing them into your new serializer which contains all the fields you would like to return return AppVersionSerializer2(qs, many=True).data.
You may have to override serialization of certain fields as they may not be handled well by the serializer automatically.

Related

Django Rest Framework - How to use url parameters in API requests to exclude fields in response

Let's say I have an API that returns some simple list of objects at the /users endpoint
{
"count": 42,
"results": [
{
"name": "David",
"age": 30,
"location": "Alaska"
},
...
]
}
I would like to have an optional parameter (boolean) that changes the output by removing a field.
So /users?abridged=True would return the same objects, but omit a field. If the field is not there, it defaults to False
{
"count": 42,
"results": [
{
"name": "David",
"age": 30,
},
...
]
}
I suppose I could create two serializers, one for the full version and one abridged, but I'm not sure how I could use a url parameter to select which serializer to use. Is there a better way to do this?
One way is to override list method and modify fields dynamically based on the presence of the query string param:
views.py
class UserViewSet(viewsets.ModelViewSet):
User = get_user_model()
queryset = User.objects.all()
serializer_class = UserSerializer
def list(self, request):
abridged = request.GET.get('abridged', None)
if abridged:
fields = ('username', 'age',)
else:
fields = ('username', 'age', 'location')
serializer = UserListSerializer(
{'count': self.get_queryset().count()},
context={'fields': fields, 'qs': self.get_queryset()}
)
return Response(serializer.data)
Note that I used a "wrapper" serializer for the list method in order to obtain the desired output. Using extra context to pass data for its SerializerMethodField.
serializers.py
class DynamicFieldsModelSerializer(serializers.ModelSerializer):
...
class UserSerializer(DynamicFieldsModelSerializer):
class Meta:
model = get_user_model()
fields = ['username', 'age', 'location']
class UserListSerializer(serializers.Serializer):
count = serializers.IntegerField()
results = serializers.SerializerMethodField()
def get_results(self, obj):
serializer = UserSerializer(self.context['qs'], many=True, fields=self.context['fields'])
return serializer.data

How to change the way ModelViewSet presents the data?

I have two serializers for my api to bring me data about company office locations.
class CountryFilialsSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = CountryFilials
fields = ['name']
class FilialsSerializer(serializers.HyperlinkedModelSerializer):
country = CountryFilialsSerializer()
class Meta:
model = Filials
fields = ['country', 'name', 'subdomain_name', 'address']
CountryFilialsSerializer brings me the country name by a foreign key, and FilialsSerializer adds this data to other filial data.
A view that utilizes them both currently looks like this:
class FilialsViewSet(viewsets.ModelViewSet):
queryset = Filials.objects.all()
serializer_class = FilialsSerializer
It returns the response looking like that:
"results": [
{
"country": {
"name": "foo"
},
"name": "city1",
"subdomain_name": "subdomain1",
"address": "location1"
},
{
"country": {
"name": "foo"
},
"name": "city2",
"subdomain_name": "subdomain2",
"address": "location2"
},
But i need it to actually present the result like this:
[
{
"country": "foo",
"cities": [
{
"name": "city1",
"subdomain_name": "subdomain1",
"address": "location1"
},
{
"name": "city2",
"subdomain_name": "subdomain2",
"address": "location2"
},
]
},
]
Basically the same data, just grouped into a list by country.
I cant come up with a way to do this. As i realized, the serializer only receives one entry from the base at a time, and if i override its to_representation() method to include some formatting of the output, i wont be able to access multiple locations and group them by one country.
My next guess was that there should be similar way to work with the result directly from the view.
But i couldnt find anything about it in documentation (or maybe i didnt know what to even look for).
Found some info about actions, and update() method, but couldnt get how to utilize it for my goal.
Can you please suggest something? I dont get where and how do i put the logic into the view to be able to catch everything it shoves into the response and reorganize it.
You can have the list of countries with cities like this (this would require you to change the view the use CountryFilialsViewset)
class FilialsSerializer(serializers.ModelSerializer):
class Meta:
model = Filials
fields = ['name',]
class CountryFilialsSerializer(serializers.ModelSerializer):
filials = FilialsSerializer(many=True, read_only=True)
class Meta:
model = CountryFilials
fields = ['name', 'filials']
class CountryFilialsViewSet(viewsets.ModelViewSet):
queryset = CountryFilials.objects.prefetch_related('filials')
serializer_class = CountryFilialsSerializer
Or if you need to override the ModelViewSet you could do something like this:
class CountryFilialsViewSet(viewsets.ModelViewSet):
queryset = CountryFilials.objects.prefetch_related('filials_set')
serializer_class = CountryFilialsSerializer
def list(self, request):
response = [
{'country': country.name, 'cities': list(country.filials_set.values('name').all())} for country in CountryFilials.objects.all()
]
return Response(response)
EDIT: change filials to filials_set
Here is an example on how to change the fields names and data:
class FilialsSerializer(serializers.ModelSerializer):
url = serializers.SerializerMethodField('get_url')
class Meta:
model = Filials
fields = ['name', 'url', 'address']
def get_url(self, obj):
# construct the url
return f'http://{obj.subdomain_name}.example.com/api/v1/cities/'
class CountryFilialsSerializer(serializers.ModelSerializer):
filials = FilialsSerializer(many=True, read_only=True, source='filials_set')
class Meta:
model = CountryFilials
fields = ['name', 'filials']
class CountryFilialsViewSet(viewsets.ModelViewSet):
queryset = CountryFilials.objects.prefetch_related('filials_set')
serializer_class = CountryFilialsSerializer
You would get the results you want using the CountryFilialsViewSet. for example in your urls.py you would have
router = routers.DefaultRouter()
router.register(r'filials', CountryFilialsViewSet),
urlpatterns = [
path('', include(router.urls)),
]
Calling http://api/filials would yield the response.

Django DRF - nested serializer (level>2) does not show up in the response

We have the follwing structure (library->books->pages)
the first serializer
class Library(serializers.ModelSerializer):
books = BookSerializer(many=True)
class Meta:
model = Library
fields = '__all__'
#transaction.atomic
def create(self, validated_data):
# create logic here
the second serializer
class BookSerializer(serializers.ModelSerializer):
pages = PageSerializer(many=True, required=False)
class Meta:
model = Book
fields = '__all__'
we have an endpoint library/, where we post the payload of the following format
{
"ref": "43a0c953-1380-43dd-a844-bbb97a325586",
"books": [
{
"name": "The Jungle Book",
"author": "Rudyard Kipling",
"pages": [
{
"content": "...",
"pagenumber": 22
}
]
}
]
}
all the objects are created in the database, but the response does not contain pages key. It looks like this
{
"id": 27,
"ref": "43a0c953-1380-43dd-a844-bbb97a325586",
"books": [
{
"id": 34,
"name": "The Jungle Book",
"author": "Rudyard Kipling"
}
]
}
depth attribute does not seem to work. What do I have to do to make pages appear in the responce?
We can achieve the desired behavior using depth in the class Meta of the BookSerializer.
class BookSerializer(serializers.ModelSerializer):
...
class Meta:
...
depth = 3
Copied from documentation
The depth option should be set to an integer value that indicates the depth of relationships that should be traversed before reverting to a flat representation.
Another way to get this behavior would be to use serializer.SerializerMethodField for getting the pages of the book serializer.
class BookSerializer(serializers.ModelSerializer):
pages = serializers.SerializerMethodField()
def get_pages(self, obj):
return PageSerializer(obj.page_set.all(), many=True,).data
...

django.db.utils.IntegrityError: NOT NULL constraint failed fom Postman

I am trying to create a simple model with foreign keys using Django rest framework.
This are the models:
class Route(models.Model):
place_origin = models.ForeignKey(
Place, null=False, on_delete=models.DO_NOTHING)
class Place(models.Model):
name = models.CharField(max_length=50, null=False)
This are the serializers for each model:
class PlaceSerializer(serializers.ModelSerializer):
class Meta:
model = Place
fields = ["id", "name"]
class RouteSerializer(serializers.ModelSerializer):
place_origin = PlaceSerializer(many=False, read_only=True)
class Meta:
model = Route
fields = ["id", "place_origin"]
This RouteSerializer has the place_origin property in order to show the place details(all the fields from it) when I am looking at the route detail. What I mean is for routes I want to display:
[
{
"id": 1,
"place_origin": {
"id": 1,
"name": "New york"
}
},
{
"id": 2,
"place_origin": {
"id": 2,
"name": "Boston"
}
}
]
And not just:
[
{
"id": 1,
"place_origin": 1
},
{
"id": 2,
"place_origin": 2
}
]
This is the view:
#api_view(['POST'])
def routes_new_route_view(request):
"""
Create a new route
"""
if request.method == "POST":
data = JSONParser().parse(request)
place_origin = Place.objects.get(id=data["place_origin"])
data["place_origin"] = PlaceSerializer(place_origin)
data["place_origin"] = data["place_origin"].data
serializer = RouteSerializer(data=data)
if serializer.is_valid():
serializer.save()
return JsonResponse(serializer.data, status=201)
else:
return JsonResponse(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
I want to send the request from postman this way:
{
"place_origin": 3
}
But I am getting the error from the title.
Thanks for all the help!
The error is that you're trying to send data via a PlaceSerializer but this field is read_only. On the other hand, your DB expects place_origin since you precised null=False in your model. Both combined gives the error "Not NULL constraint failed".
The easiest way is to slightly modify your serializer in order to have one field for write and another for read.
class RouteSerializer(serializers.ModelSerializer):
place_origin = PlaceSerializer(many=False, read_only=True)
place = serializers.PrimaryKeyRelatedField(source="place_origin",queryset=Place.objects.all(),write_only=True)
class Meta:
model = Route
fields = ["id", "place_origin", "place"]
Here, you will use place field as a way to create the relationship with your Route instance.

Django-rest-framework how to validate JSON object (not JSON value)

I'm currently trying to make a post request with only data field in models.py:
class Filter(models.Model):
class Meta:
verbose_name_plural = "Filter"
verbose_name = "Filter"
db_table= "listing_filter"
data = JSONField(default={})
user = models.ForeignKey('backend.user', on_delete=models.CASCADE)
I have the following Serializer , i use JSONField following the drf document docs:
class FilterSerializer(serializers.ModelSerializer):
data = serializers.JSONField(required=True)
user = serializers.CharField(required=False)
class Meta:
model = Filter
fields = ('__all__')
and use it in the APIView:
class FilterView(APIView):
def post(self, request):
login_user = request.user
received_json_data=json.loads(request.body)
valid_ser = FilterSerializer(data=received_json_data)
if valid_ser.is_valid():
post_data = received_json_data["data"]
filter = Filter.objects.create(data=post_data, user=login_user)
filter.save()
return JsonResponse({'code':'200','data': filter.id}, status=200)
else:
return JsonResponse({'code':'400','errors':valid_ser.errors}, status=400)
When i send the following data in body it worked and saved the object:
{
"data": {
"http://example.org/about": {
"http://purl.org/dc/terms/title": [
{
"type": "literal",
"value": "Anna's Homepage"
}
]
}
}
}
But when i send data as a string(which is not a json object) it still save, how do i prevent this ?
{
"data" : "abc"
}

Categories