I have a model that represents a house:
class House(models.Model):
name = models.CharField(...)
long = models.FloatField(...)
lat = models.FloatField(...)
and a serializer to return a list of houses in their most basic representation:
class HouseSerializer(serializers.ModelSerializer):
class Meta:
model = House
fields = ('id', 'name')
and the view
class HouseList(generics.ListAPIView):
queryset = House.objects.all()
serializer_class = HouseSerializer
this works fine. I can visit /api/house/ and I see a json list of houses:
{
'id': 1,
'name': 'Big House'
},
{
'id': 1
'name': 'Small House',
}...
Now I want to create a second view/resource at /api/maps/markers/ that returns my houses as a list of Google-Map-Friendly markers of the format:
{
'id': 1,
'long': ...,
'lat': ...,
'houseInfo': {
'title': "Big House",
}
} ...
I can foresee two approaches:
perform this as a separate serializer (using the same view as before) and mapping out the alternative field layout.
perform this as a separate view (using the same serializer as before) and simply layout the fields before creating a Response
but in neither approach am I clear on how to go about it nor which approach is preferable?
Answer 1
Looks to me like you need both - different view and serializer.
Simply because the view endpoint is not a sub-url of the first one, so they are not related - different view, even if they use the same model.
And different serializer - since you have a different field layout.
Not really sure how complicated is your case, but any code duplication can probably be solved by mixins anyway.
Answer 2
Depending on the use case:
if you also need to write data using the same struct, you need to define your own field class and handle the parsing correctly
if it's just reading data, you should be fine with this:
class HouseGoogleSerializer(HouseSerializer):
houseInfo = serializers.SerializerMethodField('get_house_info')
class Meta:
model = House
fields = [...]
def get_house_info(self, obj):
return {'title': obj.name}
where HouseSerializer is your base house serializer.
this code come from a running project and offer somethig more that you ask
but can easily adapted for your need if you want remove some features.
The current implemetation allow you:
use only one url one serializer and one view
choose the output using query string param (?serializer=std)
how to use in your code:
Case 1 (one url with ability to choose the serializer via querystring)
class HouseSerializer(HouseSerializer):
houseInfo = serializers.SerializerMethodField('get_house_info')
class Meta:
model = House
def get_house_info(self, obj):
return {'title': obj.name}
class HouseList(DynamicSerializerMixin, generics.ListAPIView):
queryset = House.objects.all()
serializer_class = HouseSerializer
serializers_fieldsets = {'std': ('id', 'name'),
'google' : ('id', 'long', 'lat', 'houseInfo')}
Case 2 (different views)
class HouseList(DynamicSerializerMixin, generics.ListAPIView):
queryset = House.objects.all()
serializer_class = HouseSerializer
serializers_fieldsets = {'std': ('id', 'name')}
class GoogleHouseList(DynamicSerializerMixin, generics.ListAPIView):
queryset = House.objects.all()
serializer_class = HouseSerializer
serializers_fieldsets = {'std': ('id', 'long', 'lat', 'houseInfo')}
==============
def serializer_factory(model, base=BaseHyperlinkedModelSerializer,
fields=None, exclude=None):
attrs = {'model': model}
if fields is not None:
attrs['fields'] = fields
if exclude is not None:
attrs['exclude'] = exclude
parent = (object,)
if hasattr(base, 'Meta'):
parent = (base.Meta, object)
Meta = type(str('Meta'), parent, attrs)
if model:
class_name = model.__name__ + 'Serializer'
else:
class_name = 'Serializer'
return type(base)(class_name, (base,), {'Meta': Meta, })
class DynamicSerializerMixin(object):
"""
Mixin that allow to limit the fields returned
by the serializer.
Es.
class User(models.Model):
country = models.ForeignKey(country)
username = models.CharField(max_length=100)
email = models.EmailField()
class UserSerializer(BaseHyperlinkedModelSerializer):
country = serializers.Field(source='country.name')
class MyViewSet(DynamicSerializerViewSetMixin, BaseModelViewSet):
model = User
serializer_class = UserSerializer
serializers_fieldsets = {'std': None,
'brief' : ('username', 'email')
}
this allow calls like
/api/v1/user/?serializer=brief
"""
serializers_fieldsets = {'std': None}
serializer_class = ModelSerializer
def get_serializer_class(self):
ser = self.request.QUERY_PARAMS.get('serializer', 'std')
fields = self.serializers_fieldsets.get(ser, 'std')
return serializer_factory(self.model,
self.serializer_class,
fields=fields)
Related
How can I remove field3 and field4 only on the first page?
I need something that can be dynamically reused as I plan to use it in multiple views.
How can I dynamically delete field3 and field4 without creating multiple serializers?
class CustomSerializer(serializers.ModelSerializer):
class Meta:
model = Model
fields = ('field', 'field2', 'field3', 'field4')
class CustomSerializer2(serializers.ModelSerializer):
class Meta:
model = Model
fields = ('field5', 'field6', 'field3', 'field4')
class CustomSerializer2(serializers.ModelSerializer):
class Meta:
model = Model
fields = ('field7', 'field8', 'field3', 'field4')
class CustomView(ListAPIView):
serializer_class = CustomSerializer
class CustomView2(ListAPIView):
serializer_class = CustomSerializer2
class CustomView3(ListAPIView):
serializer_class = CustomSerializer3
Try using serializer context. This way you can assess request data and params in you serializer methods such as to_representation which is useful in your case.
PS. This code was written in stackoverflow window, it may include some mistakes, but it shows an approach
class CustomView(ListAPIView):
def get_serializer(self, *args, **kwargs):
serializer_class = self.get_serializer_class()
kwargs['context'] = {'request': self.request}
return serializer_class(*args, **kwargs)
class CustomSerializer2(serializers.ModelSerializer):
class Meta:
model = Model
fields = ['field1', 'field2', 'field3']
def to_representation(self, instance, **kwargs):
if self.context.get('request').query_params.get('page') == 1:
return {'field1': instance.field1, 'field2': instance.field2}
return {'field1': instance.field1, 'field3': instance.field3}
You can write two different serializers the first one with field3 & field4 and other one does not include these two fields. Then from your views use get_serializer_class() method to select the appropriate serializer for the provided page.
You will need some thing like this in your views -
class CustomSerializerWithFields(serializers.ModelSerializer):
class Meta:
model = Model
fields = ('field', 'field2', 'field3', 'field4')
class CustomSerializerWithoutFields(serializers.ModelSerializer):
class Meta:
model = Model
fields = ('field', 'field2')
class CustomView(ListAPIView):
def get_serializer_class(self):
if self.request.query_params["page"]==1:
return CustomSerialierWithoutFields
return CustomSerializerWithFields
Main target
is to get query set based on multiple values in query.
Business logic is to get all contracts for multiple drivers.
Example:
request url:
/api/contract/?driver=http://localhost:8000/api/driver/1,http://localhost:8000/api/driver/2
Response should be all contracts for these two drivers.
Driver Serializer:
class DriverSerializer(serializers.HyperlinkedModelSerializer):
url = serializers.HyperlinkedIdentityField(
view_name='driver-detail',
read_only=True
)
class Meta:
model = Driver
fields = [
'url',
'id',
'first_name',
'last_name',
]
Contract serializer:
class ContractSerializer(serializers.HyperlinkedModelSerializer):
url = serializers.HyperlinkedIdentityField(
view_name='contract-detail',
read_only=True
)
driver = serializers.StringRelatedField(many=False)
class Meta:
model = Contract
fields = [
'url',
'id',
'contract_detail_fields',
'driver',
]
Contract View
class ContractViewSet(viewsets.ModelViewSet):
serializer_class = serializers.ContractSerializer
queryset = Contract.objects.all()
permission_classes = (IsAuthenticated,)
filter_backends = [DjangoFilterBackend]
filterset_class = ContractFilter
ContractFilter:
class ContractFilter(FilterSet):
driver = CustomHyperlinkedIdentityFilterList('driver')
What I have tried is to make custom filterField based on answer by Sherpa
class CustomHyperlinkedIdentityFilterList(django_filters.BaseCSVFilter,
django_filters.CharFilter):
def filter(self, qs, value):
values = value or []
for value in values:
qs = super(CustomHyperlinkedIdentityFilterList,
self).filter(qs, value)
return qs
Answer is ValueError: Field 'id' expected a number but got 'http://localhost:8000/api/drivers/driver/3/'.
Then I am trying to modify to filter by id not urlField and changing this line
qs = super(CustomHyperlinkedIdentityFilterList, self).filter(qs, value)
to this:
qs = super(CustomHyperlinkedIdentityFilterList, self).filter(qs, get_id_from_url(value))
where get_id_from_url is:
def get_id_from_url(url):
return int(resolve(urlparse(unquote(url)).path).kwargs.get('pk'))
But it return me only contracts for last driver, not for both.
Then I also tried configurations based on answer by Slava
class ContractFilter(FilterSet):
class Meta:
model = Contract
fields = ['driver']
by using this solutions response is Bad request
{"driver":["Select a valid choice. That choice is not one of the available choices."]}
I hope there is very simple solutions which I have missed.
I have list [1, 2, 3]
and TestModel queryset
[ {'pk':1,'text':'one'}, {'pk':2,'text':'two'}, {'pk':3,'text':'three'}]
and I have model and serializer like following
class TestMode(models.Model):
text = models.CharField(max_length=10)
class TestModelSerializer(serializer.ModelSerializer):
class Meta:
model = TestModel
fields = ('pk', 'text')
I want to make data like
[{'pk':1, 'text':'one', 'number':1}, {..., 'number':2}, {..., 'number':3}]
I make another serializer
class WrapperSerializer(serializer.ModelSerializer):
number = serializer.IntegerField()
class Meta:
model = TestModel
fields = ('pk', 'text')
I try to like following, but i think it's not cool
serialized_data = TestModelSerializer(qs, many=True).data
for index, data in enumerate(serializerd_data):
data['number'] = list[index]
serializer = WrapperSerializer(serialized_data, many=True)
How can i do that??? i don't have another idea... please help me
If you want to add a calculated value (read-only) to your serializer, you have two options:
On the model, define a #property number:
#property
def number(self):
return self.friends.count() # or whatever you need to calculate the number
Then in your serializer, you can just use number as any other field, but need to specify the type of serializer (e.g. IntegerField).
on the serializer use a SerializerMethodField as described here:
number = serializers.SerializerMethodField()
def get_number(self, obj):
return obj.friends.count()
I am trying to figure out the best way to add annotated fields, such as any aggregated (calculated) fields to DRF (Model)Serializers. My use case is simply a situation where an endpoint returns fields that are NOT stored in a database but calculated from a database.
Let's look at the following example:
models.py
class IceCreamCompany(models.Model):
name = models.CharField(primary_key = True, max_length = 255)
class IceCreamTruck(models.Model):
company = models.ForeignKey('IceCreamCompany', related_name='trucks')
capacity = models.IntegerField()
serializers.py
class IceCreamCompanySerializer(serializers.ModelSerializer):
class Meta:
model = IceCreamCompany
desired JSON output:
[
{
"name": "Pete's Ice Cream",
"total_trucks": 20,
"total_capacity": 4000
},
...
]
I have a couple solutions that work, but each have some issues.
Option 1: add getters to model and use SerializerMethodFields
models.py
class IceCreamCompany(models.Model):
name = models.CharField(primary_key=True, max_length=255)
def get_total_trucks(self):
return self.trucks.count()
def get_total_capacity(self):
return self.trucks.aggregate(Sum('capacity'))['capacity__sum']
serializers.py
class IceCreamCompanySerializer(serializers.ModelSerializer):
def get_total_trucks(self, obj):
return obj.get_total_trucks
def get_total_capacity(self, obj):
return obj.get_total_capacity
total_trucks = SerializerMethodField()
total_capacity = SerializerMethodField()
class Meta:
model = IceCreamCompany
fields = ('name', 'total_trucks', 'total_capacity')
The above code can perhaps be refactored a bit, but it won't change the fact that this option will perform 2 extra SQL queries per IceCreamCompany which is not very efficient.
Option 2: annotate in ViewSet.get_queryset
models.py as originally described.
views.py
class IceCreamCompanyViewSet(viewsets.ModelViewSet):
queryset = IceCreamCompany.objects.all()
serializer_class = IceCreamCompanySerializer
def get_queryset(self):
return IceCreamCompany.objects.annotate(
total_trucks = Count('trucks'),
total_capacity = Sum('trucks__capacity')
)
This will get the aggregated fields in a single SQL query but I'm not sure how I would add them to the Serializer as DRF doesn't magically know that I've annotated these fields in the QuerySet. If I add total_trucks and total_capacity to the serializer, it will throw an error about these fields not being present on the Model.
Option 2 can be made work without a serializer by using a View but if the model contains a lot of fields, and only some are required to be in the JSON, it would be a somewhat ugly hack to build the endpoint without a serializer.
Possible solution:
views.py
class IceCreamCompanyViewSet(viewsets.ModelViewSet):
queryset = IceCreamCompany.objects.all()
serializer_class = IceCreamCompanySerializer
def get_queryset(self):
return IceCreamCompany.objects.annotate(
total_trucks=Count('trucks'),
total_capacity=Sum('trucks__capacity')
)
serializers.py
class IceCreamCompanySerializer(serializers.ModelSerializer):
total_trucks = serializers.IntegerField()
total_capacity = serializers.IntegerField()
class Meta:
model = IceCreamCompany
fields = ('name', 'total_trucks', 'total_capacity')
By using Serializer fields I got a small example to work. The fields must be declared as the serializer's class attributes so DRF won't throw an error about them not existing in the IceCreamCompany model.
I made a slight simplification of elnygreen's answer by annotating the queryset when I defined it. Then I don't need to override get_queryset().
# views.py
class IceCreamCompanyViewSet(viewsets.ModelViewSet):
queryset = IceCreamCompany.objects.annotate(
total_trucks=Count('trucks'),
total_capacity=Sum('trucks__capacity'))
serializer_class = IceCreamCompanySerializer
# serializers.py
class IceCreamCompanySerializer(serializers.ModelSerializer):
total_trucks = serializers.IntegerField()
total_capacity = serializers.IntegerField()
class Meta:
model = IceCreamCompany
fields = ('name', 'total_trucks', 'total_capacity')
As elnygreen said, the fields must be declared as the serializer's class attributes to avoid an error about them not existing in the IceCreamCompany model.
You can hack the ModelSerializer constructor to modify the queryset it's passed by a view or viewset.
class IceCreamCompanySerializer(serializers.ModelSerializer):
total_trucks = serializers.IntegerField(readonly=True)
total_capacity = serializers.IntegerField(readonly=True)
class Meta:
model = IceCreamCompany
fields = ('name', 'total_trucks', 'total_capacity')
def __new__(cls, *args, **kwargs):
if args and isinstance(args[0], QuerySet):
queryset = cls._build_queryset(args[0])
args = (queryset, ) + args[1:]
return super().__new__(cls, *args, **kwargs)
#classmethod
def _build_queryset(cls, queryset):
# modify the queryset here
return queryset.annotate(
total_trucks=...,
total_capacity=...,
)
There is no significance in the name _build_queryset (it's not overriding anything), it just allows us to keep the bloat out of the constructor.
I'm trying POST a new a Nested Object, the problem is just create the "top" object (Playlist), but don't create the "ChannelItem"...
My Models:
class Playlist(models.Model):
provider = models.IntegerField()
channel_id = models.CharField(max_length=100)
channel_version = models.CharField(blank=True, max_length=100)
start = models.DateTimeField()
url = models.CharField(max_length=500)
class ChannelItem(models.Model):
playlist = models.ForeignKey(Playlist, editable=False, related_name='channelitems')
content_id = models.CharField(max_length=100)
content_version = models.CharField(blank=True, max_length=100)
My Serializer:
class ChannelItemSerializer(serializers.ModelSerializer):
class Meta:
model = ChannelItem
fields = ('content_id', 'content_version')
exclude = ('id')
depth = 1
class PlaylistSerializer(serializers.ModelSerializer):
class Meta:
model = Playlist
fields = ('id', 'provider', 'channel_id', 'channel_version', 'start',
'url', 'channelitems')
depth = 2
channelitems = ChannelItemSerializer()
I use the curl to post the following data :
'{"provider":125,"channel_id":"xyz", "channel_version":"xsqt",
"start":"2012-12-17T11:04:35","url":"http://192.168.1.83:8080/maaaaa",
"channelitems":[{"content_id":"0.flv", "content_version":"ss"},
{"content_id":"1.flv","content_version":"ss"}]}' http://localhost:8000/playlist_scheduler/playlists/
I receive the message:
HTTP/1.1 201 CREATED
Content-Type: application/json
Transfer-Encoding: chunked
Date: Mon, 17 Dec 2012 20:12:54 GMT
Server: 0.0.0.0
{"id": 25, "provider": 125, "channel_id": "xyz", "channel_version": "xsqt",
"start":"2012-12-17T11:04:35", "url": "http://localhost:8080/something",
"channelitems": []}
Nested representations do not currently support read-write, and should instead be read-only.
You should probably look into using a flat representation instead, using pk or hyperlinked relations.
If you need the nested representation, you may want to consider having two separate endpoints - a flat writable endpoint, and a nested read-only endpoint.
If someone needs a quick-and-dirty solution for that, I came up with this one I'll be temporary using in a project:
class NestedManyToManyField(serializers.WritableField):
def to_native(self, value):
serializer = self.Meta.serializer(value.all(), many=True, context=self.context)
return serializer.data
def from_native(self, data):
serializer = self.Meta.serializer(data=data, many=True, context=self.context)
serializer.is_valid()
serializer.save()
return serializer.object
class Meta:
serializer = None
Then create your own subclass of NestedManyToManyField:
class TopicNestedSerializer(NestedManyToManyField):
class Meta:
serializer = MyOriginalSerializer
An example of MyOriginalSerializer:
class MyOriginalSerializer(serializers.ModelSerializer):
class Meta:
model = models.MyModel
fields = ('id', 'title',)
This works fine for me so far. But be aware there are clean fixes coming:
https://github.com/tomchristie/django-rest-framework/issues/960
https://github.com/tomchristie/django-rest-framework/pull/817
after a long effort I made a first version that funcinasse ...
I believe that with some improvement could be included within the ModelSerializer
class ChannelItemSerializer(serializers.ModelSerializer):
class Meta:
model = ChannelItem
fields = ('id', 'content_id', 'content_version')
def field_from_native(self, data, files, field_name, into):
try:
if self._use_files:
_files = files[field_name]
else:
_data = data[field_name]
except KeyError:
if getattr(self, 'default', None):
_data = self.default
else:
if getattr(self, 'required', None):
raise ValidationError(self.error_messages['required'])
return
if type(_data) is list:
into[field_name] = []
for item in _data:
into[field_name].append(self._custom_from_native(item))
else:
into[field_name] = self._custom_from_native(_data)
def _custom_from_native(self, data):
self._errors = {}
if data is not None:
attrs = self.restore_fields(data, None)
attrs = self.perform_validation(attrs)
else:
self._errors['non_field_errors'] = ['No input provided']
if not self._errors:
return self.restore_object(attrs, instance=getattr(self, 'object', None))
class PlaylistSerializer(serializers.ModelSerializer):
class Meta:
model = Playlist
fields = ('id', 'provider', 'channel_id', 'channel_version', 'start', 'url', 'channel_items')
depth = 1
channel_items = ChannelItemSerializer()
def restore_object(self, attrs, instance=None):
self.foreign_data = {}
for (obj, model) in self.opts.model._meta.get_all_related_objects_with_model():
field_name = obj.field.related_query_name()
if field_name in attrs:
self.foreign_data[field_name] = attrs.pop(field_name)
return super(PlaylistSerializer, self).restore_object(attrs, instance)
def save(self, save_m2m=True):
super(PlaylistSerializer, self).save(save_m2m)
if getattr(self, 'foreign_data', None):
for accessor_name, object_list in self.foreign_data.items():
setattr(self.object, accessor_name, object_list)
self.foreign_data = {}
return self.object
For me, I have a hybrid workaround that I'm OK with. Namely, create a view that has:
the ManyToMany field in its un-nested serializer form
alias the nested ManyToMany field into a variable with _objs as the suffix and specify it as as read only
when you PUT back to the server reconcile the two aliased fields and store the result in the un-nested serializer field
e.g.
class MSerializer(serializers.HyperlinkedModelSerializer):
foo_objs = TempSensorSerializer(source='foos', many=True, allow_add_remove=True,required=False,read_only=True)
class Meta:
model = M
fields = ('url', 'foos', 'foo_objs')
I don't love this solution, but it beats trying to separately query and collate the nested fields after retrieving the initial container.