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.
Related
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
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
...
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.
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.
I've written some django code that allows me to request some data and returns it into a JSON format.
I request: http http://127.0.0.0.8000/api/club
[
{
"name": "Liverpool FC",
"abv": "LFC",
"stadium": "Anfield",
"manager": "Jurgen Klopp",
"trophies": "50"
},
{
"name": "Manchester City",
"abv": "MC",
"stadium": "Etihad",
"manager": "Manuel Pellegrini",
"trophies": "14"
},
{
"name": "Manchester United",
"abv": "MU",
"stadium": "Old Trafford",
"manager": "Louis Van Gaal",
"trophies": "46"
}
]
Is it possible to request only "Liverpool FC"'s data such that the request only returns. I can do this by http http://127.0.0.0.8000/api/club/1/ but I'd rather be able to type in a team name like http http://127.0.0.0.8000/api/club/liverpool/
{
"name": "Liverpool FC",
"abv": "LFC",
"stadium": "Anfield",
"manager": "Jurgen Klopp",
"trophies": "50"
}
Edit: Added two py files
views.py
# Create your views here.
class FishViewSet(viewsets.ModelViewSet):
# this fetches all the rows of data in the Fish table
queryset = Fish.objects.all()
serializer_class = FishSerializer
# Create your views here.
class TeamViewSet(viewsets.ModelViewSet):
# this fetches all the rows of data in the Fish table
queryset = Team.objects.all()
serializer_class = TeamSerializer
# Create your views here.
class ClubViewSet(viewsets.ModelViewSet):
# this fetches all the rows of data in the Fish table
queryset = Club.objects.all()
serializer_class = ClubSerializer
def getClub(request, club_name):
queryset = Club.objects.get(name=club_name)
models.py
class Fish(models.Model):
name = models.CharField(max_length=255)
created = models.DateTimeField('auto_now_add=True')
active = models.BooleanField()
class Team(models.Model):
starting = models.CharField(max_length=255)
captain = models.CharField(max_length=255)
value = models.CharField(max_length=255)
fixtures = models.CharField(max_length=255)
position = models.CharField(max_length=255)
class Club(models.Model):
name = models.CharField(max_length=255)
abv = models.CharField(max_length=255)
stadium = models.CharField(max_length=255)
manager = models.CharField(max_length=255)
trophies = models.CharField(max_length=255)
urls.py
router = routers.DefaultRouter()
#makes sure that the API endpoints work
router.register(r'api/fishes', views.FishViewSet)
router.register(r'api/teams', views.TeamViewSet)
router.register(r'api/club', views.ClubViewSet)
admin.autodiscover()
urlpatterns = router.urls
url(r'^admin/', include(admin.site.urls)),
url(r'^', include(router.urls)),
url(r'^api-auth/', include('rest_framework.urls', namespace='rest_framework'))
url(r'^/api/club/(?P<club_name>[a-zA-Z]+)/$', ('getClub'))
I know a simple way by using regex
in the url.py add this regex
url(r'^/api/club/(?P<club_name>[a-zA-Z]+)/$', ....)
then in the views add a method will the club_name variable like this
from django.http import HttpResponse
def getclub(request, club_name):
teaminfo = ModelName.objects.get(name=club_name) # something like that
return HttpResponse(teaminfo)
the club_name variable will get the the string from the regex, you can use it in anyway you want.
Here's a simple good reference for this
https://docs.djangoproject.com/en/1.9/intro/tutorial03/
The issue is that you're using queryset = Team.objects.all() in your view. By nature, this implies that you'll get all of the objects.
I have a similar program, and I use urls.py like this-
...
url(r'^teams/(?P<incoming_team>[^/]+)/$', ('team_stats')),
url(r'^teams/', ('team_stats_all')),
in views it looks something like:
def team_stats(request, incoming_team):
queryset = Team.objects.get(name=incoming_team)
Obviously your existing view would be used for all. I'd also note you'll need some Try/Except handling, in case you have duplicate teams, or the team you request doesn't exist.
(In practice I don't actually split them out like this into separate views, but it gives you an idea of the logic flow)