how to format field name and round the value - django-rest-framework

I got a query "top 3 employees in terms of total invoice value" based on the following related tables:
[{
"customerid__supportrepid__id": 3,
"sells": 833.0400000000013
},
...]
I would like the first filed to be: "employee_id" and a sells field value
class CustomerViewSet(viewsets.ReadOnlyModelViewSet):
queryset = Customer.objects.all()
serializer_class = CustomerSerializer
#action(detail=False, methods=['get'])
def top_three_employees(self, request):
total_sells_by_employees = Invoice.objects \
.select_related('customerid') \
.select_related('customerid__supportrepid') \
.values('customerid__supportrepid__id') \
.annotate(sells=Sum('total')) \
.order_by('-sells')
return Response(total_sells_by_employees)

You can rename fields in a Django queryset using the values() method [django-doc]:
MyModel.objects.values(renamed_field_name=F('original_field_name'))
This will thus construct a queryset where the original_field_name is renamed to renamed_field_name.
In a serializer, you can then rename the field with:
class MySerializer(serializers.Serializer):
renamed_field_name = serializers.CharField()
So by defining a field with the target name, and omitting a source, it will automatically take the field with the same name in the object that is serialized.

You can use .annotate() to create an aliased, transformed field, and then serialize that field:
from django.db.models.functions import Round
MyModel.objects.annotate(
rounded_field=Round('field_name', 2)
).values('rounded_field')
This will add a rounded_field to the queryset that is the field_name rounded to two digits.
You can then rename this in the serializer fields mapping, with:
class MyModelSerializer(serializers.ModelSerializer):
rounded_field = serializers.SerializerMethodField()
class Meta:
fields = ('rounded_field',)
def get_rounded_field(self, instance):
return instance.rounded_field
This will thus serialize the rounded_field as rounded_field, under the name you specified in the fields of the serializer.

Related

Django REST Framework (DRF): ModelSerializer does not validate models on serialization

I want to ask how to use Django REST Framework (DRF) ModelSerializers correctly for serializing from model.
I have Django model with two required fields:
class Book(models.Model):
title = models.CharField()
desc = models.CharField()
I have DRF ModelSerializer:
class BookSerializer(serializers.ModelSerializer):
class Meta:
model = Book
fields = ['title', 'desc']
I can deseralize and validate incoming request using:
serializer = BookSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
But how to serialize and send response? DRF allows me to break contact built using ModelSerializer. If I forgot to set one of mandatory Book fields, it will still still pass through BookSerializer!
invalid_book = Book(title="Foo") # but forgotten to set "desc"
serializer = BookSerializer(instance=invalid_book)
serializer.data # it contains book without required "desc"
Serialized created using instance parameter throws error if I try is_validate().
Why ModelSerializer can validate incoming data, but cannot outgoing?
Validation is only performed when deserializing. As per the documentation:
Serializers also provide deserialization, allowing parsed data to be converted back into complex types, after first validating the incoming data.
That makes sense (edit: in the way Django Rest Framework seems to be construed). Because it isn't the 'role' of the Serializer to make sure that your complex data such as querysets and model instances (eg. your Book instance) that you are going to serialize is construed 'legitimately', thus they also don't validate while serializing.
So if you would save the instance like invalid_book.save(), Django would throw an error because of the missing field.
Edit
After a comment about being 'a point of view' and thus being opiniated I want to stress and make clear that this seems to be the way that Django Rest Framework (DRF) is construed. After digging deeper on SO I link this answer in support.
Also if you read the documentation of DRF, it is somewhat implied that serialization and validation are two separate concepts.
Furthermore, analyzing serializers.py makes clear that validation is only run when calling is_valid() and the validation is only run on the provided data flag. In fact, it can't even be run when only an instance is provided:
def __init__(self, instance=None, data=empty, **kwargs):
self.instance = instance
if data is not empty:
self.initial_data = data
self.partial = kwargs.pop('partial', False)
self._context = kwargs.pop('context', {})
kwargs.pop('many', None)
super().__init__(**kwargs)
...
def is_valid(self, raise_exception=False):
assert hasattr(self, 'initial_data'), (
'Cannot call `.is_valid()` as no `data=` keyword argument was '
'passed when instantiating the serializer instance.'
)
if not hasattr(self, '_validated_data'):
try:
self._validated_data = self.run_validation(self.initial_data)
except ValidationError as exc:
self._validated_data = {}
self._errors = exc.detail
else:
self._errors = {}
if self._errors and raise_exception:
raise ValidationError(self.errors)
return not bool(self._errors)
You are under a very wrong assumption. A serializer (not de-serializer) does one thing. Convert an Object to JSON. Here, you are creating an object Book(name='sad book'). This is just a regular Python Object. Django Serializers will attempt to serialize any Object that is passed to it.
What you might be wondering is the field is required in Model but why doesn't the serializer validate? Because of the way DRF handles serialization. I will show some excerpts from DRF Source code.
This is how the data property is calulated.
class BaseSerializer():
...
...
#property
def data(self):
if hasattr(self, 'initial_data') and not hasattr(self, '_validated_data'):
msg = (
'When a serializer is passed a `data` keyword argument you '
'must call `.is_valid()` before attempting to access the '
'serialized `.data` representation.\n'
'You should either call `.is_valid()` first, '
'or access `.initial_data` instead.'
)
raise AssertionError(msg)
if not hasattr(self, '_data'):
if self.instance is not None and not getattr(self, '_errors', None):
# THIS IS WHERE WE GO. THE to_representation() CAN BE FOUND IN THE IMPLEMENTATION
# OF ModelSerializer() which inherits from this class BaseSerializer
self._data = self.to_representation(self.instance)
elif hasattr(self, '_validated_data') and not getattr(self, '_errors', None):
self._data = self.to_representation(self.validated_data)
else:
self._data = self.get_initial()
return self._data
What happens in ModelSerializer.to_representation() ?
class ModelSerializer(BaseSerializer):
...
...
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:
ret[field.field_name] = None
else:
ret[field.field_name] = field.to_representation(attribute)
return ret
As you can see, in this case the serializer only maps the fields from the Object being passed. So, there is no validation during serialization. For more info, check the source code of DRF. It's pretty easy if you use Pycharm Pro.

