How to build a queryset in a specific order in Django - python

I'm trying to list profiles of users. I want to list them in such a way that profiles with same city of the user should come first, then next priority should be state, then country, and finally, rest of the profiles. This is what I have tried.
model
class Profile(models.Model):
uuid = UUIDField(auto=True)
user = models.OneToOneField(User)
country = models.ForeignKey(Country, null=True)
state = models.ForeignKey(State, null=True)
city = models.ForeignKey(City, null=True)
views.py
current_user = Profile.objects.filter(user=request.user)
profiles_city = Profile.objects.filter(city=current_user.city)
profiles_state = Profile.objects.filter(state=current_user.state)
profiles_country = Profile.objects.filter(country=current_user.country)
profiles_all = Profile.objects.all()
profiles = (profiles_city | profiles_state | profiles_country | profiles_all).distinct()
But it is yielding the same result as Profile.objects.all()
Please help me. thanks in advance

You need the order_by method of QuerySet that orders objects based on passed parameters; this is done on database:
Profile.objects.order_by(
'current_user__city',
'current_user__state',
'current_user__country',
)
Edit:
If you want to sort by the city, state and country names of the logged in user, you can do this on the Python level, using sorted, and a custom key callable:
from functools import partial
def get_sort_order(profile, logged_in_profile):
# This is a simple example, you need to filter against
# the city-state-country combo to match precisely. For
# example, multiple countries can have the same city/
# state name.
if logged_in_profile.city == profile.city:
return 1
if logged_in_profile.state == profile.state:
return 2
if logged_in_profile.country == profile.country:
return 3
return 4
logged_in_profile = request.user.profile # logged-in user's profile
get_sort_order_partial = partial(get_sort_order, logged_in_profile=logged_in_profile)
sorted(
Profile.objects.all(),
key=get_sort_order_partial,
)
Doing the same on the database level, using Case and When to have a Python if-elif-else like construct:
from django.db.models import Case, When, IntegerField
Profile.objects.order_by(
Case(
When(city=logged_in_profile.city, then=1),
When(state=logged_in_profile.state, then=2),
When(country=logged_in_profile.country, then=3),
default=4,
output_field=IntegerField(),
)
)
This will result in a queryset and also has the added advantage of being faster as all the operations would be done on the database (SELECT CASE WHEN ...).

Related

How to use django-filter package for foreign key fields in Django?

