django orm multiple points ranking - django

I've a model name Points which store the user points on the base of it's actions.
class Points(CreateUpdateModelMixin):
class Action(models.TextChoices):
BASE = 'BASE', _('Base')
REVIEW = 'REVIEW', _('Review')
FOLLOW = 'FOLLOW', _('Follow')
VERIFIED_REVIEW = 'VERIFIED_REVIEW', _('Verified Review')
REFERRAL = 'REFERRAL', _('Referral')
ADD = 'ADD', _('Add')
SUBTRACT = 'SUBTRACT', _('Subtract')
user = models.ForeignKey(User, on_delete=models.CASCADE)
points = models.IntegerField()
action = models.CharField(max_length=64, choices=Action.choices, default=Action.BASE)
class Meta:
db_table = "diner_points"
Please note that there are multiple rows for the same user.
For the past few days I'm trying to write a query to get the total_points of the use and also the rank of that user.
Using:
Django 3.2
MySQL 5.7
I want to know input of you guys. Thanks.
I wrote this query and many other like it. But none of them give the results I want.
Let's suppose the data is something like this.
user
points
771
221
1083
160
1083
12
1083
10
771
-15
1083
4
1083
-10
124
0
23
1771
The current query I have written is this...
innerquery = (
DinerPoint.objects
.values("user")
.annotate(total=Sum("points"))
.distinct()
)
query = (
DinerPoint.objects
.annotate(
total = Subquery(
innerquery.filter(user=OuterRef("user")).values("total")
),
rank = Subquery(
DinerPoint.objects
.annotate(
total = Subquery(
innerquery.filter(user=OuterRef("user")).values("total")
),
rank=Func(F("user"), function="Count")
)
.filter(
Q(total__gt=OuterRef("total")) |
Q(total=OuterRef("total"), user__lt=OuterRef("user"))
)
.values("rank")[:1]
)
)
)
query.values('user', 'total', 'rank').distinct().order_by('rank')
But this give the results like this
<QuerySet [
{'user': 23, 'total': 1771, 'rank': 1},
{'user': 1083, 'total': 176, 'rank': 2},
{'user': 771, 'total': 106, 'rank': 8}, <---- Issue beacuse of dups entries
{'user': 124, 'total': 0, 'rank': 9}
]>
I've tried RANK, DENSE RANK and didn't got the results I wanted.
The only way I got the results I wanted I throught the Common Table Expression(CTE). But unfortunately I can't use that because of mysql version 5.7 in produciton.
P.S I'm using the count and greater than beacause of my use case. I have a use case where we have to get rank in the user friends.
The working code using CTE by django_cte (You can ignore this beacuse of mysql 5.7 ;) )
def get_queryset(user=None, following=False):
if not user:
user = User.objects.get(username="king")
innerquery = (
DinerPoint.objects
.values("user", "user__username", "user__first_name", "user__last_name", "user__profile_fixed_url",
"user__is_influencer", "user__is_verified", "user__instagram_handle")
.annotate(total=Sum("points"))
.distinct()
)
if following:
innerquery = innerquery.filter(Q(user__in=Subquery(user.friends.values('id'))) |
Q(user = user))
basequery = With(innerquery)
subquery = (
basequery.queryset()
.filter(Q(total__gt=OuterRef("total")) |
Q(total=OuterRef("total"), user__lt=OuterRef("user")))
.annotate(rank=Func(F("user"), function="Count"))
.values("rank")
.with_cte(basequery)
)
query = (
basequery.queryset()
.annotate(rank=Subquery(subquery) + 1)
.select_related("user")
.with_cte(basequery)
)
return query

I have done this using the Func expression field. The final query which works for me is attached below in case you are looking for an answer.
rank=Func(
F("user"),
function="Count",
template="%(function)s(DISTINCT %(expressions)s)",
),
Final query
def get_queryset(self):
following = self.request.query_params.get("following", False)
innerquery = (
DinerPoint.objects.values("user").annotate(total=Sum("points")).distinct()
)
basequery = DinerPoint.objects
if following:
innerquery = innerquery.filter(
Q(user__in=Subquery(self.request.user.friends.values("id")))
| Q(user=self.request.user)
)
basequery = basequery.filter(
Q(user__in=Subquery(self.request.user.friends.values("id")))
| Q(user=self.request.user)
)
query = (
basequery.annotate(
total=Subquery(
innerquery.filter(user=OuterRef("user")).values("total")
),
rank=Subquery(
DinerPoint.objects.annotate(
total=Subquery(
innerquery.filter(user=OuterRef("user")).values("total")
),
rank=Func(
F("user"),
function="Count",
template="%(function)s(DISTINCT %(expressions)s)",
),
)
.filter(
Q(total__gt=OuterRef("total"))
| Q(total=OuterRef("total"), user__lt=OuterRef("user"))
)
.values("rank")
)
+ 1,
)
.values(
"user",
"user__username",
"user__first_name",
"user__last_name",
"user__profile_fixed_url",
"user__is_influencer",
"user__is_verified",
"user__instagram_handle",
"total",
"rank",
)
.distinct()
)
return query

