Ordering by computed field in admin panel - python

I'm learning Django and I got stuck on this problem
I'm basically trying to order by a computed field in an admin panel view, I've read some "tutorials" like this: https://books.agiliq.com/projects/django-admin-cookbook/en/latest/sorting_calculated_fields.html on google but I can't seem to figure out how it all works (between annotations etc)
Here's my classes:
class StockAdmin(admin.ModelAdmin):
list_display = ("ticker_symbol", "security_name", "current_pretax_yield", "current_aftertax_yield", "displayed_price")
search_fields = ("ticker_symbol", "security_name")
def current_pretax_yield(self, obj):
try:
curyield = float(obj.coupon_amount/obj.last_price)
return str(round((curyield*100),3)) + "%"
except:
return "n/a"
def current_aftertax_yield(self, obj):
try:
withholding_tax = 15
at_yield = ((obj.coupon_amount/obj.last_price*100)*(1-(withholding_tax/100))*0.74)
return str(round(at_yield, 2)) + "%"
except:
return "n/a"
def get_queryset(self, request):
queryset = super().get_queryset(request)
queryset = queryset.annotate(
_current_aftertax_yield=self.current_aftertax_yield(),
_current_pretax_yield=self.current_pretax_yield(),
)
current_aftertax_yield.admin_order_field = '_current_aftertax_yield'
current_pretax_yield.admin_order_field = '_current_pretax_yield'
Basically, I want to get "coupon amount" and "last price" fields from the database, perform the calculations you see in the functions, then display those calculated files in the admin panel and be able to "order by" them
The code as I have now errors out with a TypeError: current_aftertax_yield() missing 1 required positional argument: 'obj'
I've tried to follow this: https://books.agiliq.com/projects/django-admin-cookbook/en/latest/sorting_calculated_fields.html but I can't quite figure it out on my own..
Any ideas? Is there an easier way of doing this? I used a lot of computed values in PHP and it was trivial to implement!

You can not use methods to annotate a Queryset. You should specify an expression constructed an expression for the database. You can not use a method for that. You can make use of combinations of F-expressions together with certain aggregates, etc.
Here both the current_pretax_yield and current_aftertax_yield scale with coupon_amount/last_price, so we can make an annotation, and then sort by that annotation:
from django.db.models import F
class StockAdmin(admin.ModelAdmin):
list_display = ("ticker_symbol", "security_name", "current_pretax_yield", "current_aftertax_yield", "displayed_price")
search_fields = ("ticker_symbol", "security_name")
def current_pretax_yield(self, obj):
try:
curyield = float(obj.coupon_amount/obj.last_price)
return str(round((curyield*100),3)) + "%"
except:
return "n/a"
def current_aftertax_yield(self, obj):
try:
withholding_tax = 15
at_yield = ((obj.coupon_amount/obj.last_price*100)*(1-(withholding_tax/100))*0.74)
return str(round(at_yield, 2)) + "%"
except:
return "n/a"
def get_queryset(self, request):
return super().get_queryset(request).annotate(
_yield=F('coupon_amount')/F('last_price')
)
current_aftertax_yield.admin_order_field = '_yield'
current_pretax_yield.admin_order_field = '_yield'

Related

Use a single function for multiple serializer fields with different arguments

I have a serializer for a model with an image field, for which I have saved multiple different sized thumbnail images.
I access them by returning their URL using the SerializerMethodField:
class GalleryImageSerializer(serializers.ModelSerializer):
image_sm = serializers.SerializerMethodField()
image_md = serializers.SerializerMethodField()
image_lg = serializers.SerializerMethodField()
image_compressed = serializers.SerializerMethodField()
def get_image_sm(self, obj):
return default_storage.url(f'{splitext(obj.image.name)[0]}/sm.jpg')
def get_image_md(self, obj):
return default_storage.url(f'{splitext(obj.image.name)[0]}/md.jpg')
def get_image_lg(self, obj):
return default_storage.url(f'{splitext(obj.image.name)[0]}/lg.jpg')
def get_image_compressed(self, obj):
return default_storage.url(f'{splitext(obj.image.name)[0]}/compressed.jpg')
This code works, but it kind of violates the "don't repeat yourself" guideline.
As you can see, these are all duplicate SerializerMethodFields, with the only difference being the filename, eg 'lg.jpg', 'md.jpg', etc.
I'd much prefer to have only one function that I call with an argument for the filename, as an example(pseudocode):
class GalleryImageSerializer(serializers.ModelSerializer):
image_sm = serializers.SerializerMethodField(filename='sm.jpg')
image_md = serializers.SerializerMethodField(filename='md.jpg')
image_lg = serializers.SerializerMethodField(filename='lg.jpg')
image_compressed = serializers.SerializerMethodField(filename='compressed.jpg')
def get_image(self, obj, filename=''):
return default_storage.url(f'{splitext(obj.image.name)[0]}/{filename}')
Currently I am unable to find any way to achieve this. Reading the source code of SerializerMethodField, it doesn't seem to support it.
Is there any way to avoid creating duplicate functions for fields with arbitrary differences?
You can add these fields in the to_representation method.
def to_representation(self, instance):
ret = super().to_representation(instance)
# add img urls to ret dict
for name in ['sm', 'md', 'lg', 'compressed']:
ret['image_' + name] = default_storage.url(f'{splitext(instance.image.name)[0]}/{name}.jpg')
return ret
check the docs for more details:
https://www.django-rest-framework.org/api-guide/serializers/#to_representationself-instance