How to write a dynamic django orm queryset based on the filters passed?

i need to apply filters dynamically on a queryset depending on the filters passed as a json from the front end.
for ex.
if json passed is : { id:[1,2,3] }
then queryset will be : Model.objects.filter(id__in=id)
if json is passed as: { id:[1,2,3],country:['india','australia'] }
then queryset will be : Model.objects.filter(id__in=id,country__in=country)
how to achieve this dynamically?
You could use an if/else statement to detect if the country list is empty:
if not country:
Model.objects.filter(id__in=id)
else:
Model.objects.filter(id__in=id, country__in=country)
Edit:
A comment pointed out that the amount of filters are dynamic. So in Django .filter() returns another QuerySet and Querysets are lazily evaluated, meaning they can be chained and be used in a loop.
So using unpacking (**), we can compose the kwargs to pass to .filter().
Because the exact datatype/schema of the filters is not provided, I will use a dictionary as example, filter_dictionary. In this dictionary the key is the filter that will be used and the value will be a list.
# Compose first Queryset
qs = Model.objects.all()
# Loop over dictionary
for key, value in filter_dictionary.items():
# Use unpacking to compose kwarg
qs.filter(**{'{0}__in'.format(key): value})
# do something with the QuerySet
Just use a django_filters
from django_filters import rest_framework as filters
class ContentFilter(filters.FilterSet):
id = filters.NumberFilter(lookup_expr="in")
country = filters.CharFilter(lookup_expr="in")
class Meta:
model = <your_model>
fields = ['id', 'country']
Then add to your view class
filterset_class = ContentFilter

django-filter (django rest framework) - method filter with 3 parameters

I'm using django rest framework to create an api and django-filter to provide nice way for users to see how filters work in the browsable api part of the site.
I have a need to filter queryset by a result of a method call. Unfortunately it needs 3 parameters to be provided by the user (calculate distance from centre point using lat, lng, radius).
I know I can declare a non model field in the filterset with a method to call but then just one parameter is passed to the method.
I can declare 3 non model fields but then I end with 3 different methods or calling the same one with 1 changing parameter 3 times.
example code:
class PersonFilter(FilterSet):
status = ChoiceFilter(field_name='status', choices=Person.STATUS_CHOICES)
# I show an example of what I need to achieve below, obviously it will not work as
# I need to give the user 3 fields to fill in and call the method only once with their values...
latitude = NumberFilter(label='latitude', method='check_if_in_range')
longitude = NumberFilter(label='longitude', method='check_if_in_range')
radius = NumberFilter(label='radius', method='check_if_in_range')
class Meta:
model = Person
fields = 'status', 'latitude', 'longitude', 'radius'
example method to filter by 3 parameters:
def check_if_in_range(self, queryset, name, value):
here I need access to the values from 3 non model form fields...
do calculation and filter the queryset
return <filtered queryset>
Is this even doable?
I want my users to be able to use:
<base_url>?longitude=234234&latitude=232342&radius=34
to filter persons through the API...
Thank you for your time & help!
Tomasz
You can do something like this:
class PersonFilter(FilterSet):
status = ChoiceFilter(field_name='status', choices=Person.STATUS_CHOICES)
radius = NumberFilter(label='radius', method='check_if_within_range')
class Meta:
model = Person
fields = 'status', 'radius'
def check_if_within_range(self, queryset, name, value):
base_point = Point(latitude=float(self.request.GET.get("latitude")),
longitude=float(self.request.GET.get("longitude")))
return queryset.distance_within(base_point, value)
Based on how you want to calculate the distance and filter queryset you need to have a custom method. Here I have assumed you'll have distance_within() method in your custom queryset manager.
You can refactor as per your need/structure.

