From 854d6110dbc967ec92d9e6a1fcd8738b544da813 Mon Sep 17 00:00:00 2001 From: Tuomas Suutari Date: Sat, 18 Mar 2017 10:24:42 +0200 Subject: [PATCH] wip --- shuup/core/order_creator/_source.py | 15 +++++-- shuup/front/basket/objects.py | 59 +++++++++++++++++++++------- shuup/front/basket/update_methods.py | 2 +- shuup_tests/front/test_basket.py | 8 +++- 4 files changed, 63 insertions(+), 21 deletions(-) diff --git a/shuup/core/order_creator/_source.py b/shuup/core/order_creator/_source.py index 1212e53f9d..ca2cb4b7b9 100644 --- a/shuup/core/order_creator/_source.py +++ b/shuup/core/order_creator/_source.py @@ -143,9 +143,7 @@ def __init__(self, shop): self.zero_price = shop.create_price(0) self.create_price = self.zero_price.new - self._taxes_calculated = False - self._processed_lines_cache = None - self._object_cache = {} + self.uncache() # Initialize caching variables def update(self, **values): for key, value in values.items(): @@ -462,6 +460,7 @@ def uncache(self): """ self._processed_lines_cache = None self._taxes_calculated = False + self._object_cache = {} @non_reentrant def __compute_lines(self): @@ -575,6 +574,16 @@ def total_gross_weight(self): return ((sum(l.product.gross_weight * l.quantity for l in product_lines)) if product_lines else 0) def _get_object(self, model, pk): + """ + Get model object from database by pk with caching. + + Avoids same objects being loaded many times from the database + when constructing SourceLines in the same request. + + :type model: type + :type pk: int|Any + :rtype: django.db.models.Model + """ obj = self._object_cache.get((model, pk)) if not obj: obj = model.objects.get(pk=pk) diff --git a/shuup/front/basket/objects.py b/shuup/front/basket/objects.py index f2a3f7f190..aa19498781 100644 --- a/shuup/front/basket/objects.py +++ b/shuup/front/basket/objects.py @@ -8,6 +8,7 @@ from __future__ import unicode_literals import random +import warnings from collections import Counter from decimal import Decimal @@ -93,14 +94,23 @@ def __init__(self, request, basket_name="basket"): self.ip_address = request.META.get("REMOTE_ADDR") self.storage = get_storage() self._data = None - self.dirty = False - self._orderable_lines_cache = None - self._unorderable_lines_cache = None - self._lines_cached = False self.customer = getattr(request, "customer", None) self.orderer = getattr(request, "person", None) self.creator = getattr(request, "user", None) + # Note: Being "dirty" means "not saved". It's independent of + # the caching status (which is cleared with self.uncache()). + # I.e. it's possible to be not saved but cached, or saved but + # not cached. + self.dirty = False + self.uncache() # Set empty values for cache variables + + def uncache(self): + super(BaseBasket, self).uncache() + self._orderable_lines_cache = None + self._unorderable_lines_cache = None + self._lines_by_line_id_cache = None + def _load(self): """ Get the currently persisted data for this basket. @@ -120,6 +130,7 @@ def _load(self): self.storage.delete(basket=self) self._data = self.storage.load(basket=self) self.dirty = False + self.uncache() return self._data def save(self): @@ -226,17 +237,17 @@ def remove_code(self, code): def _cache_lines(self): lines = [BasketLine.from_dict(self, line) for line in self._data_lines] - lines_map = {} + lines_by_line_id = {} orderable_counter = Counter() orderable_lines = [] for line in lines: - lines_map[line.line_id] = line + lines_by_line_id[line.line_id] = line if line.type != OrderLineType.PRODUCT: orderable_lines.append(line) else: product = line.product quantity = line.quantity + orderable_counter[product.id] - if line.shop_product.is_orderable(line.supplier, self.request.customer, quantity, allow_cache=False): + if line.shop_product.is_orderable(line.supplier, self.customer, quantity, allow_cache=False): if product.is_package_parent(): quantity_map = product.get_package_child_to_quantity_map() orderable = True @@ -245,7 +256,7 @@ def _cache_lines(self): in_basket_child_qty = orderable_counter[child_product.id] total_child_qty = ((quantity * child_quantity) + in_basket_child_qty) if not sp.is_orderable( - line.supplier, self.request.customer, total_child_qty, allow_cache=False): + line.supplier, self.customer, total_child_qty, allow_cache=False): orderable = False break if orderable: @@ -258,20 +269,19 @@ def _cache_lines(self): orderable_counter[product.id] += line.quantity self._orderable_lines_cache = orderable_lines self._unorderable_lines_cache = [line for line in lines if line not in orderable_lines] - self._lines_map = lines_map - self._lines_cached = True + self._lines_by_line_id_cache = lines_by_line_id @property def is_empty(self): return not bool(self.get_lines()) def get_unorderable_lines(self): - if self.dirty or not self._lines_cached: + if self._unorderable_lines_cache is None: self._cache_lines() return self._unorderable_lines_cache def get_lines(self): - if self.dirty or not self._lines_cached: + if self._orderable_lines_cache is None: self._cache_lines() return self._orderable_lines_cache @@ -406,22 +416,41 @@ def delete_line(self, line_id): return True return False - def get_line(self, line_id): - self.get_lines() # Populate self._lines_map - return self._lines_map.get(line_id) + def get_basket_line(self, line_id): + """ + Get basket line by line id. + + :rtype: BasketLine + """ + if self._lines_by_line_id_cache is None: + self._cache_lines() + return self._lines_by_line_id_cache.get(line_id) def find_line_by_line_id(self, line_id): + """ + Find basket data line by line id. + + :rtype: dict + """ for line in self._data_lines: if six.text_type(line.get("line_id")) == six.text_type(line_id): return line return None def find_lines_by_parent_line_id(self, parent_line_id): + """ + Find basket data lines by parent line id. + + :rtype: Iterable[dict] + """ for line in self._data_lines: if six.text_type(line.get("parent_line_id")) == six.text_type(parent_line_id): yield line def _get_orderable(self): + warnings.warn( + "basket.orderable is deprecated, use basket.is_empty instead", + DeprecationWarning) return (sum(l.quantity for l in self.get_lines()) > 0) orderable = property(_get_orderable) diff --git a/shuup/front/basket/update_methods.py b/shuup/front/basket/update_methods.py index 2b2c2c3ec9..cb88958518 100644 --- a/shuup/front/basket/update_methods.py +++ b/shuup/front/basket/update_methods.py @@ -73,7 +73,7 @@ def update_display_quantity(self, line, value, **kwargs): new_display_quantity = parse_decimal_string(value) if new_display_quantity is None: return False - basket_line = self.basket.get_line(line['line_id']) + basket_line = self.basket.get_basket_line(line['line_id']) if basket_line and basket_line.product: unit = basket_line.shop_product.unit new_quantity = unit.from_display(new_display_quantity) diff --git a/shuup_tests/front/test_basket.py b/shuup_tests/front/test_basket.py index 7e7c807b56..66cd6315b7 100644 --- a/shuup_tests/front/test_basket.py +++ b/shuup_tests/front/test_basket.py @@ -151,6 +151,7 @@ def test_basket_orderability_change(rf): assert len(basket.get_lines()) == 1 assert len(basket.get_unorderable_lines()) == 0 product.soft_delete() + basket.uncache() assert basket.dirty assert len(basket.get_lines()) == 0 assert len(basket.get_unorderable_lines()) == 1 @@ -197,14 +198,17 @@ def test_basket_package_product_orderability_change(rf): assert len(basket.get_unorderable_lines()) == 0 supplier.adjust_stock(child.id, -1) - assert basket.dirty + + # Orderability is already cached, we need to uncache force recheck + basket.uncache() # After reducing stock to 1, should only be stock for one assert len(basket.get_lines()) == 1 assert len(basket.get_unorderable_lines()) == 1 supplier.adjust_stock(child.id, -1) - assert basket.dirty + + basket.uncache() # After reducing stock to 0, should be stock for neither assert len(basket.get_lines()) == 0