How to set filter range in backend in filter class in django

I'm building a REST API using DRF. I'm using django_filters for filtering result set. In one api I want user to send his coordinates(latitude, longitude) and in backend I create a range as (latitude+2 to latitude-2) and return results, I don't want take range field from user. So I can easily change the range in my backend.
I've created two range filters in filter class that works fine but url looks like this: /posts/?latitude_min=22&latitude_max=28&longitude_min=80&longitude_max=84. And here user is responsible to decide range. I want user send latitude and longitude only, I decide about maximum and minimum range.
class PostFilter(django_filters.FilterSet):
latitude = django_filters.RangeFilter(field_name='latitude')
longitude = django_filters.RangeFilter()
class Meta:
model = Post
fields = ['latitude', 'longitude', 'country', 'state', 'district', ]
I think you can use 'MethodFilter' something like this:
from django.db.models import Q
class PostFilter(django_filters.FilterSet):
latitude = django_filters.MethodFilter(action="filter_latitude")
longitude = django_filters.MethodFilter(action="filter_longitude")
class Meta:
model = Post
fields = ['latitude', 'longitude', 'country', 'state', 'district']
def filter_latitude(self, qs, value):
try:
return qs.filter(Q(latitude__gte=value-2)&Q(latitude__lte=value+2))
except BaseException:
return qs
def filter_longitude(self, qs, value):
try:
return qs.filter(Q(longitude__gte=value-2)&Q(longitude__lte=value+2))
except BaseException:
return qs
You can write a custom filter field that does this, in just a few lines of code. It will give you a bit more flexibility to add validation later as well, or additional logic around the specifics of lat/lng.
class ApproximateNumberFilter(Filter):
field_class = FloatField # use django base float validation
def __init__(self, under_over=2, **kwargs):
super().__init__(**kwargs)
self._under_over = under_over
def filter(self, qs, value):
if value in EMPTY_VALUES:
return qs
return qs.filter(**{
f"{self.field_name}__gte": value - self._under_over,
f"{self.field_name}__lte": value + self._under_over,
})

django multiple filters on field

I'm doing a simple filter -
filters.py
class TblserversFilter(django_filters.FilterSet):
name = django_filters.CharFilter(name="servername", lookup_type="exact")
class Meta:
model = Tblservers
fields = ['servername']
What I would like to do, if possible, is to have two lookup_types associated with the field. Specifically I want exact AND contains and then somehow replace the operator depending on the filter.
name=serverabc would be an exact search and name~abc will be a fuzzy search.
You could do a method_filter and then prefix your filter queries with different symbols for exact and icontains and other filters that you want at the client side.
Since code is better than a thousand words:
exact_prefix = '#'
icontains_prefix = '~'
class TblserversFilter(django_filters.FilterSet):
name = django_filters.MethodFilter(
action=name_filter)
def name_filter(self, value):
if value:
value_prefix = value[0]
if value_prefix == exact_prefix:
return self.filter(name=value)
elif value_prefix == icontains_prefix:
return self.filter(name__icontains=value)
# this can continue for all the types of filters you want
else:
return self.filter(name=value)
else:
return self.filter(name=value)
class Meta:
model = Tblservers
fields = ['servername']
EDIT:
In django-filter 1.0 MethodFilter was replaced with Filter's method argument. So solution rewritten for v1.0 would be following (not tested):
exact_prefix = '#'
icontains_prefix = '~'
class TblserversFilter(django_filters.FilterSet):
name = django_filters.CharFilter(
method='name_filter')
def name_filter(self, qs, name, value):
if value:
value_prefix = value[0]
if value_prefix == exact_prefix:
return qs.filter(name=value)
elif value_prefix == icontains_prefix:
return qs.filter(name__icontains=value)
# this can continue for all the types of filters you want
else:
return qs.filter(name=value)
else:
return qs.filter(name=value)
class Meta:
model = Tblservers
fields = ['servername']
First my apologies for the shameless library self-plug.
At some point I was trying to do something similar in django-filters however the solution was much more complex then anticipated. I ended up creating my own library for doing filtering in Django which natively supports the exact functionality you are looking for - django-url-filter. Its API is very similar to django-filters:
from django import forms
from url_filter.filter import Filter
from url_filter.filtersets import ModelFilterSet
class TblserversFilter(FilterSet):
name = Filter(form_field=forms.CharField(max_length=15), lookups=['exact', 'contains'])
class Meta(object):
model = Tblservers
fields = ['name', 'servername']
Note that the URL will look a bit different though:
?name=foo # exact
?name__exact=foo
?name__contains=foo
Also you will need to manually call the filter set in order to filter the queryset:
fs = TblserversFilter(data=query, queryset=...)
filtered_qs = fs.filter()
Syntax of the URL parameters is very similar to Django ORM.
You can look at the docs for more examples. Hopefully it might be of use.

