django annotate with dynamic column name - django

I have a model in django app, with the following structure:
class items(models.Model):
name = models.CharField(max_length=50)
location = models.CharField(max_length=3)
I wanted to create a pivot table for the count of each location per each name/item, which I managed to do as per the following:
queryset_res = items.objects.values('name')\
.annotate(NYC=Sum(Case(When(location='NYC', then=1),default=Value('0'),output_field=IntegerField())))\
.annotate(LND=Sum(Case(When(location='LND', then=1),default=Value('0'),output_field=IntegerField())))\
.annotate(ASM=Sum(Case(When(location='ASM', then=1),default=Value('0'),output_field=IntegerField())))\
.annotate(Total=Count('location'))\
.values('name', 'NYC', 'LSA','Total')\
.order_by('-Total')
This gives me how many times each name appears against each location which is all ok.
my question is how can I make the location dynamic, and so if new locations where added I don't have come back and change the code again! either from a list or from the model data itself
Many Thanks
AB

You can bind dynamic parameter with *[1, 2, 3], **{'key': 'value'} in python.
from django.db.models import Case, Count, Sum, IntegerField, Value, When
def get_annotation(key):
return {
key: Sum(
Case(
When(location=key, then=Value(1)),
default=Value(0),
output_field=IntegerField(),
),
),
}
queryset_res = items.objects.values('name')
location_list = ['NYC', 'LSA', 'ASM', ...etc]
for key in location_list:
queryset_res = queryset_res.annotate(**get_annotation(key))
queryset_res = (
queryset_res.annotate(Total=Count("location"))
.values("name", "Total", *location_list)
.order_by("-Total")
)
Now you can implement a set of queries simply by changing location_list.

Related

Django - How and Where to sort self-referencing model by function

I have a self-referencing model "Location" like this :
class Location(BaseArticle):
name = models.CharField(max_length=200)
parent_location = models.ForeignKey("self",
blank=True,
null=True,
help_text="Fill in if this location is a smaller part of another location.",
on_delete=models.SET_NULL)
description = HTMLField(blank=True,
null=True,
help_text="A description of the location and important features about it")
def __str__(self):
parent_location_string = ""
parent_location = self.parent_location
while parent_location is not None:
parent_location_string = f'{parent_location.name} - {parent_location_string}'
parent_location = parent_location.parent_location
return f'{parent_location_string}{self.name}'
Now when I use Locations with generic Update/Create Views I get a drop-down menu of all locations, since they're ForeignKeys and that's just how generic views and forms handle ForeignKeys.
I would like to have the list of locations in the dropdown alphabetically sorted, but not by the name-field of a location, but by it's __str__() method, e.g. through sorted(location_list, key=lambda x:str(x)).
Time is generally not an issue, I am working with small datasets (<500 entries) that are unlikely to grow beyond 500.
What I tried so far:
Since I'm trying to sort with a function, I can't sort in the model through the Meta classes "ordering" option
Sorting in the Manager by overwriting get_queryset() and using sorted() loses me all the abilities of queryset objects, since sorted() returns a list. I use these functionalities (e.g. exclude/filter) fairly often, so I would like to keep them to avoid a lot of rewriting.
Sorting in the View/Form by overwriting something in the generic UpdateView and CreateView or overwriting something from their used FormClasses (I am not that familiar with manipulating forms) before the template is rendered seems like the best option. The big question here is what needs to be overwritten, as I don't think it's "get_context_data".
Where should I perform this sorting ideally?
If it is in an Update/CreateView or a Form, what do I need to overwrite to do so?
you can build a recursion function to generate expression based on the below codes.
Location.objects.annotate(
order_name=Case(
When(
~Q(parent_location__isnull=True),
then=Case(
When(
~Q(parent_location__parent_location__isnull=True),
then=Concat('parent_location__parent_location__name', Value('-'), 'parent_location__name')
),
default = Concat('parent_location__name', Value('-'), 'name'),
)
),
default = F('name'),
)
)
or try a shot with the recursion function, Noqa
def create_expression(self, fk_field_name, lookup, lookup_field_name, previous_lookup_list=[], depth=1, max_depth=3):
slug = '__'
query_lookup = slug.join([fk_field_name] * depth + [lookup])
next_lookup_list = [slug.join([fk_field_name] * depth + [lookup_field_name]), Value('-')]
if not previous_lookup_list:
previous_lookup_list = [lookup_field_name]
next_lookup_list.extend(previous_lookup_list)
if depth == 1:
default_exp = F(lookup_field_name)
else:
default_exp = Concat(*previous_lookup_list)
if depth >= max_depth:
return (
Case(
When(
~Q(**{query_lookup: True}),
then = Concat(*next_lookup_list)
),
default = default_exp
)
)
return (
Case(
When(
~Q(**{query_lookup: True}),
then= self.create_expression(fk_field_name, lookup, lookup_field_name, next_lookup_list, depth+1)
),
default = default_exp
)
)
case_expression = create_expression('parent_location', 'isnull', 'name')
qset = Location.objects.annotate(order_name = case_expression).order_by('order_name')
or try this:
objs = Location.objects.filter(parent_location__isnull = True).order_by('name')
def get_sub_objs(objs, level=1):
for obj in objs.all():
yield (obj, level)
sub_objs = obj.location_set.all()
if sub_objs:
yield get_sub_objs(sub_objs, level+1)
for obj, level in get_sub_objs(objs):
prefix = '-' * level
print(prefix, obj.name)

