Aggregation based on field in Django - django

I have a model that has a type and value, based on the type I want to change the sign of the number to negative (Without changing it in the database)
class Foo(models.Model):
EARNING_CATEGORY = 'E'
DEDUCTION_CATEGORY = 'D'
CATEGORY_CHOICES = (
(EARNING_CATEGORY, 'Earning'),
(DEDUCTION_CATEGORY, 'Deduction'),
)
name = models.CharField(max_length=50)
category = models.CharField(max_length=1, choices=CATEGORY_CHOICES)
value = models.FloatField()
Now in order to aggregate over Foo I need to consider that values with category = D should be negative so I get the real sum.
What I tried so far is to change the sign during the save() method, but I don't want to show it to the user as negative in the application. So the only way I came up with is to calculate in a method using a for loop.
class Account(models.Model):
# feilds
def sum(self):
items = self.foo_set.all()
sum = 0
for i in items:
if i.category == Foo.DEDUCTION_CATEGORY:
sum -= i.value
else:
sum += i.value
return sum

You can annotate the current sign during your query like this:
from django.db.models import Case, Value as V, F, FloatField, When
items = self.foo_set.annotate(fixed_value=Case(
When(category=Foo.DEDUCTION_CATEGORY, then=V('-1')*F('value')),
default=F('value'),
output_field=FloatField())
).all()
So during annotation we do a condition check and if our category is equal to 'D', we change sign of value field, and then do your aggregation on fixed_value field.
And here's more info:
https://docs.djangoproject.com/en/1.9/ref/models/conditional-expressions/#case

Related

How to filter by range OR "null" value? (I.e. combine NumberFilter range and BooleanFilter for null=True IntegerField)

I have a Item model with a numeric number field. This number field defaults to null.
# models.py
class Item(models.Model):
number = models.IntegerField(default=None, blank=True, null=True)
I want to set-up filters that can return a queryset of Items where number is in range - which is straightforward enough:
# filters.py
class ItemFilter(django_filters.FilterSet):
min_num = django_filters.NumberFilter(method="min_num_filter")
max_num = django_filters.NumberFilter(method="max_num_filter")
class Meta:
model = Item
fields = ("min_num", "max_num", "incl_null")
def min_num_filter(self, queryset, name, value):
return queryset.filter(number__gte=value)
def max_num_filter(self, queryset, name, value):
return queryset.filter(number__lte=value)
But what if I want to have an additional Boolean filter that can include Items that has null for number along with whatever Items matches the min_num and max_num range?
So for example, a URL query in the form of ?min_num=1&max_num=10&incl_null=True should return all Items where number is between 1 and 10 OR number is equal to None.
The following code does not work:
class ItemFilter(django_filters.FilterSet):
...
incl_null = django_filters.BooleanFilter(method="incl_null_filter")
class Meta:
model = Item
fields = ("min_num", "max_num", "incl_null")
// doesn't work
class incl_null_filter(self, queryset, name, value):
if value is True:
return queryset | Item.objects.filter(number=None)
if value is False:
return queryset
Edit: I've tried the methods in the "Filtering by empty values" documentation but I think that's for null values exclusively - where I'm looking for a range match OR a null value.
Try this query:
from django.db.models import Q
min_ = 0
max_ = 10
Item.objects.filter(Q(number__gte=min_, number__lte=max_) | Q(number__isnull=True))
Well, the only solution I can think of is to pass the min range, max range, and is_null boolean into a single char field then convert it into the 3 individual filters for actioning.
So the query URL will look like ?master_num=1-10-1 for range 1 - 10 incl. None and ?master_num=1-10-0 for range 1 - 10 excl. None.
class ItemFilter(django_filters.FilterSet):
master_num = django_filters.CharFilter(method="master_num_filter")
class Meta:
model = Item
fields = ("master_num")
def master_num_filter(self, queryset, name, value):
# array = [min, max, 1 or 0 for True and False]
array = value.split("-")
min = Q(year_published__gte=int(array[0]))
max = Q(year_published__lte=int(array[1]))
if array[2] == "1":
incl_null = Q(year_published=None)
return queryset.filter((min & max) | incl_null)
else:
return queryset.filter(min & max)
Would like to know if there's a better way to do this.

Django getting percentage filtering the object

So I have this two models:
class Freguesia(models.Model):
nome = models.CharField("Freguesia",max_length=255)
class Intervencao(models.Model):
freguesia = models.ForeignKey(Freguesia, on_delete=models.CASCADE, verbose_name="Freguesia")
.........
And I want to display in html the top 6 freguesias with more intervecao and the percentage they have in total
I already can display the freguesias and the number of intervencoes they have , but I don't know how to display the percentage.
My View:
def DashboardView(request):
freguesias = Freguesia.objects.annotate(num_intervencao=Count('intervencao')).order_by('-num_intervencao')[:6]
context = {
'freguesias':freguesias
}
You can do it using Cast and F
from django.db.models Count, F
from django.db.models.functions import Cast
freguesias = Freguesia.objects.annotate(
num_intervencao=Count('intervencao'),
total=Count('id')
).annotate(
percentage=Cast(F('num_intervencao') * 100.0 / F('total'), FloatField())
).order_by('-num_intervencao')[:6]
Important to calculate percentage in separate annotate not in one where dependant fields are calculated.

Aggregation on 'one to many' for matrix view in Django

