I have the following models:
class Contest(models.Model):
id_contest = models.AutoField(primary_key=True)
name = models.CharField(max_length=50, blank=False)
class Registration(models.Model):
id_registration = models.AutoField(primary_key=True)
team = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.DO_NOTHING,
related_name='registrations',
related_query_name='registration')
contest = models.ForeignKey(
Contest,
on_delete=models.DO_NOTHING,
related_name='registrations',
related_query_name='registration')
created_at = models.DateTimeField(null=True)
confirmed_at = models.DateTimeField(null=True)
class Submission(models.Model):
id_submission = models.AutoField(primary_key=True)
team = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.DO_NOTHING,
related_name='submissions',
related_query_name='submission')
contest = models.ForeignKey(
Contest,
on_delete=models.DO_NOTHING,
related_name='submissions',
related_query_name='submission')
submitted_at = models.DateTimeField(null=True)
is_valid = models.NullBooleanField()
public_score = models.FloatField(null=True)
And I'm working on a leaderboard query (PostgreSQL) like:
select
r.team_id,
max(t.username) as team_name,
count(s.team_id) as num_submissions,
min(s.public_score) as score,
max(s.submitted_at) as last_submission,
max(r.confirmed_at) as confirmation
from api_registration r
left join auth_user t on (r.team_id = t.id)
left join api_submission s on (r.contest_id = s.contest_id and s.team_id = t.id and s.is_valid = TRUE)
where r.contest_id = 1
group by r.team_id
order by score ASC, last_submission DESC;
Which returns the results I want.
However, when translating into Django QuerySet operations, the closest I've
come is:
leaderboard = Registration.objects \
.filter(contest=contest, contest__submission__is_valid=True) \
.annotate(team_name=Max('team__username'),
num_submissions=Count('team__submission'),
score=Min('contest__submission__public_score'),
last_submission=Max('contest__submission__submitted_at'),
confirmation=Max('confirmed_at')) \
.order_by('score', '-last_submission')
which generates the query:
SELECT
"api_registration"."id_registration",
"api_registration"."team_id",
"api_registration"."contest_id",
"api_registration"."created_at",
"api_registration"."confirmed_at",
MAX("api_registration"."confirmed_at") AS "confirmation",
MAX("api_submission"."submitted_at") AS "last_submission",
MAX("auth_user"."username") AS "team_name",
MAX("api_submission"."public_score") AS "score",
COUNT(T5."id_submission") AS "num_submissions"
FROM "api_registration" INNER JOIN "api_contest" ON ("api_registration"."contest_id" = "api_contest"."id_contest")
INNER JOIN "api_submission" ON ("api_contest"."id_contest" = "api_submission"."contest_id")
INNER JOIN "auth_user" ON ("api_registration"."team_id" = "auth_user"."id")
LEFT OUTER JOIN "api_submission" T5 ON ("auth_user"."id" = T5."team_id")
WHERE ("api_registration"."contest_id" = 1
AND "api_submission"."is_valid" = True)
GROUP BY "api_registration"."id_registration"
ORDER BY "score" ASC, "last_submission" DESC;
And doesn't properly compute the correct number of submissions per team.
Any help on how to define the Django QuerySet operations to obtain correct results?
Try change this:num_submissions=Count('team__submission'), for num_submissions=Count('team_id'),.
This is to group according to the team_id of the table registration.
Related
I want to create a new object in ModelB when specific condition are met in ModelA. I am new to Django so that I am unable to figure out how exactly I can achieve this.
For example I have two models(Product and ProductVariant), when specific condition on ProductVariant is met then I want to calculate new object value in Product model.
My Product model is like this:
PRODUCT_TYPE = (
('s', 'simple'),
('v', 'varaible')
)
class Products(models.Model):
name = models.CharField(max_length=250,null=True, blank=True,)
slug = models.SlugField(max_length=200, unique=True,null=True)
short_description = HTMLField()
description = HTMLField()
category = models.ForeignKey(Categories, related_name="products",on_delete=models.SET_NULL,null=True,blank=True,)
brand = models.ForeignKey(Brands,on_delete=models.CASCADE, default=None, null=True, blank=True,)
warranty_support = HTMLField()
product_type = models.CharField(choices=PRODUCT_TYPE, default='simple', max_length=50)
And my Product Attribute Model is like this:
class ProductVariant(models.Model):
product = models.ForeignKey(Products,on_delete=models.CASCADE)
variant = models.ForeignKey(ProductAttribute,on_delete=models.CASCADE, null = True, default=None)
managed_stock = models.IntegerField(choices=STOCK_MANAGED, default=0)
stock = models.IntegerField(default=None)
stock_threshold = models.IntegerField()
price = models.DecimalField(max_digits=10, decimal_places=2)
sku = models.CharField(max_length= 250, default=None)
sale_price = models.DecimalField(max_digits=10, decimal_places=2)
sale_start_date=models.DateField(auto_now_add=False, auto_now=False, default=None)
sale_end_date=models.DateField(auto_now_add=False, auto_now=False,default=None)
I am trying to create regular_price and sale_price on Product model if product_type is variable and if sale_end_date is greater than today. I want to set the price from the variant which has the lowest price.
I tried doing like this on Product model:
def clean(self):
if self.product_type == 'varaible' and ProductVariant.objects.filter(product=self, variant_count__gt = 1):
self.min_price = ProductVariant.objects.filter(product=self).Min('price')
self.max_price = ProductVariant.objects.filter(product=self).Max('price')
but I am not able to achieve what I want,
How can I do this?
After some research and analysis I found solution to my problem, I am posting the solution here so that someone with similar problem could be benefited.
#property
def get_price(self):
result = dict()
variants = ProductVariant.objects.filter(product=self)
count = variants.count()
if count > 1:
min_variant = variants.order_by('price').first()
max_variant = variants.order_by('-price').first()
result['min_price'] = min_variant.price
result['max_price'] = max_variant.price
elif count == 1:
variant = variants.first()
if variant.sale_price:
result['price'] = variant.price
result['sale_price'] = variant.sale_price
sale_variant = variants.order_by('sale_price').first()
result['lowest_sale_price'] = sale_variant.sale_price
result['regular_price'] = sale_variant.price
today = datetime.date.today()
if variant.sale_start_date <= today and variant.sale_end_date >= today:
result['sale_end_date'] = variant.sale_end_date
else:
result['price'] = variant.price
I am having trouble with django ORM.
I want to get the data which I can get using PostgreSQL and raw sql query in views. But is there any solution to achieve this using Django ORM.
Here is my models
class Invoice(models.Model):
class PaymentMode(models.TextChoices):
Cash = 0, _('CASH')
Credit = 1, _('CREDIT')
class PaymentStatus(models.TextChoices):
Paid = 0, _('PAID')
Pending = 1, _('PENDING')
total_amount = models.IntegerField()
payment_mode = models.CharField(choices=PaymentMode.choices,
max_length=20,
default=PaymentMode.Credit)
payment_status = models.CharField(choices=PaymentStatus.choices,
default=PaymentStatus.Pending,
max_length=15)
print_date = models.DateField(default=now)
invoice_date = models.DateField(default=now)
created_by = models.ForeignKey(User, on_delete=models.DO_NOTHING)
customer = models.ForeignKey(Customer, on_delete=models.DO_NOTHING)
class Payment(models.Model):
class PaymentMethod(models.TextChoices):
Cash = 0, _('CASH')
Cheque = 1, _('CHEQUE')
OnlineTransfer = 2, _('WEB_TRANSFER')
Other = 3, _('OTHER')
invoice = models.ForeignKey(Invoice,
on_delete=models.DO_NOTHING,
default=0)
amount = models.IntegerField()
date = models.DateField(default=now)
recieved_by = models.CharField(max_length=150)
payment_method = models.CharField(choices=PaymentMethod.choices,
default=PaymentMethod.Cash,
max_length=20)
and here is my PostgreSQL query
SELECT
A.id ,
A.total_amount ,
A.payment_mode,
A.payment_status,
A.print_date,
A.invoice_date,
A.created_by_id,
A.customer_id,
coalesce(SUM(P.amount), 0) AS "paid",
(A.total_amount - coalesce(SUM(P.amount), 0)) As "remaining"
FROM
public."Invoicing_invoice" as A
LEFT JOIN public."Invoicing_payment" as P
ON P.invoice_id = A.id
GROUP BY A.id
You can annotate with:
from django.db.models import F, Sum, Value
from django.db.models.functions import Coalesce
Invoice.objects.annotate(
paid=Coalesce(Sum(payment__amount), Value(0)),
remaining=F('total_amount') - Coalesce(Sum(payment__amount), Value(0))
)
The Invoices that arise from this queryset will have two extra attribute .paid and .remaining with the paid and remaining amount of the invoice.
I just need to apply right join with query set.
vdata = (VisVisitData.objects.
.select_related('vpvalue','vparameter')
.filter(vpvalue__vparameter=9,)
.values(name=F('vpvalue__parameter_value'),
visit_day=F('visit__visit_day'),
question=F('vparameter'),
value_id=F('vpvalue'))
.annotate(data=Count('vpvalue_id'))
.order_by('visit__visit_day'))
above code generate following join statement.
FROM vis_visit_data INNER JOIN vis_visit_parameter_values ON
(vis_visit_data.vpvalue_id =
vis_visit_parameter_values.vpvalue_id) LEFT OUTER JOIN
vis_visits ON (vis_visit_data.visit_id =
vis_visits.visit_id)
But I need to do right join instead of Inner Join with vis_visit_parameter_values and vis_visit_data table. below is the snapshot of sql in which I want to make changes.
INNER JOIN vis_visit_parameter_values ON
(vis_visit_data.vpvalue_id =
vis_visit_parameter_values.vpvalue_id)
Model classes (using these 3 models in query set
class VisVisitParameterValues(models.Model):
vpvalue_id = models.IntegerField(primary_key=True)
vparameter = models.ForeignKey('VisVisitParameters', models.DO_NOTHING,related_name='values')
parameter_value = models.TextField(blank=True, null=True)
class VisVisits(models.Model):
visit_id = models.IntegerField(primary_key=True,auto_created=True)
app_local_id = models.IntegerField(blank=True, null=True)
visit_day = models.IntegerField(blank=True, null=True,db_column='visit_no')
class VisVisitData(models.Model):
vdata_id = models.IntegerField(primary_key=True,auto_created=True)
app_local_id = models.IntegerField(blank=True, null=True)
visit = models.ForeignKey('VisVisits', models.DO_NOTHING, blank=True, null=True, related_name='data')
vparameter = models.ForeignKey('VisVisitParameters', models.DO_NOTHING, blank=True, null=True,related_name='parameters')
vpvalue = models.ForeignKey('VisVisitParameterValues', models.DO_NOTHING, blank=True, null=True,related_name='parameters_values')
Have you tried using:
.filter(Q(vpvalue__isnull=True) | Q(vpvalue__vparameter=9))
Now I'm trying to build complex queryset that uses annotations with conditional related queries.
I have the following models:
class MenuItemCategory(CreateUpdateModel):
name = models.CharField(max_length=255, blank=True, null=True)
class MenuItem(CreateUpdateModel):
category = models.ForeignKey(MenuItemCategory, blank=True, null=True)
name = models.CharField(max_length=255, blank=True, null=True)
class LineItem(models.Model):
order = models.ForeignKey(Orders, blank=True, null=True)
menu_item = models.ForeignKey(MenuItems, blank=True, null=True)
price = models.DecimalField(max_digits=10, decimal_places=2)
quantity = models.DecimalField(max_digits=10, decimal_places=3)
amount = models.DecimalField(max_digits=10, decimal_places=2)
class Order(CreateUpdateModel):
waiter = models.ForeignKey(Employees, blank=True, null=True)
guests_count = models.IntegerField(blank=True, null=True, default=0)
closed_at = models.DateTimeField(blank=True, null=True, db_index=True)
class Employees(CreateUpdateModel):
restaurant = models.ForeignKey(Restaurants, blank=True, null=True)
name = models.CharField(max_length=255, blank=True, null=True)
My goal is to build json with following scheme:
[
{
employee_name: 'Jane',
menu_item_categories: [
{
name: 'Drinks',
line_items_quantity: 10, //times when this waiter brings any item from this category to the customer at the period
amount: 49.00, // price of all drinks sold by this waiter at the period
menu_items: [
name: 'Vodka',
amount: 1.00,
line_items_quantity: 4, # times when this item has been ordered for this waiter at the period
]
}
],
visits: 618,
guests: 813,
cycle_time: 363
}
]
With following serializer:
class EmployeeSerializer(serializers.ModelSerializer):
name = serializers.CharField(max_length=255)
visits = serializers.SerializerMethodField()
guests = serializers.SerializerMethodField()
cycle_time = serializers.SerializerMethodField()
menu_item_categories = serializers.SerializerMethodField()
def get_visits(self, obj):
# works
def get_guests(self, obj):
# works
def get_cycle_time(self, obj):
# works
def get_menu_item_categories(self, obj):
qs = MenuItemCategories.objects.annotate(
line_items_quantity=Count('menuitems__lineitems__order',
filter=Q(
menuitems__lineitems__order__closed_at__range=self.context.get('period'),
menuitems__lineitems__order__waiter=obj)
),
amount=Sum('menuitems__lineitems__amount',
filter=Q(
menuitems__lineitems__order__closed_at__range=self.context.get('period'),
menuitems__lineitems__order__waiter=obj)
),
menu_items=Subquery(
MenuItems.objects.filter(
lineitems__order__closed_at__range=self.context.get('period'),
lineitems__order__waiter=obj
).annotate(amount=Sum('lineitems__amount', filter=Q(lineitems__order__closed_at__range=self.context.get('period'),
lineitems__order__waiter=obj)))
)
)
return MenuItemCategorySerializer(qs, many=True).data
But when I try to build menu_item_categories value - it gives me an error: subquery must return only one column. As I understand, my goal is to annotate categories queryset with custom subquery and my trouble is that I don't understand how subquery works or I use incorrect toolkit to build orm query. So, how can I build this json with orm query and this serializer?
UPD
current query is
SELECT
"menu_item_categories"."id", "menu_item_categories"."created_at",
"menu_item_categories"."updated_at", "menu_item_categories"."restaurant_id",
"menu_item_categories"."name", "menu_item_categories"."is_active",
COUNT("line_items"."order_id") AS "line_items_quantity",
(SELECT
U0."id", U0."created_at", U0."updated_at",
U0."restaurant_id", U0."category_id", U0."name",
SUM(U1."amount") AS "amount"
FROM "menu_items"
U0 INNER JOIN "line_items" U1
ON (U0."id" = U1."menu_item_id")
INNER JOIN "orders" U2
ON (U1."order_id" = U2."id")
WHERE (
U2."waiter_id" = 5 AND U2."closed_at"
BETWEEN 2017-12-20 14:19:16+00:00 AND 2017-12-26 14:19:16+00:00)
GROUP BY U0."id")
AS "menu_items",
SUM("line_items"."amount") AS "amount"
FROM "menu_item_categories"
LEFT OUTER JOIN "menu_items"
ON ("menu_item_categories"."id" = "menu_items"."category_id")
LEFT OUTER JOIN "line_items"
ON ("menu_items"."id" = "line_items"."menu_item_id")
GROUP BY "menu_item_categories"."id",
(
SELECT
U0."id", U0."created_at",
U0."updated_at", U0."restaurant_id",
U0."category_id", U0."name", SUM(U1."amount"
) AS "amount"
FROM "menu_items" U0
INNER JOIN "line_items" U1
ON (U0."id" = U1."menu_item_id")
INNER JOIN "orders" U2
ON (U1."order_id" = U2."id")
WHERE (U2."waiter_id" = 5
AND U2."closed_at"
BETWEEN 2017-12-20 14:19:16+00:00
AND 2017-12-26 14:19:16+00:00)
GROUP BY U0."id")
You can't get that kind of structure directly with one single query, simply because of how RDBMS's work. You can, however, get a big result with lots of redundant information, all the way from the simplest items so you can group data programatically to generate your json structure, or you can just do that in one step by iterating over your querysets:
t_range=self.context.get('period')
employees = Employees.objects.filter(order__closed_at__range=t_range) \
.annotate(
visits=Count(...),
guests=Count(...),
cycle_time=Sum(...),
)
result = []
for employee in employees:
menu_item_categories = MenuItemCategory.objects.filter(menuitem__lineitem__order__waiter=employee) \
.annotate(
line_items_quantity=Count(...),
amount=Sum(...),
)
_cats = []
for cat in menu_item_categories:
menu_items = cat.menuitem_set.filter(order__waiter=employee) \
.annotate(
amount=Sum(...),
line_items_quantity=Count(...),
)
_menu_items = []
for menu_item in menu_items:
_menu_item = {
'name': menu_item.name,
'amount': menu_item.amount,
'line_items_quantity': menu_item.line_items_quantity,
}
_menu_items.append(_menu_item)
_cats.append({
'name': cat.name,
'line_items_quantity': cat.line_items_quantity,
'amount': cat.amount,
'menu_items': _menu_items
})
result.append({
'employee_name': employee.name,
'visits': employee.visits,
'guests': employee.guests,
'cycle_time': employee.cycle_time,
'menu_item_categories': _cats
})
Sure, this will hit the database more than once so unless you prefer performance over this approach, this will do the trick.
class Office(models.Model):
person = models.ForeignKey(Person, verbose_name="Person")
department = models.ForeignKey(Department, verbose_name="Department")
office_desc = models.CharField('Office', max_length=100,unique=True)
office_acronym = models.CharField('Office Acronym', max_length=20,blank=True,help_text="Add acronym if any, not required")
location = models.CharField('Location',max_length=100,blank=True)
trunkline = models.CharField('Trunk Line',max_length=30,blank=True)
directline = models.CharField('Direct Line',max_length=30,blank=True)
localnumber = models.CharField('Local Number',max_length=30,blank=True)
telefax = models.CharField('Telefax',max_length=30,blank=True)
active = models.BooleanField('Active',default=True)
class Department(models.Model):
department_desc = models.CharField('Department', max_length=100,unique=True)
department_acronym = models.CharField('Department Acronym', max_length=20,blank=True,help_text="Add acronym if any, not required")
active = models.BooleanField('Active',default=True)
class Person(models.Model):
GENDER = (
('M','Male'),
('F','Female'),
)
first_name = models.CharField("First Name", max_length=100)
last_name = models.CharField("Last Name",max_length=100)
middle_name = models.CharField("Middle Name", max_length=100, blank=True)
salutation = models.ForeignKey(Salutation, verbose_name="Salutation", null=True, blank=True) #
suffix_name = models.ManyToManyField(SuffixName, verbose_name="Suffix Name",null=True, blank=True) #
job_title = models.ManyToManyField(JobTitle, verbose_name="Job Title",null=True, blank=True) #
gender = models.CharField(max_length=1, choices=GENDER,default='Male')
birthdate = models.DateField('Birthdate',null=True,blank=True)
image = models.ImageField('Image',upload_to='persons',blank=True)
email = models.EmailField('Email',blank=True)
street = models.CharField('Street',max_length=100, blank=True)
brgy = models.CharField('Barangay',max_length=100, blank=True)
town_city = models.CharField('Town/City',max_length=100, blank=True)
zip_code = models.IntegerField('ZIP Code',null=True, blank=True)
department = models.ManyToManyField(Department, verbose_name="Department",null=True, blank=True) #
office = models.ManyToManyField(Office, verbose_name="Office", null=True, blank=True) #
sql_query
select pd.department_desc, pp.last_name, o.office_desc from person_person as pp
INNER JOIN person_person_department as ppd on pp.id = ppd.person_id
INNER JOIN person_department as pd on pd.id = ppd.id
INNER JOIN person_office as o on o.department_id = pd.id
where pd.department_desc = 'Executive'
views code:
per = Person.objects
qry_name = per.values_list('salutation__salutation_desc','first_name','middle_name','last_name', 'office__office_desc', 'office__location','office__localnumber','office__trunkline','office__directline','office__telefax').filter(department__department_desc='Executive')
Result: query result includes person with different department
Expected result: only person with Executive department
If I query directly from the database I get the correct result but when I translate the query into django code it's returning different queryset. Query returns ok if certain person has a single office but if a person has multiple office thats where the inconsistency starts .Query returns list with different description other than what was specified. Am I doing the translation of the query to python right?
A closer representation of the sql query would be:
from django.db.models import F
qry_name = Person.objects.filter(
department__department_desc='Executive',
office__department=F('department')
).values_list(<..FIELDS..>)
Let know if this works.