DRF - Add/Exclude fields based on instance's field values

I have a model named Notification which has a field named notification_type. Now in NotificationSerializer.__init__, I want to check the value of the notification_type field of the model instance and based on it's value, I want to add / remove some fields on the serializer. Is that possible?
I have tried self.instance inside the __init__ method but in case of the many=True, it's a queryset. I want to modify based on each model instance. Is that possible?
It's possible, but not on the serializer's __init__. Use the serializer's to_representation method instead.
def to_representation(self, obj):
data = super().to_representation(obj)
# data is your serialized instance
if obj.notification_type == 'type1':
data.pop('attr2')
elif obj.notification_type == 'type2':
data.pop('attr1')
return data

Dealing with unique constraints that should be replaced in django_rest_framework

tl;dr: How can I ignore (turn off) a unique constraint in django_rest_framework Create calls with a ListCreateAPIView, because I'm going to deal with it manually in the perform_create method?
Im using a third party library django-push-notifications. It has a nice model for APNSDevice (apple push notification service device) that has a unique constraint on a registration_id field.
My problem is that sometimes I want to manually delete old values in the table that have the registration ID, so that I can insert a new value. I'd like to use this serializer:
class APNSDeviceSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = APNSDevice
fields = ('name', 'active', 'device_id', 'registration_id')
along with this code for PUT
class MyAppleDevices(generics.ListCreateAPIView):
permission_classes = (permissions.IsAuthenticated,)
serializer_class = APNSDeviceSerializer
model = APNSDevice
def get_queryset(self):
return APNSDevice.objects.filter(user = self.request.user)
def perform_create(self, serializer):
print "Looking for old devices with registration id "+str(self.request.registration_id)
oldDevices = APNSDevice.objects.filter(registration_id = self.request.registration_id)
for oldDevice in oldDevices:
oldDevice.delete()
apnsDevice = serializer.save(user=self.request.user)
In other words, I'm trying to manually delete other entries that have the unique constraint in this particular PUT, so that I can insert the new one without violating the unique constraint. The problem is the validator runs before the perform_create method is called, and I can't figure out how to turn off the validator's unique constraint. I tried adding this to the Serializer
def get_validation_exclusions(self, instance = None):
exclusions = super(APNSDeviceSerializer, self).get_validation_exclusions(instance)
return exclusions + ['registration_id']
but it doesn't help so obviously I have no clue even though I've been pouring through the documentation and Stack Overflow posts. Any help appreciated, thanks. I suppose as a last resort I could remove the unique constraint from the model, but it is a valid constraint so I'd rather leave it in.
I found this question because I had this exact problem with that exact library. You can get around it by subclassing the serializer and manually overriding the field definition:
class APNSDeviceSerializerWithNonUniqueRegistrationId(APNSDeviceSerializer):
registration_id = serializers.CharField(min_length=64, max_length=64)
class Meta(APNSDeviceSerializer.Meta):
fields = ("name", "registration_id", "device_id", "active", "date_created")
Then, if you're using django-push-notifications, you'll also need to override the ViewSet that uses that serializer:
class APNSDeviceAuthorizedViewSetWithNonUniqueRegistrationId(AuthorizedMixin, APNSDeviceViewSet):
"""
The out of the box viewset/serializer combo require the registration ID to be unique and won't
allow setting a registration ID to a new user (which is useful if we have potentially more than
one account on a device.)
"""
serializer_class = APNSDeviceSerializerWithNonUniqueRegistrationId
def perform_create(self, serializer):
if self.request.user.is_authenticated():
try:
existing_registration = APNSDevice.objects.get(
registration_id=serializer.validated_data['registration_id'])
existing_registration.delete()
except APNSDevice.DoesNotExist:
pass
serializer.save(user=self.request.user)
return super(DeviceViewSetMixin, self).perform_create(serializer)

Resources