Related

Django query to get List of Sums of the Count of different Choices

I am trying to get a list of the sum of the count of different choices. The choices are strings, so I want to count the same strings and get their sum, and then do the same for other choices. I made a query but it does not give me the total count of each choice instead list them all with the count 1.
model.py
class sublist(models.Model):
Music = 'Music'
Video = 'Video'
Gaming = 'Gaming'
News = 'News'
Lifestyle = 'Lifestyle'
Access = 'Access'
SUBTYPE_CHOICES =(
(Music , "Music"),
(Video , "Video"),
(Gaming , "Gaming"),
(News , "News"),
(Lifestyle , "Lifestyle"),
(Access , "Online Access"),
)
author = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE,)
name = models.CharField(max_length=150)
cost = models.FloatField(default = 0)
subtype = models.CharField(max_length= 50, choices = SUBTYPE_CHOICES, default = Access)
This is my query. (i tried coming up with other combinations by looking at the documentation but no luck)
expenselist = sublist.objects.filter(author = curruser.id)
subtypecount = list((expenselist
.annotate(subcount=Count('subtype'))
.values('subtype', 'subcount')
))
result of query above: [{'subtype': 'Access', 'subcount': 1}, {'subtype': 'Access', 'subcount': 1},{'subtype': 'Video', 'subcount': 1}]
Desired result: [{'subtype': 'Access', 'subcount': 2},{'subtype': 'Video', 'subcount': 1}]
Here is the query that will produce your desired result.
First use .values() then .annotate()
subtypecount = sublist.objects.filter(author = curruser.id).values('subtype').annotate(subcount=Count('subtype'))
or
expenselist = sublist.objects.filter(author = curruser.id)
subtypecount = list(expenselist.values('subtype').annotate(subcount=Count('subtype')))

How to model a complex list in Flask

My API is returning lists with three entries like so (pseudocode only)
results=[]
for db_row in db_rows:
result = []
result.append (db_row[0]) # Name (string)
result.append (db_row[1]) # Age (int)
result.append (db_row[2]) # DOB (timestamp)
results.append (results)
I want to create a model for it to show up in swagger docs like so
_results_model = api.model (
'results model',{
'results': fields.List(
fields.String(
description = 'Name of user',
min_length = 6,
max_length = 64,
pattern = '.*',
help = "...",
example = 'john.smith'
),
fields.Integer(
description = 'Age of user',
minimum=0,
help = "...",
example = '25'
),
fields.DateTime(
description = 'Date of birth',
help = "...",
example = '1918-11-11'
)
)
}
)
But it appears it is not possible to pass more than one parameter to fields.List
'results': fields.List
TypeError: __init__() takes 2 positional arguments but 4 were given
Question: What other option do I have for creating a model to represent a complex list?

Use of Prefetch with an inner select_related in Django

