I am building an API backend with django rest framework for angular cli and I have no idea how to access verbose_name's model fields attribute in order to serialize it. This is my code:
models.py
class MyModel(model.Models):
myField = models.CharField(verbose_name='My Verbose Name')
# Here I have other 30 fields
this is the serializer for this model
serializers.py
class MyModelSerializer(ModelSerializer):
myField = SerializerMethodField()
def get_myField(self, obj):
field = next(f for f in obj._meta.fields if f.name == 'myField')
myField= {
'verbose_name': field.verbose_name.title(),
'value': obj.myField
}
return myField
# Do I Have to repeat this function for every field on my Model in order to have verbose-name ??
class Meta:
model = MyModel
fields = ['nominativo' ]
and my view is:
class MyModelListAPIView(ListAPIView):
queryset = Archivio.objects.all()
serializer_class = MyModelSerializer
My output is like i desire and is like this:
[
{
"myField":
{
"verbose_name":"My Verbose Name",
"value":"My Field value"
}
}
]
But this is okay with only few fields. In My case I need to have an output like this with 30 fields of my Model. Is there a better solution instead of making 30 get_field functions on my ModelSerializer class?
I think playing around with the fields itself (changing representation to object) might not be the best way to achieve what you need (surely not the easiest).
Instead, I would try to define my ModelSerializer which autogenerates "labels" field, for example (basic implementation):
from rest_framework import serializers
from rest_framework.fields import SerializerMethodField
class MyModelSerializer(serializers.ModelSerializer):
def __init__(self, *args, **kwargs):
super(MyModelSerializer, self).__init__(*args, **kwargs)
if 'labels' in self.fields:
raise RuntimeError(
'You cant have labels field defined '
'while using MyModelSerializer'
)
self.fields['labels'] = SerializerMethodField()
def get_labels(self, *args):
labels = {}
for field in self.Meta.model._meta.get_fields():
if field.name in self.fields:
labels[field.name] = field.verbose_name
return labels
Then, if you would use MyModelSerializer instead of serializers.ModelSerializer, you would get output like:
{
'name': 'Test',
'email': 'test#test.com',
'labels': {
'name': 'Full name',
'email': 'Email address'
}
}
This way, logic for all fields generated by django rest framework stays the same and you have anohter read only field. Nice and clean.
You will have to override .to_representation for your ModelSerializer class.
Docs
Source code
You want something just like this (modified original source) -
from rest_framework import serializers
from rest_framework.relations import PKOnlyObject
class MyModelSerializer(serializers.ModelSerializer):
def to_representation(self, instance):
"""Object instance -> Dict of primitive datatypes."""
ret = OrderedDict()
fields = self._readable_fields
for field in fields:
try:
attribute = field.get_attribute(instance)
except SkipField:
continue
# We skip `to_representation` for `None` values so that fields do
# not have to explicitly deal with that case.
#
# For related fields with `use_pk_only_optimization` we need to
# resolve the pk value.
check_for_none = attribute.pk if isinstance(attribute, PKOnlyObject) else attribute
if check_for_none is None:
value = None
else:
value = field.to_representation(attribute)
ret[field.field_name] = {
'value': value,
# You can find more field attributes here
# https://github.com/encode/django-rest-framework/blob/master/rest_framework/fields.py#L324
'verbose_name': field.label,
'read_only': field.read_only,
'write_only': field.write_only,
'help_text': field.help_text,
}
return ret
Related
django-filter allows you to easily declare filterable fields of a model.
For example,
class UserFilter(django_filters.FilterSet):
class Meta:
model = User
fields = ['username']
provides an exact lookup for the username field which is equivalent to this...
class UserFilter(django_filters.FilterSet):
class Meta:
model = User
fields = {
'username': ['exact']
}
I'm looking for a way support all possible lookup filters given the field so that I don't have to do this:
class UserFilter(django_filters.FilterSet):
class Meta:
model = User
fields = {
"username": ["exact", "iexact", "contains", "icontains", "startswith", ..., etc.]
}
Override the get_fields(...) class method of FilterSet class as,
import django_filters as filters
# If you are using DRF, import `filters` as
# from django_filters import rest_framework as filters
class AnyModelFilter(filters.FilterSet):
class Meta:
model = AnyModel
fields = '__all__'
#classmethod
def get_fields(cls):
fields = super().get_fields()
for field_name in fields.copy():
lookup_list = cls.Meta.model._meta.get_field(field_name).get_lookups().keys()
fields[field_name] = lookup_list
return fields
You can get all possible lookups of a field by django lookup up api
lookups_list = []
lookups = User._meta.get_field("username").get_lookups()
for lookup in lookups:
lookups_list.append(lookup)
result of lookups_list:
['exact', 'iexact', 'gt', 'gte', 'lt', 'lte', 'in', 'contains',
'icontains', 'startswith', 'istartswith', 'endswith', 'iendswith',
'range', 'isnull', 'regex', 'iregex']
So you can use it in your FilterSet
def fields_lookups(MyModel):
lookups = {}
fields = [x.name for x in MyModel._meta.fields] #<=1) get all fields names
for field in fields:
lookups[field] = [*MyModel._meta.get_field(
field).get_lookups().keys()] #<=2) add each field to a `dict`and set it vlaue to the lookups
return lookups
class StatsticsView(ItemsView):
queryset = MyModel.objects.all()
serializer_class = StatisticSer
filterset_fields = fields_lookups(MyModel) #<= ✅
def get(self, request, *args, **kwargs):
....
def put(self, request, *args, **kwargs):
....
.
.
.
I want to create a deferred method field in a model serializer using drf-flexfields.
I am using Django Rest Framework and drf-flexfields. I want to create a method field in my model serializer to make a complex query. Because retrieving this field will incur extra database lookups, I want this to be a deferred field, i.e. it will only be retrieved if the client specifically asks for it.
The DRF-Flexfields documentation seems to infer that a field can be deferred by only listing it in "expanded_fields" and not in the normal "fields" list, but gives no further explanation or example. https://github.com/rsinger86/drf-flex-fields#deferred-fields
I have tried creating a simple SerializerMethodField to test this:
class Phase(models.Model):
name = models.CharField(max_length=100)
assigned = models.ManyToManyField(settings.AUTH_USER_MODEL,
blank=True,
related_name="phases_assigned")
class PhaseSerializer(FlexFieldsModelSerializer):
assignable_users = serializers.SerializerMethodField("get_assignable_users")
expandable_fields = {
'assignable_users': (UserSerializer, {'source': 'assignable_users', 'many': True}),
}
class Meta:
model = Phase
fields = ['name', 'assigned']
def get_assignable_users(self, phase):
return {'test': 'this is a deferred field. It should only shows up when /?expand=assigned_users is given in '
'the api get request url'}
I get the following error :
"The field 'assignable_users' was declared on serializer PhaseSerializer, but has not been included in the 'fields' option."
the desired result would be that a call to the api at /phase/ will return just the default fields specified in the meta "fields" list. "assignable_users" will only get returned if the client specifically asks for it with /phase/?expand=assignable_users.
What is the correct way to go about accomplishing this?
No matter it is a SerializerMethodField, you should add your assignable_users field in Meta fields:
class PhaseSerializer(FlexFieldsModelSerializer):
...
class Meta:
model = Phase
fields = ['name', 'assigned', 'assignable_users']
# _____________________________^
Check the docs for more information.
If you declare a field in the serializer, you must include it in the fields option, as the error says. But if you do that, the field will be shown by default.
So, if you want a deferred field (on demand field), you can declare it in the model as a property:
class MyModel(models.Model):
my_field1 = ...
my_field2 = ...
my_field3 = ...
...
#property
def deferred_field(self):
return 'extra database lookups'
Then, in your serializer, you include it in expandable_fields as serializers.StringRelatedField:
class MySerializer(FlexFieldsModelSerializer):
class Meta:
model = MyModel
fields = ['my_field1', 'my_field2']
expandable_fields = {
'my_field3': (serializers.StringRelatedField),
'deferred_field': (serializers.StringRelatedField),
}
class PhaseSerializer(FlexFieldsModelSerializer):
...
expandable_fields = {
'assignable_users': (serializers.SerializerMethodField, {'read_only': True}),
'assignable_users_m2m': (UserSerializer, {'source': 'assigned', 'many': True}),
}
class Meta:
model = Phase
fields = ['name', 'assigned']
def get_assignable_users(self, phase):
return {'test': '1111'}
I have a field that I need to hide from a get put it needs to be there for a put in a Viewset, how would I achieve this?
my serialiser is as the below, the field stores data as JSON so I need to load it as JSON to perform a get. but having the original field (routing_data) there on a get will cause a 500 error, so I need to hide it from get. but when I'm using a put, it will be this field I put into.
Thanks
serializers.py
class MonitoringSerializerRoutingTable(serializers.ModelSerializer):
hostname = serializers.ReadOnlyField(source='device.hostname', )
site_id = serializers.ReadOnlyField(source='device.site_id', )
rt = serializers.SerializerMethodField(source='routing_data',)
use = serializers.ReadOnlyField(source='device_use.use', )
def get_rt(self, instance):
try:
return json.loads(instance.routing_data)
except:
return instance.routing_data
class Meta:
model = DeviceData
fields = ('id','site_id','device_id','hostname','use', 'timestamp', 'rt','routing_data')
views.py
class MonitoringRoutingTableUpload(viewsets.ModelViewSet):
queryset = DeviceData.objects.select_related('device','device_use').order_by('monitoring_order')
serializer_class = MonitoringSerializerRoutingTable
permission_classes = (IsAdminUser,)
filter_class = DeviceData
filter_backends = (filters.SearchFilter,)
search_fields = ('device__hostname','device_use__use')
EDIT
the update serialiser doesn't seem to be updating the data
class MonitoringRoutingTableUpload(viewsets.ModelViewSet):
queryset = DeviceData.objects.select_related('device','device_use').order_by('monitoring_order')
permission_classes = (IsAdminUser,)
filter_class = DeviceData
filter_backends = (filters.SearchFilter,)
search_fields = ('device__hostname','device_use__use')
def get_serializer_class(self):
serializers = {
create: MonitoringCreateSerializer,
update: MonitoringCreateSerializer,
list: MonitoringSerializerRoutingTable,
retrieve: MonitoringSerializerRoutingTable,
}
return serializers.get(self.action)
serialiezr.py
class MonitoringSerializerRoutingTable(serializers.ModelSerializer):
hostname = serializers.ReadOnlyField(source='device.hostname', )
site_id = serializers.ReadOnlyField(source='device.site_id', )
rt = serializers.SerializerMethodField(source='routing_data',)
use = serializers.ReadOnlyField(source='device_use.use', )
def get_rt(self, instance):
try:
return json.loads(instance.routing_data)
except:
return instance.routing_data
class Meta:
model = DeviceData
fields = ('id','site_id','device_id','hostname','use', 'timestamp', 'rt')
class MonitoringCreateSerializer(serializers.ModelSerializer):
class Meta:
model = DeviceData
fields = ('id','site_id','device_id','routing_data')
PUT request
URL: http://10.66.193.200:8100/api/rt_upload/9/
data: {"routing_data": "[{'subnet': '10.10.0.0/16', 'age': '6w3d', 'next_hop': '10.20.0.0/16'}, {'subnet': '10.30.0.0/16', 'age': '6w3d', 'next_hop': '10.40.0.0/16'}, {'subnet': '10.50.0.0/16', 'age': '6w3d'...}]"}
response from postman:
{
"id": 9,
"site_id": 118,
"device_id": 460,
"hostname": "EDGE",
"use": "Remote Site Connectivity",
"timestamp": "2019-05-31T10:12:58.300252",
"rt": ""
}
Add write_only=True to your serializer field
or
Create two serializers one for get request and one for put request.
change the serializer based on request method inside view's get_serializer_class method.
In this case, you can use a different serializer for get and for put/post. Create a new serializer with the fields you need, then drop the serializer_class attribute and override the get_serializer_class in the view. Something like this:
class MonitoringRoutingTableUpload(viewsets.ModelViewSet):
...
def get_serializer_class(self):
serializers = {
create: MonitoringCreateSerializer,
update: MonitoringCreateSerializer,
list: MonitoringSerializerRoutingTable,
retrieve: MonitoringSerializerRoutingTable,
}
return serializers.get(self.action)
I have some models
class RootModel(models.Model):
# Some fields
class ElementModel(models.Model):
root = models.ForeignKey(RootModel, related_name='elements', on_delete=models.CASCADE)
class TextModel(ElementModel):
text = models.TextField()
class BooleanModel(ElementModel):
value = models.BooleanField()
a viewset
class RootViewSet(viewsets.ModelViewSet):
queryset = RootModel.objects.all()
serializer_class = RootSerializer
and serializers
class TextSerializer(serializers.ModelSerializer):
type = serializers.SerializerMethodField()
class Meta:
model = TextModel
fields = '__all__'
def get_type(self, obj):
return 'TEXT'
class BooleanSerializer(serializers.ModelSerializer):
type = serializers.SerializerMethodField()
class Meta:
model = BooleanModel
fields = '__all__'
def get_type(self, obj):
return 'BOOL'
class RootSerializer(WritableNestedModelSerializer):
elements = ...
class Meta:
model = RootModel
fields = '__all__'
WritableNestedModelSerializer comes from drf_writable_nested extension.
I want to GET/POST/PUT a root containing all data
example with GET (same data for POST/PUT)
{
elements: [
{
type: "TEXT",
text: "my awesome text"
},
{
type: "BOOL",
value: true
}
],
...
root fields
...
}
What is the best way for elements field in RootSerializer ?
I also want to have information with OPTIONS method, how can I have it ?
Thanks
Finally I found a solution.
First we need a PolymorphicSerializer class :
from enum import Enum
from rest_framework import serializers
class PolymorphicSerializer(serializers.Serializer):
"""
Serializer to handle multiple subclasses of another class
- For serialized dict representations, a 'type' key with the class name as
the value is expected: ex. {'type': 'Decimal', ... }
- This type information is used in tandem with get_serializer_map(...) to
manage serializers for multiple subclasses
"""
def get_serializer_map(self):
"""
Return a dict to map class names to their respective serializer classes
To be implemented by all PolymorphicSerializer subclasses
"""
raise NotImplementedError
def to_representation(self, obj):
"""
Translate object to internal data representation
Override to allow polymorphism
"""
if hasattr(obj, 'get_type'):
type_str = obj.get_type()
if isinstance(type_str, Enum):
type_str = type_str.value
else:
type_str = obj.__class__.__name__
try:
serializer = self.get_serializer_map()[type_str]
except KeyError:
raise ValueError('Serializer for "{}" does not exist'.format(type_str), )
data = serializer(obj, context=self.context).to_representation(obj)
data['type'] = type_str
return data
def to_internal_value(self, data):
"""
Validate data and initialize primitive types
Override to allow polymorphism
"""
try:
type_str = data['type']
except KeyError:
raise serializers.ValidationError({
'type': 'This field is required',
})
try:
serializer = self.get_serializer_map()[type_str]
except KeyError:
raise serializers.ValidationError({
'type': 'Serializer for "{}" does not exist'.format(type_str),
})
validated_data = serializer(context=self.context).to_internal_value(data)
validated_data['type'] = type_str
return validated_data
def create(self, validated_data):
"""
Translate validated data representation to object
Override to allow polymorphism
"""
serializer = self.get_serializer_map()[validated_data['type']]
validated_data.pop('type')
return serializer(context=self.context).create(validated_data)
def update(self, instance, validated_data):
serializer = self.get_serializer_map()[validated_data['type']]
validated_data.pop('type')
return serializer(context=self.context).update(instance, validated_data)
and now :
class ElementSerializer(PolymorphicSerializer):
class Meta:
model = ElementModel
def get_serializer_map(self):
return {
BooleanSerializer.__class__: BooleanSerializer,
TextSerializer.__class__: TextSerializer,
}
class RootSerializer(WritableNestedModelSerializer):
elements = ElementSerializer(many=True)
class Meta:
model = RootModel
fields = '__all__'
Reference link: https://stackoverflow.com/a/44727343/5367584
My aim is to build endpoint which will surve to create objects of model with GenericForeignKey. Since model also includes ContentType, the actual type of model which we will reference is not known before object creation.
I will provide an example:
I have a 'Like' model which can reference a set of other models like 'Book', 'Author'.
class Like(models.Model):
created = models.DateTimeField()
content_type = models.ForeignKey(ContentType)
object_id = models.PositiveIntegerField()
content_object = GenericForeignKey('content_type', 'object_id')
Serializer may look like this:
class LikeSerializer(serializers.ModelSerializer):
class Meta:
model = models.Like
fields = ('id', 'created', )
What I want to achieve is to determine type of Like based on keys passed in request. The problem is that DRF do not pass those keys from request if they were not expilictly specified in Serializer fields. For example, POST request body contains:
{
"book":2
}
I want to do next
def restore_object(self, attrs, instance=None)
if attrs.get('book', None) is not None:
# create Like instance with Book contenttype
elif attrs.get('author', None) is not None:
# create Like instance with Author contenttype
In this case first if clause will be executed.
As you can see, The type determined based on key passed in request, without specifying special Field.
Is there any way to achieve this?
Thanks
You might try instantiating your serializer whenever your view is called by wrapping it in a function (you make a serializer factory):
def like_serializer_factory(type_of_like):
if type_of_like == 'book':
class LikeSerializer(serializers.ModelSerializer):
class Meta:
model = models.Like
fields = ('id', 'created', )
def restore_object(self, attrs, instance=None):
# create Like instance with Book contenttype
elif type_of_like == 'author':
class LikeSerializer(serializers.ModelSerializer):
class Meta:
model = models.Like
fields = ('id', 'created', )
def restore_object(self, attrs, instance=None):
# create Like instance with Author contenttype
return LikeSerializer
Then override this method in your view:
def get_serializer_class(self):
return like_serializer_factory(type_of_like)
Solution 1
Basically there is a method you can add on GenericAPIView class called get_context_serializer
By default your view, request and format class are passed to your serializer
DRF code for get_context_serializer
def get_serializer_context(self):
"""
Extra context provided to the serializer class.
"""
return {
'request': self.request,
'format': self.format_kwarg,
'view': self
}
you can override that on your view like this
def get_serializer_context(self):
data = super().get_serializer_context()
# Get the book from post and add to context
data['book'] = self.request.POST.get('book')
return data
And use this on your serializer class
def restore_object(self, attrs, instance=None):
# Get book from context to use
book = self.context.get('book', None)
author = attrs.get('author', None)
if book is not None:
# create Like instance with Book contenttype
pass
elif author is not None:
# create Like instance with Author contenttype
pass
Solution 2
Add a field on your serializer
class LikeSerializer(serializers.ModelSerializer):
# New field and should be write only, else it will be
# return as a serializer data
book = serializers.IntegerField(write_only=True)
class Meta:
model = models.Like
fields = ('id', 'created', )
def save(self, **kwargs):
# Remove book from validated data, so the serializer does
# not try to save it
self.validated_data.pop('book', None)
# Call model serializer save method
return super().save(**kwargs)