Django attribute of most recent reverse relation

I have two models:
class Test(models.Model):
test_id = models.CharField(max_length=20, unique=True, db_index=True)
class TestResult(models.Model):
test = models.ForeignKey("Test", to_field="test_id", on_delete=models.CASCADE)
status = models.CharField(max_length=30, choices=status_choices)
with status_choices as an enumeration of tuples of strings.
Some Test objects may have zero related TestResult objects, but most have at least one.
I want to filter Test objects based on their most recent TestResult status.
I have tried this:
queryset = Test.objects.all()
queryset = queryset.annotate(most_recent_result_pk=Max("testresult__pk"))
queryset = queryset.annotate(current_status=Subquery(TestResult.objects.filter(pk=OuterRef("most_recent_result")).values("status")[:1]))
But I get the error:
column "u0.status" must appear in the GROUP BY clause or be used in an
aggregate function LINE 1: ...lts_testresult"."id") AS
"most_recent_result_pk", (SELECT U0."status...
I can find the most recent TestResult object fine with the first annotation of the pk, but the second annotation breaks everything. It seems like it ought to be easy to find an attribute of the TestResult object, once its pk is known. How can I do this?
You can do this with one subquery, without annotating this first:
from django.db.models import OuterRef, Subquery
queryset = Test.objects.annotate(
current_status=Subquery(
TestResult.objects.filter(
test=OuterRef('pk')
).order_by('-pk').values('status')[:1])
)
This will generate a query that looks like:
SELECT test.*,
(SELECT U0.status
FROM testresult U0
WHERE U0.test_id = test.id
ORDER BY U0.id DESC
LIMIT 1
) AS current_status
FROM test
or without subquery:
from django.db.models import F, Max
queryset = Test.objects.annotate(
max_testresult=Max('testresult__test__testresult__pk')
).filter(
testresult__pk=F('max_testresult')
).annotate(
current_status=F('testresult__status')
)
That being said, ordering by primary key is not a good idea to retrieve the latest object. You can see primary keys as "blackboxes" that simply hold a value to refer to it.
It is often better to use a column that stores the timestamp:
class TestResult(models.Model):
test = models.ForeignKey("Test", to_field="test_id", on_delete=models.CASCADE)
status = models.CharField(max_length=30, choices=status_choices)
created = models.DateTimeField(auto_now_add=True)
and then query with:
from django.db.models import OuterRef, Subquery
queryset = Test.objects.annotate(
current_status=Subquery(
TestResult.objects.filter(
test=OuterRef('pk')
).order_by('-created').values('status')[:1])
)

Django annotate by sum of two values with multiple relations

I have 4 models:
class App(models.Model):
...
class AppVersion(models.Model):
app = models.ForeignKey(App)
version_code = models.IntegerField()
class Meta:
ordering = ('-version_code',)
...
class Apk(models.Model):
version = models.OneToOneField(AppVersion)
size = models.IntegerField()
class Obb(models.Model):
version = models.ForeignKey(AppVersion)
size = models.IntegerField()
AppVersion version always has one Apk, but may have 0, 1 or 2 Obb's
I want to annotate QuerySet by total size of the App (which is Apk.size + sum of all Obb.size for given AppVersion).
My App QuerySet looks like this:
qs = App.objects.filter(is_visible=True)
and versions subquery is:
latest_versions = Subquery(AppVersion.objects.filter(application=OuterRef(OuterRef('pk'))).values('pk')[:1])
This subquery always gives the latest AppVersion of the App.
So what subquery should I use to annotate qs with size attribute calculated as shown above?
How about something like this - from my understanding you want an Apps apk, and obb sizes summed. apk and obb_set can be replaced by the fields related name if you added one. What I chose should be the defaults for a django OneToOne and Fk related name.
from django.db.models import F, Value, Sum, IntegerField
qs = App.objects.filter(
is_visible=True
).annotate(
apk_size=Sum('apk__size'),
obb_size=Sum('obb_set__size')
).annotate(
total_size=Value(
F('apk_size') + F('obb_size'),
output_field=IntegerField()
)

Django Models with Choices as Integer

I am trying to make a feedback app in Django, but I can not make my evaluations.
In my models.py, I have 5 choices from very bad to excellent. But I want them to be usable as numbers so I can evaluate the overall value.
After reading Set Django IntegerField by choices=... name I changed my Ratings from VERYBAD = 'Very bad' to VERYBAD = 1 but no I can't even save my value/form.
Feedback(models.Model):
event = models.ForeignKey(Event)
# Choice
VERYBAD = 'Very bad' #old
BAD = 2 #new
OKAY = 3
GOOD = 4
EXCELLENT = 5
RATING = (
(VERYBAD, 'Very bad'),
(BAD, 'Bad'),
(OKAY, 'Okay'),
(GOOD, 'Good'),
(EXCELLENT, 'Excellent'),
)
# The ratings
organisation = models.CharField(
max_length=9,
choices=RATING,
default=OKAY,
)
....
So in my view, I thought I can do the math but I can not.
def rating(request, event_id):
myevent = Event.objects.get(id=event_id)
feedback_items = Feedback.objects.filter(event=myevent)
num_of_items = len(feedback_items)
def evaluate(feedback_items):
# The overall rating
organisation = 0
for item in feedback_items:
organisation += item.organisation
organisation /= num_of_items
context = {'feedback_items':feedback_items,
'num_of_items': num_of_items,
'myevent': myevent,}
return render(request, 'feedback/rating.html', context)
`
You have to make organisation an IntegerField, obviously - else you can't hope to do integer operations. If you already have this in production, use a set of migrations to add a new IntegerField, populate it from the existing CharField values (using a dict for mapping old string values to new int values), and finally getting rid of the original field.
Also, you may want to learn how to properly use Django's ORM features like getting related models instances and performing aggregations at the DB level, ie:
from django.db.models import Sum, Count, Avg
def rating(request, event_id):
myevent = Event.objects.get(id=event_id)
# this is how you get related models...
feedback_items = myevent.feedback_set.all()
# and this is how you use SQL aggregation functions:
values = feedback_items.aggregate(
sum=Sum('organisation'),
count=Count('pk'),
avg=Avg('organisation')
)
# Now 'values' should be a dict with 'sum', 'count' and 'avg'
# keys, and values['avg'] should be equal to
# float(values['sum']) / values['count']
# FWIW you probably don't need the 'sum' field at all
# I mentionned it so you can check whether 'avg' value
# is correct...
context = {
'feedback_items':feedback_items,
'num_of_items': values['count'],
'avg': values['avg'],
'myevent': myevent,
}
# etc

Annotate django query if filtered row exists in second table

I have two tables (similar to the ones below):
class Piece(models.Model):
cost = models.IntegerField(default=50)
piece = models.CharField(max_length=256)
class User_Piece (models.Model):
user = models.ForeignKey(User)
piece = models.ForeignKey(Piece)
I want to do a query that returns all items in Piece, but annotates each row with whether or not the logged in user owns that piece (so there exists a row in User_Piece where user is the logged in user).
I tried:
pieces = Piece.objects.annotate(owned=Count('user_piece__id'))
But it puts a count > 0 for any piece that is owned by any user. I'm not sure where/how I put in the condition that the user_piece must have the specified user I want. If I filter on user__piece__user=user, then I don't get all the rows from Piece, only those that are owned.
You could use Exist subquery wrapper:
from django.db.models import Exists, OuterRef
subquery = User_Piece.objects.filter(user=user, piece=OuterRef('pk'))
Piece.objects.annotate(owned=Exists(subquery))
https://docs.djangoproject.com/en/dev/ref/models/expressions/#exists-subqueries
In newer versions of Django, you can do:
from django.db.models import Exists, OuterRef
pieces = Piece.objects.annotate(
owned=Exists(UserPiece.objects.filter(piece=OuterRef('id'), user=request.user))
)
for piece in pieces:
print(piece.owned) # prints True or False
Of course, you can replace the name owned with any name you want.
Easy approach, be careful with performance:
pk_pices = ( User_Piece
.objects
.filter(user=user)
.distinct()
.values_list( 'id', flat=True)
)
pieces = pieces.objects.filter( id__in = pk_pieces )
Also, notice that you have a n:m relation ship, you can rewrite models as:
class Piece(models.Model):
cost = models.IntegerField(default=50)
piece = models.CharField(max_length=256)
users = models.ManyToManyField(User, through='User_Piece', #<- HERE!
related_name='Pieces') #<- HERE!
And get user pieces as:
pieces = currentLoggedUser.pieces.all()