I basically have to display a list of service providers and in each, I need to display the categories of service they offer.
So as an example:
Possible Service Type Categories:
[id: 1, name:'Programming']
[id: 2, name:'Design']
Possible Service Types:
[id: 1, name: 'PHP Service', service_type_category_id: 1]
[id: 2, name: 'JAVA Service', service_type_category_id: 1]
[id: 3, name: 'Web Design Service', service_type_category_id: 2]
Example of Display Results:
Company Blue offers 'Programming'
Company Test offers 'Programming' and 'Design'
Company Orange offers 'Design' ....
I'm trying to write the least number of queries:
I have these models:
class ServiceTypeCategory( BaseModel ):
# Model Attributes
name = models.CharField( _( "name" ), max_length = 40 )
class ServiceType( BaseModel ):
# Model Attributes
service_type_category = models.ForeignKey( 'ServiceTypeCategory', verbose_name = _( 'category' ) )
name = models.CharField( _( "name" ), max_length = 60 )
description = models.TextField( _( "description" ) )
class Provider( BaseModel ):
# Model Attributes
display_name = models.CharField( _( "name" ), max_length = 80 )
# Many to many relations
countries = models.ManyToManyField( 'core.Country' ) # countries this provider support
service_types = models.ManyToManyField( 'ServiceType', through = 'Provider_ServiceTypes', related_name = 'service_types' )
class Provider_ServiceTypes( BaseModel ):
# Model Attributes
service_type = models.ForeignKey( 'ServiceType', verbose_name = _( 'service type' ) )
provider = models.ForeignKey( 'Provider', verbose_name = _( 'provider' ) )
is_top = models.BooleanField( _( "is top service" ), default = False )
Then, to run the query, I have the following:
providers = Provider.objects.select_related(
'user',
).prefetch_related(
Prefetch(
'service_types__service_type_category',
queryset = ServiceTypeCategory.objects
.only( 'name' )
)
).filter(
countries = country_id,
).only(
'id', 'display_name', 'user'
).order_by(
'-user__last_login'
)
This works out well, but it runs the 3 following queries:
SELECT app_provider.id, app_provider.user_id, app_provider.display_name, core_user.id, core_user.password, core_user.last_login, core_user.is_superuser, core_user.created_date, core_user.modified_date, core_user.email, core_user.name, core_user.is_active, core_user.is_admin
FROM app_provider
INNER JOIN app_provider_countries ON ( app_provider.id = app_provider_countries.provider_id )
INNER JOIN core_user ON ( app_provider.user_id = core_user.id )
LEFT OUTER JOIN core_userpersonal ON ( core_user.id = core_userpersonal.user_id )
LEFT OUTER JOIN core_userstats ON ( core_user.id = core_userstats.user_id )
WHERE app_provider_countries.country_id = 204
ORDER BY core_userstats.total_reviews DESC, core_userstats.total_contracts DESC, core_userstats.total_answers DESC, core_user.last_login DESC LIMIT 5
SELECT (app_provider_servicetypes.provider_id) AS _prefetch_related_val_provider_id, app_servicetype.id, app_servicetype.created_date, app_servicetype.modified_date, app_servicetype.service_type_category_id, app_servicetype.name, app_servicetype.description
FROM app_servicetype
INNER JOIN app_provider_servicetypes ON ( app_servicetype.id = app_provider_servicetypes.service_type_id )
WHERE app_provider_servicetypes.provider_id IN (2)
SELECT app_servicetypecategory.id, app_servicetypecategory.name
FROM app_servicetypecategory
WHERE app_servicetypecategory.id IN (1, 2)
Question is: How can I make to run just 2 queries in total? (The last 2 queries should be joined with INNER JOIN and a group by per service_type_category_name)
Thanks in advance!
Try this:
providers = Provider.objects.select_related(
'user',
).prefetch_related(
Prefetch(
'service_types',
queryset = ServiceType.objects\
.select_related('service_type_category')\
.only( 'service_type_category', 'name' )
)
).filter(
countries = country_id,
).only(
'id', 'display_name', 'user'
).order_by(
'-user__last_login'
)

Django aggregate queryset - combine the results of two querysets which both contain aggregates

