From ecbeb9cf4a50616610f40c08be275e0bef96cff0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 26 Nov 2025 18:30:48 +0000 Subject: [PATCH 1/3] Initial plan From c6d97d78d389232093d1812f859420d3500a9e15 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 26 Nov 2025 18:40:37 +0000 Subject: [PATCH 2/3] Optimize database queries with select_related and prefetch_related to fix N+1 query issues Co-authored-by: Collert <17819526+Collert@users.noreply.github.com> --- deliveries/views.py | 18 +++++----- inventory/views.py | 5 ++- online_store/views.py | 21 ++++++++++-- pos_server/models.py | 24 +++++++++---- pos_server/views.py | 78 +++++++++++++++++++++++++++++++++---------- 5 files changed, 111 insertions(+), 35 deletions(-) diff --git a/deliveries/views.py b/deliveries/views.py index 3c7a034..6692beb 100644 --- a/deliveries/views.py +++ b/deliveries/views.py @@ -44,11 +44,15 @@ def ready_orders(request): completed=False, order__picked_up=False, timestamp__date=today - ) + ).select_related('order') if courier: - for d in Delivery.objects.filter(completed = False).all(): - if d.courier == request.user: - return redirect("delivery_order", id=d.id) + # Use a single optimized query with select_related to check for user's incomplete delivery + user_delivery = Delivery.objects.filter( + completed=False, + courier=request.user + ).select_related('order').first() + if user_delivery: + return redirect("delivery_order", id=user_delivery.id) return render(request, "deliveries/orders.html", { "route":"orders", "orders":orders, @@ -143,10 +147,8 @@ def profile(request): working = check_if_active_courier(request) != None delivering = False if working: - for d in Delivery.objects.filter(completed = False).all(): - if d.courier == request.user: - delivering = True - break + # Use exists() for a more efficient check instead of iterating all deliveries + delivering = Delivery.objects.filter(completed=False, courier=request.user).exists() return render(request, "deliveries/profile.html", { "working":working, "delivering":delivering, diff --git a/inventory/views.py b/inventory/views.py index d705a36..22b0499 100644 --- a/inventory/views.py +++ b/inventory/views.py @@ -341,7 +341,10 @@ def craft_component(component_id:int, qty:int): - Decreases the inventory of each ingredient used in the component by the required amount, unless the ingredient has unlimited supply. """ - component = Component.objects.get(pk=component_id) + # Prefetch componentingredient_set with ingredient to avoid N+1 queries + component = Component.objects.prefetch_related( + 'componentingredient_set__ingredient' + ).get(pk=component_id) component.inventory += qty component.save() for ci in component.componentingredient_set.all(): diff --git a/online_store/views.py b/online_store/views.py index d685939..fbcb40d 100644 --- a/online_store/views.py +++ b/online_store/views.py @@ -97,7 +97,13 @@ def dish(request, id): allergens, and serialized dish data in JSON format. """ try: - item = Dish.objects.get(pk=id) + # Prefetch related data to avoid N+1 queries + item = Dish.objects.prefetch_related( + 'dishcomponent_set__component__componentingredient_set__ingredient', + 'dishcomponent_set__component__child_dishes', + 'components__child_dishes', + 'menu' + ).get(pk=id) except Dish.DoesNotExist: return HttpResponseNotFound(_("Dish not found")) allergens = set() @@ -237,8 +243,19 @@ def place_order(request): card.charge_card(payment["amount"]) GiftCardAuthorization.objects.create(card=card, order=order, charged_balance=payment["amount"]) dish_counts = Counter(dish_ids) + + # Fetch all dishes at once with prefetched data to avoid N+1 queries + unique_dish_ids = list(dish_counts.keys()) + dishes_queryset = Dish.objects.filter(id__in=unique_dish_ids).prefetch_related( + 'components__child_dishes', + 'dishcomponent_set__component' + ) + dishes_map = {dish.id: dish for dish in dishes_queryset} + for dish_id, quantity in dish_counts.items(): - dish = Dish.objects.get(id=dish_id) + dish = dishes_map.get(dish_id) + if not dish: + continue if check_if_only_choice_dish(dish): continue if dish.station == "bar": diff --git a/pos_server/models.py b/pos_server/models.py index fa91317..1f6efdf 100644 --- a/pos_server/models.py +++ b/pos_server/models.py @@ -88,9 +88,11 @@ def check_if_only_choice_dish(self): Returns: bool: True if all components of the dish have child dishes, False otherwise. """ - if not self.components.all(): + # Use prefetch_related to avoid N+1 queries if not already prefetched + components = self.components.prefetch_related('child_dishes').all() + if not components: return False - for component in self.components.all(): + for component in components: if not component.child_dishes.all(): return False return True @@ -123,8 +125,14 @@ def serialize_with_options(self): - pk (int): The primary key of the dish (same as the ID). """ choice_components = [] - for component in self.components.all(): - if component.child_dishes.all(): + # Prefetch components and child_dishes to avoid N+1 queries + components = self.components.prefetch_related('child_dishes').all() + all_have_children = True + has_components = False + for component in components: + has_components = True + child_dishes = list(component.child_dishes.all()) + if child_dishes: choices = { "parent":{ "title":component.title, @@ -132,7 +140,7 @@ def serialize_with_options(self): }, "children":[] } - for child in component.child_dishes.all(): + for child in child_dishes: choices["children"].append({ "title":child.title, "id":child.id, @@ -140,6 +148,10 @@ def serialize_with_options(self): "force_in_stock":child.force_in_stock }) choice_components.append(choices) + else: + all_have_children = False + # Calculate only_choices inline to avoid redundant query + only_choices = has_components and all_have_children return { "fields":{ "title":self.title, @@ -151,7 +163,7 @@ def serialize_with_options(self): "in_stock":self.in_stock, "force_in_stock":self.force_in_stock, "choice_components":choice_components, - "only_choices":self.check_if_only_choice_dish() + "only_choices":only_choices }, "model":"pos_server.dish", "id":self.id, diff --git a/pos_server/views.py b/pos_server/views.py index 68fa200..e5e807f 100644 --- a/pos_server/views.py +++ b/pos_server/views.py @@ -128,7 +128,12 @@ def order_marking(request): else: status_conditions |= ~Q(**{f"{station}_status__in": [0, 1, 2, 3, 4]}) conditions &= status_conditions - orders = Order.objects.filter(conditions) + # Prefetch related data to avoid N+1 queries in collect_order + orders = Order.objects.filter(conditions).prefetch_related( + 'orderdish_set__dish', + 'delivery', + 'authorization' + ).select_related('authorization') print(orders) # Prepare data for each order @@ -328,7 +333,11 @@ def pos(request): menu = Menu.objects.filter(is_active=True).first() # Sort dishes by ID before grouping to ensure consistent ordering - dishes = Dish.objects.filter(menu=menu).order_by('id') + # Prefetch components and child_dishes to optimize serialize_with_options + dishes = Dish.objects.filter(menu=menu).prefetch_related( + 'components__child_dishes', + 'menu' + ).order_by('id') # Group dishes by station grouped_dishes = defaultdict(list) @@ -357,8 +366,19 @@ def pos(request): new_order = Order(special_instructions=instructions, to_go_order=is_to_go, channel="store") new_order.name = body["name"] if body["name"].strip() != '' else None new_order.save() + + # Fetch all dishes at once with prefetched data to avoid N+1 queries + dish_ids = list(dish_counts.keys()) + dishes_queryset = Dish.objects.filter(id__in=dish_ids).prefetch_related( + 'components__child_dishes', + 'dishcomponent_set__component' + ) + dishes_map = {dish.id: dish for dish in dishes_queryset} + for dish_id, quantity in dish_counts.items(): - dish = Dish.objects.get(id=dish_id) + dish = dishes_map.get(dish_id) + if not dish: + continue if check_if_only_choice_dish(dish): continue if dish.station == "bar": @@ -466,9 +486,11 @@ def check_if_only_choice_dish(dish:Dish): Returns: bool: True if all components of the dish point to other dishes, False otherwise. """ - if not dish.components.all(): + # Use prefetch_related to avoid N+1 queries if not already prefetched + components = dish.components.prefetch_related('child_dishes').all() + if not components: return False - for component in dish.components.all(): + for component in components: if not component.child_dishes.all(): return False return True @@ -493,10 +515,14 @@ def component_choice(request): """ dish_id = request.GET.get('dish_id') if dish_id: - dish = Dish.objects.filter(pk=dish_id).first() + # Prefetch components and child_dishes to avoid N+1 queries + dish = Dish.objects.filter(pk=dish_id).prefetch_related( + 'components__child_dishes' + ).first() choice_components = [] for component in dish.components.all(): - if component.child_dishes.all(): + child_dishes = list(component.child_dishes.all()) + if child_dishes: choices = { "parent":{ "title":component.title, @@ -504,7 +530,7 @@ def component_choice(request): }, "children":[] } - for child in component.child_dishes.all(): + for child in child_dishes: choices["children"].append({ "title":child.title, "id":child.id, @@ -594,7 +620,11 @@ def day_stats(request): menu = Dish.objects.all() # Fetch all orders for the specific date, ordered by timestamp - orders = Order.objects.filter(timestamp__date=day).order_by('timestamp') + # Prefetch related data to avoid N+1 queries + orders = Order.objects.filter(timestamp__date=day).prefetch_related( + 'orderdish_set__dish__dishcomponent_set__component__componentingredient_set__ingredient', + 'dishes' + ).order_by('timestamp') # Initialize stats dictionary to store various metrics stats = { @@ -626,7 +656,7 @@ def get_15_min_window(dt): # Group order occasions into 15-minute time windows time_window = get_15_min_window(order.timestamp) - # Calculate total price of the order + # Calculate total price of the order (uses prefetched data) order_price = sum(od.quantity * od.dish.price for od in order.orderdish_set.all()) # Update the count and total earnings for the time window @@ -644,7 +674,7 @@ def get_15_min_window(dt): average_prep = data['total_prep_time'] / count stats['prep_times'][window] = average_prep - # Process each dish in the order + # Process each dish in the order (uses prefetched data) for item in order.dishes.all(): # Count the quantity of each dish sold if item.title not in stats["item_stats"]: @@ -658,7 +688,7 @@ def get_15_min_window(dt): else: stats["stations"][item.station] += 1 - # Track the quantity of each component used in the dish + # Track the quantity of each component used in the dish (uses prefetched data) for dc in item.dishcomponent_set.all(): if dc.component.title not in stats["components"]: stats["components"][dc.component.title] = [None] * 2 @@ -667,7 +697,7 @@ def get_15_min_window(dt): else: stats["components"][dc.component.title][0] += dc.quantity - # Track the quantity of each ingredient used in the components + # Track the quantity of each ingredient used in the components (uses prefetched data) for ci in dc.component.componentingredient_set.all(): if ci.ingredient.title not in stats["ingredients"]: stats['ingredients'][ci.ingredient.title] = [None] * 2 @@ -737,7 +767,11 @@ def compile_menu(menu): "bar":[], "gng":[], } - for dish in menu.dishes.all().order_by("id"): + # Prefetch related data to avoid N+1 queries + dishes = menu.dishes.prefetch_related( + 'dishcomponent_set__component__child_dishes' + ).all().order_by("id") + for dish in dishes: categories[dish.station].append(prettify_dish(dish)) return categories, components_out @@ -761,7 +795,8 @@ def prettify_dish(dish): "price":format_float(dish.price), "available":(dish.in_stock or dish.force_in_stock) and dish.visible_in_menu, } - dcs = dish.dishcomponent_set.all() + # Use list to leverage prefetched data if available + dcs = list(dish.dishcomponent_set.all()) for index, dc in enumerate(dcs): if dc.component.type == "food": if dc.component.unit_of_measurement == "l" or dc.component.unit_of_measurement == "ml": @@ -784,9 +819,11 @@ def prettify_dish(dish): final_dish["components"] += f"{dc.component.title.lower()}" if dc.quantity > 1 and not dc.component.type == 'beverage' and not (dc.component.unit_of_measurement == "g" or dc.component.unit_of_measurement == "kg"): final_dish["components"] += "s" - if dc.component.child_dishes.all(): + # Use prefetched child_dishes + child_dishes = list(dc.component.child_dishes.all()) + if child_dishes: final_dish["components"] += _(" (choice of: ") - for choice in dc.component.child_dishes.all(): + for choice in child_dishes: final_dish["components"] += f"{choice.title}/" final_dish["components"] = final_dish["components"][:-1] final_dish["components"] += ")" @@ -1084,7 +1121,12 @@ def active_orders(request): active_order_ids = cache.get('active_orders') # Fetch active orders from database using IDs from the cache - active_orders = Order.objects.filter(id__in=active_order_ids) + # Prefetch related data to avoid N+1 queries in collect_order + active_orders = Order.objects.filter(id__in=active_order_ids).prefetch_related( + 'orderdish_set__dish', + 'delivery', + 'authorization' + ).select_related('authorization') serialized_orders = [collect_order(order) for order in active_orders] # Adjust serialization as needed return JsonResponse(serialized_orders, safe=False) From e913b0f34b8ac3424e3e4770d35a6cc09c0e34b0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 26 Nov 2025 18:44:03 +0000 Subject: [PATCH 3/3] Address code review feedback: optimize collect_order, add logging for missing dishes Co-authored-by: Collert <17819526+Collert@users.noreply.github.com> --- online_store/views.py | 4 ++++ pos_server/views.py | 13 ++++++++++--- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/online_store/views.py b/online_store/views.py index fbcb40d..4cbd54a 100644 --- a/online_store/views.py +++ b/online_store/views.py @@ -23,6 +23,9 @@ from django.conf import settings from gift_cards.models import GiftCard, GiftCardAuthorization from django.utils.translation import gettext_lazy as _ +import logging + +logger = logging.getLogger(__name__) def menu(request): """ @@ -255,6 +258,7 @@ def place_order(request): for dish_id, quantity in dish_counts.items(): dish = dishes_map.get(dish_id) if not dish: + logger.warning(f"Dish with id {dish_id} not found while processing online order") continue if check_if_only_choice_dish(dish): continue diff --git a/pos_server/views.py b/pos_server/views.py index e5e807f..010f171 100644 --- a/pos_server/views.py +++ b/pos_server/views.py @@ -32,6 +32,9 @@ from gift_cards.models import GiftCard from online_store.models import RejectedOrder from django.utils.translation import gettext_lazy as _ +import logging + +logger = logging.getLogger(__name__) # Create a configparser object file_dir = os.path.dirname(os.path.abspath(__file__)) @@ -378,6 +381,7 @@ def pos(request): for dish_id, quantity in dish_counts.items(): dish = dishes_map.get(dish_id) if not dish: + logger.warning(f"Dish with id {dish_id} not found while processing POS order") continue if check_if_only_choice_dish(dish): continue @@ -1176,8 +1180,8 @@ def collect_order(order, done=False): """ if not order: return None - # Fetch related OrderDish instances for each order - order_dishes = OrderDish.objects.filter(order=order) + # Use prefetched OrderDish instances from order.orderdish_set if available + order_dishes = order.orderdish_set.all() # Prepare dish details for this order dishes_data = [] @@ -1189,6 +1193,9 @@ def collect_order(order, done=False): 'station': od.dish.station }) + # Use prefetched delivery data if available + delivery = order.delivery.first() if hasattr(order, 'delivery') else None + # Add the order and its dishes to the orders_data list return({ 'order_id': order.id, @@ -1197,7 +1204,7 @@ def collect_order(order, done=False): 'to_go_order':order.to_go_order, 'channel':order.channel, 'phone':order.phone, - 'address':order.delivery.first().destination if order.delivery.first() else None, + 'address':delivery.destination if delivery else None, "special_instructions": order.special_instructions, "timestamp":order.timestamp.isoformat(), "timestamp_pretty":order.timestamp,