I have two tables like below.
These are in 'one(History.testinfoid) to many(Result.testinfoid)' relationship.
(Result table is external database)
class History(models.Model): # default database
idx = models.AutoField(primary_key=True)
scenario_id = models.ForeignKey(Scenario)
executor = models.CharField(max_length=255)
createdate = models.DateTimeField()
testinfoid = models.IntegerField(unique=True)
class Result(models.Model): # external (Result.objects.using('external'))
idx = models.AutoField(primary_key=True)
testinfoid = models.ForeignKey(History, to_field='testinfoid', related_name='result')
testresult = models.CharField(max_length=10)
class Meta:
unique_together = (('idx', 'testinfoid'),)
So, I want to express the Count by 'testresult' field in Result table.
It has some condition such as 'Pass' or 'Fail'.
I want to express a count query set for each condition. like this.
[{'idx': 1, 'pass_count': 10, 'fail_count': 5, 'executor': 'someone', ...} ...
...
{'idx': 10, 'pass_count': 1, 'fail_count': 10, 'executor': 'someone', ...}]
Is it possible?
It is a two level aggregation where the second level should be displayed as table columns - "matrix view".
A) Solution with Python loop to create columns with annotations by the second level ("testresult").
from django.db.models import Count
from collections import OrderedDict
qs = (History.objects
.values('pk', 'executor', 'testinfoid',... 'result__testresult')
.annotate(result_count=Count('pk'))
)
qs = qs.filter(...).order_by(...)
data = OrderedDict()
count_columns = ('pass_count', 'fail_count', 'error_count',
'expected_failure_count', 'unexpected_success_count')
for row in qs:
data.setdefault(row.pk, dict.fromkeys(count_columns, 0)).update(
{(k if k != result_count else row['result__testresult'] + '_count'): v
for k, v in row_items()
if k != 'result__testresult'
}
)
out = list(data.values())
The class OrderedDict is used to preserve order_by().
B) Solution with Subquery in Django 1.11+ (if the result should be a queryset. e.g. to be sorted or filtered finally in an Admin view by clicking, and if a more complicated query is acceptable and number of columns *_count is very low.). I can write a solution with subquery, but I'm not sure if the query will be fast enough with different database backends. Maybe someone other answers.

Building up subqueries of derived django fields

I have a few transformations I need to perform on my table before I aggregate.
I need to multiply transaction_type (which is either 1 or -1) by amount to yield a signed_amount. Then I need to sum all signed_amounts by primary_category (which is a foreign key to secondary category which is a foreign key of my table).
DEBIT = -1
CREDIT = 1
TRANSACTION_TYPE_CHOICES = (
(DEBIT, 'debit'),
(CREDIT, 'credit'),
)
class Transaction(models.Model):
amount = models.DecimalField(max_digits=7, decimal_places=2)
transaction_type = models.IntegerField(choices=TRANSACTION_TYPE_CHOICES)
secondary_category = models.ForeignKey(Secondary_Category)
class Primary_Category(models.Model):
name = models.CharField("Category Name", max_length=30)
category = models.ForeignKey(Primary_Category_Bucket)
class Secondary_Category(models.Model):
name = models.CharField("Category Name", max_length=30)
primary_category = models.ForeignKey(Primary_Category)
I'm stuck on the first bit though.
from django.db.models import Sum, Count, F
original_transactions = Transaction.objects.all()
original_transactions.signed_amount = F('transaction_type') * F('amount')
for transaction in original_transactions:
print transaction.signed_amount
When I try to sanity check that signed_amount is being calculated, I get an error that 'Transaction' object has no attribute 'signed_amount'. I don't want to save signed_amount to the database. I just want to generate it as derived field so I can calculate my totals.
How do I calculate this derived field and subsequently aggregate by primary_category.name?
User python decorator property on a method for class Transaction:
class Transaction(models.Model):
amount = models.DecimalField(max_digits=7, decimal_places=2)
transaction_type = models.IntegerField(choices=TRANSACTION_TYPE_CHOICES)
secondary_category = models.ForeignKey(Secondary_Category)
#property
def signed_amount(self):
return self.amount * self.transaction_type
Then for each Transaction object you can do transaction.signed_amount.
I'm not sure if the aggregation part could be done using queries, but if you don't have that many PrimaryCategory, then python would be good enough to achieve it.
Or you can do this.
all_transactions = Transaction.objects.all().order_by('secondary_category__primary_category_id')
sum = 0
if all_transactions:
primary_category_id = all_transactions[0].secondary_category.primary_category_id
for transaction in all_transactions:
if primary_category_id == transaction.secondary_category.primary_category_id:
sum += (transaction.amount * transaction_type)
else:
sum = (transaction.amount * transaction_type)
print sum

Using Django Forms: How can i use the chosen value of a forms.ModelChoiceField within a userdef validation

I got the following model:
class currency(models.Model):
currency = models.CharField(max_length=50)
dollaramount = models.DecimalField(max_digits=10, decimal_places=2)
def __unicode__(self):
return self.currency
This Model contains 2 entrys with currency-value "$" and "€"
The currency-model is related with a ModelChoiceField within the following form, which has a userdefined validation "clean_amount" for checking the amount, depending which currency is actual choosen in the currencyfield from the from itself.
How can i do the compare inside the userdef-validation "clean_amount", if the value from the selected object is "$" or "€"?
from django import forms
from currencies.models import currency
class createform(forms.Form):
currency = forms.ModelChoiceField(queryset = currency.objects.all())
amount = forms.DecimalField(initial = 0)
def clean_amount(self):
currencyfield = self.cleaned_data['currency']
amount = self.cleaned_data['amount']
if amount < 0:
raise forms.ValidationError("Amount must be at least 0.")
#HERE I WANT TO CHECK
choosencurrency = currencyfield.??????
if choosencurrency == "$" :
#check $amount
if choosencurrency == "€" :
#check €amount
return amount
You should do this in clean(), not clean_amount(). From clean you can access self.cleaned_data['amount'] and self.cleaned_data['currency'].