Hi all!
New in Django, and confused, help is appreciated! I've created a table, , thanks to a stackoverflow users, like:
Organization
Total amount of appeals
Amount of written form appeals
Amount of oral form appeals
Organization 1
3
1
2
Organization 2
2
1
1
Have three models:
class Organization(models.Model):
organization_name = models.CharField(max_length=50)
class AppealForm(models.Model):
form_name = models.CharField(max_length=50)
class Appeal(models.Model):
organization = models.ForeignKey(Organization, on_delete=models.CASCADE)
appeal_form = models.ForeignKey(AppealForm, on_delete=models.CASCADE)
applicant_name = models.CharField(max_length=150)
appeal_date = models.DateField()
Objects of Organization model:
organization_name
Organization 1
Organization 2
Objects of AppealForm model:
form_name
In written form
In oral form
Objects of Appeal model:
organization
appeal_form
applicant_name
Organization 1
In written form
First and Last name
Organization 1
In oral form
First and Last name
Organization 1
In oral form
First and Last name
Organization 2
In written form
First and Last name
Organization 2
In oral form
First and Last name
In the function of views.py file, I've created a query to render, like:
from django.db.models import Count, Q
organizations = Organization.objects.annotate(
).annotate(
total=Count('appeal'),
total_written=Count('appeal', filter=Q(appeal__appeal_form__form_name='in written form')),
total_oral=Count('appeal', filter=Q('appeal__appeal_form__form_name='in oral form'))
)
And now I want to filter table contents by AppealForm model and date of appeals (appeal_date field of Appeal model). Case: User opens a table and from search bar above the table chooses which appeal form and/or the range of dates to see.
Question: How to filter the query which is above in views.py using django-filter package?
The most general way to define a complicated filter is to use the method argument. I can't say I completely understand your problem, but you can apply any filter for which you can dream up a queryset in this way. In outline:
import django-filters as DF
class SomeFilters( DF.FilterSet):
name = DF.xxxFilter( method='my_method', field_name='object_field', label='whatever', ...)
...
def my_method( self, qs, name, value):
# in here you create a new more restrictive queryset based on qs
# to implement your filter, and return it.
# name is the field name. Note, you don't have to use or follow it
# value is the value that the user typed
qs = qs.filter( ...) # or .exclude, or complicated stuff
return qs
Here's a fairly simple method that I wrote to create an annotation with the value of a field stripped of spaces, and then to do a text-contains filter on that.
def filter_array_desc( self, qs, name, value):
value = value.replace(' ','')
qs = qs.annotate(
arr_d_nospaces = Replace( 'array_desc', Value(' '), Value('')) # delete all spaces
).filter(
arr_d_nospaces__icontains = value )
return qs
Here's a general one that can be applied to any field through a ChoiceFilter to filter whether the field is blank or not:
YESNO = (('Y','Yes'), ('N','No'))
marr_b = FD.ChoiceFilter( field_name='marr', label='M_array is blank', method='filter_blank_yesno',
choices=YESNO, empty_label="Don't Care" )
...
def filter_blank_yesno( self, qs, name, value):
if value=="Y":
return qs.filter(**{ name:'' })
elif value=="N":
return qs.exclude( **{ name:'' })
raise ValueError(f'filter_blank_yesno received value="{value}" which is neither "Y" nor "N"')
Hope this helps. You will basically be filtering by following relationships between your models, using double-underscores to move between models, and possibly annotating and filtering on the anotation, or doing things with Q objects and suchlike.

Alternative way of querying through a models' method field

I have this model about Invoices which has a property method which refers to another model in order to get the cancelation date of the invoice, like so:
class Invoice(models.Model):
# (...)
#property
def cancel_date(self):
if self.canceled:
return self.records.filter(change_type = 'cancel').first().date
else:
return None
And in one of my views, i need to query every invoice that has been canceled after max_date or hasn't been canceled at all.
Like so:
def ExampleView(request):
# (...)
qs = Invoice.objects
if r.get('maxDate'):
max_date = datetime.strptime(r.get('maxDate'), r'%Y-%m-%d')
ids = list(map(lambda i: i.pk, filter(lambda i: (i.cancel_date == None) or (i.cancel_date > max_date), qs)))
qs = qs.filter(pk__in = ids) #Error -> django.db.utils.OperationalError: too many SQL variables
However, ids might give me a huge list of ids which causes the error too many SQL variables.
What's the smartest approach here?
EDIT:
I'm looking for a solution that does not involve adding cancel_date as a model field since invoice.records refers to another model where we store every date attribute of the invoice
Like so:
class InvoiceRecord(models.Model):
invoice = models.ForeignKey(Invoice, related_name = 'records', on_delete = models.CASCADE)
date = models.DateTimeField(default = timezone.now)
change_type = models.CharField(max_length = 32) # Multiple choices field
And every invoice might have more than one same date attribute. For example, one invoice might have two cancelation dates
You can annotate a Subquery() expression [Django docs] which will give you the date to do this:
from django.db.models import OuterRef, Q, Subquery
def ExampleView(request):
# (...)
qs = Invoice.objects.annotate(
cancel_date=Subquery(
InvoiceRecords.objects.filter(invoice=OuterRef("pk")).values('date')[:1]
)
)
if r.get('maxDate'):
max_date = datetime.strptime(r.get('maxDate'), r'%Y-%m-%d')
qs = qs.filter(Q(cancel_date__isnull=True) | Q(cancel_date__gt=max_date))
I would set cancel_date as database field when you set cancel flag. Then you can use single query:
qs = Invoice.objects.filter(Q(cancel_date__isnull=True) | Q(cancel_date__gt=max_date))
It's say cancel_date is NULL or greater than max_date
Not sure about your property cancel_date. It will return first record with change_type='cancel' which can be (don't know your code flow) other record then you call that property on.

How to add a property to a model which will be calculated based on input?

class TransactionHistory(models.Model):
from_account = models.ForeignKey(
'Account',
on_delete=models.CASCADE,
related_name='from_account'
)
to_account = models.ForeignKey(
'Account',
on_delete=models.CASCADE,
related_name='to_account'
)
amount = models.DecimalField(
max_digits=12,
decimal_places=2
)
created_at = models.DateTimeField(default=timezone.now)
#property
def way(self):
# Here I need to access a list of user's accounts
# (request.user.accounts) to mark the transaction
# as going out or in.
return
def get_own_transaction_history(me_user):
my_accounts = me_user.accounts.all()
# TODO: mark transactions with way as in and out
own_transactions = TransactionHistory.objects.filter(
Q(from_account__in=my_accounts) |
Q(to_account__in=my_accounts)
)
return own_transactions
I want to add a "way" property for the model so when I return the queryset via serializer, the user could understand if the transaction is for going out from his account or in. But if I just add property, it can not be calculated with me_user user in mind, AFAIK the property can only access the local model fields like "from_account" or "to_account".
Something like the following should work as an annotation using conditional expressions, using __in in the When expressions may give a bit of trouble though. The objects returned by this queryset will have an attribute way added to them by the annotation
from django.db.models import Case, CharField, Value, When
def get_own_transaction_history(me_user):
my_accounts = me_user.accounts.all()
return TransactionHistory.objects.filter(
Q(from_account__in=my_accounts) |
Q(to_account__in=my_accounts)
).annotate(
way=Case(
When(from_account__in=my_accounts, then=Value('out')),
When(to_account__in=my_accounts, then=Value('in')),
output_field=CharField(),
)
)

Django: Filter a Queryset made of unions not working

I defined 3 models related with M2M relationsships
class Suite(models.Model):
name = models.CharField(max_length=250)
title = models.CharField(max_length=250)
icon = models.CharField(max_length=250)
def __str__(self):
return self.title
class Role(models.Model):
name = models.CharField(max_length=250)
title = models.CharField(max_length=250)
suites = models.ManyToManyField(Suite)
services = models.ManyToManyField(Service)
Actions = models.ManyToManyField(Action)
users = models.ManyToManyField(User)
def __str__(self):
return self.title
In one of my views I tried to collect all the Suites related to an specific User. The user may be related to several Roles that can contain many Suites. And then filter Suites by name. But the filter seem to have no effects
queryset = Suite.objects.union(*(role.suites.all() for role in
self.get_user().role_set.all()))
repr(self.queryset)
'<QuerySet [<Suite: energia>, <Suite: waste 4 thing>]>'
self.queryset = self.queryset.filter(name="energia")
repr(self.queryset)
'<QuerySet [<Suite: energia>, <Suite: waste 4 thing>]>'
The query atribute inside the queryset not alter its content before executin the filter:
(SELECT "navbar_suite"."id", "navbar_suite"."name", "navbar_suite"."title", "navbar_suite"."icon" FROM "navbar_suite") UNION (SELECT "navbar_suite"."id", "navbar_suite"."name", "navbar_suite"."title", "navbar_suite"."icon" FROM "navbar_suite" INNER JOIN "navbar_role_suites" ON ("navbar_suite"."id" = "navbar_role_suites"."suite_id") WHERE "navbar_role_suites"."role_id" = 1)
(SELECT "navbar_suite"."id", "navbar_suite"."name", "navbar_suite"."title", "navbar_suite"."icon" FROM "navbar_suite") UNION (SELECT "navbar_suite"."id", "navbar_suite"."name", "navbar_suite"."title", "navbar_suite"."icon" FROM "navbar_suite" INNER JOIN "navbar_role_suites" ON ("navbar_suite"."id" = "navbar_role_suites"."suite_id") WHERE "navbar_role_suites"."role_id" = 1)
As stated in django docs, only count(), order_by(), values(), values_list() and slicing of union queryset is allowed. You can't filter on union queryset.
That means, you have to apply filters on queries before applying union on them.
Also, you can achieve your goal without even using union():
Suite.objects.filter(role_set__users=self.get_user(), name="energia")
You may need to adjust field name in filter if you've used related_name or related_query_name in definition of suites M2M field in Role model.
I had the same issue and ended up using the union query as a subquery so that the filters could work:
yourModelUnionSubQuerySet = YourModelQS1.union(YourModelQS2)
yourModelUnionQuerySet = YourModel.objects.filter(id__in=yourModelUnionSubQuerySet.values('id'))
There is a simple solution. Just use
self.queryset = self.queryset | <querySet you want to append>
instead of
self.queryset = self.queryset.union(<QuerySet you want to append>)
Worked for me. I hope this is understandable. After this you will be able to use filter.

django left join with where clause subexpression

I'm currently trying to find a way to do something with Django's (v1.10) ORM that I feel should be possible but I'm struggling to understand how to apply the documented methods to solve my problem.
Edit: So here's the sql that I've hacked together to return the data that I'd like from the dbshell, with a postgresql database now, after I realised that my original sqlite3 backed sql query was incorrect:
select
voting_bill.*,vv.vote
from
voting_bill
left join
(select
voting_votes.vote,voting_votes.bill_id
from
voting_bill
left join
voting_votes
on
voting_bill.id=voting_votes.bill_id
where
voting_votes.voter_id = (select id from auth_user where username='richard' or username is Null)
)
as
vv
on
voting_bill.id=vv.bill_id;
Here's the 'models.py' for my voting app:
from django.db import models
from django.contrib.auth.models import User
class Bill(models.Model):
name = models.CharField(max_length=255)
description = models.TextField()
result = models.BooleanField()
status = models.BooleanField(default=False)
def __str__(self):
return self.name
class Votes(models.Model):
vote = models.NullBooleanField()
bill = models.ForeignKey(Bill, related_name='bill',
on_delete=models.CASCADE,)
voter = models.ForeignKey(User, on_delete=models.CASCADE,)
def __str__(self):
return '{0} {1}'.format(self.bill, self.voter)
I can see that my sql works as I expect with the vote tacked onto the end, or a null if the user hasn't voted yet.
I was working to have the queryset in this format so that I can iterate over it in the template to produce a table and if the result is null I can instead provide a link which takes the user to another view.
I've read about select_related and prefetch_related, but as I said, I'm struggling to work out how I translate this to how I can do this in SQL.
Hope I correctly understood your problem. Try this:
votes = Votes.objects.filter(voter__username='django').select_related('bill')
You can use this. But I think you do not need select_related in this case.
bills_for_user = Bill.objects.filter(votes__voter__username='django').select_related('votes').distinct()
Now you can iterate your bills_for_user
for bill in bills_for_user:
bill_name = bill.name
bill_description = bill.description
bill_result = bill.result
bill_status = bill.status
# and there are several variants what you can with votes
bill_votes = bill.votes_set.all() # will return you all votes for this bill
bill_first_vote1 = bill.votes_set.first() # will return first element in this query or None if its empty
bill_first_vote2 = bill.votes_set.all()[0] # will return first element in this query or Error if its empty
bill_last_vote = bill.votes_set.last()[0] # will return last element in this query or None if its empty
# you can also filter it for example by voting
bill_positive_votes = bill.votes_set.filter(vote=True) # will return you all votes for this bill with 'vote' = True
bill_negative_votes = bill.votes_set.filter(vote=False) # will return you all votes for this bill with 'vote' = False
bill_neutral_votes = bill.votes_set.filter(vote=None) # will return you all votes for this bill with 'vote' = None

Categories