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.
Related
I am making a web page that display a "Kanban".
I have columns as well as cards.
I want to filter the results according to a field (priority) in the cards (Sale).
My models:
class PipelineColumn(models.Model):
name = models.CharField(_("Name"), max_length=80)
order = models.PositiveIntegerField(_("Order of column"), unique=True)
default_probability = models.PositiveSmallIntegerField(_("probability"))
class Meta:
verbose_name = _("PipelineColumn")
verbose_name_plural = _("PipelineColumns")
ordering = ["order"]
def __str__(self):
return self.name
class Sale(models.Model):
name = models.CharField(_("name"), max_length=255)
expected_income = models.FloatField(_("expected income"))
probability = models.PositiveSmallIntegerField(_("probability"))
contact = models.ForeignKey(
"crm.Person",
verbose_name=_("person"),
related_name="sale_person",
on_delete=models.PROTECT,
)
date = models.DateField(_("date"), auto_now_add=True)
scheduled_closing_date = models.DateField(_("scheduled closing date"))
priority = models.PositiveSmallIntegerField(_("priority"), default=0)
column = models.ForeignKey(
"crm.PipelineColumn",
verbose_name=_("column"),
related_name="sale_column",
on_delete=models.CASCADE,
)
class Meta:
verbose_name = _("Sale")
verbose_name_plural = _("Sales")
ordering = ["scheduled_closing_date", "-priority"]
def __str__(self):
return self.name
For example, I would like to display only the columns and cards that have a priority of 2. If I try with PipelineColumn.objects.filter(sale_column__priority=2) it filters the columns well, only the columns with a sale with a priority equal to 2 are displayed. On the other hand, in the displayed columns all the sales are displayed even those whose priority is not equal to 2.
I would also like to be able to filter the sales returned by my query and get only the one whose priority is equal to 2.
For the moment I use the django-url-filter module to do this filtering.
My viewset:
class PipelineColumnViewSet(viewsets.ModelViewSet):
"""
A simple ViewSet for listing or retrieving PipelineColumn.
"""
permission_classes = [permissions.IsAuthenticated]
serializer_class = PipelineColumnSerializer
filter_backends = [
DjangoFilterBackend,
filters.SearchFilter,
filters.OrderingFilter,
]
search_fields = [
"sale_column__name",
"sale_column__contact__last_name",
"sale_column__contact__first_name",
]
filter_fields = ["is_done", "sale_column"]
ordering = ["order"]
def get_queryset(self):
total = PipelineColumn.objects.aggregate(
total=Sum("sale_column__expected_income")
)["total"]
return (
PipelineColumn.objects.all()
.annotate(total_ca=Sum("sale_column__expected_income"))
.annotate(percentage_ca=(Sum("sale_column__expected_income") * 100 / total))
.prefetch_related("sale_column")
)
How to filter the sales in addition to the columns?
I'm making a medication tracking site in Django and am having trouble assigning multiple times to a medication. Currently I have a model for the Medication:
class Medication(models.Model):
UNDEFINED = ''
MILIGRAMS = 'mg'
MILIEQUIVILENT = 'MEQ'
MILILITERS = 'ml'
MEASUREMENT_CHOICES = [
(UNDEFINED, ' '),
(MILIGRAMS, 'mg'),
(MILIEQUIVILENT, 'MEQ'),
(MILILITERS, 'ml'),
]
ORAL = 'orally'
OPTICALLY = 'optically'
NASALLY = 'nasally'
OTTICALLY = 'per ear'
SUBLINGUAL = 'sublingual'
SUBCUTANEOUS = 'subcutaneous'
PER_RECTUM = 'per rectum'
TOPICAL = 'topical'
INHALATION = 'inhalation'
ROUTE_CHOICES = [
(ORAL, 'orally'),
(OPTICALLY, 'optically'),
(NASALLY, 'nasally'),
(OTTICALLY, 'per ear'),
(SUBLINGUAL, 'sublingual'),
(SUBCUTANEOUS, 'subcutaneous'),
(PER_RECTUM, 'per rectum'),
(TOPICAL, 'topical'),
(INHALATION, 'inhalation'),
]
AS_NEEDED = "PRN"
EVERY = "every"
EVERY_TWO = "every two"
EVERY_THREE = "every three"
EVERY_FOUR = "every four"
EVERY_FIVE = "every five"
EVERY_SIX = "six"
EVERY_SEVEN = "seven"
EVERY_EIGHT = "eight"
EVERY_NINE = "nine"
FREQUENCY_NUM = [
(AS_NEEDED, "PRN"),
(EVERY, "every"),
(EVERY_TWO, "every two"),
(EVERY_THREE, "every three"),
(EVERY_FOUR, "every four"),
(EVERY_FIVE, "every five"),
(EVERY_SIX, "six"),
(EVERY_SEVEN, "seven"),
(EVERY_EIGHT, "eight"),
(EVERY_NINE, "nine"),
]
MINUTE = 'minute'
HOUR = "hour"
DAY = "day"
WEEK = 'week'
MONTH = "month"
FREQUENCY_MEASUREMENT = [
(MINUTE, 'minute'),
(HOUR, "hour"),
(DAY, "day"),
(WEEK, 'week'),
(MONTH, "month"),
]
ONE_TAB = 'tab'
MULTIPLE_TABS = 'tabs'
HALF_TAB = 'half of a tab'
THIRD_TAB = 'third of a tab'
QUARTER_TAB = 'quarter of a tab'
ONE_DROP = 'drop'
MULTIPLE_DROPS = 'drops'
FORM_FACTORS = [
(ONE_TAB, 'tab'),
(MULTIPLE_TABS, 'tabs'),
(HALF_TAB, 'half of a tab'),
(THIRD_TAB, 'third of a tab'),
(QUARTER_TAB, 'quarter of a tab'),
(ONE_DROP, 'drop'),
(MULTIPLE_DROPS, 'drops')
]
client = models.ForeignKey(
Client_Medlist,
blank=True,
default=None,
on_delete=models.CASCADE)
name = models.CharField(max_length=255)
drug_class = models.CharField(max_length=255)
dosage = models.FloatField(default=10)
measurement = models.CharField(
max_length=3,
choices=MEASUREMENT_CHOICES,
default=MILIGRAMS,)
number_of_form = models.IntegerField(default=1)
drug_form_factor = models.CharField(
max_length=255,
choices=FORM_FACTORS,
default=ONE_TAB,
)
route = models.CharField(
max_length=255,
choices=ROUTE_CHOICES,
default=ORAL,)
frequency_num = models.CharField(
max_length=255,
choices=FREQUENCY_NUM,
default=EVERY,)
frequency_measurement = models.CharField(
max_length=255,
choices=FREQUENCY_MEASUREMENT,
default=DAY,)
times_taken = models.ManyToManyField(Medication_Times)
start_date = models.DateField(default=timezone.now)
is_med_active = models.BooleanField(default=True)
transcription_author = models.ForeignKey(User, on_delete=models.PROTECT)
transcription_date = models.DateField(default=timezone.now)
def __str__(self):
return self.name
This model has a 'ManyToMany' relation to the medication times model:
class Medication_Times(models.Model):
times_taken = models.TimeField(default=timezone.now, auto_now=False, auto_now_add=False)
display_name = models.CharField(default='Scheduled Time', max_length=255)
def __str__(self):
return self.times_taken.strftime('%I:%M %p')
For the view leading to this page I am using LoginRequiredMixin and CreateView:
class MedicationNew(LoginRequiredMixin, CreateView):
model = Medication
template_name = 'client_profile/new_med.html'
context_object_name = 'posts'
fields = ['client', 'name', 'start_date', 'drug_class', 'dosage', 'measurement', 'route', 'frequency_num', 'frequency_measurement', 'times_taken']
def form_valid(self, form):
form.instance.author = self.request.user
return super().form_valid(form)
This all works well, but where the maximum value between times is fifteen minutes, when the user is creating a new medication to add to a client's chart, the times are displayed in a long scrolling list with only ctrl+click to select multiple.
Where this needs to be as user friendly as possible, I was thinking that selecting a time, then adding it to a box displayed below the scrolling selection might be a better approach. This way if a user selects the wrong thing, it would be in the box for easy removal, and would be easy to scroll back and select the correct time(s). Then after the correct time(s) are chosen, the whole form could be submitted.
Is there a straightforward way to do this in Django? I have been looking everywhere for a manner to do so, but have yet to come up with anything. Any help would be greatly appreciated.
how use limit_choices_to in django
class Category(models.Model):
"""
商品类别
"""
CATEGORY_TYPE = (
(1, "一级类目"),
(2, "二级类目"),
(3, "三级类目"),
)
def limit_category_type_choice(self):
obj = Category.objects.get(category_type=self.category_type)
field_object = Category._meta.get_field('category_type')
field_value = field_object.value_from_object(obj)
return {'category_type__lte': field_value}
category_id = models.AutoField(primary_key=True, verbose_name='category_id')
category_title = models.CharField(default='', max_length=50, verbose_name='目录标题', help_text='目录标题')
category_name = models.ForeignKey(LinkDesignate,blank=True, null=True, to_field='link_des_text_id', related_name='category', on_delete=models.CASCADE)
category_type = models.IntegerField(choices=CATEGORY_TYPE, verbose_name="类目级别", help_text="类目级别")
parent_category = models.ForeignKey("self", limit_choices_to=self.limit_category_type_choice, null=True, blank=True, verbose_name="父类目级别", help_text="父目录",
related_name="sub_cat", on_delete=models.CASCADE)
class Meta:
verbose_name = "产品目录"
verbose_name_plural = verbose_name
i know this
limit_choices_to=self.limit_category_type_choice,
this is wrong , because name 'self' is not defined
how can to use the function limit_category_type_choice
the document is:
def limit_pub_date_choices():
return {'pub_date__lte': datetime.date.utcnow()}
limit_choices_to = limit_pub_date_choices
how can i change my Function limit_category_type_choice without self
but can use the self instance
Put the def out of the class ;-)
This was my solution, it is similar.
It limits the films which can be choosen for an event.
Films with status '2' in class Film over ForeignKey OR
over reverse ForeignKey: Films with an Event with date in the future
the model:
def limit_film_choices():
date = str(datetime.datetime.now())
result = Q( status = '2') | Q( event_film__date__gte = date)
return result
class Film(models.Model):
STATUS_COICES = [
('1' , 'a'),
('2' , 'b'),
]
status = models.CharField( max_length=16, choices=STATUS_COICES, default='1')
class Event(models.Model):
film = models.ForeignKey('filme.Film', on_delete=models.CASCADE, related_query_name='event_film',
related_name='event_film', blank = True, null=True, limit_choices_to = limit_film_choices)
date = models.DateTimeField(auto_now=False, null=True)
I'm trying to do an aggregate operation between two tables using Django, my models are:
class Cusinetype(models.Model):
hometype_en = models.TextField()
active = models.BooleanField()
hometype_es = models.TextField(blank=True, null=True)
class Meta:
managed = False
db_table = 'cusinetype'
class Foodpreferences(models.Model):
id_client = models.ForeignKey(Client, models.DO_NOTHING, db_column='id_client')
id_cusinetype = models.ForeignKey(Cusinetype, models.DO_NOTHING, db_column='id_cusinetype')
created_at = models.DateTimeField()
class Meta:
managed = False
db_table = 'foodpreferences'
The query that I'm trying to build is:
SELECT
ct.id,
ct.hometype_en,
ct.hometype_es
,
((SELECT COUNT(*)
FROM foodpreferences fp
WHERE fp.id_cusinetype = ct.id AND fp.id_client = 3 ) > 0 ) selected
FROM
Cusinetype ct
I'm trying to generate a model, to store the information of those tables in a single one query, but anything works.
Someone has an idea about how to do it?
serializers.py
class PreferencesSerializer(serializers.ModelSerializer):
selected = serializers.IntegerField()
class Meta:
model = Cusinetype
fields = ('id', 'trucktype_en', 'trucktype_es', 'selected')
views.py
qs = Cusinetype.objects.filter().filter(active = True)
qs = qs.annotate(
selected=Sum(Case(
When(foodpreferences__id_client=3, then=1),
output_field=IntegerField()
))
)
serializers = PreferencesSerializer(qs, many = True)
return Response({ "result": serializers.data })
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.