I have the following model in django:
action = models.CharField("Action performed",max_length=200,db_index=True)
date = models.DateField("Date when event occured",blank=True)
time = models.TimeField("Time when event occured",blank=True)
timestamp = models.DateTimeField("Timestamp when event occured",blank=True,null=True,db_index=True)
username = models.ForeignKey(USER)
computer = models.ForeignKey(COMPUTER)
location = models.ForeignKey(LOCATION)
I wish to return data to a HTML page, which has the following columns:
COMPUTER NAME, COUNT(LOGON), COUNT(LOGOFF)
I am using
logOnOffData = LOGONOFF.objects.all()
computerBreakDown = logOnOffData.values("computer__computerName").annotate(Count("timestamp"))
This generates the number of occurrences of each computer being included in the database table. I can easily generate a queryset for each action, but cannot see how to include the results of the two querysets into one.
Would be wonderful to filter on annotations, somethink like:
loging_logut = ( Computer
.objects
.all()
.annotate( longons = Count( action__action = 'logon' ) )
.annotate( longouts = Count( action__action = 'logout' ) )
But this syntax is not allowed on django ORM. You can learn about it on FILTERING ON ANNOTATIONS IN DJANGO post.
In quoted article you can learn about some workaround: for example writing your own sql code in extra field.
If performance is not the issue, the most code readable approach is a dictionary like this:
{ 'john s computer': { 'n_logins': 100, 'n_logounts' :99 },
'mathiew s computer': { 'n_logins': 80, 'n_logouts' :80 },
...
}
To get this dictionary you can annotate logins and logouts separately, then join it:
#logons = { 'john s computer': 100, 'mathiew s computer': 80, ... }
logons = dict(
Computer
.objects
.filter( action__action = 'logon' )
.annotate( longons = Count( ) )
.values_list( 'name', 'logons' )
)
#logons = { 'john s computer': 99, 'mathiew s computer': 80, ... }
logouts = dict( ...
#joining dicsts
computers = {}
for computer in Computer.objects.all().values_list( 'name', flat = True):
computers[computer]['n_logins'] = logons[computer]
computers[computer]['n_logouts'] = logouts[computer]

How do you have a single select to select two different loosely related models in a django form

I have these models:
OrderLine
StockItem
WaybillItem
Waybill
The Waybill consists of a number of WaybillItem connected to one Order Number.
The WaybillItem consists of a OrderLine & a StockItem
The relation between StockItem & Orderline is a loose many to many dependent on a query from the data of the OrderLine:
StockItem.objects.filter( wh_code = OrderLine.origin_wh_code ).filter( si_code = OrderLine.si_code ).filter( commodity_code = OrderLine.commodity_code )
In the form I would like the user to have a single select to choose the possible options where for each matching StockItem/OrderLine there is one item.
Example:
In OrderLines there is the following OrderLines:
Line_ID, OrderNumber, commodity_code, si_code, origin_wh_code
1001,1, Wheat, 2222,Rome
1002,1, Oat, 2222, Rome
1003,2, Oat, 2222, Rome
In StockItem you have:
Stock_ID, commodity_code, si_code, wh_code, reference_code
10, Wheat, 2222, Rome, 222201
12, Oat, 2222, Rome, 222202
13, Wheat, 2222, Rome, 222203
14, Wheat, 2222, Paris, 222203
This should result in a dropdown for waybillline for waybill with number 1 with:
Line_ID, Stock_ID, (commodity_code, si_code, reference_code)
1001, 10, (Wheat, 2222, 222201
1001, 13, (Wheat, 2222, 222203)
1002, 12, (Oat,2222, 222202)
When selected the Waybill line should have both the Line_ID & Stock_ID saved, but the user is only using a single Dropdown.
I am using inlineformset_factory to create the list of wabill lines.
Can anyone help?
Edited Adding Models View and Template
Models:
The Waybill
class Waybill( models.Model ):
ltiNumber = models.CharField( max_length = 20 )
waybillNumber = models.CharField( max_length = 20 )
dateOfLoading = models.DateField( null = True, blank = True )
recipientLocation = models.CharField( max_length = 100, blank = True )
destinationWarehouse = models.ForeignKey( Places, blank = True )
The Order
class OrderLine( models.Model ):
lti_pk = models.CharField( max_length = 50, primary_key = True, db_column = 'LTI_PK' )
lti_id = models.CharField( max_length = 40, db_column = 'LTI_ID' )
code = models.CharField( max_length = 40, db_column = 'CODE' )
origin_wh_code = models.CharField( max_length = 13, blank = True, db_column = 'ORIGIN_WH_CODE' )
destination_location_code = models.CharField( max_length = 10, db_column = 'DESTINATION_LOCATION_CODE' )
si_code = models.CharField( max_length = 8, db_column = 'SI_CODE' )
commodity_code = models.CharField( max_length = 18, db_column = 'COMMODITY_CODE' )
number_of_units = models.DecimalField( max_digits = 7, decimal_places = 0, db_column = 'NUMBER_OF_UNITS' )
This is the stock model
class StockItem( models.Model ):
wh_pk = models.CharField( max_length = 90, blank = True, primary_key = True )
wh_code = models.CharField( max_length = 13 )
wh_name = models.CharField( max_length = 50, blank = True )
si_code = models.CharField( max_length = 8 )
origin_id = models.CharField( max_length = 23 )
commodity_code = models.CharField( max_length = 18 )
number_of_units = models.IntegerField()
This is the WaybillItem
class WaybillItem( models.Model ):
wbNumber = models.ForeignKey( Waybill )
siNo = models.ForeignKey( OrderLine )
coi_code = models.ForeignKey( StockItem )
numberUnitsLoaded = models.DecimalField( default = 0, blank = False, null = False, max_digits = 10, decimal_places = 3 )
View:
def waybillCreate( request, lti_code ):
#Retrive the LTIs for this Waybill
current_lti = OrderLine.objects.filter( code = lti_code )
for lti in current_lti:
c_sis.append( lti.si_code )
current_stock = StockItem.in_stock_objects.filter( si_code__in = c_sis ).filter( wh_code = current_lti[0].origin_wh_code )
class LoadingDetailDispatchForm( ModelForm ):
#Here are the order lines
siNo = ModelChoiceField( queryset = current_lti, label = 'Commodity' )
#This is from the Stock
coi_code = ModelChoiceField( queryset = current_stock )
overload = forms.BooleanField( required = False )
class Meta:
model = LoadingDetail
fields = ( 'siNo', 'numberUnitsLoaded', 'wbNumber', 'overloadedUnits', 'overOffloadUnits' , 'coi_code' )
LDFormSet = inlineformset_factory( Waybill, LoadingDetail, form = LoadingDetailDispatchForm, fk_name = "wbNumber", formset = BaseLoadingDetailFormFormSet, extra = 5, max_num = 5 )
current_wh = ''
if request.method == 'POST':
form = WaybillForm( request.POST )
form.fields["destinationWarehouse"].queryset = Places.objects.filter( geo_name = current_lti[0].destination_loc_name )
formset = LDFormSet( request.POST )
if form.is_valid() and formset.is_valid():
wb_new = form.save()
instances = formset.save( commit = False )
wb_new.waybillNumber = new_waybill_no( wb_new )
for subform in instances:
subform.wbNumber = wb_new
subform.save()
wb_new.save()
return HttpResponseRedirect( '../viewwb/' + str( wb_new.id ) )
else:
loggit( formset.errors )
loggit( form.errors )
loggit( formset.non_form_errors )
else:
qs = Places.objects.filter( geo_name = current_lti[0].destination_loc_name ).filter( organization_id = current_lti[0].consegnee_code )
if len( qs ) == 0:
qs = Places.objects.filter( geo_name = current_lti[0].destination_loc_name )
else:
current_wh = qs[0]
form = WaybillForm(
initial = {
'dispatcherName': request.user.profile.compasUser.person_pk,
'dispatcherTitle': request.user.profile.compasUser.title,
'ltiNumber': current_lti[0].code,
'dateOfLoading': datetime.date.today(),
'dateOfDispatch': datetime.date.today(),
'recipientLocation': current_lti[0].destination_loc_name,
'recipientConsingee':current_lti[0].consegnee_name,
'transportContractor': current_lti[0].transport_name,
'invalidated':'False',
'destinationWarehouse':current_wh,
'waybillNumber':'N/A'
}
)
form.fields["destinationWarehouse"].queryset = qs
formset = LDFormSet()
return render_to_response( 'waybill/createWaybill.html', {'form': form, 'lti_list':current_lti, 'formset':formset}, context_instance = RequestContext( request ) )
Template (part):
<table cellpadding="1px" cellspacing="1px" id="example" style="border:1px;">
<thead>
<tr><th></th><th>Commodity</th><th>{%if lti_list.0.is_bulk%}Metric Tons{%else%}Number of Units{%endif%}</th><th>Overload</th></tr>
</thead>
{% for sform in formset.forms %}
<tr>
<td>{{ sform.id }}{{forloop.counter}}</td>
<td>{{ sform.siNo }} {{sform.coi_code}}</td>
<td>{{ sform.numberUnitsLoaded }} {%for error in sform.numberUnitsLoaded.errors%}<span class='error'> {{error|escape}}</span>{%endfor%}</td>
<td>{{ sform.overloadedUnits}}</td>
</tr>
{% endfor %}
</table>
First of all, you should refactor your models.
Create model WareHouse:
class WareHouse(models.Model):
code = models.CharField...
Link OrderItem and StockItem to warehouse with foreign keys instead of 'origin_wh_code' and 'wh_code'. Only then you will be able to select data using joins.
Use choice field with custom choices or even better - create inherited from forms.TypedChoiceField field, which encapsulates logic of serializing (stock item, order item) pks.
choices = StockItem.objects.filter(commodity_code=F("warehouse__orders__commodity_code"))\
.values('pk', 'warehouse__orders__pk')# or , <other required fields>)
item = CustomChoiceField(choices= choices, required=True)
After post you could retrieve pair:
stock_item, order_item = form.cleaned_data['item']
#Save'em here