How to sort by many custom methods in Django Admin

I want to be able to sort by several custom methods in Django Admin. This question provides solution for one method only.
I tried to modify it:
from django.db import models
class CustomerAdmin(admin.ModelAdmin):
list_display = ('number_of_orders','number_of_somevalue') # added field
def queryset(self, request):
qs = super(CustomerAdmin, self).queryset(request)
qs = qs.annotate(models.Count('order'))
qs = qs.annotate(models.Count('somevalue')) # added line
return qs
def number_of_orders(self, obj):
return obj.order__count
number_of_orders.admin_order_field = 'order__count'
def number_of_somevalue(self, obj): # added method
return obj.somevalue__count
number_of_somevalue.admin_order_field = 'somevalue__count'
and it works incorrectly. It seems that it multiplies the count values instead of counting them separately.
Example:
I have 2 orders and 2 somevalues, but in the panel I see 4 orders and 4 somevalues.
Adding another method with yet another value makes it 8 (2*2*2).
How can I fix it?
You can try this to sort by many custom methods (Tested):
from django.db.models import Count
class CustomerAdmin(admin.ModelAdmin):
# The list display must contain the functions that calculate values
list_display = ('number_of_orders','number_of_somevalue') # added field
# Overwrite queryset in model admin
def queryset(self, request):
qs = super(CustomerAdmin, self).queryset(request)
# The query have to return multiple annotation, for this use distinct=True in the Count function
qs = qs.annotate(number_orders = Count('order', distinct=True)).annotate(number_somevalue = Count('somevalue',distinct=True))
return qs
# This function return the new field calculated in queryset (number_orders)
def number_of_orders(self, obj):
return obj.number_orders
number_of_orders.admin_order_field = 'numberorders' # sortable new column
# And this one will return the another field calculated (number_somevalue)
def number_of_somevalue(self, obj): # added method
return obj.number_somevalue
number_of_somevalue.admin_order_field = 'number_somevalue'# sortable new column

Django get_queryset() trouble creating multiple annotation labels

I am trying to get a few different annotation results to show in the Django Admin. I need these new results to be sortable columns as well. I came up with this (after reading EXTENSIVELY on SO):
def queryset(self, request):
qs = super(BrandAdmin, self).get_queryset(request)
qs = Brand.objects.annotate(
last_connection=Max('account__computer__last_connect')
).filter(
account__computer__last_connect__gte=(datetime.datetime.now() - datetime.timedelta(weeks=4))
).annotate(
brand_count=Count('account__computer')
)
return qs
def billable_computers(self, obj):
return obj.brand_count
billable_computers.admin_order_field = 'brand_count'
def last_connection(self, obj):
return obj.last_connection
last_connection.admin_order_field = 'last_connection'
This works, but returns with really weird (multiplied by some factor) results for the 'billable_computers'. I figured this was because of the chained annotations.
So I tried this:
def queryset(self, request):
qs = super(BrandAdmin, self).get_queryset(request)
qs = Brand.objects.filter(
account__computer__last_connect__gte=(datetime.datetime.now() - datetime.timedelta(weeks=4))
).all().distinct().annotate(
brand_count=Count('account__computer')
)
qss = super(BrandAdmin, self).get_queryset(request)
qss = Brand.objects.annotate(
last_connection=Max('account__computer__last_connect')
)
qs_all = qs | qss
return qs_all
def billable_computers(self, obj):
return obj.brand_count
billable_computers.admin_order_field = 'brand_count'
def last_connection(self, obj):
return obj.last_connection
last_connection.admin_order_field = 'last_connection'
But this causes an error complaining that "'Brand' object has no attribute 'last_connection'".
So I tried this:
def queryset(self, request):
qs = super(BrandAdmin, self).get_queryset(request)
qs = Brand.objects.filter(
account__computer__last_connect__gte=(datetime.datetime.now() - datetime.timedelta(weeks=4))
).annotate(
brand_count=Count('account__computer')
)
qs = Brand.objects.annotate(
last_connection=Max('account__computer__last_connect')
)
return qs
But this give a "'Brand' object has no attribute 'brand_count'" error.
About the models: The Computer model has a foreign key to Account and Account has a foreign key to Brand. The Computer model has a datetime field labeled 'last_connect'. Every Brand can have unlimited Accounts and every Account can have unlimited Computers.
I just need to tally all the Computers (at each level) that have connected in the last 4 weeks. I also need a separate (sortable) column for the latest 'last_connect' value across all computers in the unfiltered set.
Obviously I am getting this wrong, but it seems like it should be so simple. I need two different metrics. I want to query them in the Admin and not at the model level. I don't want to make my models any more complicated than they are now.
Any advice is welcome! Thank you!

Categories

Resources